Client or integration
Claude Code
Area
Proxy and routing
Summary
In src/claude/inbound.ts:334-348, OpenCodex translates incoming Claude Messages by iterating through raw.messages. When it encounters msg.role === "system", it appends the message text to systemParts, which is then joined and assigned to body.instructions:
// src/claude/inbound.ts:334-348
else if (msg.role === "system") {
const text = systemMessageText(msg.content);
if (text.length > 0) systemParts.push(text);
}
...
if (systemParts.length > 0) body.instructions = systemParts.join("\n\n");
When clients like Claude Desktop or Claude Code inject dynamic system messages during a multi-turn conversation (e.g., style reminders, date changes, or subagent notices):
- Turn 1:
instructions = [topLevelSystem + reminder_1]
- Turn 2:
instructions = [topLevelSystem + reminder_1 + reminder_2]
Because instructions is placed at the very head of the LLM prompt sequence, mutating instructions on every turn completely invalidates the prompt cache prefix (KV cache) for all subsequent user/assistant/tool message history.
Real-world Impact & Production Evidence
In real-world long-running Claude Desktop sessions (e.g. 100+ turns), this defect causes prompt cache reuse to freeze at the initial system prompt boundary:
- Observed historical degradation:
cachedInputTokens remained strictly frozen at 24,576 tokens across dozens of turns while total input expanded from 60,000 to 137,000 tokens. The cache hit rate collapsed from 40.7% down to 19.4%, resulting in >110,000 un-cached tokens billed at full price on every single turn.
- Controlled mitigation proof: By suppressing dynamic mid-conversation reminders on the client side (
outputStyle: default and totalTokensReminder: off), instructions length growth across turns became exactly 0. In the exact same Claude Desktop 3P harness over 10 turns (70k-92k tokens), cachedInputTokens successfully grew with the conversation (51k -> 70k -> 79k), achieving 96.24% ~ 99.93% cache hit rate.
Reproduction
- Start OpenCodex on port 10100.
- Send a multi-turn Claude Messages request where Turn 1 contains
messages: [{ role: "user", content: "..." }, { role: "system", content: "reminder 1" }].
- Send Turn 2 with
messages: [{ role: "user", ... }, { role: "system", ... }, { role: "assistant", ... }, { role: "user", ... }, { role: "system", content: "reminder 2" }].
- Inspect the translated Responses request
body.instructions:
- Observe that Turn 1
instructions has length L1;
- Turn 2
instructions has length L2 = L1 + delta;
- The top-level instruction header has changed, breaking the prefix match for all preceding conversation history on upstream providers (Anthropic, OpenCode Go, DeepSeek, etc.).
Version
2.48.0
Operating system
macOS 15 (Darwin arm64)
Provider and model
All providers routed through Claude inbound (opencode-go/muse-spark-1.3-contributor, sensenova/deepseek-v4-flash, etc.)
Logs or error output
Turn 1: input=60,153, cached=0 (0.0%)
Turn 2: input=60,444, cached=24,576 (40.7%)
Turn 6: input=65,020, cached=24,576 (37.8%)
Turn 11: input=75,423, cached=24,576 (32.6%)
Turn 21: input=84,487, cached=24,576 (29.1%)
Turn 51: input=105,858, cached=24,576 (23.2%)
Turn 101: input=137,458, cached=26,624 (19.4%)
// Note: cachedInputTokens frozen at 24,576 while input expanded to 137k.
Expected Behavior & Design Consideration
Mid-conversation role: system messages should either:
- Be mapped into the message sequence as chronological input items (e.g.
role: "developer" in Responses input) rather than hoisted into top-level instructions, preserving the immutable prefix; OR
- Provide a deduplication/filter policy so static/identical recurring reminders do not continuously append to
instructions.
Note on downstream adapters: Care must be taken with src/adapters/openai-chat.ts:724, which currently hoists developer items back into systemParts for non-native OpenAI routes.
Checks
Client or integration
Claude Code
Area
Proxy and routing
Summary
In
src/claude/inbound.ts:334-348, OpenCodex translates incoming Claude Messages by iterating throughraw.messages. When it encountersmsg.role === "system", it appends the message text tosystemParts, which is then joined and assigned tobody.instructions:When clients like Claude Desktop or Claude Code inject dynamic system messages during a multi-turn conversation (e.g., style reminders, date changes, or subagent notices):
instructions=[topLevelSystem + reminder_1]instructions=[topLevelSystem + reminder_1 + reminder_2]Because
instructionsis placed at the very head of the LLM prompt sequence, mutatinginstructionson every turn completely invalidates the prompt cache prefix (KV cache) for all subsequent user/assistant/tool message history.Real-world Impact & Production Evidence
In real-world long-running Claude Desktop sessions (e.g. 100+ turns), this defect causes prompt cache reuse to freeze at the initial system prompt boundary:
cachedInputTokensremained strictly frozen at 24,576 tokens across dozens of turns while total input expanded from 60,000 to 137,000 tokens. The cache hit rate collapsed from 40.7% down to 19.4%, resulting in >110,000 un-cached tokens billed at full price on every single turn.outputStyle: defaultandtotalTokensReminder: off),instructionslength growth across turns became exactly 0. In the exact same Claude Desktop 3P harness over 10 turns (70k-92k tokens),cachedInputTokenssuccessfully grew with the conversation (51k -> 70k -> 79k), achieving 96.24% ~ 99.93% cache hit rate.Reproduction
messages: [{ role: "user", content: "..." }, { role: "system", content: "reminder 1" }].messages: [{ role: "user", ... }, { role: "system", ... }, { role: "assistant", ... }, { role: "user", ... }, { role: "system", content: "reminder 2" }].body.instructions:instructionshas length L1;instructionshas length L2 = L1 + delta;Version
2.48.0
Operating system
macOS 15 (Darwin arm64)
Provider and model
All providers routed through Claude inbound (opencode-go/muse-spark-1.3-contributor, sensenova/deepseek-v4-flash, etc.)
Logs or error output
Expected Behavior & Design Consideration
Mid-conversation
role: systemmessages should either:role: "developer"in Responses input) rather than hoisted into top-levelinstructions, preserving the immutable prefix; ORinstructions.Note on downstream adapters: Care must be taken with
src/adapters/openai-chat.ts:724, which currently hoistsdeveloperitems back intosystemPartsfor non-native OpenAI routes.Checks