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
OpenAI APIKey accounts with base_url pointing to third-party OpenAI-compatible
upstreams (DeepSeek, Kimi, GLM, Qwen, etc.) were failing because the gateway
unconditionally converted Chat Completions requests to Responses format and
forwarded to {base_url}/v1/responses, which only exists on OpenAI's official
endpoint.
Detection-based routing:
- Probe upstream capability on account create/update via a minimal POST to
/v1/responses; HTTP 404/405 means 'unsupported', any other response means
'supported'.
- Persist result as accounts.extra.openai_responses_supported (bool).
- ForwardAsChatCompletions branches at function entry: APIKey accounts with
explicit support=false go through new forwardAsRawChatCompletions which
passthrough-forwards CC body to /v1/chat/completions without protocol
conversion.
Default behavior for accounts without the marker preserves the legacy
'always Responses' path — existing OpenAI APIKey accounts that were working
before this change continue to work without modification (the 'reality is
evidence' principle: an account that has been running implies upstream
capability).
Probe is fired async after Create / Update / BatchCreate; failures only log,
never block the admin flow. BulkUpdate omitted (low signal of base_url
changes; can be added if needed).
Implementation:
- New pkg internal/pkg/openai_compat: marker key + ShouldUseResponsesAPI
- New service file openai_apikey_responses_probe.go: probe + persist
- New service file openai_gateway_chat_completions_raw.go: CC pass-through
- Account test endpoint short-circuits with explicit message for
probed-unsupported accounts (full CC test path is a TODO)
Zero schema changes, zero migrations, zero frontend changes, zero wire
modifications — all wired through existing AccountTestService injection.
Closes: DeepSeek-OpenAI account (id=128) production failure
- When tools contain both web_search and function declarations, use
requestType=agent instead of web_search (Google web_search route
rejects functionDeclarations)
- Set toolConfig.mode=AUTO when mixed tools detected (VALIDATED is
incompatible with googleSearch + functionDeclarations)
- Add hasOnlyWebSearchTools helper
- Fix buildParts test calls missing 4th arg (stripSignatures)
Codex CLI 0.125+ defaults to sending request bodies with
Content-Encoding: zstd. Without server-side decompression the gateway
returns 'Failed to parse request body' on /v1/responses (and any other
JSON endpoint) because gjson sees raw zstd bytes.
ReadRequestBodyWithPrealloc now inspects Content-Encoding and
transparently decodes zstd, gzip/x-gzip, and deflate bodies before
returning them, then strips the encoding headers and updates
ContentLength so downstream code can reuse the bytes safely.
Unsupported encodings produce a clear error.
Adds unit tests covering identity, zstd, gzip, deflate, unsupported
encoding, corrupt zstd payloads, nil bodies, and explicit identity.
Real Claude Code CLI always sends a 2-block system array:
[0] {"type":"text", "text":"x-anthropic-billing-header: cc_version=X.Y.Z.{fp}; cc_entrypoint=cli; cch=00000;"}
[1] {"type":"text", "text":"You are Claude Code...", "cache_control":{...}}
Before this commit, sub2api's mimicry path only produced block [1].
The missing billing block is one of the primary third-party detection
signals Anthropic uses for Claude-Code-scoped OAuth tokens.
New file gateway_billing_block.go ports the fingerprint algorithm
(byte-for-byte from Parrot cc_mimicry.py:compute_fingerprint):
pick chars at positions [4,7,20] of the first user text, then
`sha256(SALT + chars + cc_version)[:3]`.
- claude/constants.go: CLICurrentVersion = "2.1.92" (must match UA)
- gateway_billing_block.go: computeClaudeCodeFingerprint +
buildBillingAttributionBlockJSON + extractFirstUserText
- gateway_service.go: rewriteSystemForNonClaudeCode now emits both
blocks in order; cch=00000 is filled in later by
signBillingHeaderCCH in buildUpstreamRequest.
Downstream compat note: syncBillingHeaderVersion's regex
`cc_version=\d+\.\d+\.\d+` only matches the semver triple,
leaving the `.{fp}` suffix intact when rewriting in buildUpstreamRequest.