When an OpenAI Chat Completions client targets an Anthropic-platform group,
ForwardAsChatCompletions converts the request CC → Responses → Anthropic
(ChatCompletionsToResponses → ResponsesToAnthropicRequest) before forwarding it
upstream. The Responses→Anthropic converter emits each function_call as its own
assistant message and each function_call_output as its own user message and
relies solely on mergeConsecutiveMessages to alternate roles. That is not enough
to satisfy Anthropic's tool-pairing invariants, so a trimmed or partial tool
history produces an upstream 400, e.g.:
tool_use_id found in tool_result blocks: call_00_...
Each tool_result block must have a corresponding tool_use block in the
previous message.
The failures this leaves unrepaired:
- orphan tool_result — a client that does sliding-window context management
keeps a recent tool result but drops the assistant tool_calls message that
announced it, so the tool_result has no matching tool_use;
- unanswered/dangling tool_use — a parallel call whose sibling result never
came back, or a call left dangling, which Anthropic also rejects.
Add normalizeAnthropicToolPairing, run between two merge passes: the first merge
groups parallel calls and their results; the pairing pass indexes every
tool_result by its tool_use id, keeps only answered tool_use blocks (dropping
unanswered/dangling calls, and the assistant message entirely when nothing else
remains) and re-emits the matching tool_result blocks as the immediately
following user message; standalone/orphan tool_results are dropped from their
original position; the second merge restores alternation. This mirrors
normalizeChatMessages on the Responses→Chat path.
Tested two ways: responses_to_anthropic_tool_pairing_test.go covers the repair
on direct Responses input (developer message between call and output, parallel
both-answered kept grouped, parallel one-unanswered dropped, orphan tool_result,
dangling call, single-call baseline); responses_to_anthropic_cc_chain_test.go
drives the real ChatCompletionsToResponses → ResponsesToAnthropicRequest chain
and reproduces the production 400 (orphan and unanswered-parallel) — both fail
without the repair and pass with it. The full apicompat suite stays green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
errcheck (check-type-assertions) flagged unchecked single-value type
assertions; switch to the comma-ok form so golangci-lint passes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Codex CLI speaks the OpenAI Responses protocol (streaming, store:false), while
many upstreams (e.g. DeepSeek in thinking mode) only expose Chat Completions.
The bridge that translates between the two had grown field by field and leaned
on Go's serialization defaults, which both the Responses client (Codex) and the
Chat upstream reject in ways the official OpenAI endpoints tolerate.
Problems this fixes (all observed running Codex CLI against a DeepSeek upstream):
- Streaming reasoning was never shown in the Codex TUI (the answer appeared
with no visible thinking): reasoning deltas were emitted before the reasoning
item was opened, so the strict client discarded them.
- A tool-using turn could wedge the session into a "no response" state: the
function_call stream was never closed (no function_call_arguments.done /
output_item.done), so Codex never saw the tool call complete.
- Parallel tool calls were rejected upstream (400/502): each function_call
became its own assistant message, producing consecutive assistant messages
with mismatched tool replies.
- A tool turn was rejected with "reasoning_content in the thinking mode must be
passed back": the reasoning that produced the tool call was dropped instead
of being returned on the assistant message.
- Items with no Chat equivalent (web_search_call, ...) and Codex's
command-approval notice landed between an assistant tool_calls message and
its tool reply, triggering "An assistant message with 'tool_calls' must be
followed by tool messages responding to each 'tool_call_id'".
- Interrupt/reconnect left an unanswered or dangling tool_call in the history,
triggering the same 400.
The shared root cause is reliance on serialization defaults — omitempty dropping
protocol-required zero values, and unrecognized item types falling through a
generic path — rather than deliberately reproducing the target protocol. The
bridge is reworked into two explicit layers.
Request direction (Responses input -> Chat messages): a parse -> build ->
normalize pipeline.
- reasoning_content is carried back on the assistant message that produced a
tool call (DeepSeek thinking mode requires it to continue the same thought)
- consecutive function_call items (parallel tool calls) are merged into a
single assistant message's tool_calls array
- item types with no Chat equivalent are skipped instead of leaking through a
generic path
- normalizeChatMessages is the single invariant gate: it guarantees every
assistant tool_calls message is immediately followed by one tool reply per
tool_call_id — reordering any intervening message (such as a command-approval
notice) to after the replies, dropping unanswered tool_calls and orphan tool
replies, and preserving bare passthrough tool messages.
Response direction (Chat SSE -> Responses SSE): ResponsesStreamEvent.MarshalJSON
constructs each streamed event explicitly so protocol-required fields are always
present (output_index/content_index/summary_index at 0, message content:[],
reasoning summary:[], function_call call_id/name/arguments, output_text part
text/annotations/logprobs). This is a single source of truth that removes any
post-hoc JSON patching. Reasoning is emitted as its own output item, opened
before its deltas, and tool calls are fully closed
(function_call_arguments.done + output_item.done with complete arguments).
Tests cover request-direction message invariants against golden Codex request
shapes (parallel calls, unknown items, intervening messages, partial/dangling
calls), per-event wire completeness, and streaming lifecycle ordering.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Anthropic Messages reports input_tokens excluding cache_read/cache_creation, but
OpenAI Responses input_tokens is the total including cached tokens. The reverse
converter passed Anthropic's input_tokens straight through, so client-facing
prompt_tokens/input_tokens were short by the cached count and cache_creation
was dropped entirely.
Fix the non-stream path and the streaming state machine to add cache_read +
cache_creation back into input_tokens, and track CacheCreationInputTokens on
the streaming state. Six downstream paths benefit (Anthropic->Responses,
Anthropic->ChatCompletions, Gemini->ChatCompletions, each sync + stream).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
gpt-5.x models served via the OpenAI Responses API reject requests that
include temperature or top_p with:
{"detail":"Unsupported parameter: temperature"}
This caused ClaudeCode agent/subagent tool requests to fail with a 400
error when an OpenAI group had the Messages-format support enabled.
Root cause: AnthropicToResponses and ChatCompletionsToResponses were
unconditionally forwarding temperature and top_p from the incoming
request to the ResponsesRequest, even though all gpt-5.x reasoning
models reject these sampling parameters.
Fix:
- Add isReasoningModel(model string) bool helper that returns true for
any model whose name starts with "gpt-5".
- Skip temperature and top_p when converting to ResponsesRequest for
reasoning models. Non-reasoning models (e.g. gpt-4o) are unaffected.
- ResponsesRequest.Temperature and TopP are already *float64 with
omitempty, so nil values are safely omitted from the JSON body.
Tests:
- TestAnthropicToResponses_TemperatureStrippedForReasoningModel
- TestAnthropicToResponses_TemperatureStrippedForAllGpt5Variants
- TestChatCompletionsToResponses_TemperatureStrippedForReasoningModel
- TestChatCompletionsToResponses_TemperaturePreservedForNonReasoningModel
Fixes#2487
Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
DeepSeek thinking-mode tool-call conversations may require the assistant
reasoning_content from previous turns to be sent back in later requests. Without
preserving it, those conversations can fail or lose reasoning context.
Changes:
- Preserve assistant reasoning_content when converting Chat Completions messages
to Responses input by wrapping it as a thinking block.
- Add regression coverage for non-streaming DeepSeek responses.
- Add regression coverage for streaming DeepSeek deltas.
- Add regression coverage that request-side messages[].reasoning_content is
passed through with tool calls.
Tests:
go test -tags=unit ./internal/pkg/apicompat ./internal/service -run
'TestChatCompletionsToResponses_AssistantReasoningContentPreserved|
TestChatCompletionsToResponses_AssistantThinkingTagPreserved|
TestForwardAsRawChatCompletions_PreservesDeepSeekReasoningContent|
TestForwardAsRawChatCompletions_ForcesStreamUsageUpstreamAndPassesUsageDownstream
When a chat-completions message has no usable content parts (empty array,
empty text part, or filtered-out image part), marshalChatInputContent
marshalled a nil slice to JSON null. The upstream Responses API rejects a
null content field with HTTP 400. Fall back to an empty string instead.
Fixes#2515
Real Claude CLI traffic sends cache_control as
`{"type":"ephemeral","ttl":"1h"}`. Our previous payload only
sent `{"type":"ephemeral"}`, which is a bytewise mismatch with
the official CLI and one more third-party detection signal.
Policy: client-provided ttl is always passed through unchanged.
Proxy-generated cache_control blocks default to 5m (vs Parrot's 1h)
to avoid burning the 1h cache budget on automatic breakpoints while
still aligning with the `ttl` field being present.
- claude/constants.go: DefaultCacheControlTTL = "5m"
- apicompat/types.go: new AnthropicCacheControl type with TTL field;
AnthropicTool gains optional CacheControl pointer so the mimicry
path can attach a cache breakpoint to tools[-1] later.
- service/gateway_service.go: anthropicCacheControlPayload gains TTL;
marshalAnthropicSystemTextBlock and rewriteSystemForNonClaudeCode
emit ttl=5m by default.
Before: the OpenAI-compat forwarders only called injectClaudeCodePrompt,
which prepends the Claude Code banner but leaves the rest of the body
in its original non-Claude-Code shape. The codebase already admits this
is insufficient (see the comment on rewriteSystemForNonClaudeCode in
gateway_service.go: "仅前置追加 Claude Code 提示词无法通过检测").
Effect: OAuth accounts served through /v1/chat/completions or /v1/responses
were detected as third-party apps and bled plan quota with:
Third-party apps now draw from your extra usage, not your plan limits.
Fix:
- apicompat.AnthropicRequest: add Metadata json.RawMessage so metadata
survives the OpenAI->Anthropic->Marshal round trip; without it the
downstream rewrite has no user_id to work with.
- service: extract applyClaudeCodeOAuthMimicryToBody, a ParsedRequest-free
variant of the /v1/messages mimicry pipeline
(rewriteSystemForNonClaudeCode + normalizeClaudeOAuthRequestBody +
metadata.user_id injection) so the OpenAI-compat forwarders can reuse it.
- service: add buildOAuthMetadataUserIDFromBody + hashBodyForSessionSeed
for the same reason (no ParsedRequest at the call site).
- ForwardAsChatCompletions / ForwardAsResponses: replace the 3-line
prompt-prepend with the full mimicry pipeline.
- applyClaudeCodeMimicHeaders: set x-client-request-id per-request
(real Claude CLI always does); missing/duplicated values are one more
third-party fingerprint signal.
No change to the native /v1/messages path: it already called the full
pipeline, we only lift those helpers into a reusable function.
Tests:
- go build ./... passes
- go test ./internal/service/... ./internal/pkg/apicompat/... passes
- lsp_diagnostics clean on all touched files
- pre-existing failures in internal/config are unrelated (env-sensitive
tests that also fail on upstream main)
Bug fixes:
- Detached context for GetAccountConcurrencyBatch (prevent all-zero on request cancel)
- Filter soft-deleted users in GetByGroupID
- Stripe CSP policy (allow Stripe.js in script-src and frame-src)
- WebSearch API key validation on save
- RECHARGING status in payment result success check
- Windows test fixes (logger Sync deadlock, config path escaping)
Feature enhancements:
- Webhook multi-instance dispatch (extractOutTradeNo + GetWebhookProvider)
- EasyPay mobile H5 payment (device param + PayURL2)
- SSE error propagation in WebSearch emulation
- AccountStatsCost DTO field for admin usage logs
- Plans sort by sort_order instead of created_at
- UsageMapHook for streaming response usage data
- apicompat Instructions field passthrough
- EffectiveLoadFactor for ops concurrency/metrics
- Usage billing RETURNING balance for notify system
- BulkUpdate mixed channel warning with details
- println to slog migration in auth cache
- Wire ProviderSet cleanup
- CI cache-dependency-path optimization
Frontend:
- Refund eligibility check per provider (canRequestRefund)
- Plan sort_order editing
- Dead code cleanup (simulate_claude_max, client_affinity)
- GroupsView platform switch guard
- channels features_config API type
- UsageView account_stats_cost export
- apply default mapped model only when scheduling fallback is actually used
- preserve reasoning in OpenAI-compatible output via reasoning_content and avoid invalid input function_call ids
Add Anthropic Messages API support for OpenAI platform groups, enabling
clients using Claude-style /v1/messages format to access OpenAI accounts
through automatic protocol conversion.
- Add apicompat package with type definitions and bidirectional converters
(Anthropic ↔ Chat, Chat ↔ Responses, Anthropic ↔ Responses)
- Implement /v1/messages endpoint for OpenAI gateway with streaming support
- Add model mapping UI for OpenAI OAuth accounts (whitelist + mapping modes)
- Support prompt caching fields and codex OAuth transforms
- Fix tool call ID conversion for Responses API (fc_ prefix)
- Ensure function_call_output has non-empty output field
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>