Repository navigation
v0.2.6 — aliases on the Responses path
The model alias map now runs on /v1/responses — the endpoint Codex actually uses.
The gap
[defaults.model_aliases] existed to stop a bare gpt-6 / gpt-5.6 from reaching the upstream, where a ChatGPT account answers:
400 {"detail":"The 'gpt-6' model is not supported when using Codex with a ChatGPT account."}
But it only ever ran on the /v1/chat/completions translation. Codex speaks wire_api = "responses", so the one client the map exists for never reached it: the bare name went upstream verbatim, 400'd, and the request fell through to a paid fallback provider. Nothing failed loudly — the fallback answered, so a session looked healthy while being billed off-subscription. Worse, /v1/models advertises those bare names, so a client picking one out of the proxy's own list was the one being diverted.
--model gpt-5.6 against this proxy now lands on the subscription pool.
How the rewrite stays a passthrough
Three steps, and the ordering is the design:
- Read the model with a one-field struct. serde walks the JSON but builds no tree — every other field is skipped as
IgnoredAny. A body with no alias to apply returns here and is forwarded byte-for-byte, never parsed into aValue. - Splice the new name into the raw bytes. Not premature optimisation: bodies reach
server.max_body_bytes(16 MiB), and an aliased Codex client pays this on every request of a session whose history keeps growing. Measured on a 16 MiB body — the obviousValueround-trip costs +64 MB and 75 ms, the splice +17 MB and 17 ms. Deployments size this proxy as "baseline + the bodies currently in flight"; the splice keeps that arithmetic true. - Verify by re-reading the model, and fall back to the
Valueround-trip when it doesn't match. Correctness rests on serde, never on the byte scanner.
That third step is what makes step 2 safe rather than clever. A body can defeat the scan — a nested "model" member appearing first, a JSON-escaped model name, even a member whose name ends in an escaped quote ({"a\"model": …}, whose trailing bytes are literally "model"). Each is now a test; each fails closed into the tree rewrite and forwards a correct body.
An unrecognized id is deliberately not replaced by defaults.model the way resolve_model does on the other path. Here the id is the client's own, and rejecting it is the upstream's call, not the proxy's.
Breaking, if you configured a fallback model_map
model_map keys are matched against the model actually sent upstream. On /v1/responses that used to be whatever the client sent; it is now the aliased name, matching what /v1/chat/completions already did. A provider keyed by a bare name stops matching there — and an unmapped model makes the provider skipped, not degraded:
[fallback.model_map]
"gpt-5.6-sol" = "..." # not "gpt-5.6"The old wording in config.toml promised the previous behaviour; it and the README are corrected in this release.
Full changelog: v0.2.5...v0.2.6