Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Windows composed-fixture cache ownership

## Trigger and plan amendment

Final integrated run 34049728209 failed only Windows shard 2/6's composed-toggle B case with a request timeout. Product changes did not run in that management-only case. Three-head diagnostic run 34051777446 kept all deadlines/assertions, comparing base 24c761a, prior d60a0716 and current 84c94f3; all samples passed, but OFF consistently consumed 20-26 seconds of a 30-second request budget.

## Controlled evidence

Same-VM run https://github.com/lidge-jun/opencodex/actions/runs/34053472964 changed only fixture child PowerShell module-cache policy, on fixed source 84c94f3. The parent cache was read into a private job-owned regular file; no original cache path was passed to experiment subprocesses. All test assertions and deadlines remained unchanged.

| Condition | Startup | OFF request | Result |
| --- | --- | --- | --- |
| Original A1 | 35.26 s | abort at 30.00 s | timeout; held-sync 45 s abort also recorded |
| Original A2 | 30.11 s | 25.50 s | pass |
| Owned empty, two samples | 26.71-27.41 s | 26.07-26.96 s | pass |
| Owned prepared copy, two samples | 6.12-6.29 s | 3.36-3.37 s | pass |
| Restored original | 27.50 s | 26.15 s | pass |

The copied-cache whole composed file also passed (7 pass, 1 pre-existing skip, 0 fail). A1 has mixed abort observations, so the latency comparison uses the complete A2 and restored-control samples. A mere empty destination did not remove the delay. In copied samples the stale sync completed after the provider release; original controls exhausted discovery before release. No guard-ablation claim is made.

## Scoped delta and acceptance

MODIFY tests/codex-integration/codex-composed-acceptance.test.ts only: seed a Windows-only constructor-owned module cache using the existing Desktop fixture policy, outside the Codex manifest root, and pass only that owned destination to children. Preserve HOME/USERPROFILE variants, real SID/service evidence, coordinator paths, provider hold/release, cleanup, all assertions and all deadlines. No product, workflow, or identity-policy changes.

The isolated diagnostic workflows are not delivery changes. Independent source/security review and a fresh full cross-platform run of the combined latest-dev integration head remain required. Local suites/typecheck/build are NOT RUN per instruction. This support layer is separate from the four product PRs.

## Follow-up source review

External review of #3808 identified a construction-failure cleanup gap: the fixture is registered only after its constructor returns. The cache-setup block now removes its own temporary root with bounded retries before rethrowing. Parent cache sources and shared coordinator paths remain outside that cleanup. Independent source review passed; final remote verification will include the delta.

Run 34054412656 passed Windows composed shard 2, including B at 8.511 seconds. It failed a different competing-OFF fixture in shard 6 before the flip started; this does not negate the cache-control result and is tracked separately.
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Windows competing-OFF preparation allocation

## Failure and bounded plan

Final run 34054412656 passed every individual job except Windows shard 6. The competing-OFF test refused to begin its real second-process flip: 32,262 ms remained of the 85,000 ms child budget after 52,738 ms of preparation. The unchanged flip plus reap requires 45,000 ms. This was a pre-flip budget rejection, not an outer timeout or a product assertion failure. Unlike the composed fixture, this test already inherits the ambient child environment; no cache-causation claim is made.

MODIFY only the test's named preparation allocation: on Windows reserve two existing boot budgets for import, identity and admission preparation, then retain the original single flip and reap budgets. Windows CI child/test limits become 125/130 seconds; other platforms retain 85/90. No product deadline, assertion, coordinator, identity or service evidence changes.

## Remote control

Diagnostic run https://github.com/lidge-jun/opencodex/actions/runs/34056824267 checked out fixed source 0f8936b1f692d72ff1d2c1dd6218183dd0e9b882 and used a 52-second TOTAL preparation floor before the original remaining-budget guard.

- Unchanged control passed and completed its real flip.
- Old allocation rejected the floor before flip, without timeout.
- New allocation completed the real flip and passed the original skip and unchanged-config assertions.
- With the new allocation, a one-site diagnostic mutation stored ON for the OFF request. The real flip process completed successfully, but the original result assertion failed: applied instead of skipped/desired_disabled. No timeout or budget refusal contaminated that failure.

The isolated job restored both files byte-for-byte. Its synthetic floor, traces, workflow and production mutation are excluded from delivery. This proves the preparation allocation and test sensitivity to broken OFF persistence, not every downstream guard. Independent diagnostic security review passed. Full integration CI remains the delivery gate; no local suites, typecheck, install or build were run.
14 changes: 14 additions & 0 deletions devlog/_plan/260907_track2_protocol/090_windows_shim_budget.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Windows unreadable-config shim fixture deadline

Final run 34057173038 passed 23 individual jobs and failed only Windows shard 2 (plus aggregate). The original shim unreadable-config test had a 10-second outer timeout and no owned child deadline. Bun killed the dangling child at 10.24 seconds, yielding status null. Fixture and inspected runtime source were unchanged since 8615f1a. The specific slow runtime stage is unproven; this is not an environmental or cache-causation claim.

MODIFY only that subprocess fixture: use the existing 45-second spawn budget on Windows and a named 5-second outer cleanup allowance. Preserve POSIX's 10-second limit, all temporary paths, fake launcher, real install/diagnosis/advisory path and original exit/output assertions. Error/signal diagnostics are fixed and omit captured output. Product behavior is unchanged.

Remote diagnostic https://github.com/lidge-jun/opencodex/actions/runs/34058337624 pinned c7a96b14f and demonstrated:

- Unchanged control: real CLI exit 0 and original assertions pass.
- With a 12-second preload delay, old limit: child is signalled and test times out.
- Same delayed CLI with owned Windows deadline: exit 0 and original assertions pass.
- Same new limit with the readiness collector's advisory catch changed to rethrow: real CLI exits 1 without timeout/signal; the original exit-0 assertion fails.

The diagnostic restored source bytes and is excluded from delivery. Its initial run 34058187661 failed before tests because Python selected a Windows legacy codec; explicit UTF-8 corrected that diagnostic-only error. No passing rerun was used to erase a product failure. Source/security review of the diagnostic and independent source review of the candidate passed. No local suites, installs, typecheck or builds were run. Final cross-platform CI remains required for the published combined head.
20 changes: 20 additions & 0 deletions docs-site/src/content/docs/reference/configuration/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -317,12 +317,32 @@ These settings govern `/v1/messages`, `/v1/messages/count_tokens`, the `ocx clau
| --- | --- | --- | --- |
| `claudeCode.bodyStallSec?` | `number` | `90` | Native-passthrough body inactivity budget in seconds while a read is pending, not total duration. Minimum 1; exactly `0` disables. |
| `claudeCode.bodyMaxBytes?` | `number` | `67108864` | Cumulative native-passthrough body cap for streamed and buffered responses. Exactly `0` disables. |
| `claudeCode.compatibility?` | `"shadow" \| "enforce"` | unset | Optional compatibility admission for translated `/v1/messages` requests. `shadow` records unsupported features and continues; `enforce` returns an Anthropic-shaped 400 before inference. Native Anthropic passthrough remains unchanged. |
| `claudeCode.authMode?` | `"proxy" \| "subscription"` | auto | How launch handles `ANTHROPIC_AUTH_TOKEN`. Auto detects auth each launch; an explicit value is never overridden. |
| `claudeCode.authModeMigratedAt?` | `string` | unset | Internal one-time upgrade marker. Do not set manually. |
| `claudeCode.classifierModel?` | `string` | unset | Explicit target for Claude Code Auto Mode classifier turns, as a qualified `provider/model` (for example `RelayA/claude-opus-5`). Auto Mode sends bare safety checks such as `claude-opus-5` with no provider, so without this they fall through to `defaultProvider` — which may not speak Anthropic at all. Nothing is inferred automatically: only a target you declare here is used. |
| `claudeCode.classifierFallbacks?` | `string[]` | unset | Ordered classifier targets used when `classifierModel` is not set. Same qualified `provider/model` form; the first usable entry wins. An explicit `modelMap` entry for the classifier model still outranks both. |
| `claudeCode.subagentEffort?` | `"low" \| "medium" \| "high" \| "xhigh" \| "max"` | inherit | Effort written to generated `~/.claude/agents/ocx-*.md`; separate from Codex guidance and proxy caps. Restart through `ocx claude` to regenerate. |

The compatibility policy applies to Claude Code, Desktop and other clients using translated
Messages, including `?beta=true` and non-streaming requests. Every translated target uses the
same conservative policy, including Anthropic and native Responses adapters. It rejects
document content, thinking/redacted-thinking replay, hosted search and execution tools,
tool-search references, active deferred loading, strict tools, non-default caller modes,
structured-output formats, explicit service-tier intent, MCP connector features, context
management, containers, inference placement and unsupported protocol fields or blocks.

Unset preserves legacy translation. Cache hints, tool input examples and ordinary
thinking/effort settings are deliberately admitted with possible degradation: this setting
does not guarantee cache breakpoints or TTL, retained examples, exact thinking budgets or
lossless translation. Beta headers alone are not validated for feature support. Shadow
evidence contains only fixed protocol codes and derived reasons, retained in request logs
and `usage.jsonl` and restored on restart. An invalid non-unset mode returns a fixed 503
configuration error on translated Messages. Configure the value in `config.json` and
restart the proxy to load it; there is no dedicated GUI setter. Count-tokens and direct
Responses/Chat APIs are outside this policy; successful token counting does not imply
Messages admission. This setting does not add a global authorization boundary.

Auto auth selects subscription when stored Claude auth is found, proxy when none is found, and
subscription with a warning when detection is inconclusive. See
[Claude Code auth mode](/guides/claude-code/#auth-mode).
Expand Down
1 change: 1 addition & 0 deletions scripts/test-layout/layout.json
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@
"claude-authmode-migration.test.ts": "claude-integration",
"claude-cli.test.ts": "claude-integration",
"claude-code-thought-signature-scope.test.ts": "claude-integration",
"claude-compatibility.test.ts": "claude-integration",
"claude-context-windows.test.ts": "claude-integration",
"claude-desktop-1m.test.ts": "claude-integration",
"claude-desktop-cli.test.ts": "claude-integration",
Expand Down
192 changes: 192 additions & 0 deletions src/claude/compatibility.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
/** Opt-in admission for the translated Messages path; no adapter or credential state. */
export type ClaudeCompatibilityMode = "shadow" | "enforce";

// False means deliberately tolerated degradation, not lossless representation.
const FEATURES = {
cache_control: false,
input_examples: false,
thinking_settings: false,
unknown_beta: false,
thinking_replay: true,
documents: true,
web_search_tool: true,
tool_search: true,
tool_reference: true,
deferred_tools: true,
strict_tools: true,
caller_mode: true,
structured_output: true,
service_tier: true,
mcp_tool: true,
code_execution: true,
computer_use: true,
server_tool: true,
context_management: true,
container: true,
inference_geo: true,
user_profile: true,
unknown_body_field: true,
unknown_content_block: true,
} as const;

export type ClaudeFeatureCode = keyof typeof FEATURES;
const FEATURE_CODES = Object.keys(FEATURES) as ClaudeFeatureCode[];
const MAX_FEATURE_CODES = 32;
const MAX_REASON_LENGTH = 512;
type Rec = Record<string, unknown>;
const isRec = (value: unknown): value is Rec =>
value !== null && typeof value === "object" && !Array.isArray(value);

export function isClaudeCompatibilityMode(value: unknown): value is ClaudeCompatibilityMode {
return value === "shadow" || value === "enforce";
}

/** Project only closed codes, including when reading an untrusted persisted row. */
export function normalizeClaudeFeatureCodes(value: unknown): ClaudeFeatureCode[] {
if (!Array.isArray(value)) return [];
const codes = new Set<ClaudeFeatureCode>();
for (const code of value) {
if (typeof code === "string" && Object.hasOwn(FEATURES, code)) codes.add(code as ClaudeFeatureCode);
}
return FEATURE_CODES.filter(code => codes.has(code)).sort().slice(0, MAX_FEATURE_CODES);
}

/** Never accept a caller-supplied reason, header value, model name or tool name. */
export function claudeCompatibilityReason(codes: readonly ClaudeFeatureCode[], shadow: boolean): string | undefined {
const unsupported = codes.filter(code => FEATURES[code]);
if (unsupported.length === 0) return undefined;
return `${shadow ? "shadow: would reject" : "unsupported translated Claude features"}: ${unsupported.join(", ")}`
.slice(0, MAX_REASON_LENGTH);
}

const BODY_FIELDS = new Set([
"model", "max_tokens", "messages", "system", "tools", "tool_choice", "thinking",
"output_config", "metadata", "service_tier", "stop_sequences", "stream",
"temperature", "top_p", "top_k", "cache_control", "context_management",
"container", "inference_geo", "user_profile_id", "mcp_servers", "defer_tools", "deferred_tools",
]);

function activeDeferred(value: unknown): boolean {
return value === true || (Array.isArray(value) ? value.length > 0 : isRec(value) && Object.keys(value).length > 0);
}

function nonDirectCaller(value: unknown): boolean {
return value !== undefined && !(Array.isArray(value) && value.length === 1 && value[0] === "direct");
}

/** Complete finite detection. Only protocol content positions are visited, never schemas/arguments. */
function detectFeatures(body: unknown, anthropicBeta?: string): Set<ClaudeFeatureCode> {
const codes = new Set<ClaudeFeatureCode>();
// Header-only beta semantics are outside this policy. No header bytes become codes.
if (anthropicBeta?.trim()) codes.add("unknown_beta");
if (!isRec(body)) return codes; // The existing Messages parser owns malformed top-level input.
if (Object.keys(body).some(key => !BODY_FIELDS.has(key))) codes.add("unknown_body_field");
for (const [field, code] of [
["cache_control", "cache_control"], ["context_management", "context_management"],
["container", "container"], ["inference_geo", "inference_geo"],
["user_profile_id", "user_profile"], ["mcp_servers", "mcp_tool"],
] as const) {
if (Object.hasOwn(body, field)) codes.add(code);
}
if (body.service_tier !== undefined && body.service_tier !== null) codes.add("service_tier");
if (isRec(body.thinking)) codes.add("thinking_settings");
if (isRec(body.output_config)
&& (body.output_config.format != null || body.output_config.output_format != null)) codes.add("structured_output");
if (activeDeferred(body.defer_tools) || activeDeferred(body.deferred_tools)) codes.add("deferred_tools");

if (Array.isArray(body.tools)) {
for (const tool of body.tools) {
if (!isRec(tool)) continue;
if (Object.hasOwn(tool, "cache_control")) codes.add("cache_control");
if (Object.hasOwn(tool, "input_examples")) codes.add("input_examples");
if (tool.strict === true) codes.add("strict_tools");
if (tool.defer === true || tool.defer_loading === true) codes.add("deferred_tools");
if (nonDirectCaller(tool.allowed_callers)) codes.add("caller_mode");
const type = tool.type;
// Ordinary client function names do not convey hosted execution semantics.
if (type === undefined || type === "function" || type === "custom") continue;
if (type === "mcp_toolset") codes.add("mcp_tool");
else if (typeof type === "string" && /^web_search_\d{8}$/.test(type)) codes.add("web_search_tool");
else if (typeof type === "string" && /^tool_search(?:_tool_(?:regex|bm25))?(?:_\d{8})?$/.test(type)) codes.add("tool_search");
else if (typeof type === "string" && /^code_execution_\d{8}$/.test(type)) codes.add("code_execution");
else if (typeof type === "string" && /^computer(?:_toolset)?_\d{8}$/.test(type)) codes.add("computer_use");
else codes.add("server_tool");
}
}

const scanBlock = (block: unknown, position: "message" | "system" | "result") => {
if (!isRec(block)) return;
if (Object.hasOwn(block, "cache_control")) codes.add("cache_control");
if (position === "system" && block.type !== "text") codes.add("unknown_content_block");
if (position === "result" && (typeof block.type !== "string" || !["text", "image", "document", "tool_reference"].includes(block.type))) {
codes.add("unknown_content_block");
}
switch (block.type) {
case "text":
case "image": break;
case "document": codes.add("documents"); break;
case "thinking":
case "redacted_thinking": codes.add("thinking_replay"); break;
case "tool_reference": codes.add("tool_reference"); break;
case "tool_search_tool_result": codes.add("tool_search"); break;
case "web_search_tool_result": codes.add("web_search_tool"); break;
case "code_execution_tool_result":
case "bash_code_execution_tool_result":
case "text_editor_code_execution_tool_result": codes.add("code_execution"); break;
case "mcp_tool_use":
case "mcp_tool_result": codes.add("mcp_tool"); break;
case "tool_use":
if (block.caller !== undefined && (!isRec(block.caller) || block.caller.type !== "direct")) codes.add("caller_mode");
break;
case "server_tool_use":
switch (block.name) {
case "tool_search":
case "tool_search_tool_regex":
case "tool_search_tool_bm25": codes.add("tool_search"); break;
case "web_search": codes.add("web_search_tool"); break;
case "code_execution": codes.add("code_execution"); break;
case "computer": codes.add("computer_use"); break;
default: codes.add("server_tool");
}
break;
case "tool_result": break; // Children are visited below at the one supported nesting level.
default: codes.add("unknown_content_block");
}
};
if (Array.isArray(body.system)) for (const block of body.system) scanBlock(block, "system");
if (Array.isArray(body.messages)) {
for (const message of body.messages) {
if (!isRec(message) || !Array.isArray(message.content)) continue;
for (const block of message.content) {
scanBlock(block, "message");
if (isRec(block) && block.type === "tool_result" && Array.isArray(block.content)) {
for (const child of block.content) scanBlock(child, "result");
}
}
}
}
return codes;
}

export interface ClaudeCompatibilityResult {
featureCodes: ClaudeFeatureCode[];
compatible: boolean;
decision: "allow" | "shadow" | "reject";
reason?: string;
}

/** All translated targets share this policy; native passthrough never calls it. */
export function analyzeClaudeCompatibility(
body: unknown,
opts: { mode: ClaudeCompatibilityMode; anthropicBeta?: string },
): ClaudeCompatibilityResult {
const detected = detectFeatures(body, opts.anthropicBeta);
const compatible = !FEATURE_CODES.some(code => detected.has(code) && FEATURES[code]);
const featureCodes = normalizeClaudeFeatureCodes([...detected]);
return {
featureCodes,
compatible,
decision: compatible ? "allow" : opts.mode === "shadow" ? "shadow" : "reject",
...(!compatible ? { reason: claudeCompatibilityReason(featureCodes, opts.mode === "shadow") } : {}),
};
}
Loading
Loading