Skip to content

fix: map StructuredOutput finish to stop, not tool_calls - #97

Open
djlongy wants to merge 1 commit into
KochC:mainfrom
djlongy:fix/structured-output-finish-reason
Open

djlongy wants to merge 1 commit into
KochC:mainfrom
djlongy:fix/structured-output-finish-reason

Conversation

@djlongy

@djlongy djlongy commented Sep 30, 2026

Copy link
Copy Markdown

Summary

Opencode ends response_format: json_schema turns on its internal StructuredOutput tool, so the finish string contains "tool". mapFinishReason previously mapped any finish containing "tool" to tool_calls even when the proxy had already flattened schema JSON into message.content and toolCalls was empty.

Clients using the Vercel AI SDK (generateText + Output.object / supportsStructuredOutputs: true) only parse structured output when the unified finish reason is stop. They otherwise throw AI_NoOutputGeneratedError despite valid JSON in message.content.

Fix

  • mapFinishReason: stop mapping "tool" → tool_calls; return stop (callers already force tool_calls when toolCalls.length > 0).
  • mapFinishReasonToAnthropic: same for "tool" → tool_use (callers force tool_use when tools present).
  • Gemini already returned STOP for tool finishes — unchanged.
  • Unit tests updated to match.

Real tool calling is unchanged: createChatCompletionResponse / streaming / Anthropic builders still set finish_reason: "tool_calls" / stop_reason: "tool_use" from toolCalls.length.

Minimal repro (before)

curl -sS -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{
    "model":"openai/gpt-5.5",
    "messages":[{"role":"user","content":"Return {\"ok\":true} only."}],
    "response_format":{
      "type":"json_schema",
      "json_schema":{
        "name":"probe",
        "schema":{"type":"object","properties":{"ok":{"type":"boolean"}},"required":["ok"]}
      }
    }
  }' http://127.0.0.1:4010/v1/chat/completions
Field Before After
finish_reason tool_calls stop
message.content {"ok":true} JSON same
message.tool_calls absent / empty absent

Test plan

  • npm test — 242 pass locally
  • Confirm chat/completions with caller tools still returns finish_reason=tool_calls + populated tool_calls
  • Confirm json_schema returns finish_reason=stop + JSON content

Notes

Opencode ends json_schema turns on its internal StructuredOutput tool, so
the finish string contains "tool". mapFinishReason previously mapped any
finish containing "tool" to tool_calls even when the proxy had already
flattened schema JSON into message.content and toolCalls was empty.

Vercel AI SDK Output.object only accepts structured results when the
unified finish reason is stop, so clients using response_format json_schema
(e.g. generateText + Output.object) threw AI_NoOutputGeneratedError despite
valid JSON in message.content.

Callers already set finish_reason to tool_calls / stop_reason to tool_use
when toolCalls.length > 0, so real tool calling is unchanged. Apply the
same fix to mapFinishReasonToAnthropic; Gemini already returned STOP.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant