diff --git a/packages/llm/src/llm.ts b/packages/llm/src/llm.ts index 8ade8a0d0d..e8f80e6276 100644 --- a/packages/llm/src/llm.ts +++ b/packages/llm/src/llm.ts @@ -47,6 +47,7 @@ export type ToolChoiceInput = | ConstructorParameters[0] | ToolDefinition | string +export type ToolChoiceMode = Exclude export type ToolResultInput = Omit & { readonly result: unknown @@ -111,7 +112,7 @@ export const model = (input: ModelInput) => { }) } -export const tool = (input: ToolDefinition | ConstructorParameters[0]) => { +export const toolDefinition = (input: ToolDefinition | ConstructorParameters[0]) => { if (input instanceof ToolDefinition) return input return new ToolDefinition(input) } @@ -141,10 +142,15 @@ export const toolResult = (input: ToolResultInput): ToolResultPart => ({ export const toolMessage = (input: ToolResultPart | ToolResultInput) => message({ role: "tool", content: ["type" in input ? input : toolResult(input)] }) +export const toolChoiceName = (name: string) => new ToolChoice({ type: "tool", name }) + +const isToolChoiceMode = (value: string): value is ToolChoiceMode => + value === "auto" || value === "none" || value === "required" + export const toolChoice = (input: ToolChoiceInput) => { if (input instanceof ToolChoice) return input if (input instanceof ToolDefinition) return new ToolChoice({ type: "tool", name: input.name }) - if (typeof input === "string") return new ToolChoice({ type: "tool", name: input }) + if (typeof input === "string") return isToolChoiceMode(input) ? new ToolChoice({ type: input }) : toolChoiceName(input) return new ToolChoice(input) } @@ -159,7 +165,7 @@ export const request = (input: RequestInput) => { ...rest, system: systemParts(requestSystem), messages: [...(messages?.map(message) ?? []), ...(prompt === undefined ? [] : [user(prompt)])], - tools: tools?.map(tool) ?? [], + tools: tools?.map(toolDefinition) ?? [], toolChoice: requestToolChoice ? toolChoice(requestToolChoice) : undefined, generation: generation(requestGeneration), }) diff --git a/packages/llm/test/llm.test.ts b/packages/llm/test/llm.test.ts index 0150c782d5..807f342034 100644 --- a/packages/llm/test/llm.test.ts +++ b/packages/llm/test/llm.test.ts @@ -21,13 +21,25 @@ describe("llm constructors", () => { }) test("builds tool choices from names and tools", () => { - const tool = LLM.tool({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }) + const tool = LLM.toolDefinition({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }) expect(tool).toBeInstanceOf(ToolDefinition) expect(LLM.toolChoice("lookup")).toEqual(new ToolChoice({ type: "tool", name: "lookup" })) + expect(LLM.toolChoiceName("required")).toEqual(new ToolChoice({ type: "tool", name: "required" })) expect(LLM.toolChoice(tool)).toEqual(new ToolChoice({ type: "tool", name: "lookup" })) }) + test("builds tool choice modes from reserved strings", () => { + expect(LLM.toolChoice("auto")).toEqual(new ToolChoice({ type: "auto" })) + expect(LLM.toolChoice("none")).toEqual(new ToolChoice({ type: "none" })) + expect(LLM.toolChoice("required")).toEqual(new ToolChoice({ type: "required" })) + expect(LLM.request({ + model: LLM.model({ id: "fake-model", provider: "fake", protocol: "openai-chat" }), + prompt: "Use tools if needed.", + toolChoice: "required", + }).toolChoice).toEqual(new ToolChoice({ type: "required" })) + }) + test("builds assistant tool calls and tool result messages", () => { const call = LLM.toolCall({ id: "call_1", name: "lookup", input: { query: "weather" } }) const result = LLM.toolResult({ id: "call_1", name: "lookup", result: { temperature: 72 } }) diff --git a/packages/llm/test/provider/anthropic-messages.recorded.test.ts b/packages/llm/test/provider/anthropic-messages.recorded.test.ts index 78d01f6e19..5e1a85e209 100644 --- a/packages/llm/test/provider/anthropic-messages.recorded.test.ts +++ b/packages/llm/test/provider/anthropic-messages.recorded.test.ts @@ -18,7 +18,7 @@ const request = LLM.request({ generation: { maxTokens: 20, temperature: 0 }, }) -const getWeather = LLM.tool({ +const getWeather = LLM.toolDefinition({ name: "get_weather", description: "Get current weather for a city.", inputSchema: { diff --git a/packages/llm/test/provider/gemini.recorded.test.ts b/packages/llm/test/provider/gemini.recorded.test.ts index 5950a87c61..c4f21ff99e 100644 --- a/packages/llm/test/provider/gemini.recorded.test.ts +++ b/packages/llm/test/provider/gemini.recorded.test.ts @@ -18,7 +18,7 @@ const request = LLM.request({ generation: { maxTokens: 80, temperature: 0 }, }) -const getWeather = LLM.tool({ +const getWeather = LLM.toolDefinition({ name: "get_weather", description: "Get current weather for a city.", inputSchema: { diff --git a/packages/llm/test/provider/openai-chat.recorded.test.ts b/packages/llm/test/provider/openai-chat.recorded.test.ts index cf3807778d..5e5a86c7dd 100644 --- a/packages/llm/test/provider/openai-chat.recorded.test.ts +++ b/packages/llm/test/provider/openai-chat.recorded.test.ts @@ -18,7 +18,7 @@ const request = LLM.request({ generation: { maxTokens: 20, temperature: 0 }, }) -const getWeather = LLM.tool({ +const getWeather = LLM.toolDefinition({ name: "get_weather", description: "Get current weather for a city.", inputSchema: { diff --git a/packages/llm/test/provider/openai-compatible-chat.recorded.test.ts b/packages/llm/test/provider/openai-compatible-chat.recorded.test.ts index 414f5875c2..296429848c 100644 --- a/packages/llm/test/provider/openai-compatible-chat.recorded.test.ts +++ b/packages/llm/test/provider/openai-compatible-chat.recorded.test.ts @@ -31,7 +31,7 @@ const togetherRequest = LLM.request({ generation: { maxTokens: 20, temperature: 0 }, }) -const getWeather = LLM.tool({ +const getWeather = LLM.toolDefinition({ name: "get_weather", description: "Get current weather for a city.", inputSchema: { diff --git a/packages/opencode/src/session/llm-native.ts b/packages/opencode/src/session/llm-native.ts index 41a53a9177..b5dc88ada8 100644 --- a/packages/opencode/src/session/llm-native.ts +++ b/packages/opencode/src/session/llm-native.ts @@ -160,7 +160,7 @@ const messages = (input: MessageV2.WithParts): ReadonlyArray => { } export const toolDefinition = (input: { readonly model: Provider.Model; readonly tool: Tool.Def }) => - LLM.tool({ + LLM.toolDefinition({ name: input.tool.id, description: input.tool.description, inputSchema: EffectZod.toJsonSchema(input.tool.parameters),