- Replace ApplyCodexCanonicalIdentity with CodexCanonicalAuthIdentity /
ApplyCodexCanonicalAuthIdentity: the credential face (auth.openai.com
token exchange / refresh / PAT whoami) now sends the originator +
canonical User-Agent pair and no version header, matching codex-rs
default_headers(); the version gate (#3901) only exists on the
/backend-api/codex inference face. whoami keeps its original header
shape (originator + UA) with the canonical UA source.
- Token exchange and refresh send the full pair instead of a bare UA,
eliminating the half-identity (UA without originator) combination no
real client ever emits.
- Codex models manifest: the Version header now follows the client's
own client_version when it is valid and >= the upstream floor (same
source as the query param, restoring the pre-refactor consistency),
falling back to the canonical version otherwise; the query param
keeps its verbatim passthrough contract.
- Drop the now-unreferenced openAICodexProbeVersion constant and its
vacuous consistency assertions; probes resolve their version through
resolveCodexOutboundIdentity at runtime.
The struct field alone never reached the wire: the raw passthrough
pipeline is covered by enableMixedGeminiToolInvocations (#5711), but
TransformClaudeToGeminiWithOptions builds GeminiToolConfig from scratch
and never set the flag, so gemini-* models entering through the Claude
format gateway could still hit the upstream 400 from issue #5709.
- Set IncludeServerSideToolInvocations=true when the built tool
declarations mix functionDeclarations with googleSearch, matching the
raw-path injection semantics.
- Replace the marshal-roundtrip-only test with behavior tests that
drive TransformClaudeToGeminiWithOptions: mixed tools set the flag,
function-only and web-search-only requests leave it unset.
- user_repo.create(): keep the TxFromContext fast path, but restore
tolerance for dbent.ErrTxStarted in the self-owned-transaction branch.
ent's Client.Tx only inspects the driver type, so a repository built
from a tx-bound client (client-injected transactions, e.g. the
integration fixture testEntTx + tx.Client()) hits ErrTxStarted; reuse
that client instead of failing. Fixes the two red integration tests in
allowed_groups_contract_integration_test.go.
- createUserAndClaimInvitation: roll back via defer (matching the OAuth
registration precedent) so a panic inside the transaction cannot leak
the connection.
- settingRepoStub: guard call counters and state with a mutex; the new
concurrency regression test exercises it from multiple goroutines and
the unsynchronized counters were flagged by -race.
Token exchange, PAT whoami, models, probes, and pre-writes now follow
the same UA/version chain as Codex inference instead of hardcoded
codex-cli/0.91.0 or compile-time constants.
- Add IncludeServerSideToolInvocations field to GeminiToolConfig to prevent dropping client tool settings.
- Fix HTTP 400 error when mixing built-in tools (e.g. Google Search) with function calling on Gemini 3.6/3.7 models.
- Add serialization/deserialization unit test TestGeminiToolConfig_IncludeServerSideToolInvocations.
Fixes#5709
RegisterWithVerification checked CanUse() and then marked the code used in
two separate, non-transactional steps; the second step's failure was
swallowed ("invitation code mark failure does not affect registration").
Concurrent registrations with the same invitation code could all pass the
check and each create an account, turning a one-time invitation code into
an unlimited account factory (TOCTOU race).
Fix:
- AuthService: create user and claim the invitation code inside one DB
transaction (createUserAndClaimInvitation). The claim reuses
redeemRepo.Use's conditional UPDATE (WHERE status='unused'); losers are
rejected with INVITATION_CODE_INVALID and their transaction (including
the user insert) is rolled back. No-code registration path unchanged.
- userRepository.create: explicitly join an outer ent transaction via
TxFromContext instead of relying on Client.Tx returning ErrTxStarted
(ent's Tx never inspects the context, so the old reuse branch was dead
code and user inserts always committed in their own transaction,
leaving orphan users behind when the outer transaction rolled back).
Regression tests:
- unit: concurrent register with one invitation code must succeed exactly
once (8 goroutines -> 1 success, 7 x INVITATION_CODE_INVALID)
- integration: outer-tx rollback removes user and releases the claim;
commit persists both atomically
89d826be2 raised backend/go.mod to `go 1.26.6` and updated the three CI
workflows' version assertions, but left the Go builder image in all three
Dockerfiles pinned at 1.26.5. Since the official golang images set
GOTOOLCHAIN=local, the toolchain is not auto-downloaded and any image build
fails hard at `go mod download`.
CI does not catch this: the workflows build with actions/setup-go, not with
these Dockerfiles.
Also extend the Go-upgrade checklist in DEV_GUIDE.md, which listed only the
CI files -- that omission is why the Dockerfiles were missed.
Default codex_fingerprint_mode to off. v0.1.175 treated a missing key as
"session", so upgrading silently rewrote installation/session/thread/turn/
window identifiers for every existing OAuth account that had never configured
this field. The quota regressions in #5555, #5556 and #5582 line up with that
version boundary, with A/B reports that rolling back to v0.1.173 restores
quota. Convergence is now explicit opt-in (#5610).
Only accounts that never set the field change behaviour; explicit off /
device / session / full keep working exactly as configured. That required
flipping the persistence condition in all three account modals from
"!== 'session'" to "!== 'off'": the old rule deleted the key when it equalled
the default, which after the flip would have silently discarded an
administrator's explicit opt-in to session.
Also extend convergence to the passthrough path, which previously left client
identifiers untouched:
- resolve the ids once in forwardOpenAIPassthrough and rewrite
client_metadata on the raw bytes (gjson extract + sjson splice) because
passthrough is a hot path that must not fully unmarshal multi-MB bodies;
a shared core keeps the raw and map variants from drifting
- both request builders apply the staged ids at the same relative position
(after session isolation, before identity enforcement) so headers and body
share one id set and turn_id stays consistent
- stage the ids unconditionally, including nil: a failover from a converged
account to an off account must not leave the previous account's ids behind
OpenAI sunset the legacy unary /responses/compact endpoint (404, #5598,
#5624), so the account "compact probe" in the admin UI kept failing even for
healthy accounts, and the beta-feature negotiation header was only attached
to compaction turns.
Beta features (codex-rs session/mod.rs build_model_client_beta_features_header
+ client.rs build_responses_headers): the header is a session-level constant
attached to every /responses request, the WS handshake and /responses/compact.
Enumerating FEATURES shows no Experimental feature is enabled by default, so a
default install sends exactly "remote_compaction_v2". Mirror that:
- OAuth requests without a client-declared header get the default shape, so we
no longer produce a "header only on compaction turns" pattern real Codex
never emits (#5586 chains that strip the header)
- a client-declared header is preserved as-is: non-empty without v2 means the
user disabled the feature and the gateway must not rewrite that
- native v2 turns (compaction_trigger in body) always ensure v2 is present
- non-OAuth upstreams keep the compaction-turn-only behaviour
- the WS injection sits outside the client-header copy block so prewarm and
turn handshakes cannot land in different pool compatibility buckets
Compact probe now exercises native v2 (streaming /responses +
compaction_trigger) instead of the dead endpoint. Success requires an actual
compaction output item — scanning output_item.done/added, the terminal
response.output[] and the whole-JSON fallback — so a 2xx that silently drops
the trigger is reported as unsupported (the "got 0 items" class, #5478,
#5648). Probe identity is now UUID-shaped and applies the account's
convergence, matching real traffic on the same endpoint.