-
Notifications
You must be signed in to change notification settings - Fork 1.1k
[WRONG BRANCH] fix: stabilize cache inputs #625
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3a0b88f
41deeef
23ec63f
ad58ac8
0215404
44d0294
152682b
627e43c
5823712
4430ed3
d9a2a55
9ce0464
00b3861
ff0596d
e20c21e
8298112
b001af9
7b0fb67
ba2085b
2f3a7dd
b4f95ae
bae00dd
4da468a
479390e
a0972c2
11cdee2
d637087
3af6d4a
bfda29d
d99a373
b8ddda0
8cc4747
9e89c41
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| /** | ||
| * Deterministic ordering of Claude Code's own tool/skills/deferred-tools listings on | ||
| * the native Anthropic passthrough (`anthropicNativePassthrough`, `claude-messages.ts`). | ||
| * Claude Code enumerates MCP tools/skills in whatever order its own reconnect/discovery | ||
| * race resolves them, so byte-identical conversations can arrive with different array | ||
| * order turn to turn — busting Anthropic's prompt-cache prefix for no reason. Sorting is | ||
| * safe: `tool_choice` targets tools by name, not position, and the two system-reminder | ||
| * blocks below are pure listings with no inherent order the model depends on. | ||
| * | ||
| * Ported (algorithm only, re-implemented in TypeScript) from `sort-stabilization.mjs`, | ||
| * MIT licensed, github.com/cnighswonger/claude-code-cache-fix. | ||
| */ | ||
|
|
||
| type Rec = Record<string, unknown>; | ||
|
|
||
| function isRec(v: unknown): v is Rec { | ||
| return !!v && typeof v === "object" && !Array.isArray(v); | ||
| } | ||
|
|
||
| const SKILLS_BLOCK_RE = /^([\s\S]*?\n\n)(- [\s\S]+?)(\n<\/system-reminder>\s*)$/; | ||
| const DEFERRED_TOOLS_BLOCK_RE = /^(<system-reminder>\nThe following deferred tools are now available[^\n]*\n)([\s\S]+?)(\n<\/system-reminder>\s*)$/; | ||
|
|
||
| export function isSkillsBlockText(text: unknown): text is string { | ||
| return typeof text === "string" && text.includes("User-invocable skills"); | ||
| } | ||
|
|
||
| export function isDeferredToolsBlockText(text: unknown): text is string { | ||
| return typeof text === "string" && text.includes("deferred tools are now available"); | ||
| } | ||
|
|
||
| export function sortSkillsBlockText(text: string): string { | ||
| const match = text.match(SKILLS_BLOCK_RE); | ||
| if (!match) return text; | ||
| const [, header, entriesText, footer] = match; | ||
| const entries = entriesText.split(/\n(?=- )/); | ||
| entries.sort(); | ||
| return header + entries.join("\n") + footer; | ||
| } | ||
|
|
||
| export function sortDeferredToolsBlockText(text: string): string { | ||
| const match = text.match(DEFERRED_TOOLS_BLOCK_RE); | ||
| if (!match) return text; | ||
| const [, header, toolsList, footer] = match; | ||
| const tools = toolsList.split("\n").map(t => t.trim()).filter(Boolean); | ||
| tools.sort(); | ||
| return header + tools.join("\n") + footer; | ||
| } | ||
|
|
||
| /** Normalize known system-reminder listings; leave all other instructions untouched. */ | ||
| export function normalizeSystemReminderText(text: string): string { | ||
| if (isSkillsBlockText(text)) return sortSkillsBlockText(text); | ||
| if (isDeferredToolsBlockText(text)) return sortDeferredToolsBlockText(text); | ||
| return text; | ||
| } | ||
|
|
||
| /** Sort tool definitions by name; unnamed server tools sort first. */ | ||
| export function sortToolsByName(tools: unknown[]): void { | ||
| tools.sort((a, b) => { | ||
| const nameA = isRec(a) && typeof a.name === "string" ? a.name : ""; | ||
| const nameB = isRec(b) && typeof b.name === "string" ? b.name : ""; | ||
| return nameA.localeCompare(nameB); | ||
| }); | ||
| } | ||
|
|
||
| /** Sort `body.system` skills/deferred-tools listings and `body.tools` by name, in place. */ | ||
| export function stabilizeSystemAndToolOrder(body: Rec): void { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This helper is exported for native Anthropic passthrough, but it has no callers in this commit, so Useful? React with 👍 / 👎. |
||
| 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[]); | ||
| } | ||
|
Comment on lines
+66
to
+78
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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" -C3Repository: 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.tsRepository: lidge-jun/opencodex Length of output: 11231 Call 🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -660,10 +660,14 @@ function usageFromResponsesPayload(payload: unknown): OcxUsage | undefined { | |
| const inputTokens = typeof usage.input_tokens === "number" ? usage.input_tokens : 0; | ||
| const outputTokens = typeof usage.output_tokens === "number" ? usage.output_tokens : 0; | ||
| if (inputTokens === 0 && outputTokens === 0) return undefined; | ||
| 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 } : {}), | ||
|
Comment on lines
+663
to
+670
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
As per path instructions, adapter behavior changes under 🤖 Prompt for AI AgentsSource: Path instructions |
||
| }; | ||
| } | ||
|
|
||
|
|
@@ -741,7 +745,16 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig): | |
| outBody = stripUnsupportedForwardParams(outBody); | ||
| } | ||
| else outBody = stripConflictingHostedTools(outBody); | ||
| if (forward || parsed._previousResponseInputExpanded === true) { | ||
| // Claude Code's Messages API never sets previousResponseId at all, so on a | ||
| // custom apiKey-auth provider (non-forward) the original forward-only guard | ||
| // never fired, and long tool-chains hit "Invalid 'input[N].call_id': string | ||
| // too long" (call_id replay can exceed the Responses API's 64-char limit). | ||
| // !unexpandedMiss is a safe superset of the old condition: it still covers | ||
| // forward mode and already-expanded replays, but also covers any request with | ||
| // no previousResponseId at all (every Claude-surfaced request) — while still | ||
| // excluding the one genuinely risky case, a raw unexpanded native-Codex | ||
| // continuation that might reference call ids stored upstream. | ||
| if (!unexpandedMiss) { | ||
| outBody = repairOversizedReplayCallIds(outBody); | ||
| } | ||
| outBody = stripUnsupportedReasoningSummaryDelivery(outBody, parsed.modelId); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -387,6 +387,7 @@ async function handleEnsure() { | |
|
|
||
| /** Fixed tray action: start the proxy without depending on codexAutoStart. */ | ||
| async function handleTrayProxyStart(): Promise<void> { | ||
| const config = loadConfig(); | ||
| const ok = await runTrayProxyStart({ | ||
| findLive: findLiveProxy, | ||
| diagnoseService: () => { | ||
|
|
@@ -395,7 +396,6 @@ async function handleTrayProxyStart(): Promise<void> { | |
| }, | ||
| startService: () => serviceCommand("start"), | ||
| startDirect: () => { | ||
| const config = loadConfig(); | ||
| const port = (config.port ?? 10100) > 0 ? (config.port ?? 10100) : 10100; | ||
| const child = spawn(process.execPath, startArgv(port), { | ||
| detached: true, | ||
|
|
@@ -406,6 +406,19 @@ async function handleTrayProxyStart(): Promise<void> { | |
| child.unref(); | ||
| }, | ||
| waitForProxy, | ||
| onStarted: async port => { | ||
| await syncModelsToCodex(port).catch(e => { | ||
| console.error(`⚠️ Model sync skipped: ${e instanceof Error ? e.message : String(e)}`); | ||
| }); | ||
| try { | ||
| const { syncGrokConfig } = await import("../grok/sync"); | ||
| const g = await syncGrokConfig(port, config, config.hostname ? { hostname: config.hostname } : {}); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Useful? React with 👍 / 👎. |
||
| if (g.changed) console.log(" + Grok Build config updated (~/.grok/config.toml)"); | ||
| else if (!g.ok) console.error(`⚠️ ${g.message}`); | ||
| } catch (err) { | ||
| console.error(`⚠️ ${grokSyncFailureMessage(err)}`); | ||
| } | ||
| }, | ||
| info: message => console.log(message), | ||
| error: message => console.error(message), | ||
| }); | ||
|
|
@@ -887,7 +900,13 @@ switch (command) { | |
| case "restart": { | ||
| // A failed stop must not be followed by a re-inject: with a foreign service still running | ||
| // (ownership mismatch) we would rewrite shared config we just declined to touch. | ||
| if (await handleStop()) await handleEnsure(); | ||
| // | ||
| // handleEnsure() early-returns without relaunching when codexAutoStart is disabled — | ||
| // that flag governs whether opencodex registers itself with the Codex CLI on boot, an | ||
| // unrelated concern, but it silently ate `restart` too: stop succeeded, nothing ever | ||
| // came back up. handleTrayProxyStart() (already used by the tray's own restart action) | ||
| // relaunches the proxy unconditionally, independent of codexAutoStart. | ||
| if (await handleStop()) await handleTrayProxyStart(); | ||
| else console.error("↩️ Restart aborted: the proxy was not stopped cleanly."); | ||
| break; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,13 +14,22 @@ export interface TrayProxyStartIo { | |
| waitForProxy: () => Promise<TrayProxyLive | null>; | ||
| info: (message: string) => void; | ||
| error: (message: string) => void; | ||
| /** | ||
| * 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>; | ||
|
Comment on lines
+17
to
+24
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
📍 Affects 2 files
🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| /** Side-effect coordinator for the tray's fixed proxy-start action. */ | ||
| export async function runTrayProxyStart(io: TrayProxyStartIo): Promise<boolean> { | ||
| const live = await io.findLive(); | ||
| if (live) { | ||
| io.info(`Proxy already running on port ${live.port}.`); | ||
| await io.onStarted?.(live.port); | ||
| return true; | ||
| } | ||
|
|
||
|
|
@@ -40,6 +49,7 @@ export async function runTrayProxyStart(io: TrayProxyStartIo): Promise<boolean> | |
| return false; | ||
| } | ||
| io.info(`Proxy running on port ${started.port}.`); | ||
| await io.onStarted?.(started.port); | ||
| return true; | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
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:
Repository: lidge-jun/opencodex
Length of output: 368
🏁 Script executed:
Repository: lidge-jun/opencodex
Length of output: 8058
🏁 Script executed:
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, anddocs-site/src/content/docs/zh-cn/guides/claude-code.md:295-297still describe the old session-scopedprompt_cache_key/session_idmodel. Please align them withdocs-site/src/content/docs/guides/claude-code.md:386-389, which now uses a content-scopedprompt_cache_keyand only derivessession_idseparately for backend affinity whenmetadata.user_idis present.🤖 Prompt for AI Agents
Source: Path instructions