Skip to content

[WRONG BRANCH] fix: stabilize cache inputs - #625

Closed
jgautheron wants to merge 33 commits into
lidge-jun:mainfrom
jgautheron:fix/gpt-codex-cache-stability-pr
Closed

[WRONG BRANCH] fix: stabilize cache inputs#625
jgautheron wants to merge 33 commits into
lidge-jun:mainfrom
jgautheron:fix/gpt-codex-cache-stability-pr

Conversation

@jgautheron

@jgautheron jgautheron commented Jul 28, 2026

Copy link
Copy Markdown

No description provided.

…d routes too

Claude Code's Messages API never sets previousResponseId, so the
forward-only guard around repairOversizedReplayCallIds never fired on
custom apiKey-auth providers. Long tool-chains hit a 400 'Invalid
input[N].call_id: string too long' once replayed call_id values exceed
the Responses API's 64-char limit.

Broaden the guard to !unexpandedMiss: still covers forward mode and
already-expanded replays, plus any request with no previousResponseId
at all (every Claude-surfaced request), while still excluding the one
risky case — a raw unexpanded native-Codex continuation that might
reference call ids stored upstream.
…on-scoped

OpenAI's Responses API caching is routed by prompt_cache_key, not pure
byte-prefix matching. The session-scoped key (hash of Claude Code's
metadata.user_id) meant every NEW Claude Code session got a fresh key,
so a cache warmed by a prior session was unreachable even for
byte-identical system+tools content — a guaranteed cold rewrite on the
first call of every session, compounding heavily with subagent-heavy
workloads that spawn many short-lived sessions.

Flip the priority: try the content-based fallback key first (hash of
resolved model + translated system + translated tools in wire order),
falling back to the session-scoped key only when there's no system
content to fingerprint. Any session can now hit a cache any other
session already warmed.

Session-scoped keying was originally chosen to avoid herding many
different sessions/models/toolsets onto one content-hash key and
burning OpenAI's ~15 RPM per-key routing budget at multi-tenant scale
(audit R1#4/R2#5/R1#10). That collision risk is much smaller for a
single-operator deployment, where the dominant cost is inter-session
cache misses instead.
…s disabled

restart was: stop, then handleEnsure(). handleEnsure() early-returns
without relaunching whenever codexAutoStart is false — that flag only
governs whether opencodex registers itself with the Codex CLI on boot,
an unrelated concern, but it silently swallowed restart too: the proxy
stopped cleanly and simply never came back, with no error.

Swap to handleTrayProxyStart(), already used by the tray's own restart
action and explicitly documented as relaunching unconditionally,
independent of codexAutoStart.
…he_key

Content-based prompt_cache_key (session-boundary fix) makes same-persona
subagents agree on one cache cohort, but two dispatched in the same
message can both miss if neither's cache write has landed before the
other's request goes out. Opt-in leader/follower lease: first request
for an unseen key proceeds immediately: any request sharing that key
while the leader is in flight awaits the leader's release (fired on
first output token, with a finally-block fallback) before dispatching.

Each follower still gets its own real upstream response, only dispatch
timing is delayed, not response sharing (unlike single-flight/
request-coalescing). Gated behind serializeColdSubagentCache, default
false.
Empirically re-confirmed on the real phantomOpenai route (specter-debugger
persona, 30 sequential + 4 concurrent probes): steady-state hit rate is
effectively 100% and survives a 100s idle gap, but 1/4 truly concurrent
requests on a fresh key still missed - validates this fix's premise.
…nreachable

isBareOpenAiFamilyModel claims any bare gpt-/o1-/o3-/o4- prefixed model id
for the native openai (Codex) provider before any other provider's
defaultModel/models[] match is ever consulted. A provider that reuses
one of those bare names (e.g. our phantomOpenai config reusing the
upstream DEFAULT_SUBAGENT_MODELS names) silently loses to native Codex
with no error - just the wrong provider and no prompt-cache benefit.

Model is still reachable via the qualified <provider>/<model> form; this
only surfaces the silent case at startup so it's not discovered by
losing cache hits. Diagnostic only, never changes routing.
…llisions

Same bug class, different table: a bare id whose prefix matches the
hardcoded claude-/llama-/mixtral-/gemma- pattern table routes to
whichever active provider is literally named after that pattern first,
before any provider's own models[] array is ever consulted in
routeModelInternal. A provider that explicitly lists such an id in
models[] loses to the pattern-table provider silently.

Fixed a bug in the extension itself: the pattern-table check was
nested inside the openai-provider early-return guard, so it never ran
when no native openai provider was configured. Caught by adding the
test before assuming it worked.
Repointing restart at handleTrayProxyStart() (this session's earlier
codexAutoStart-no-op fix) traded one silent bug for another: that
action never synced the Grok Build config fence or Codex model list
at all, unlike handleEnsure()'s two branches which both did. Caught by
tests/grok-lifecycle.test.ts asserting the old handleEnsure() wiring —
running the full suite (not just manual ocx restart smoke tests) is
what surfaced it.

runTrayProxyStart() gains an optional onStarted(port) hook, fired once
a live proxy is confirmed whether it was already running or this call
just started it (matching handleEnsure's own live+fresh parity).
handleTrayProxyStart wires it to the same syncModelsToCodex +
syncGrokConfig pair handleEnsure performs, fixing both the CLI restart
and the tray's own restart button (both route through this action).
…ache-key priority

The metadata.user_id-wins test encoded the old priority order that this
session's cache-key-priority fix intentionally inverted: system/content now
wins over metadata.user_id when both are present, with metadata as the
fallback for requests with no system content to fingerprint. Split the old
test into two cases matching the new contract and updated a stale comment
in an already-passing test nearby.
…cing

claude-opus-5 is absent from the jawcode bundle entirely, so pricing
relies on explicit per-provider overlay rows (WP7). Only anthropic/
cursor/kiro had one, so any custom anthropic-adapter gateway (private
proxies, company gateways) showed an em dash for claude-opus-5 in Logs.

Naming each such gateway individually in this table would leak private
provider names into the repo. Instead, findExpectedPriceOverlay now
falls back to a "*" (any-provider) row for the same modelId when no
exact-provider row exists, and one wildcard row covers claude-opus-5
for every unenumerated gateway. Exact-provider rows still take priority
over the wildcard at the same status tier.
usageFromResponsesPayload only read input_tokens/output_tokens/total_tokens,
silently dropping input_tokens_details.cached_tokens and
output_tokens_details.reasoning_tokens. Cache-hit visibility for
openai-responses traffic was always reported as zero regardless of actual
upstream caching. Mirrors the existing openai-chat.ts extraction.
warn-unreachable-bare-model-aliases.test.ts used a fixture provider key
that echoed an internal codename. Renamed to a neutral placeholder;
behavior/assertions unchanged.
@github-actions

Copy link
Copy Markdown

⚠️ Wrong target branch

This pull request currently targets main, but pull requests must target one of dev or dev2-go.

Its title has been prefixed with [WRONG BRANCH].

@jgautheron Please retarget this PR to dev. Most contributions go to dev first; use dev2-go only for scoped Go native-port work. main receives only release promotions. See our Contributing guide for details. Thanks! 🙏

This pull request is being kept as a draft automatically. Once the target branch is corrected, it will be marked ready for review again.

@github-actions github-actions Bot changed the title fix(claude): stabilize GPT/Codex prompt cache [WRONG BRANCH] fix(claude): stabilize GPT/Codex prompt cache Jul 28, 2026
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR canonicalizes Anthropic tools and system reminders, changes prompt-cache and session-ID derivation, adds optional cold-cache serialization, updates tray-proxy restart hooks, introduces routing collision warnings, extends Responses usage parsing, and adds wildcard pricing fallback.

Changes

Prompt cache stabilization and serialization

Layer / File(s) Summary
System and tool order stabilization
src/adapters/anthropic-sort-stabilize.ts, src/claude/inbound.ts
Lines 23-78 add system-reminder normalization and tool sorting; inbound translation applies these transformations at lines 76-81, 237-243, and 350-357.
Content-scoped cache key selection
src/claude/inbound.ts, docs-site/src/content/docs/guides/claude-code.md, tests/claude-inbound.test.ts
Lines 460-497 prioritize system/tool content for cache keys, with metadata fallback only without system content; tests cover canonical ordering and key stability.
Session affinity and cold-key leases
src/types.ts, src/lib/cold-cache-key-lease.ts, src/server/claude-messages.ts, tests/*cache*
A configurable per-key leader/follower lease is added, and Claude message handling derives session IDs from hashed metadata user IDs and releases leases on output or failure.

Tray-proxy lifecycle

Layer / File(s) Summary
Tray-proxy startup callback
src/cli/tray-proxy.ts, src/cli/index.ts
Startup now invokes onStarted for live and newly started proxies; the CLI callback synchronizes Codex models and Grok Build state.
Restart wiring validation
src/cli/index.ts, tests/grok-lifecycle.test.ts
Restart now relaunches through handleTrayProxyStart() after a successful stop, with updated lifecycle assertions.

Routing collision diagnostics

Layer / File(s) Summary
Model collision detection
src/router.ts
New diagnostics warn about bare OpenAI-family aliases and model IDs captured by another provider’s pattern routing.
Startup warning integration
src/server/index.ts, tests/warn-unreachable-bare-model-aliases.test.ts
Server startup invokes the diagnostics, with tests covering configured, disabled, qualified, and pattern-provider cases.

Usage and replay handling

Layer / File(s) Summary
Responses usage and replay behavior
src/adapters/openai-responses.ts
Usage parsing includes cached-input and reasoning-output tokens; oversized replay call-ID repair now runs only when unexpandedMiss is false.
Wildcard pricing overlays
src/usage/expected-prices.ts, tests/usage-cost.test.ts
A wildcard claude-opus-5 overlay is added, with exact-provider lookup preceding wildcard fallback and tests for both paths.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ClaudeMessages
  participant ColdCacheLease
  participant HandleResponses
  ClaudeMessages->>ColdCacheLease: acquire prompt_cache_key lease
  ColdCacheLease-->>ClaudeMessages: leader or follower wait promise
  ClaudeMessages->>HandleResponses: dispatch request
  HandleResponses-->>ClaudeMessages: first output or failure
  ClaudeMessages->>ColdCacheLease: release leader lease
Loading

Possibly related PRs

Suggested labels: bug

Suggested reviewers: ingwannu, wibias, lidge-jun

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main change: stabilizing GPT/Codex prompt-cache behavior in Claude routing.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install timed out. The project may have too many dependencies for the sandbox.


Comment @coderabbitai help to get the list of available commands.

@jgautheron jgautheron closed this Jul 28, 2026
@jgautheron jgautheron changed the title [WRONG BRANCH] fix(claude): stabilize GPT/Codex prompt cache fix: stabilize cache inputs Jul 28, 2026
@github-actions github-actions Bot changed the title fix: stabilize cache inputs [WRONG BRANCH] fix: stabilize cache inputs Jul 28, 2026
@github-actions github-actions Bot added the bug Something isn't working label Jul 28, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs-site/src/content/docs/guides/claude-code.md`:
- Around line 386-389: Update the translated caching sections in the Japanese,
Korean, Russian, and Simplified Chinese Claude Code guides to match the current
behavior described in the native caching section: derive content-scoped
prompt_cache_key values from resolved model, normalized system content, and full
tool schemas, and derive session_id separately only when metadata.user_id is
present for backend affinity. Replace the outdated session-scoped
prompt_cache_key/session_id description while preserving the localized
documentation structure.

In `@src/adapters/anthropic-sort-stabilize.ts`:
- Around line 66-78: Invoke stabilizeSystemAndToolOrder on the native Anthropic
request body inside anthropicNativePassthrough before the body is serialized or
forwarded, ensuring system reminders and tools are normalized for cache
stability. Keep the existing passthrough behavior otherwise unchanged.

In `@src/adapters/openai-responses.ts`:
- Around line 663-670: Add a focused regression test in
openai-responses-passthrough.test.ts that exercises usageFromResponsesPayload
through the adapter and verifies the emitted done.usage includes
cachedInputTokens from input_tokens_details.cached_tokens and
reasoningOutputTokens from output_tokens_details.reasoning_tokens. Use a payload
containing both nested fields and assert both mapped values to guard against
parser field-name or event-shape regressions.

In `@src/cli/tray-proxy.ts`:
- Around line 17-24: Update the onStarted callback contract in
src/cli/tray-proxy.ts:17-24 to carry the live proxy hostname or TrayProxyLive
object, pass that value from both the already-running path at
src/cli/tray-proxy.ts:29-32 and fresh-start path at src/cli/tray-proxy.ts:46-52,
then use the propagated hostname instead of config.hostname when invoking
syncGrokConfig in src/cli/index.ts:409-421.

In `@src/router.ts`:
- Around line 349-359: The collision check in warnUnreachablePatternTableModels
currently excludes every active provider’s defaultModel; instead, exclude only
the inspected provider’s own defaultModel while evaluating each prov.models
entry. In tests/warn-unreachable-bare-model-aliases.test.ts lines 97-102, update
the existing expectation to require a warning for cursor and add a separate
regression test confirming the inspected provider’s own default model is
excluded.

In `@src/server/claude-messages.ts`:
- Around line 676-689: Scope the cold-key lease acquisition in the current
request flow to nativeRoute in addition to serializeColdSubagentCache and a
string prompt_cache_key. Update the condition around acquireColdCacheKeyLease so
followers wait only for native routes, while preserving the existing leader
release and wait behavior.
- Around line 701-717: Update the cold-key lease handling around onFirstOutput
and the handleResponses try/finally flow: track whether onFirstOutput has
executed, set that guard before releasing the lease in the callback, and only
perform the finally fallback release when no first output occurred. Preserve
release behavior for error or abort paths while keeping the lease until the
first output callback runs.

In `@src/usage/expected-prices.ts`:
- Around line 148-154: Update the overlay lookup so any existing exact
provider/model entry takes precedence: after computing exact and its
verified/verified-derived match, return the exact result even when it is
undefined, preventing wildcard fallback when exact rows are only unverified.
Only evaluate wildcard overlays when exact.length is zero, and add a regression
test for an unverified exact row alongside a verified wildcard row.

In `@tests/grok-lifecycle.test.ts`:
- Around line 90-95: Replace the CLI source-text assertions around the restart
case with focused Bun behavioral tests for runTrayProxyStart. Use fake
TrayProxyStartIo implementations to cover already-live and fresh-start paths,
asserting that onStarted is invoked in the correct path with the expected
arguments and metadata, and that restart produces the expected result.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 884bfb97-e49e-4bdb-8c22-4738be32e93f

📥 Commits

Reviewing files that changed from the base of the PR and between 7cb15bf and 9e89c41.

📒 Files selected for processing (18)
  • docs-site/src/content/docs/guides/claude-code.md
  • src/adapters/anthropic-sort-stabilize.ts
  • src/adapters/openai-responses.ts
  • src/claude/inbound.ts
  • src/cli/index.ts
  • src/cli/tray-proxy.ts
  • src/lib/cold-cache-key-lease.ts
  • src/router.ts
  • src/server/claude-messages.ts
  • src/server/index.ts
  • src/types.ts
  • src/usage/expected-prices.ts
  • tests/claude-inbound.test.ts
  • tests/claude-messages-endpoint.test.ts
  • tests/cold-cache-key-lease.test.ts
  • tests/grok-lifecycle.test.ts
  • tests/usage-cost.test.ts
  • tests/warn-unreachable-bare-model-aliases.test.ts

Comment on lines +386 to +389
**Native OpenAI/ChatGPT routing:** derives a content-scoped `prompt_cache_key` from the
resolved model, normalized system content, and full tool schemas. When `metadata.user_id` is
present, it separately derives a per-session `session_id` header for backend affinity. Tool and
system-reminder listings are canonicalized before either cache input is built.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -i claude-code.md docs-site/src/content/docs | grep -E '/(ja|ko|ru|zh-cn)/'

Repository: lidge-jun/opencodex

Length of output: 368


🏁 Script executed:

#!/bin/bash
set -euo pipefail

for f in \
  docs-site/src/content/docs/ja/guides/claude-code.md \
  docs-site/src/content/docs/ko/guides/claude-code.md \
  docs-site/src/content/docs/ru/guides/claude-code.md \
  docs-site/src/content/docs/zh-cn/guides/claude-code.md \
  docs-site/src/content/docs/guides/claude-code.md
do
  echo "===== $f ====="
  # show the caching section area if present
  rg -n -C 3 'prompt_cache_key|session_id|session-scoped|content-scoped|cache' "$f" || true
  echo
done

Repository: lidge-jun/opencodex

Length of output: 8058


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
files = [
    Path('docs-site/src/content/docs/ja/guides/claude-code.md'),
    Path('docs-site/src/content/docs/ko/guides/claude-code.md'),
    Path('docs-site/src/content/docs/ru/guides/claude-code.md'),
    Path('docs-site/src/content/docs/zh-cn/guides/claude-code.md'),
]
for f in files:
    txt = f.read_text(encoding='utf-8')
    print(f"\n===== {f} =====")
    for needle in ['session-scoped', 'content-scoped', 'prompt_cache_key', 'session_id', 'normalized system content', 'tool schemas']:
        if needle in txt:
            print(f"contains: {needle}")
    # print the surrounding paragraph for prompt_cache_key if present
    idx = txt.find('prompt_cache_key')
    if idx != -1:
        start = max(0, txt.rfind('\n\n', 0, idx))
        end = txt.find('\n\n', idx)
        if end == -1:
            end = len(txt)
        print(txt[start:end].strip())
PY

Repository: lidge-jun/opencodex

Length of output: 1353


Update the translated caching section in the locale docs. docs-site/src/content/docs/ja/guides/claude-code.md:300-302, docs-site/src/content/docs/ko/guides/claude-code.md:337-339, docs-site/src/content/docs/ru/guides/claude-code.md:317-319, and docs-site/src/content/docs/zh-cn/guides/claude-code.md:295-297 still describe the old session-scoped prompt_cache_key / session_id model. Please align them with docs-site/src/content/docs/guides/claude-code.md:386-389, which now uses a content-scoped prompt_cache_key and only derives session_id separately for backend affinity when metadata.user_id is present.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs-site/src/content/docs/guides/claude-code.md` around lines 386 - 389,
Update the translated caching sections in the Japanese, Korean, Russian, and
Simplified Chinese Claude Code guides to match the current behavior described in
the native caching section: derive content-scoped prompt_cache_key values from
resolved model, normalized system content, and full tool schemas, and derive
session_id separately only when metadata.user_id is present for backend
affinity. Replace the outdated session-scoped prompt_cache_key/session_id
description while preserving the localized documentation structure.

Source: Path instructions

Comment on lines +66 to +78
export function stabilizeSystemAndToolOrder(body: Rec): void {
if (Array.isArray(body.system)) {
const system = body.system as unknown[];
for (let i = 0; i < system.length; i++) {
const block = system[i];
if (!isRec(block) || block.type !== "text" || typeof block.text !== "string") continue;
const normalized = normalizeSystemReminderText(block.text);
if (normalized !== block.text) system[i] = { ...block, text: normalized };
}
}

if (Array.isArray(body.tools)) sortToolsByName(body.tools as unknown[]);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "stabilizeSystemAndToolOrder" -C3

Repository: lidge-jun/opencodex

Length of output: 157


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## Files mentioning anthropic-sort-stabilize or related helpers\n'
rg -n "stabilizeSystemAndToolOrder|normalizeSystemReminderText|sortToolsByName|anthropicNativePassthrough|wantsNativePassthrough" src -C 2 || true

printf '\n## Candidate file outline\n'
ast-grep outline src/adapters/anthropic-sort-stabilize.ts || true

printf '\n## Relevant slices\n'
sed -n '1,220p' src/adapters/anthropic-sort-stabilize.ts

Repository: lidge-jun/opencodex

Length of output: 11231


Call stabilizeSystemAndToolOrder from the native passthrough path. src/adapters/anthropic-sort-stabilize.ts:66-78 exports the cache-stabilizing helper, but there is no call site anywhere in the repo, and src/server/claude-messages.ts:291, 573-574, 856-857 forwards the raw body into anthropicNativePassthrough(...). That means the system/tool ordering normalization never runs for native Anthropic requests, so byte-identical conversations can still miss the prompt cache. Invoke it before serialization in anthropicNativePassthrough, or drop the export/docstring if the passthrough is meant to stay unchanged.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/adapters/anthropic-sort-stabilize.ts` around lines 66 - 78, Invoke
stabilizeSystemAndToolOrder on the native Anthropic request body inside
anthropicNativePassthrough before the body is serialized or forwarded, ensuring
system reminders and tools are normalized for cache stability. Keep the existing
passthrough behavior otherwise unchanged.

Comment on lines +663 to +670
const inputDetails = isPlainObject(usage.input_tokens_details) ? usage.input_tokens_details : undefined;
const outputDetails = isPlainObject(usage.output_tokens_details) ? usage.output_tokens_details : undefined;
return {
inputTokens,
outputTokens,
...(typeof usage.total_tokens === "number" ? { totalTokens: usage.total_tokens } : {}),
...(typeof inputDetails?.cached_tokens === "number" ? { cachedInputTokens: inputDetails.cached_tokens } : {}),
...(typeof outputDetails?.reasoning_tokens === "number" ? { reasoningOutputTokens: outputDetails.reasoning_tokens } : {}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add a parser-level regression test for the new token details.

usageFromResponsesPayload now maps input_tokens_details.cached_tokens and output_tokens_details.reasoning_tokens, but the supplied test changes do not exercise this adapter parser. A field-name or event-shape regression could silently drop cached and reasoning usage before aggregation. Add a focused test in tests/openai-responses-passthrough.test.ts asserting both fields on the emitted done.usage.

As per path instructions, adapter behavior changes under src/** should have a focused regression test under tests/.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/adapters/openai-responses.ts` around lines 663 - 670, Add a focused
regression test in openai-responses-passthrough.test.ts that exercises
usageFromResponsesPayload through the adapter and verifies the emitted
done.usage includes cachedInputTokens from input_tokens_details.cached_tokens
and reasoningOutputTokens from output_tokens_details.reasoning_tokens. Use a
payload containing both nested fields and assert both mapped values to guard
against parser field-name or event-shape regressions.

Source: Path instructions

Comment thread src/cli/tray-proxy.ts
Comment on lines +17 to +24
/**
* Called once a live proxy is confirmed on `port` — whether it was already running or
* this call just started it. `handleEnsure` performs the Grok-fence and Codex-model
* sync inline in both of its branches; this action skipped both entirely, so `ocx restart`
* silently lost them once restart was repointed at this action. Optional so tests that
* don't care about the sync can omit it.
*/
onStarted?: (port: number) => void | Promise<void>;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Propagate the actual proxy hostname through startup callbacks.

The callback contract discards TrayProxyLive.hostname, causing Grok synchronization to use potentially stale config.hostname.

  • src/cli/tray-proxy.ts#L17-L24: extend onStarted to receive the hostname or full TrayProxyLive.
  • src/cli/tray-proxy.ts#L29-L32: pass the live proxy’s hostname on the already-running path.
  • src/cli/tray-proxy.ts#L46-L52: pass the started proxy’s hostname after a fresh start.
  • src/cli/index.ts#L409-L421: use the propagated hostname when calling syncGrokConfig.
📍 Affects 2 files
  • src/cli/tray-proxy.ts#L17-L24 (this comment)
  • src/cli/tray-proxy.ts#L29-L32
  • src/cli/tray-proxy.ts#L46-L52
  • src/cli/index.ts#L409-L421
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/tray-proxy.ts` around lines 17 - 24, Update the onStarted callback
contract in src/cli/tray-proxy.ts:17-24 to carry the live proxy hostname or
TrayProxyLive object, pass that value from both the already-running path at
src/cli/tray-proxy.ts:29-32 and fresh-start path at src/cli/tray-proxy.ts:46-52,
then use the propagated hostname instead of config.hostname when invoking
syncGrokConfig in src/cli/index.ts:409-421.

Comment thread src/router.ts
Comment on lines +349 to +359
function warnUnreachablePatternTableModels(config: OcxConfig): void {
const active = activeProviderEntries(config);
const defaultModels = new Set(active.map(([, prov]) => prov.defaultModel).filter((m): m is string => typeof m === "string"));
for (const [name, prov] of active) {
if (!Array.isArray(prov.models)) continue;
for (const modelId of prov.models) {
if (typeof modelId !== "string" || modelId.includes("/") || defaultModels.has(modelId)) continue;
for (const { providerNames, prefixes } of MODEL_PROVIDER_PATTERNS) {
if (!prefixes.some(prefix => modelId.startsWith(prefix))) continue;
const patternProvider = active.find(([n]) => providerNames.some(pn => n === pn || n.startsWith(`${pn}-`)));
if (patternProvider && patternProvider[0] !== name) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep provider-specific collision detection consistent.

A bare model being reachable through another provider does not make it reachable through the provider whose models[] lists it. Default-model precedence therefore requires both the router diagnostic and its regression test to treat this as a collision.

  • src/router.ts#L349-L359: exclude only the inspected provider’s own defaultModel, not every active provider’s default model.
  • tests/warn-unreachable-bare-model-aliases.test.ts#L97-L102: expect a warning for cursor and add a separate test covering the provider’s own default model.
📍 Affects 2 files
  • src/router.ts#L349-L359 (this comment)
  • tests/warn-unreachable-bare-model-aliases.test.ts#L97-L102
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/router.ts` around lines 349 - 359, The collision check in
warnUnreachablePatternTableModels currently excludes every active provider’s
defaultModel; instead, exclude only the inspected provider’s own defaultModel
while evaluating each prov.models entry. In
tests/warn-unreachable-bare-model-aliases.test.ts lines 97-102, update the
existing expectation to require a warning for cursor and add a separate
regression test confirming the inspected provider’s own default model is
excluded.

Comment on lines +676 to +689
// Cold-key lease (opt-in): a parallel Task-tool fan-out that dispatches several
// requests sharing the SAME content-based
// prompt_cache_key races all of them before any has written its cache entry, so
// every one misses even though they'd agree on the same key. The leader (first
// arrival for this key) proceeds immediately; followers wait for the leader's
// first output token before dispatching, so they land on a warm cache instead of
// racing a cold one. See src/lib/cold-cache-key-lease.ts.
let coldKeyRelease: (() => void) | undefined;
if (config.claudeCode?.serializeColdSubagentCache && typeof internalBody.prompt_cache_key === "string") {
const { acquireColdCacheKeyLease } = await import("../lib/cold-cache-key-lease");
const lease = acquireColdCacheKeyLease(internalBody.prompt_cache_key);
if (lease.waitForLeader) await lease.waitForLeader;
else coldKeyRelease = lease.release;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Cold-key lease should be scoped to nativeRoute, not applied to every provider.

The lease acquisition at Lines 684-689 only checks config.claudeCode?.serializeColdSubagentCache and internalBody.prompt_cache_key. prompt_cache_key is set on internalBody whenever systemParts.length > 0 (essentially always), regardless of which adapter the request eventually routes to — it's never stripped for non-native routes (unlike max_output_tokens/temperature/top_p/stop/user, which are deleted only for route.provider.adapter === "openai-responses" at Lines 600-607). Since the whole point of this feature (per src/lib/cold-cache-key-lease.ts's docstring) is coordinating OpenAI's prompt_cache_key cold-cache race, gating on serializeColdSubagentCache alone means a user who opts in gets follower-wait latency injected on every routed (non-OpenAI) Task-tool fan-out too, with no cache benefit to show for it. nativeRoute is already computed and in scope by this point (Lines 594-630).

🔧 Proposed fix
   let coldKeyRelease: (() => void) | undefined;
-  if (config.claudeCode?.serializeColdSubagentCache && typeof internalBody.prompt_cache_key === "string") {
+  if (nativeRoute && config.claudeCode?.serializeColdSubagentCache && typeof internalBody.prompt_cache_key === "string") {
     const { acquireColdCacheKeyLease } = await import("../lib/cold-cache-key-lease");
     const lease = acquireColdCacheKeyLease(internalBody.prompt_cache_key);
     if (lease.waitForLeader) await lease.waitForLeader;
     else coldKeyRelease = lease.release;
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Cold-key lease (opt-in): a parallel Task-tool fan-out that dispatches several
// requests sharing the SAME content-based
// prompt_cache_key races all of them before any has written its cache entry, so
// every one misses even though they'd agree on the same key. The leader (first
// arrival for this key) proceeds immediately; followers wait for the leader's
// first output token before dispatching, so they land on a warm cache instead of
// racing a cold one. See src/lib/cold-cache-key-lease.ts.
let coldKeyRelease: (() => void) | undefined;
if (config.claudeCode?.serializeColdSubagentCache && typeof internalBody.prompt_cache_key === "string") {
const { acquireColdCacheKeyLease } = await import("../lib/cold-cache-key-lease");
const lease = acquireColdCacheKeyLease(internalBody.prompt_cache_key);
if (lease.waitForLeader) await lease.waitForLeader;
else coldKeyRelease = lease.release;
}
// Cold-key lease (opt-in): a parallel Task-tool fan-out that dispatches several
// requests sharing the SAME content-based
// prompt_cache_key races all of them before any has written its cache entry, so
// every one misses even though they'd agree on the same key. The leader (first
// arrival for this key) proceeds immediately; followers wait for the leader's
// first output token before dispatching, so they land on a warm cache instead of
// racing a cold one. See src/lib/cold-cache-key-lease.ts.
let coldKeyRelease: (() => void) | undefined;
if (nativeRoute && config.claudeCode?.serializeColdSubagentCache && typeof internalBody.prompt_cache_key === "string") {
const { acquireColdCacheKeyLease } = await import("../lib/cold-cache-key-lease");
const lease = acquireColdCacheKeyLease(internalBody.prompt_cache_key);
if (lease.waitForLeader) await lease.waitForLeader;
else coldKeyRelease = lease.release;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/claude-messages.ts` around lines 676 - 689, Scope the cold-key
lease acquisition in the current request flow to nativeRoute in addition to
serializeColdSubagentCache and a string prompt_cache_key. Update the condition
around acquireColdCacheKeyLease so followers wait only for native routes, while
preserving the existing leader release and wait behavior.

Comment on lines +701 to +717
const onFirstOutput = () => {
if (logIds) recordFirstOutput(logCtx, logIds.start);
coldKeyRelease?.();
};
let upstream: Response;
try {
upstream = await handleResponses(internalReq, buildClaudeReplayConfig(config), logCtx, {
abortSignal: req.signal,
onFirstOutput,
onNativePassthroughTerminal: status => finalizeNativeLog(httpStatusForTerminalStatus(status), { terminalStatus: status, closeReason: "terminal" }),
onNativePassthroughCancel: () => finalizeNativeLog(499, { closeReason: "client_cancel" }),
});
} finally {
// Fallback release: the leader errored/aborted before any output token, so
// onFirstOutput never fired — followers must not wait forever.
coldKeyRelease?.();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "onFirstOutput" -C6 src/server/responses/core.ts

Repository: lidge-jun/opencodex

Length of output: 5587


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== claude-messages around lines 680-725 =="
sed -n '680,725p' src/server/claude-messages.ts

echo
echo "== core.ts around onFirstOutput plumbing and response creation =="
sed -n '1380,1515p' src/server/responses/core.ts

echo
echo "== core.ts around consumeForResponseLogMetadata =="
sed -n '1515,1605p' src/server/responses/core.ts

Repository: lidge-jun/opencodex

Length of output: 12775


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate helper definitions =="
rg -n "function relaySseEagerBounded|const relaySseEagerBounded|function consumeForResponseLogMetadata|const consumeForResponseLogMetadata|function createSseInspector|const createSseInspector" src/server/responses/core.ts

echo
echo "== inspect the helper implementation boundaries =="
ast-grep outline src/server/responses/core.ts --view expanded

Repository: lidge-jun/opencodex

Length of output: 189


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== handleResponses branch around eager relay =="
sed -n '1400,1495p' src/server/responses/core.ts

echo
echo "== helper definitions for streaming inspection =="
sed -n '220,360p' src/server/responses/core.ts

echo
echo "== search for any awaited consumption in handleResponses =="
rg -n "await .*relaySseEagerBounded|await .*consumeForResponseLogMetadata|return new Response|new Response\\(" src/server/responses/core.ts

Repository: lidge-jun/opencodex

Length of output: 10776


Keep the cold-key lease until the first output callback runs

src/server/claude-messages.ts:701-717 releases coldKeyRelease in finally, but src/server/responses/core.ts:1412-1500 returns a Response immediately after wiring onFirstOutput into the streaming inspector/relay. That means the finally can fire before any body chunk is emitted, so followers stop waiting before the cache is warm.

Keep the fallback release only for the no-output error/abort path, e.g. with a didFirstOutput guard.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/claude-messages.ts` around lines 701 - 717, Update the cold-key
lease handling around onFirstOutput and the handleResponses try/finally flow:
track whether onFirstOutput has executed, set that guard before releasing the
lease in the callback, and only perform the finally fallback release when no
first output occurred. Preserve release behavior for error or abort paths while
keeping the lease until the first output callback runs.

Comment on lines 148 to +154
const exact = overlays.filter(row => row.provider === provider && row.modelId === modelId);
return exact.find(row => row.status === "verified")
const found = exact.find(row => row.status === "verified")
?? exact.find(row => row.status === "verified-derived");
if (found) return found;
const wildcard = overlays.filter(row => row.provider === "*" && row.modelId === modelId);
return wildcard.find(row => row.status === "verified")
?? wildcard.find(row => row.status === "verified-derived");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not let a wildcard bypass an exact unverified overlay.

When an exact {provider, modelId} row exists but is only unverified, found is undefined and the function falls through to the wildcard row. That can silently apply a verified-derived price despite an explicit provider-specific fail-closed entry, contradicting the documented “when no exact provider match exists” behavior.

Return found whenever exact.length > 0; add a regression test covering an unverified exact row plus a verified wildcard row.

Proposed fix
   const found = exact.find(row => row.status === "verified")
     ?? exact.find(row => row.status === "verified-derived");
-  if (found) return found;
+  if (exact.length > 0) return found;

   const wildcard = overlays.filter(row => row.provider === "*" && row.modelId === modelId);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const exact = overlays.filter(row => row.provider === provider && row.modelId === modelId);
return exact.find(row => row.status === "verified")
const found = exact.find(row => row.status === "verified")
?? exact.find(row => row.status === "verified-derived");
if (found) return found;
const wildcard = overlays.filter(row => row.provider === "*" && row.modelId === modelId);
return wildcard.find(row => row.status === "verified")
?? wildcard.find(row => row.status === "verified-derived");
const exact = overlays.filter(row => row.provider === provider && row.modelId === modelId);
const found = exact.find(row => row.status === "verified")
?? exact.find(row => row.status === "verified-derived");
if (exact.length > 0) return found;
const wildcard = overlays.filter(row => row.provider === "*" && row.modelId === modelId);
return wildcard.find(row => row.status === "verified")
?? wildcard.find(row => row.status === "verified-derived");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/usage/expected-prices.ts` around lines 148 - 154, Update the overlay
lookup so any existing exact provider/model entry takes precedence: after
computing exact and its verified/verified-derived match, return the exact result
even when it is undefined, preventing wildcard fallback when exact rows are only
unverified. Only evaluate wildcard overlays when exact.length is zero, and add a
regression test for an unverified exact row alongside a verified wildcard row.

Comment on lines +90 to +95
// handleEnsure() early-returns without relaunching when codexAutoStart is disabled,
// which silently ate `restart` too — handleTrayProxyStart() relaunches
// unconditionally instead.
const restartCase = sliceFn(CLI_SOURCE, 'case "restart"', 'case "health"');
expect(restartCase).toContain("if (await handleStop()) await handleEnsure()");
expect(restartCase).toContain("if (await handleStop()) await handleTrayProxyStart()");
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Exercise the startup behavior instead of inspecting source text.

These assertions can pass while runTrayProxyStart never invokes onStarted, invokes it in the wrong branch, or passes incorrect metadata. Add focused Bun tests with fake TrayProxyStartIo implementations covering both the already-live and fresh-start paths, asserting the callback arguments and restart result.

As per path instructions, tests/** behavior changes should have focused behavioral regression coverage near the subsystem.

Also applies to: 97-111

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/grok-lifecycle.test.ts` around lines 90 - 95, Replace the CLI
source-text assertions around the restart case with focused Bun behavioral tests
for runTrayProxyStart. Use fake TrayProxyStartIo implementations to cover
already-live and fresh-start paths, asserting that onStarted is invoked in the
correct path with the expected arguments and metadata, and that restart produces
the expected result.

Source: Path instructions

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9e89c411dc

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

} finally {
// Fallback release: the leader errored/aborted before any output token, so
// onFirstOutput never fired — followers must not wait forever.
coldKeyRelease?.();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Release cold-cache leases only after stream output

When claudeCode.serializeColdSubagentCache is enabled, this finally releases the leader as soon as handleResponses returns. For Claude Messages, internalBody.stream is forced true and handleResponses constructs an SSE Response before the upstream stream has produced a first output event, so followers arriving in that window stop waiting and dispatch while the cache is still cold. Move the fallback release to an actual stream terminal/error/cancel path, and keep the normal release in onFirstOutput.

Useful? React with 👍 / 👎.

}

/** Sort `body.system` skills/deferred-tools listings and `body.tools` by name, in place. */
export function stabilizeSystemAndToolOrder(body: Rec): void {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Wire the native Anthropic stabilizer into passthrough

This helper is exported for native Anthropic passthrough, but it has no callers in this commit, so anthropicNativePassthrough still serializes the original body unchanged. In native Anthropic mode, Claude Code tool/system-reminder discovery can still arrive in different orders and continue busting the prompt-cache prefix; call this before JSON.stringify(body) on the passthrough path (including count_tokens if counts must match sends).

Useful? React with 👍 / 👎.

Comment on lines +152 to +154
const wildcard = overlays.filter(row => row.provider === "*" && row.modelId === modelId);
return wildcard.find(row => row.status === "verified")
?? wildcard.find(row => row.status === "verified-derived");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restrict wildcard price overlays to matching providers

With this fallback, any unpriced provider that happens to log native model id claude-opus-5 now receives the Anthropic Opus price, even if it is not one of the private Anthropic-compatible gateways described in the comment (for example an OpenRouter/local/company provider with different or zero billing). That turns the estimator from exact-provider/fail-closed into a cross-provider fuzzy price for this model; keep the wildcard scoped by provider/adapter metadata or enumerate only the intended gateways.

Useful? React with 👍 / 👎.

Comment thread src/cli/index.ts
});
try {
const { syncGrokConfig } = await import("../grok/sync");
const g = await syncGrokConfig(port, config, config.hostname ? { hostname: config.hostname } : {});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the live hostname when syncing Grok

When runTrayProxyStart finds an already-running proxy (or a newly started fallback runtime), findLiveProxy()/waitForProxy() can return the hostname that actually answered, but this callback only receives port and then falls back to config.hostname. If the running proxy was discovered from the runtime record or the config hostname has drifted, this rewrites ~/.grok/config.toml to the wrong host; pass the live hostname through the tray start callback and use it like handleEnsure does.

Useful? React with 👍 / 👎.

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

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant