refactor(llm): share tool choice lowering

This commit is contained in:
Kit Langton
2026-05-06 20:40:47 -04:00
parent 8cdb7233b3
commit 2fe21224ac
6 changed files with 54 additions and 44 deletions
@@ -209,15 +209,13 @@ const lowerTool = (tool: ToolDefinition): AnthropicTool => ({
input_schema: tool.inputSchema,
})
const lowerToolChoice = Effect.fn("AnthropicMessages.lowerToolChoice")(function* (
toolChoice: NonNullable<LLMRequest["toolChoice"]>,
) {
if (toolChoice.type === "none") return undefined
if (toolChoice.type === "required") return { type: "any" as const }
if (toolChoice.type !== "tool") return { type: "auto" as const }
if (!toolChoice.name) return yield* invalid("Anthropic Messages tool choice requires a tool name")
return { type: "tool" as const, name: toolChoice.name }
})
const lowerToolChoice = (toolChoice: NonNullable<LLMRequest["toolChoice"]>) =>
ProviderShared.matchToolChoice("Anthropic Messages", toolChoice, {
auto: () => ({ type: "auto" as const }),
none: () => undefined,
required: () => ({ type: "any" as const }),
tool: (name) => ({ type: "tool" as const, name }),
})
const lowerToolCall = (part: ToolCallPart): AnthropicToolUseBlock => ({
type: "tool_use",
@@ -232,15 +232,13 @@ const textWithCache = (text: string, cache: CacheHint | undefined): Array<Bedroc
return cachePoint ? [{ text }, cachePoint] : [{ text }]
}
const lowerToolChoice = Effect.fn("BedrockConverse.lowerToolChoice")(function* (
toolChoice: NonNullable<LLMRequest["toolChoice"]>,
) {
if (toolChoice.type === "none") return undefined
if (toolChoice.type === "required") return { any: {} } as const
if (toolChoice.type !== "tool") return { auto: {} } as const
if (!toolChoice.name) return yield* invalid("Bedrock Converse tool choice requires a tool name")
return { tool: { name: toolChoice.name } } as const
})
const lowerToolChoice = (toolChoice: NonNullable<LLMRequest["toolChoice"]>) =>
ProviderShared.matchToolChoice("Bedrock Converse", toolChoice, {
auto: () => ({ auto: {} }) as const,
none: () => undefined,
required: () => ({ any: {} }) as const,
tool: (name) => ({ tool: { name } }) as const,
})
const lowerToolCall = (part: ToolCallPart): BedrockToolUseBlock => ({
toolUse: {
+7 -11
View File
@@ -170,17 +170,13 @@ const lowerTool = (tool: ToolDefinition) => ({
parameters: GeminiToolSchema.convert(tool.inputSchema),
})
const lowerToolConfig = Effect.fn("Gemini.lowerToolConfig")(function* (
toolChoice: NonNullable<LLMRequest["toolChoice"]>,
) {
if (toolChoice.type === "required") return { functionCallingConfig: { mode: "ANY" as const } }
if (toolChoice.type === "none") return { functionCallingConfig: { mode: "NONE" as const } }
if (toolChoice.type !== "tool") return { functionCallingConfig: { mode: "AUTO" as const } }
if (!toolChoice.name) return yield* invalid("Gemini tool choice requires a tool name")
return {
functionCallingConfig: { mode: "ANY" as const, allowedFunctionNames: [toolChoice.name] },
}
})
const lowerToolConfig = (toolChoice: NonNullable<LLMRequest["toolChoice"]>) =>
ProviderShared.matchToolChoice("Gemini", toolChoice, {
auto: () => ({ functionCallingConfig: { mode: "AUTO" as const } }),
none: () => ({ functionCallingConfig: { mode: "NONE" as const } }),
required: () => ({ functionCallingConfig: { mode: "ANY" as const } }),
tool: (name) => ({ functionCallingConfig: { mode: "ANY" as const, allowedFunctionNames: [name] } }),
})
const lowerUserPart = (part: TextPart | MediaPart) =>
part.type === "text"
+7 -7
View File
@@ -166,13 +166,13 @@ const lowerTool = (tool: ToolDefinition): OpenAIChatTool => ({
},
})
const lowerToolChoice = Effect.fn("OpenAIChat.lowerToolChoice")(function* (
toolChoice: NonNullable<LLMRequest["toolChoice"]>,
) {
if (toolChoice.type !== "tool") return toolChoice.type
if (!toolChoice.name) return yield* invalid("OpenAI Chat tool choice requires a tool name")
return { type: "function" as const, function: { name: toolChoice.name } }
})
const lowerToolChoice = (toolChoice: NonNullable<LLMRequest["toolChoice"]>) =>
ProviderShared.matchToolChoice("OpenAI Chat", toolChoice, {
auto: () => "auto" as const,
none: () => "none" as const,
required: () => "required" as const,
tool: (name) => ({ type: "function" as const, function: { name } }),
})
const lowerToolCall = (part: ToolCallPart): OpenAIChatAssistantToolCall => ({
id: part.id,
@@ -158,13 +158,13 @@ const lowerTool = (tool: ToolDefinition): OpenAIResponsesTool => ({
parameters: tool.inputSchema,
})
const lowerToolChoice = Effect.fn("OpenAIResponses.lowerToolChoice")(function* (
toolChoice: NonNullable<LLMRequest["toolChoice"]>,
) {
if (toolChoice.type !== "tool") return toolChoice.type
if (!toolChoice.name) return yield* invalid("OpenAI Responses tool choice requires a tool name")
return { type: "function" as const, name: toolChoice.name }
})
const lowerToolChoice = (toolChoice: NonNullable<LLMRequest["toolChoice"]>) =>
ProviderShared.matchToolChoice("OpenAI Responses", toolChoice, {
auto: () => "auto" as const,
none: () => "none" as const,
required: () => "required" as const,
tool: (name) => ({ type: "function" as const, name }),
})
const lowerToolCall = (part: ToolCallPart): OpenAIResponsesInputItem => ({
type: "function_call",
+19 -1
View File
@@ -2,7 +2,7 @@ import { Buffer } from "node:buffer"
import { Cause, Effect, Schema, Stream } from "effect"
import * as Sse from "effect/unstable/encoding/Sse"
import { Headers, HttpClientRequest, type HttpClientResponse } from "effect/unstable/http"
import { InvalidRequestError, ProviderChunkError, type MediaPart, type ToolResultPart } from "../schema"
import { InvalidRequestError, ProviderChunkError, type LLMRequest, type MediaPart, type ToolResultPart } from "../schema"
export const Json = Schema.fromJsonString(Schema.Unknown)
export const decodeJson = Schema.decodeUnknownSync(Json)
@@ -168,6 +168,24 @@ export const sseFraming = (
*/
export const invalidRequest = (message: string) => new InvalidRequestError({ message })
export const matchToolChoice = <Auto, None, Required, Tool>(
adapter: string,
toolChoice: NonNullable<LLMRequest["toolChoice"]>,
cases: {
readonly auto: () => Auto
readonly none: () => None
readonly required: () => Required
readonly tool: (name: string) => Tool
},
) =>
Effect.gen(function* () {
if (toolChoice.type === "auto") return cases.auto()
if (toolChoice.type === "none") return cases.none()
if (toolChoice.type === "required") return cases.required()
if (!toolChoice.name) return yield* invalidRequest(`${adapter} tool choice requires a tool name`)
return cases.tool(toolChoice.name)
})
/**
* Build a `validate` step from a Schema decoder. Replaces the per-adapter
* lambda body `(payload) => decode(payload).pipe(Effect.mapError((e) =>