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>
Keep the Responses hot path on raw request bytes until complex mutation or retry branches need a decoded map, reducing large-body retention under high concurrency.
Keep Anthropic request body rewrites attempt-local and synchronize the accepted wire body only after upstream success so failover and retry paths do not reuse stale parsed state.
Keep large gateway payloads as raw body ranges and bind OpenAI parsed-body caches to the body bytes so failover and mapping do not reuse stale mutable state.
When the Gemini->Anthropic streaming bridge for the /v1/messages endpoint
receives a functionCall part followed by a text part, the text branch in
handleStreamingResponse opened a new text content block without closing the
already-open tool_use block. The tool block's content_block_stop was only
emitted at end-of-stream, after the text block's content_block_start, so the
Anthropic SSE stream contained overlapping/unterminated content blocks. Clients
that assemble messages by block index (e.g. Claude Code) can drop the tool
input or mis-parse the response.
The functionCall branch already closes an open text block before opening a tool
block, and the chat-completions sibling closes the tool block in its text branch
via closeOpenTool(). This applies the same symmetric handling to the messages
variant: close any open tool_use block (resetting openToolIndex/openToolName/
seenToolJSON) before starting text.
Adds a regression test that replays a tool->text Gemini stream and asserts the
Anthropic content-block lifecycle never overlaps.
- TopK initial filter now drops quota-paused accounts: fold the quota check
into isAccountRequestCompatible so session-hash, TopK pool, and per-candidate
rechecks all skip paused accounts. Previously the candidate pool was built
without the quota check, so paused accounts could fill TopK and leave the
scheduler returning "no available accounts" even with healthy ones available.
- Add per-account explicit disable flags auto_pause_5h_disabled /
auto_pause_7d_disabled with toggles in EditAccountModal. Without these,
leaving the account threshold blank silently falls back to the global default,
so admins could not exempt a single account once a global default existed.
Disable is per-window: an account can opt out of 5h auto-pause while still
honoring 7d. Schedule snapshot whitelist includes the new fields, i18n EN/ZH
updated, threshold-hint text revised to explain "blank = global default".
- Move quota auto-pause settings off the request hot path: replace the per-repo
TTL+singleflight sync DB read with a per-SettingService stale-while-revalidate
in-memory snapshot. Get is non-blocking (atomic.Pointer load + async refresh
on staleness); writes via UpdateOpsAdvancedSettings push directly into the
cache through an injected sink; wire warms the cache at startup. Adds Warm
(sync) for tests/init and SetOpenAIQuotaAutoPauseSettings (sink target).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>