diff --git a/index.js b/index.js index 1561d4d..5b3d5e4 100644 --- a/index.js +++ b/index.js @@ -817,7 +817,12 @@ function createResponsesApiResponse(result, model) { export function mapFinishReason(finish) { if (!finish) return "stop" if (finish.includes("length")) return "length" - if (finish.includes("tool")) return "tool_calls" + // Opencode ends json_schema / StructuredOutput turns with a finish string that + // contains "tool". Callers already set finish_reason to "tool_calls" when + // proxy-level toolCalls.length > 0. Mapping the internal structured finish to + // tool_calls here leaves message.content JSON with finish_reason tool_calls + // and an empty tool_calls array — Vercel AI SDK Output.object then throws + // AI_NoOutputGeneratedError. Keep "stop" for content-only completions. return "stop" } @@ -1627,7 +1632,9 @@ export function normalizeAnthropicSystem(system) { export function mapFinishReasonToAnthropic(finish) { if (!finish) return "end_turn" if (finish.includes("length")) return "max_tokens" - if (finish.includes("tool")) return "tool_use" + // Same as mapFinishReason: do not map Opencode StructuredOutput finish to + // tool_use when the proxy already flattened schema JSON into text content. + // Real tool_use is still set by createAnthropicResponse when toolCalls.length > 0. return "end_turn" } diff --git a/index.test.js b/index.test.js index c27c36e..ac4b1b8 100644 --- a/index.test.js +++ b/index.test.js @@ -845,9 +845,13 @@ describe("mapFinishReason", () => { assert.equal(mapFinishReason("length"), "length") }) - it("returns 'tool_calls' when finish includes 'tool'", () => { - assert.equal(mapFinishReason("tool_use"), "tool_calls") - assert.equal(mapFinishReason("tool"), "tool_calls") + it("returns 'stop' when finish includes 'tool' (StructuredOutput); real tools use toolCalls.length", () => { + // Opencode json_schema ends on the internal StructuredOutput tool. Mapping + // that finish to tool_calls breaks AI SDK Output.object. Callers force + // tool_calls when proxy-level toolCalls.length > 0. + assert.equal(mapFinishReason("tool_use"), "stop") + assert.equal(mapFinishReason("tool"), "stop") + assert.equal(mapFinishReason("tool-calls"), "stop") }) it("returns 'stop' for unrecognised values", () => { @@ -1580,8 +1584,9 @@ describe("mapFinishReasonToAnthropic", () => { assert.equal(mapFinishReasonToAnthropic("max_length"), "max_tokens") }) - it("returns tool_use when finish includes tool", () => { - assert.equal(mapFinishReasonToAnthropic("tool_use"), "tool_use") + it("returns end_turn when finish includes tool (StructuredOutput); real tools use toolCalls.length", () => { + assert.equal(mapFinishReasonToAnthropic("tool_use"), "end_turn") + assert.equal(mapFinishReasonToAnthropic("tool"), "end_turn") }) it("returns end_turn for unrecognised values", () => {