Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}

Expand Down Expand Up @@ -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"
}

Expand Down
15 changes: 10 additions & 5 deletions index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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", () => {
Expand Down