- Bypass short cooldown checks and recording in execution flows when cooling is disabled globally or per auth.
- Skip marking credits permanently disabled and refreshing credit hints when cooling is disabled.
- Export quota cooldown status helper functions for auth and configuration evaluations.
Closes: #4793
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.
Redefine request-retry as additional credential retry rounds and
enforce max-retry-credentials per round. Home dispatch now carries
excluded and pinned auth constraints, supports remote retry limits, and
propagates cooldown retry-after metadata across exhausted rounds.
Move Antigravity upstream retries under conductor ownership to avoid
double-consuming retry attempts. Update configuration comments and add
coverage for Home retry rounds, cooldown handling, pinned credentials,
and legacy dispatcher compatibility.
* fix(executor): prepend empty user turn for model-first requests targeting Gemini/Antigravity (#4959)
When forwarding sliced conversation histories or tool calls across OpenAI Responses,
OpenAI Chat Completions, Claude Messages, and native Gemini, native Gemini and Antigravity
Gemini endpoints require that conversation contents begin with a user turn.
Normalize leading turns at the executor boundary rather than the translator layer:
- Prepend an empty user turn ({"role":"user","parts":[{"text":""}]}) for Gemini, Gemini Vertex,
AI Studio, and Antigravity Gemini generation and CountTokens requests if the first turn is 'model'.
- Keep Antigravity Claude requests untouched to avoid adapter 400 errors.
- Ensure normalization runs after payload rules so payload index overrides target the original turns.
- Use no-copy GJSON inspection to keep overhead zero on valid user-first requests.
* fix(executor): inject Antigravity leading user after reasoning replay (#4959)
Replay can insert a model functionCall at contents[0] for sliced
tool-result history. Run the empty-user prepend on the final
requestPayload, after sanitize and prepareAntigravityGeminiReasoningReplayPayload.
Execute, executeClaudeNonStream and ExecuteStream all assign the validated
original payload to the request and then translate both values. Since both
translations saw the same bytes, every Antigravity request paid for a second
full scan of the client payload. On a captured 24MB tool-history request that
second pass cost roughly 0.9s of CPU and 651MB of allocations.
Translate once when both inputs share a backing array and hand the caller an
independent duplicate, because later stages edit the working copy in place.
Payloads that genuinely differ still get two translations.
Client.KVCompareAndSwap sent Redis EVAL with a Lua script, but the Home RESP
subset does not implement EVAL, so Home replied "ERR unknown command 'eval'".
That broke the Antigravity and Codex reasoning replay caches in Home mode.
Switch the transport to Home's dedicated CAS command:
CAS <key> <expected-exists 0|1> <expected-value> <new-value> [PX <ttl-ms>]
The semantics match the old script argument for argument, so KVCompareAndSwap's
signature and all its callers are unchanged. Omitting PX when ttl <= 0 mirrors
the script's SET-without-PX branch, which clears the TTL.
Deployments that predate CAS reject the command. Detect that by matching the
unsupported-command error, latch ErrCompareAndSwapUnsupported for the client
lifetime so later calls skip the round trip, and warn exactly once. The latch is
deliberately not carried across NewLifetime, so a Home upgrade takes effect on
the next reconnect rather than requiring a CPA restart.
Also stop replay-state failures from failing the request. A bare replay error has
no HTTP status, so resultErrorFromError does not classify it as request-scoped and
MarkResult marks the credential unavailable for that model, walking every
candidate credential until alias resolution has nothing left and returns 503. A
ledger miss is already a tolerated outcome, so degrade to "no replay this turn"
instead. The pairing failure still returns its 400.
Verified end to end against a real Home over RESP/mTLS on PostgreSQL with
Antigravity OAuth credentials: patched Home recreates the replay row through CAS
with its TTL, while a pre-CAS Home latches once, keeps returning 200 instead of
503, and records no credential error attributable to the replay path.
Refs router-for-me/CLIProxyAPIHome#79
- Removed deprecated interceptor and executor-related methods, including `callRequestInterceptor`, `callResponseInterceptor`, and `callStreamChunkInterceptor`.
- Consolidated unused logic and pruned redundant imports to streamline `adapters.go`.
- No functional changes.