Skip to content

[Bug]: Non-Codex clients on public endpoints get zero prompt-cache reuse on ChatGPT-backed OpenAI models (no synthesized session_id/originator) #6520

Closed

Description

@mayigululu-hash

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 input

Suggested 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

  1. added
    bugSomething isn't working
    proxyHTTP proxy, routing, reverse-proxy / management auth
    on Oct 3, 2026
  2. github-actions commented on Oct 3, 2026

    @github-actions
    Contributor

    Issue reopened

    The report now contains the information required by the automated check. Thanks for updating it.

  3. lidge-jun commented on Oct 4, 2026

    @lidge-jun
    Owner

    Partial implementation landed on dev via #6554, commit f85925d.

    For /v1/responses and /v1/messages, a safe caller-provided x-session-id is 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.

  4. lidge-jun commented on Oct 8, 2026

    @lidge-jun
    Owner

    Triage on current dev (a12bb1f).

    Already addressed (#6554, #6588): on /v1/responses and /v1/messages, a safe caller-provided x-session-id is promoted to session_id when no stronger identity exists, and session-id / thread-id aliases are normalized at ChatGPT egress. Explicit session_id and body prompt_cache_key are preserved.

    What remains is a product decision rather than a defect:

    • Callers that send no session marker get no invented identity. OpenCodex does not synthesize originator either; doing so would contradict the documented non-impersonation contract.
    • Chat Completions ingress does not yet apply the x-session-id promotion that Responses has.
    • A shared prefix/cache cohort is intentionally never turned into conversation identity.

    Your A/B changed session_id and originator together, so we do not yet know which one the backend needs for cache reuse. A sanitized four-way comparison on current dev would settle it: neither header, session only, originator only, and both, with the same stable prefix and prompt_cache_key, one pinned account, the exact version, and the raw upstream usage.

  5. chano-gpt commented on Oct 10, 2026

    @chano-gpt

    Four-way A/B requested in the triage above, on opencodex 2.81.0 (npm), Linux, /v1/responses, loopback ingress, model gpt-6-astra--fast.

    Setup: synthetic ~9.6k-token instructions prefix with a fresh random nonce per variant (so every variant starts cold), a fixed body prompt_cache_key per variant, stream: true, 4 identical requests 3 s apart. Values are cached_tokens / input_tokens from the upstream response.completed usage. OpenAI pool: 4 Codex accounts, one selected, auto-switch not touched during the run.

    Variant req 1 req 2 req 3 req 4
    neither header 0/9643 0/9643 0/9643 0/9643
    session_id only 0/9638 9472/9638 9472/9638 9472/9638
    originator only 0/9643 0/9643 0/9643 9472/9643
    both 0/9645 9472/9645 9472/9645 9472/9645

    So session_id alone is sufficient (98.3% reuse from request 2); originator is not needed and does not help on its own. A stable body prompt_cache_key without session_id gets no reuse.

    Real-client impact, from oh-my-pi (omp 18.8.7) configured with a custom provider pointing at opencodex: omp sends a stable per-session prompt_cache_key but only attaches the session_id header when its provider id is literally openai, so through a provider named opencodex every GPT turn in a 5-turn session measured 3.8% cached overall (74,945 of 88,086 cacheable tokens missed). Adding a per-process session_id header 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-id is present, promote a safe body prompt_cache_key to session_id (or document that non-Codex clients must send session_id/x-session-id themselves, with this table as the reason).

    Repro script (Python stdlib only)
    import json, time, uuid, urllib.request
    URL = "http://127.0.0.1:10100/v1/responses"
    BASE = "You are a careful assistant. " + " ".join(
        f"Rule {i}: when handling input category {i}, prefer explicit, verifiable steps and cite the relevant section number." for i in range(400))
    
    def send(prefix, pck, headers):
        body = {"model": "gpt-6-astra--fast", "instructions": prefix, "prompt_cache_key": pck,
                "input": [{"role": "user", "content": "Reply with just: ok"}],
                "stream": True, "store": False, "max_output_tokens": 64, "reasoning": {"effort": "low"}}
        h = {"content-type": "application/json", "authorization": "Bearer <loopback-key>", **headers}
        usage = None
        with urllib.request.urlopen(urllib.request.Request(URL, data=json.dumps(body).encode(), headers=h), timeout=120) as r:
            for line in r:
                line = line.decode().strip()
                if line.startswith("data:") and '"response.completed"' in line:
                    usage = json.loads(line[5:])["response"]["usage"]
        return usage["input_tokens"], usage.get("input_tokens_details", {}).get("cached_tokens", 0)
    
    variants = {"neither": lambda s: {}, "session_id only": lambda s: {"session_id": s},
                "originator only": lambda s: {"originator": "codex_cli_rs"},
                "both": lambda s: {"session_id": s, "originator": "codex_cli_rs"}}
    for name, mk in variants.items():
        prefix, pck, sid = f"[{uuid.uuid4().hex}] " + BASE, str(uuid.uuid4()), str(uuid.uuid4())
        res = []
        for _ in range(4):
            res.append(send(prefix, pck, mk(sid))); time.sleep(3)
        print(f"{name:16s}", " ".join(f"{c}/{t}" for t, c in res))
  6. added a commit that references this issue on Oct 10, 2026
    0025374
  7. lidge-jun commented on Oct 10, 2026

    @lidge-jun
    Owner

    Fixed on dev by #6880 (squash 0025374d7b). A caller-supplied x-session-id on 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingproxyHTTP proxy, routing, reverse-proxy / management auth

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions