Skip to content

fix: preserve reasoning.server_tool_call reasoning details - #606

Open
LukasParke wants to merge 2 commits into
mainfrom
claude/server-tool-call-reasoning
Open

LukasParke wants to merge 2 commits into
mainfrom
claude/server-tool-call-reasoning

Conversation

@LukasParke

@LukasParke LukasParke commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Description

OpenRouter returns a fourth reasoning detail type, reasoning.server_tool_call. It records a completed OpenRouter server-tool call (e.g. openrouter:fusion, openrouter:tool_search) so the call can be rehydrated on a later turn. The SDK's reasoning detail union only knew summary, encrypted and text, so the unknown-type fallback silently dropped these entries at parse time. They never reached providerMetadata and were never sent back, which breaks server-tool continuity across turns.

Backend shape (OpenRouterTeam/openrouter-web)

  • packages/llm-interfaces/schemas/response/reasoning-schemas/index.ts: ReasoningDetailType.ServerToolCall = 'reasoning.server_tool_call' with tool_name: string, tool_call_id?: string | null, arguments: string, result: string, plus the common id/format/index fields. It is part of ReasoningDetailUnionSchema.
  • packages/llm-interfaces/reasonings/duplicate-tracker.ts: dedupes it by tool_call_id, falling back to ${tool_name}:${arguments}.
  • Returned to clients: packages/router/skins/openai-chat-completions/from-internal-stream/handlers.ts (buildRoundTripReasoningDetail / handleGenerationServerToolCall) emits it in delta.reasoning_details as a discrete, non-mergeable entry. It is built by packages/router/plugins/server-tools/server-tool-reasoning-detail.ts.
  • Accepted on input: packages/router/plugins/normalize-request/reconstruct-server-tool-messages-openai.ts rehydrates echoed reasoning.server_tool_call details into assistant tool_calls and tool messages.
  • Docs: the public OpenAPI spec (/openapi.json) includes ReasoningDetailServerToolCall. The reasoning-tokens guide doesn't mention it yet.

Changes

  • src/schemas/reasoning-details.ts: added ReasoningDetailType.ServerToolCall and ReasoningDetailServerToolCallSchema to the union. The response, stream delta and providerOptions schemas all build on it, so all three now accept the type.
  • src/utils/reasoning-details-duplicate-tracker.ts: uses the same canonical key as the backend.
  • src/chat/index.ts: handles the new type in the exhaustive switches. It produces no visible reasoning part or delta, and streaming pushes it as a discrete entry, never merged with text or summary.
  • The converter needed no change. It forwards non-text details unchanged once they parse.

Tests (all fail on origin/main, pass here)

  • src/chat/server-tool-call-reasoning.test.ts: parse (doGenerate), stream accumulation (doStream finish metadata keeps text merged and the server tool call separate), and a streamText two-turn round trip that checks the second request's reasoning_details.
  • src/utils/reasoning-details-duplicate-tracker.test.ts: dedup by tool_call_id and by tool_name:arguments.

Overlap with #593

#593 changes src/schemas/format.ts. This PR doesn't touch that file. format stays optional on the new schema, which matches the backend (server-tool details are emitted without a format).

Checklist

  • I have run pnpm stylecheck and pnpm typecheck
  • I have run pnpm test and all tests pass
  • I have added tests for my changes (if applicable)
  • I have updated documentation (if applicable)

Changeset

  • I have run pnpm changeset to create a changeset file

🤖 Generated with Claude Code


Devin Review

OpenRouter emits reasoning.server_tool_call entries in reasoning_details to
record server-tool invocations (e.g. openrouter:fusion) so they can be
rehydrated on later turns. The SDK schema did not know the type, so these
entries were dropped at parse time and never replayed.

Add the type to the reasoning detail schemas, dedupe it by tool_call_id
(or tool_name:arguments) like the backend, and pass it through streaming
accumulation without merging.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Devin Review

#600 adds an empty reasoning part that carries encrypted-only or
text-less reasoning details, so the non-streaming content is no longer
exactly [text]. Assert the text part is present, any reasoning part is
empty, and the server_tool_call detail is reachable from part or response
providerMetadata, so the test passes before and after #600.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
LukasParke added a commit that referenced this pull request Oct 23, 2026
Resolve the tracker conflict so server_tool_call keys get the same type
prefix as the backend, and add a test that a server_tool_call and an
encrypted detail sharing an id are both kept.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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