Skip to content

fix(claude): use canonical reasoning_content in non-stream response - #193

Open
warelik wants to merge 2 commits into
kaitranntt:mainfrom
warelik:fix/claude-openai-nonstream-reasoning
Open

fix(claude): use canonical reasoning_content in non-stream response#193
warelik wants to merge 2 commits into
kaitranntt:mainfrom
warelik:fix/claude-openai-nonstream-reasoning

Conversation

@warelik

@warelik warelik commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Problem

When translating non-streaming Claude responses to OpenAI Chat Completions (ConvertClaudeResponseToOpenAINonStream), thinking blocks were written to the non-canonical field choices.0.message.reasoning. In contrast, the streaming converter in the same file (ConvertClaudeResponseToOpenAI) outputs reasoning deltas using the canonical choices.0.delta.reasoning_content.

Because downstream OpenAI clients and multi-turn request adapters expect choices.0.message.reasoning_content, non-streaming responses failed to preserve thinking tokens across subsequent turns.

Fix

  • internal/translator/claude/openai/chat-completions/claude_openai_response.go:437: Updated ConvertClaudeResponseToOpenAINonStream to serialize reasoning content to choices.0.message.reasoning_content.

Tests

  • TestConvertClaudeResponseToOpenAINonStream_ReasoningContent: Verifies reasoning_content is properly populated and legacy reasoning field is omitted.
  • TestConvertClaudeResponseToOpenAINonStream_OmitsReasoningContentWhenAbsent: Verifies reasoning fields are omitted when responses lack thinking blocks.
  • TestConvertClaudeResponseToOpenAI_StreamAndNonStreamParity: Verifies stream and non-stream parity for thinking content, visible text, and finish reason.
  • TestConvertClaudeResponseToOpenAI_RedactedThinkingIgnored: Verifies redacted_thinking blocks are never leaked into reasoning_content in streaming or non-streaming mode.

Reverse bite-check

Reverting internal/translator/claude/openai/chat-completions/claude_openai_response.go to the pre-fix state reproduces the failure where reasoning_content is missing:

=== RUN   TestConvertClaudeResponseToOpenAINonStream_ReasoningContent
    claude_openai_response_test.go:226: expected choices.0.message.reasoning_content to exist, payload={"id":"msg_123","object":"chat.completion","created":1787286777,"model":"claude-opus-4-6","choices":[{"index":0,"message":{"role":"assistant","content":"Here is the solution.","reasoning":"Let me analyze the problem. Step 2 is clear."},"finish_reason":"stop"}],"usage":{"prompt_tokens":10,"completion_tokens":20,"total_tokens":30,"prompt_tokens_details":{"cached_tokens":0,"cached_creation_tokens":0}}}
--- FAIL: TestConvertClaudeResponseToOpenAINonStream_ReasoningContent (0.00s)
=== RUN   TestConvertClaudeResponseToOpenAI_StreamAndNonStreamParity
    claude_openai_response_test.go:317: parity mismatch for reasoning_content: nonStream="", stream="First thought. Second thought."
--- FAIL: TestConvertClaudeResponseToOpenAI_StreamAndNonStreamParity (0.00s)
FAIL
FAIL	github.com/router-for-me/CLIProxyAPI/v7/internal/translator/claude/openai/chat-completions	0.385s
FAIL

Verification

TMPDIR=/Users/warelik/.cache/gotmp GOCACHE=/Users/warelik/.cache/gocache go build ./...
TMPDIR=/Users/warelik/.cache/gotmp GOCACHE=/Users/warelik/.cache/gocache go vet ./internal/translator/claude/openai/chat-completions/...
gofmt -l internal/translator/claude/openai/chat-completions/claude_openai_response.go internal/translator/claude/openai/chat-completions/claude_openai_response_test.go
TMPDIR=/Users/warelik/.cache/gotmp GOCACHE=/Users/warelik/.cache/gocache go test -v -run "TestConvertClaudeResponseToOpenAINonStream_ReasoningContent|TestConvertClaudeResponseToOpenAI_StreamAndNonStreamParity|TestConvertClaudeResponseToOpenAI_RedactedThinkingIgnored" ./internal/translator/claude/openai/chat-completions/...

Output:

=== RUN   TestConvertClaudeResponseToOpenAINonStream_ReasoningContent
--- PASS: TestConvertClaudeResponseToOpenAINonStream_ReasoningContent (0.00s)
=== RUN   TestConvertClaudeResponseToOpenAI_StreamAndNonStreamParity
--- PASS: TestConvertClaudeResponseToOpenAI_StreamAndNonStreamParity (0.00s)
=== RUN   TestConvertClaudeResponseToOpenAI_RedactedThinkingIgnored
--- PASS: TestConvertClaudeResponseToOpenAI_RedactedThinkingIgnored (0.00s)
PASS
ok  	github.com/router-for-me/CLIProxyAPI/v7/internal/translator/claude/openai/chat-completions	0.388s

Tooling note

The jbcontext semantic-search CLI is installed but non-functional in this environment
(jbcontext search fails with the OS keychain is not accessible). The equivalent
review passes were performed with the repository's own tooling and manual inspection
instead; this is disclosed for transparency about how the change was reviewed.

Align non-streaming Claude-to-OpenAI Chat Completions translator with streaming path by writing message.reasoning_content instead of message.reasoning.

Refs router-for-me/CLIProxyAPI#5104
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