Keep round-robin rotation stable in the scheduler fast path when
credentials enter cooldown or are removed. Replace the numeric readyView
cursor modulo normalization with ID-based successor binary search,
aligning readyView with RoundRobinSelector.
- Support CRLF line endings and skip hidden dot directories in nocopy invariant tests.
- Handle environment variable overrides and skip case-sensitive token priority tests on Windows.
- Use static OS and architecture values in Claude header fingerprint assertions for deterministic test results.
Closes: #5295
- Map Claude `output_config.format` with `json_schema` type to Codex `text.format`.
- Preserve custom schema name and strict configuration with appropriate defaults.
Closes: #5280
- Drop unsupported trailing assistant prefill messages when converting requests for Claude Fable models.
- Fall back to an empty user message turn when dropping assistant messages leaves the request empty.
Closes: #5279
- Treat `allowed_warning` status as allowed for shared 5h and 7d rate limit windows.
- Ensure Fable-only rejections with warning-level shared windows remain model-scoped instead of credential-scoped.
Closes: #5275
- Cache trailing thought signatures associated with preceding text blocks via best-effort cache.
- Suppress emitting detached thinking carrier blocks after visible text in streaming and non-streaming responses.
Closes: #5272
- Use `newCodexStatusErr` when handling HTTP handshake rejections in WebSocket execution and streaming.
- Ensure rate limit retry-after metadata and error payload details are properly parsed from handshake responses.
Closes: #5270
- Filter `max` and `ultra` reasoning effort levels for Codex client versions prior to `0.144.0`.
- Extract and forward the `client_version` query parameter across model catalog response handlers.
- Add dotted version parsing and comparison utilities to verify extended reasoning level compatibility.
Closes: #5262
- Introduce `WebSocketResponseObserver` capability and bump plugin ABI schema version to 4.
- Forward upstream WebSocket response frames from Codex and xAI executors to configured observers.
- Wire `WebSocketResponseObserver` across API handlers and plugin host dispatchers.
Closes: #5248
- Deduct `cachedContentTokenCount` from `promptTokenCount` for `usage.input_tokens`.
- Set `usage.cache_read_input_tokens` when cached tokens are present in streaming and non-streaming responses.
Closes: #5238
- Map Claude `server_tool_use` and `web_search_tool_result` content blocks to OpenAI Responses `web_search_call` items in streaming and non-streaming modes.
- Support replaying `web_search_call` items and text search annotations back to Claude server tool blocks and citations.
Closes: #5236
- Track tool call indices independently using a sequential counter instead of reusing Claude content block indices.
- Set the sequential tool call index when emitting streaming delta chunks.
Closes: #5229
- Avoid overwriting parent properties when resolving `anyOf`/`oneOf` unions on object schemas, merging branch properties instead.
- Add `contains` to unsupported constraints and preserve object/array constraint hints in descriptions.
- Strip `required` arrays when the schema does not define a `properties` object.
Closes: #5219
- Introduce `xaiNamespaceRestorer` to track and restore folded dispatcher tool calls across SSE and WebSocket response events.
- Support unwrapping dispatcher tool calls and arguments in `output_item.added` and `function_call_arguments.done` events.
- Normalize historical input namespace tool calls to dispatcher format when namespace folding is active.
Closes: #5214
- Export `InlineLocalRefs` utility to resolve local JSON Pointer references.
- Inline local definitions and remove `$defs`/`definitions` in tool function parameters.
- Support `mcp__` prefixes and `codex_apps` namespace variations when identifying Codex app automation update tools.
- Handle `$ref` entries when inspecting and normalizing root union schema branches.
Both built-in rotation strategies lost their position whenever the candidate
set shrank, which happens on every retry that excludes an already tried
credential and on every cooldown transition.
Smooth weighted round-robin reset every accumulated credit as soon as the
weight vector differed from the previous call. A transient subset is not a
configuration change, so the reset fired constantly. With all credits back at
zero and equal weights, the strict `>` comparison always resolves ties to the
first entry in slice order, and candidates are sorted by auth ID, so every
retry restarted the cascade at the alphabetically first credential. Credits are
now reset only when a credential's configured weight actually changes, and the
accumulator is bounded so permanently removed credentials cannot leak entries.
Plain round-robin indexed a monotonic counter into the filtered slice via
`available[index%len(available)]`. Once the slice shrank, the modulo re-seated
the rotation instead of resuming it. Rotation now continues from the identity
of the previous pick, resolved with a binary search over the sorted ring.
Measured over 9 equally weighted credentials with a realistic mix of long
sessions, new sessions and retries, the busiest-to-quietest ratio drops from
11331x to 1.1x. Distribution is exact for equal weights, matches configured
ratios for unequal weights, and tracks the theoretical optimum within 3% under
random credential unavailability.
The Antigravity backend sometimes ends a 200 stream without ever emitting
finishReason. Evidence from local request logs: 25 of 18,346 captured
cloudcode-pa streams have no finishReason at all (gemini-3.7-flash x23,
gemini-3.6-flash x2). Gemini and OpenAI chat clients then never see a
terminal event and wait forever.
Only synthesize on a clean end of stream
- The [DONE] tail is now translated only when scanner.Err() is nil. A
truncated upstream stream previously still produced a terminal event:
replaying a cut stream to a Claude client emitted the full
content_block_stop / message_delta / message_stop sequence, so the
truncation was reported as a completed message.
- Only Antigravity translators synthesize on [DONE], so the other
executors that emit the tail before checking scanner.Err() cannot leak a
fake terminal event and are left unchanged.
Never finalize a stream that produced nothing
- Synthesis requires at least one chunk carrying candidates or token
accounting. Both translators share the same check, and presence alone is
not enough: `{}`, `{"response":{}}` and `{"response":{"candidates":[]}}`
leave the stream unstarted.
- Without that guard the synthetic chunk defeats the existing empty_stream
detection in sdk/cliproxy/auth/conductor_stream.go, which only fires when
the executor produced no chunk at all. An empty 200 would be reported as
a successful empty completion instead of a failure.
Synthetic chunks mirror the observed upstream shape
- All 18,321 real terminal chunks carry candidates/usageMetadata/
modelVersion/responseId with a model-role candidate whose parts are
[{"text":""}]. The Gemini synthetic chunk now reproduces that shape and
key order instead of a bare finishReason candidate.
- The last known usage snapshot is carried into the synthetic chunk.
Without it the final chunk a client sees reports no tokens, because
FilterSSEUsageMetadata renames non-terminal usage to cpaUsageMetadata
and the Gemini path restores it per chunk.
- The OpenAI chat path keeps the latest cpaUsageMetadata as pending usage
and emits it on [DONE] for the same reason.
Do not mistake an intermediate chunk for the terminal one
- A chunk carrying usage but no finishReason stays non-terminal.
FilterSSEUsageMetadata forwards real usageMetadata on such a chunk only
after an earlier chunk already carried finishReason, which the existing
condition covers; finalizing on usage alone would cut the stream short.
- finish_reason and native_finish_reason are resolved by one shared
helper, so the upstream terminal chunk and the synthesized [DONE] chunk
cannot drift apart.
- The non-stream Gemini conversion defaults a missing finishReason for
every candidate rather than only the first one.
Also fixes the unreachable alt != "" branch, which parsed an always-nil
buffer, and replaces an unchecked param type assertion.
Verified by replaying byte-exact upstream bodies extracted from request
logs through a mock backend, comparing this change against the unmodified
branch point: clean streams keep exactly one terminal event, streams
without finishReason gain one carrying the last usage snapshot, a stream
cut mid-chunk surfaces the read error with no terminal event, and an empty
200 now fails with empty_stream instead of reporting a successful empty
completion.
- Add `AlignClaudeToolResults` to order `tool_result` blocks to match the preceding `tool_use` IDs while preserving other content parts.
- Apply tool result alignment in Claude-to-Gemini and Claude-to-Antigravity request translators.
- Preserve mixed non-response parts when normalizing and reordering parallel function responses in Antigravity executor.
Closes: #5199
- Add `util.ResolveGitHubToken` to resolve GitHub API tokens with priority order (`GITHUB_TOKEN`, `github_token`, and `GITSTORE_GIT_TOKEN` for GitHub repositories).
- Set GitHub `Authorization` headers in management version check and asset updater requests when a token is resolved.
Closes: #5189
- Add new `plugin.quiesce` ABI method and propagate RPC error codes from plugin call failures.
- Invoke quiesce on the replaced plugin before loading a new version, then only activate replacement after quiesce succeeds.
- Improve hot-reload safety by serializing lifecycle transitions, cleaning up failed/canceled loads, and rolling back to the previous plugin state when replacement fails or is canceled.
Closes: #5134
Codex and Claude already emit credential-level quota watermarks on ordinary
responses. CPA used to drop them. Keep the latest watermark in memory and
return it from the management auth-file API.
Hard rule: this is observation only. It must not change scheduling, cooldown
selection, or auth-file persistence.
Snapshot, not accumulation
- QuotaState now has ObservedAt and a bounded Signals map. MarkResult fills
them from the response headers already recorded on the request.
- Signals is the current response, not a union of earlier ones. Retry-After
and "limit reached" only appear on the response that produced them; merging
across responses would keep an expired value forever.
- A response with no quota header (transport failure, 5xx, unrelated endpoint)
leaves the previous snapshot in place.
- ObservedAt is the time of the current snapshot. It advances even when the
values did not change, so a consumer can tell a fresh reading from a stale
one.
- When two model states merge, keep the newer snapshot. Do not union keys
captured at different times.
What is observed, and what is not
- One predicate, ProviderSupportsQuotaObservation, decides the provider set.
- Keep Codex and Claude. Drop Kimi, xAI/Grok, Antigravity, and the Gemini
family (gemini/vertex/aistudio): their ordinary headers are not a reliable
credential-level remaining quota.
- Count-tokens reuses the credential but is not generation traffic.
ExecuteCount sets SkipQuotaObservation so those headers cannot replace the
last generation snapshot. Cooldown and success/failure accounting still run.
Cooldown must not overwrite the last snapshot
- Observation writes only ObservedAt and Signals.
- Cooldown writes only Exceeded, Reason, NextRecoverAt, and BackoffLevel,
through applyCooldownFields. Never assign a fresh QuotaState{...} over a
live value: that would zero the snapshot on 429, Cloudflare, credential-
scope sibling updates, and cooldown clears.
- If a credential-quota cooldown is still active, MarkResult still observes
an already-present model state. It does not create scheduler state just to
record a watermark.
- .cds files persist cooldownFieldsOf(Quota) only. Restore keeps the newer
ObservedAt, so reloading cooldown cannot clobber a newer in-memory snapshot.
- cooldownQuotaEqual still ignores observation fields, so a watermark change
cannot by itself persist cooldown or move the scheduler.
- The management payload omits every cooldown field, so it cannot be mistaken
for scheduler state or wired back into scheduling.
- Manual ResetQuota still clears the full QuotaState.
Codex websocket events
- Codex WS reports quota as codex.rate_limits frames, not HTTP headers.
ParseCodexQuotaEventHeaders turns one event into the same bounded header
shape, and MergeResponseHeaders folds it into the request-scoped holder.
additional_rate_limits is accepted as an object (websocket) or an array
(/wham/usage).
- Parse only through AppendCodexAPIWebsocketResponse. The shared
AppendAPIWebsocketResponse is also used by xAI, and xAI error frames really
do carry x-ratelimit-* headers. Parsing every frame as Codex quota would
forge Codex headers into another provider's request log.
- Also capture code_review_rate_limits.
- A malformed active-limit name drops only that one header, not the window
watermarks parsed from the same event.
- The type discriminator scans a bounded frame prefix, not every byte of
every frame.
- HTTP namespaces an extra limit by short name (x-codex-bengalfox-*); WS
namespaces it by limit name (GPT-5.3-Codex-Spark). The two paths cannot
emit the same header names. The X-Codex-Additional- prefix marks the WS
origin, and snapshot replacement keeps the two spellings from piling up.
Hardening
- Reject observed values with control characters. These strings reach the
plain-text request log, and Limit-Name is upstream-controlled, so CR/LF
could forge a header line.
- When the header cap is hit, keep plan/credits/primary ahead of
additional-limit namespaces, then sort names so truncation is deterministic.
- QuotaState.Clone deep-copies Signals and is used by Auth.Clone and
ModelState.Clone.
- Token stores still serialize credential metadata only, so observation adds
no auth-file writes.
- Keep `functionResponse.response.result` as a string in Gemini responses translation instead of JSON-parsing tool output.
- Apply the same string-preserving behavior for antigravity tool responses to avoid upstream 400 errors from parsed payloads.
Rewriting {type: image_generation} to string required would let later
x_search injection or leftover web_search satisfy the choice. Reduce
the tools list to image_generation for that forced case, and skip
x_search injection while the remaining tools are image-only.
Closes: #5183
An allowed_tools list that only names image_generation cannot be sent
to chat-proxy. Map that empty remainder to the original mode so
mode=auto stays optional instead of becoming required.
Closes: #5183
chat-proxy rejects allowed_tools lists that name image_generation.
Rewrite a forced image_generation choice, and an allowed_tools list
that only names that hosted tool, to the string "required". Mixed
allowed_tools lists drop the image_generation entry. Prune orphans
before this rewrite so older models still lose the leftover choice.
Closes: #5183
- Added a preprocessing pass to repair malformed MCP-style JSON schemas.
- Wraps bare property maps as object schemas, promotes `required: true` flags to parent `required` arrays, and removes boolean `required` from properties.
- Recurses through nested schema containers and skips known API request envelopes to avoid rewriting non-schema docs.
- Switched schema decoding/serialization path to preserve large numeric values and avoid HTML-escaping side effects.
Closes: #5178
Cloud Code resolves newer Antigravity models only for clients reporting
at least 2.9.0; below that it answers 404 Requested entity was not found.
The offline fallback still reported 2.9.0's predecessor 2.2.1, so every
request sent before the hub manifest is first fetched — or from a
deployment that cannot reach the manifest at all — asked for models such
as gemini-3.7-flash-high with a version the backend rejects.
The hub manifest the updater already polls currently publishes 2.9.1, so
this only aligns the offline floor with what the online path resolves.
The test now asserts the floor rather than a literal, so a future
downgrade below 2.9.0 fails instead of silently reintroducing the 404.
Compact deletes tools after prepareResponsesRequestTo. On grok-4.6+
image_generation is now kept and rewritten to allowed_tools, so the
leftover choice would be sent without tools. Reuse the existing
normalizer to drop that orphaned selection.
Closes: #5173