diff --git a/packages/opencode/test/provider/copilot/finish-reason.test.ts b/packages/opencode/test/provider/copilot/finish-reason.test.ts new file mode 100644 index 0000000000..f06e3c0051 --- /dev/null +++ b/packages/opencode/test/provider/copilot/finish-reason.test.ts @@ -0,0 +1,89 @@ +import { describe, test, expect } from "bun:test" +import { mapOpenAICompatibleFinishReason } from "../../../src/provider/sdk/copilot/chat/map-openai-compatible-finish-reason" +import { mapOpenAIResponseFinishReason } from "../../../src/provider/sdk/copilot/responses/map-openai-responses-finish-reason" + +// --------------------------------------------------------------------------- +// mapOpenAICompatibleFinishReason (Chat API path) +// --------------------------------------------------------------------------- + +describe("mapOpenAICompatibleFinishReason", () => { + test("maps 'stop' to 'stop'", () => { + expect(mapOpenAICompatibleFinishReason("stop")).toBe("stop") + }) + + test("maps 'length' to 'length'", () => { + expect(mapOpenAICompatibleFinishReason("length")).toBe("length") + }) + + test("maps 'content_filter' to 'content-filter'", () => { + expect(mapOpenAICompatibleFinishReason("content_filter")).toBe("content-filter") + }) + + test("maps 'function_call' to 'tool-calls'", () => { + expect(mapOpenAICompatibleFinishReason("function_call")).toBe("tool-calls") + }) + + test("maps 'tool_calls' to 'tool-calls'", () => { + expect(mapOpenAICompatibleFinishReason("tool_calls")).toBe("tool-calls") + }) + + test("maps null to 'unknown'", () => { + expect(mapOpenAICompatibleFinishReason(null)).toBe("unknown") + }) + + test("maps undefined to 'unknown'", () => { + expect(mapOpenAICompatibleFinishReason(undefined)).toBe("unknown") + }) + + test("maps empty string to 'unknown'", () => { + expect(mapOpenAICompatibleFinishReason("")).toBe("unknown") + }) + + test("maps unrecognized string to 'unknown'", () => { + expect(mapOpenAICompatibleFinishReason("something_else")).toBe("unknown") + }) +}) + +// --------------------------------------------------------------------------- +// mapOpenAIResponseFinishReason (Responses API path) +// --------------------------------------------------------------------------- + +describe("mapOpenAIResponseFinishReason", () => { + test("null without function call returns 'stop'", () => { + expect(mapOpenAIResponseFinishReason({ finishReason: null, hasFunctionCall: false })).toBe("stop") + }) + + test("null with function call returns 'tool-calls'", () => { + expect(mapOpenAIResponseFinishReason({ finishReason: null, hasFunctionCall: true })).toBe("tool-calls") + }) + + test("undefined without function call returns 'stop'", () => { + expect(mapOpenAIResponseFinishReason({ finishReason: undefined, hasFunctionCall: false })).toBe("stop") + }) + + test("undefined with function call returns 'tool-calls'", () => { + expect(mapOpenAIResponseFinishReason({ finishReason: undefined, hasFunctionCall: true })).toBe("tool-calls") + }) + + test("'max_output_tokens' maps to 'length'", () => { + expect(mapOpenAIResponseFinishReason({ finishReason: "max_output_tokens", hasFunctionCall: false })).toBe("length") + }) + + test("'max_output_tokens' maps to 'length' even with function call", () => { + expect(mapOpenAIResponseFinishReason({ finishReason: "max_output_tokens", hasFunctionCall: true })).toBe("length") + }) + + test("'content_filter' maps to 'content-filter'", () => { + expect(mapOpenAIResponseFinishReason({ finishReason: "content_filter", hasFunctionCall: false })).toBe( + "content-filter", + ) + }) + + test("unknown string without function call returns 'unknown'", () => { + expect(mapOpenAIResponseFinishReason({ finishReason: "something_else", hasFunctionCall: false })).toBe("unknown") + }) + + test("unknown string with function call returns 'tool-calls'", () => { + expect(mapOpenAIResponseFinishReason({ finishReason: "something_else", hasFunctionCall: true })).toBe("tool-calls") + }) +}) diff --git a/packages/opencode/test/provider/copilot/prepare-tools.test.ts b/packages/opencode/test/provider/copilot/prepare-tools.test.ts new file mode 100644 index 0000000000..71576bb0be --- /dev/null +++ b/packages/opencode/test/provider/copilot/prepare-tools.test.ts @@ -0,0 +1,114 @@ +import { describe, test, expect } from "bun:test" +import { prepareTools } from "../../../src/provider/sdk/copilot/chat/openai-compatible-prepare-tools" + +describe("prepareTools", () => { + test("undefined tools returns all undefined", () => { + const result = prepareTools({ tools: undefined }) + expect(result.tools).toBeUndefined() + expect(result.toolChoice).toBeUndefined() + expect(result.toolWarnings).toEqual([]) + }) + + test("empty tools array returns all undefined", () => { + const result = prepareTools({ tools: [] }) + expect(result.tools).toBeUndefined() + expect(result.toolChoice).toBeUndefined() + expect(result.toolWarnings).toEqual([]) + }) + + test("converts a single function tool to OpenAI format", () => { + const result = prepareTools({ + tools: [ + { + type: "function", + name: "get_weather", + description: "Get weather for a city", + inputSchema: { type: "object", properties: { city: { type: "string" } } }, + }, + ], + }) + expect(result.tools).toEqual([ + { + type: "function", + function: { + name: "get_weather", + description: "Get weather for a city", + parameters: { type: "object", properties: { city: { type: "string" } } }, + }, + }, + ]) + expect(result.toolWarnings).toEqual([]) + }) + + test("provider-defined tool emits unsupported-tool warning", () => { + const providerTool = { + type: "provider-defined" as const, + id: "some-provider-tool", + name: "provider_tool", + args: {}, + } + const result = prepareTools({ + tools: [providerTool], + }) + expect(result.toolWarnings).toHaveLength(1) + expect(result.toolWarnings[0]).toEqual({ + type: "unsupported-tool", + tool: providerTool, + }) + // Provider-defined tools are not included in the output tools array + expect(result.tools).toEqual([]) + }) + + test("toolChoice 'auto' passes through", () => { + const result = prepareTools({ + tools: [ + { type: "function", name: "foo", description: "test", inputSchema: {} }, + ], + toolChoice: { type: "auto" }, + }) + expect(result.toolChoice).toBe("auto") + }) + + test("toolChoice 'none' passes through", () => { + const result = prepareTools({ + tools: [ + { type: "function", name: "foo", description: "test", inputSchema: {} }, + ], + toolChoice: { type: "none" }, + }) + expect(result.toolChoice).toBe("none") + }) + + test("toolChoice 'required' passes through", () => { + const result = prepareTools({ + tools: [ + { type: "function", name: "foo", description: "test", inputSchema: {} }, + ], + toolChoice: { type: "required" }, + }) + expect(result.toolChoice).toBe("required") + }) + + test("toolChoice type 'tool' converts to function format", () => { + const result = prepareTools({ + tools: [ + { type: "function", name: "my_func", description: "desc", inputSchema: {} }, + ], + toolChoice: { type: "tool", toolName: "my_func" }, + }) + expect(result.toolChoice).toEqual({ + type: "function", + function: { name: "my_func" }, + }) + }) + + test("no toolChoice returns undefined toolChoice", () => { + const result = prepareTools({ + tools: [ + { type: "function", name: "foo", description: "test", inputSchema: {} }, + ], + }) + expect(result.toolChoice).toBeUndefined() + expect(result.tools).toHaveLength(1) + }) +})