Skip to content

fix(codex): write a config the client can actually start with - #41

Merged
MartinForReal merged 2 commits into
mainfrom
fix/codex-provider-config
Aug 9, 2026
Merged

fix(codex): write a config the client can actually start with#41
MartinForReal merged 2 commits into
mainfrom
fix/codex-provider-config

Conversation

@MartinForReal

Copy link
Copy Markdown
Owner

Audited the Codex integration against the config reference and the client's own request builder.

The protocol was already correct

Worth stating first, since it's the part that could have been badly wrong. /v1/responses is a verbatim byte passthrough to Copilot's native endpoint, and sending Codex's exact payload shape (store:false, stream:true, include:["reasoning.encrypted_content"], prompt_cache_key, text, reasoning, tool_choice, parallel_tool_calls) produced the full canonical sequence:

response.created → response.in_progress → response.output_item.added
→ response.content_part.added → response.output_text.delta → .done
→ response.content_part.done → response.output_item.done → response.completed

Tool calling works too — 11× response.function_call_arguments.delta, .done, function_call items, with the type:"custom" apply_patch tool accepted. wire_api = "responses" matches the reference (it's the only supported value), and base_url correctly ends in /v1 because Codex does not append a version.

The defects were all on the config surface.

model_context_window was never written

The parameter was plumbed through configure_codex and unit tested with Some(272000) — but both call sites passed None.

Codex budgets context from its own built-in table, sized for OpenAI's public limits. Copilot serves the same slugs with different windows:

model Copilot window
gpt-5.5, gpt-5.6-sol 1,050,000
gpt-5.3-codex 400,000
gpt-5-mini 264,000

So the client compacted at the wrong point in both directions. It's now read from the live catalog for whichever model ends up selected — looking it up for the recommended default would hand a 1M window to a config pinned to a 264K model. Verified:

model = "gpt-5-mini"
model_context_window = 264000

The key stays unwritten when the catalog is unreachable, and the headless path uses a token already on disk rather than starting a device flow nobody is watching.

model was overwritten unconditionally

Running setup replaced a deliberate choice with the recommended default. Now only written when absent:

# before: model = "gpt-5.5"
model = "gpt-5.6-sol"          # kept
model_context_window = 1050000
model_provider = "ghc-proxy"   # still moves — that's the point of the flag
approval_policy = "on-request" # untouched

The setup guide also printed the default model name even when it had kept a different one, so configure_codex now returns the model it settled on.

The provider block named no API key

A proxy configured with api_key was unreachable from Codex with no config path to fix it. It now gets env_key = "GHC_PROXY_API_KEY" — the same variable the proxy reads, so one exported value serves both sides — but only when a key is set, because Codex fails a turn when a provider names an environment variable that is missing.

service_tier: no change, and that's the finding

This looked like an unconditional override worth gating. Probing Copilot says otherwise:

value result
"priority" / "auto" / "default" / "flex" 400 service_tier is not supported
null 200
omitted 200

The existing nulling is load-bearing. Gating it would have been a regression, so it stays — now with a comment recording the probe so the next reader doesn't undo it. Codex's Fast mode silently does nothing as a consequence, which is documented.

Also verified, no action

Copilot returns no reasoning items or encrypted_content despite the request asking for them. That's upstream, not the proxy — passthrough is byte-verbatim. Codex tolerates it; reasoning continuity across turns is simply unavailable.

The WebSocket transport stays unadvertised. The proxy implements ws:/responses, but Codex's WS path is a much larger contract — v2 response.create with OpenAI-Beta: responses_websockets=2026-02-06, previous_response_id, incremental input deltas and x-codex-turn-state sticky routing.

169 tests pass; cargo fmt --all --check and cargo clippy --all-targets -- -D warnings are clean.

added 2 commits August 9, 2026 09:29
…ting the chosen model

Audited the Codex provider config against the published reference and the
client's own request builder. The wire protocol was already correct --
`wire_api = "responses"`, `base_url` ending in `/v1`, and a verbatim SSE
passthrough that produces the full canonical event sequence including
`response.function_call_arguments.delta`. The defects were all on the
config surface.

`model_context_window` was plumbed through `configure_codex` and unit
tested, but both call sites passed `None`, so it was never written. Codex
then budgets context from its built-in table, which is sized for OpenAI's
public limits; Copilot serves the same slugs with different windows
(1,050,000 for gpt-5.5, 264,000 for gpt-5-mini), so the client compacted
at the wrong point in both directions. The value is now read from the live
catalog for whichever model ends up selected -- looking it up for the
recommended default instead would hand a 1M window to a config pinned to a
264K model. The key stays unwritten when the catalog is unreachable, and
the headless path reads a token already on disk rather than starting a
device flow nobody is watching.

`model` was overwritten unconditionally, so running setup replaced a
deliberate choice with the recommended default. It is now only written
when absent. `model_provider` still moves, since repointing it is the
entire purpose of the flag.

The provider block named no API key. A proxy configured with `api_key` was
therefore unreachable from Codex with no config path to fix it. It now
gets `env_key = "GHC_PROXY_API_KEY"` -- the same variable the proxy reads
-- but only when a key is actually set, because Codex fails a turn when a
provider names an environment variable that is missing.

The setup guide printed the default model name even when it had kept a
different one, so `configure_codex` now returns the model it settled on.

No change to `service_tier`. It looked like an unconditional override
worth gating, but probing Copilot showed every value -- `priority`,
`auto`, `default`, `flex` -- answers 400 "service_tier is not supported",
while explicit null and omission both succeed. The existing nulling is
load-bearing; it is now commented so the next reader does not undo it.
Fast mode silently does nothing as a result, which is documented.
…n env var

Codex only reads provider credentials from an environment variable named by
env_key, and it fails a turn when that variable is unset. The proxy's api_key
can come from config.yaml, in which case nothing is ever exported and the
generated config broke Codex on startup.

Write http_headers = { Authorization = "Bearer <key>" } instead. The value
only opens the local proxy -- the credential that reaches GitHub is the Copilot
token the proxy holds -- so there is no reason to make it depend on the shell.
@MartinForReal MartinForReal changed the title fix(codex): size the context window from the catalog and stop overwriting the chosen model fix(codex): write a config the client can actually start with Aug 9, 2026
@MartinForReal
MartinForReal merged commit 275a471 into main Aug 9, 2026
1 check passed
@MartinForReal
MartinForReal deleted the fix/codex-provider-config branch August 9, 2026 02:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant