Repository navigation
[Bug]: Non-Codex clients on public endpoints get zero prompt-cache reuse on ChatGPT-backed OpenAI models (no synthesized session_id/originator) #6520
Description
Client or integration
Other — a third-party OpenAI/Anthropic-compatible coding agent (ZCode desktop) wired to the public local endpoints, plus plain curl. Not the Grok-managed surface.
Area
Proxy and routing
Version
2.74.0 and 2.76.0 (latest npm at report time; both reproduce)
Operating system
macOS 26 (Darwin 25.6.0, arm64)
Summary
Non-Codex clients that hit the public endpoints (/v1/responses, /v1/messages) get zero prompt-cache reuse on ChatGPT-backed OpenAI models, even with byte-identical prefixes across turns: every request reports input_tokens_details.cached_tokens: 0 and — via usage.attribution — cache_write_tokens: 0, i.e. the backend never even warms the prefix.
Same machine, same single OpenAI (ChatGPT) account, same model (gpt-6.1-sol): real Codex client sessions (which also route through opencodex via openai_base_url = 127.0.0.1:10100/v1) showed up to 226,432 cached_input_tokens in ~/.codex/sessions rollouts the same evening, so neither the account nor the model is the variable.
Cost impact example: one 25-turn agent task through /v1/responses billed 1.92M uncached input tokens in ~13 minutes.
What I ruled out (tested on 2.74.0 and latest npm 2.76.0)
| Variation | cached_tokens on repeat request |
|---|---|
/v1/messages (claude inbound synthesizes a prompt_cache_key via the system+tools cohort fallback) |
0 |
/v1/responses with an explicit body prompt_cache_key |
0 |
| Prefix sizes ~1.8K / 3.3K / 7.0K tokens; 5s and 30s gaps | 0 |
stream: true vs non-stream |
0 |
| Upgrading 2.74.0 → 2.76.0 | 0 |
Same request + originator: Codex Desktop + session_id: <stable id> headers |
2176 / 2341 (93%) |
Prefix stability was verified by hashing tools/system/first-user across all 25 captured requests (identical), and the account pool has exactly one account.
This matches the root cause confirmed in #6481 ("The Codex Codex backend only reuses a warmed prefix when session_id is present"), but #6482 only forwards a session identity on the managed Grok surface (x-opencodex-grok: 1). Generic clients — third-party agents, Claude-Code-compatible tools, curl — still get no synthesis, so they pay full price on every turn.
Reproduction
Two identical requests, 5 s apart (<PREFIX> ≈ a stable ~3300-token instructions string):
curl -s http://127.0.0.1:10100/v1/responses \
-H 'content-type: application/json' \
-H 'authorization: Bearer <loopback-key>' \
-d '{"model":"gpt-6.1-sol","max_output_tokens":32,
"instructions":"<PREFIX>",
"input":[{"role":"user","content":[{"type":"input_text","text":"hi"}]}]}' \
| jq '.usage.input_tokens_details' # both times: {"cache_write_tokens":0,"cached_tokens":0}
# now add the two Codex identity headers to the same pair of requests:
# -H 'originator: Codex Desktop' -H 'session_id: <stable-id>'
# → request 2: cached_tokens ≈ 93–97% of inputSuggested fix
Extend the #6482 treatment beyond the Grok surface: on the OpenAI/ChatGPT route, when the caller sends no session_id header, synthesize a stable one from identity the proxy already computes — e.g. the inbound x-session-id header, or the affinity key from anthropicSessionKeyFromParts in src/oauth/anthropic-routing.ts — and synthesize an originator when absent. That would bring third-party clients to parity with Codex clients.
Workaround in use
A ~40-line local header-injection shim (listens on 127.0.0.1:10101, forwards to :10100, adds originator: Codex Desktop and derives session_id from the client's x-session-id), with the third-party client's baseUrl pointed at the shim. Verified end-to-end: 3968/4101 cached (96.7%) on the second request. Happy to turn the synthesis into a PR if the approach sounds right.
Related: #6481 / #6482 (Grok surface), #4245 (Cursor), #3433 (Hermes).
Activity
github-actions commentedon Oct 3, 2026
Issue reopened
The report now contains the information required by the automated check. Thanks for updating it.
lidge-jun commentedon Oct 4, 2026
Partial implementation landed on dev via #6554, commit f85925d.
For
/v1/responsesand/v1/messages, a safe caller-providedx-session-idis now promoted when no stronger explicit session identity exists. Authenticated identities are scoped by the admitted principal; loopback caller markers retain their value. Forwarding, precedence, admission, body/abort preservation and isolation have regression coverage, and exact-head hosted CI passed before integration.Keeping this issue open for the broader acceptance in the report: this change does not synthesize an
originator, does not invent a stable identity for a caller that supplies none, and has not independently reproduced the reported live cache ratio on the affected third-party/plain-curl flows. The original PR's live measurements remain source-author evidence, not a new deployed-runtime result. No installed runtime was changed during integration.lidge-jun commentedon Oct 8, 2026
Triage on current
dev(a12bb1f).Already addressed (#6554, #6588): on
/v1/responsesand/v1/messages, a safe caller-providedx-session-idis promoted tosession_idwhen no stronger identity exists, andsession-id/thread-idaliases are normalized at ChatGPT egress. Explicitsession_idand bodyprompt_cache_keyare preserved.What remains is a product decision rather than a defect:
originatoreither; doing so would contradict the documented non-impersonation contract.x-session-idpromotion that Responses has.Your A/B changed
session_idandoriginatortogether, so we do not yet know which one the backend needs for cache reuse. A sanitized four-way comparison on currentdevwould settle it: neither header, session only, originator only, and both, with the same stable prefix andprompt_cache_key, one pinned account, the exact version, and the raw upstreamusage.chano-gpt commentedon Oct 10, 2026
Four-way A/B requested in the triage above, on opencodex 2.81.0 (npm), Linux,
/v1/responses, loopback ingress, modelgpt-6-astra--fast.Setup: synthetic ~9.6k-token
instructionsprefix with a fresh random nonce per variant (so every variant starts cold), a fixed bodyprompt_cache_keyper variant,stream: true, 4 identical requests 3 s apart. Values arecached_tokens / input_tokensfrom the upstreamresponse.completedusage. OpenAI pool: 4 Codex accounts, oneselected, auto-switch not touched during the run.session_idonlyoriginatoronlySo
session_idalone is sufficient (98.3% reuse from request 2);originatoris not needed and does not help on its own. A stable bodyprompt_cache_keywithoutsession_idgets no reuse.Real-client impact, from oh-my-pi (
omp18.8.7) configured with a custom provider pointing at opencodex: omp sends a stable per-sessionprompt_cache_keybut only attaches thesession_idheader when its provider id is literallyopenai, so through a provider namedopencodexevery GPT turn in a 5-turn session measured 3.8% cached overall (74,945 of 88,086 cacheable tokens missed). Adding a per-processsession_idheader on the client side brought in-session requests to 91–98% cached.Given the non-impersonation contract, a narrow option that does not invent identity: on loopback ingress, when no
session_id/session-id/thread-id/x-session-idis present, promote a safe bodyprompt_cache_keytosession_id(or document that non-Codex clients must sendsession_id/x-session-idthemselves, with this table as the reason).Repro script (Python stdlib only)
fix(chat): promote caller x-session-id on Chat Completions ingress (#…
lidge-jun commentedon Oct 10, 2026
Fixed on
devby #6880 (squash0025374d7b). A caller-suppliedx-session-idon Chat Completions ingress is now promoted to the upstream session identity. Non-Codex clients that send one get prompt-cache reuse on ChatGPT-backed models.