@bitkyc08/opencodex 2.23.0-preview.20260816 → 2.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/gui/dist/assets/index-CFqJKF2L.js +102 -0
  2. package/gui/dist/index.html +1 -1
  3. package/package.json +2 -2
  4. package/src/adapters/anthropic.ts +39 -7
  5. package/src/adapters/cursor/tool-definitions.ts +48 -0
  6. package/src/adapters/google.ts +18 -12
  7. package/src/adapters/openai-chat.ts +106 -13
  8. package/src/adapters/tool-call-id.ts +119 -0
  9. package/src/adapters/tool-catalog-nudge.ts +3 -0
  10. package/src/bridge.ts +16 -5
  11. package/src/chat/inbound.ts +5 -11
  12. package/src/claude/context-windows.ts +20 -4
  13. package/src/claude/desktop-3p.ts +11 -6
  14. package/src/claude/inbound.ts +39 -1
  15. package/src/claude/model-info.ts +28 -8
  16. package/src/cli/account-api.ts +5 -1
  17. package/src/cli/claude-desktop.ts +3 -0
  18. package/src/cli/config-command.ts +37 -14
  19. package/src/codex/app-server-restart-service.ts +1 -1
  20. package/src/codex/auth-api.ts +5 -0
  21. package/src/codex/auth-context.ts +43 -2
  22. package/src/codex/catalog/metadata.ts +135 -12
  23. package/src/codex/catalog/native-models.ts +32 -2
  24. package/src/codex/catalog/parsing.ts +61 -13
  25. package/src/codex/catalog/provider-fetch.ts +37 -7
  26. package/src/codex/catalog/sync.ts +49 -25
  27. package/src/codex/catalog-refresh-status.ts +21 -3
  28. package/src/codex/catalog.ts +1 -1
  29. package/src/codex/convergence-types.ts +23 -2
  30. package/src/codex/desired-state.ts +1 -1
  31. package/src/codex/inject.ts +38 -7
  32. package/src/codex/injected-marker.ts +28 -0
  33. package/src/codex/journal.ts +40 -1
  34. package/src/codex/management-convergence.ts +55 -2
  35. package/src/codex/quota-rejection.ts +61 -1
  36. package/src/codex/quota.ts +60 -6
  37. package/src/codex/routing.ts +30 -3
  38. package/src/combos/failover.ts +20 -0
  39. package/src/config.ts +271 -4
  40. package/src/generated/compatibility-version.json +90 -78
  41. package/src/grok/sync.ts +3 -1
  42. package/src/lab/artifacts/sanitize.ts +1 -1
  43. package/src/lab/live/manifest.ts +1 -1
  44. package/src/lib/codex-restart-contract.ts +1 -1
  45. package/src/lib/config-ownership.ts +1 -0
  46. package/src/lib/errors.ts +9 -0
  47. package/src/lib/lab-activation.ts +1 -1
  48. package/src/lib/optional-shutdown-hooks.ts +1 -1
  49. package/src/lib/pinned-http.ts +7 -2
  50. package/src/lib/windows-elevation.ts +3 -3
  51. package/src/providers/quota.ts +10 -4
  52. package/src/providers/registry.ts +2 -2
  53. package/src/responses/parser.ts +42 -7
  54. package/src/responses/provider-opaque-metadata.ts +1 -1
  55. package/src/responses/thought-signature-replay.ts +261 -0
  56. package/src/router.ts +6 -1
  57. package/src/routing/compatibility/provider-slot.ts +1 -1
  58. package/src/routing/evaluator.ts +12 -2
  59. package/src/routing/health.ts +16 -5
  60. package/src/routing/history/schema.ts +1 -1
  61. package/src/routing/trace.ts +1 -1
  62. package/src/server/auth-cors.ts +96 -21
  63. package/src/server/chat-completions.ts +6 -2
  64. package/src/server/chat-native.ts +32 -6
  65. package/src/server/index.ts +5 -3
  66. package/src/server/management/agent-settings-routes.ts +26 -4
  67. package/src/server/management/config-routes.ts +79 -2
  68. package/src/server/management/context.ts +1 -1
  69. package/src/server/management/model-rows.ts +5 -0
  70. package/src/server/management/native-integration-routes.ts +4 -1
  71. package/src/server/management/provider-routes.ts +19 -0
  72. package/src/server/management/shared.ts +3 -3
  73. package/src/server/management-api.ts +13 -6
  74. package/src/server/passive-route-linker.ts +1 -1
  75. package/src/server/relay.ts +16 -0
  76. package/src/server/responses/compact.ts +10 -3
  77. package/src/server/responses/core.ts +160 -33
  78. package/src/server/responses/fetch-helpers.ts +34 -2
  79. package/src/server/responses/input-admission.ts +17 -9
  80. package/src/server/responses-undeclared-tool-guard.ts +153 -0
  81. package/src/server/system-env.ts +4 -2
  82. package/src/service.ts +22 -7
  83. package/src/types.ts +35 -1
  84. package/gui/dist/assets/index-Ch-YtWdA.js +0 -102
@@ -33,7 +33,7 @@ import {
33
33
  import { isInjectionDebugEnabled } from "../../lib/debug-settings";
34
34
  import { injectionDebugLog } from "../../lib/injection-debug-log";
35
35
  import { modelInList, namespacedToolName } from "../../types";
36
- import type { AdapterEvent, OcxConfig, OcxParsedRequest, OcxProviderConfig, OcxProviderContinuationState, OcxUsage } from "../../types";
36
+ import type { AdapterEvent, OcxConfig, OcxParsedRequest, OcxProviderConfig, OcxProviderContinuationState, OcxUsage, UpstreamHttpVersion } from "../../types";
37
37
  import {
38
38
  forceRefreshOAuthAccessSnapshot,
39
39
  getOAuthCredentialApiBaseUrl,
@@ -149,6 +149,38 @@ export interface ProviderFetchOptions {
149
149
  modelId?: string;
150
150
  }
151
151
 
152
+ /**
153
+ * Bun's fetch accepts a non-standard `protocol` init to pin the HTTP version
154
+ * (BunFetchRequestInit.protocol). The DOM lib types do not include it, so the
155
+ * value is carried on an intersection and stripped before non-Bun callers.
156
+ * The accepted values are the shared UPSTREAM_HTTP_VERSION_VALUES enum from types.
157
+ */
158
+ const UPSTREAM_HTTP_VERSION_PROTOCOL: Record<Exclude<UpstreamHttpVersion, "auto">, string> = {
159
+ "http1.1": "http1.1",
160
+ h1: "h1",
161
+ http2: "http2",
162
+ h2: "h2",
163
+ };
164
+
165
+ /** Attach Bun's `protocol` pin when the provider opted into a fixed HTTP version. */
166
+ export function withUpstreamHttpVersion(
167
+ input: Parameters<typeof globalThis.fetch>[0],
168
+ init: RequestInit | undefined,
169
+ provider: OcxProviderConfig,
170
+ ): RequestInit | undefined {
171
+ const version = provider.upstreamHttpVersion;
172
+ if (!version || version === "auto") return init;
173
+ // Bun's protocol pin requires an https: target; local/plaintext upstreams keep
174
+ // their existing transport untouched.
175
+ const target = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
176
+ try {
177
+ if (new URL(target).protocol !== "https:") return init;
178
+ } catch {
179
+ return init;
180
+ }
181
+ return { ...(init ?? {}), protocol: UPSTREAM_HTTP_VERSION_PROTOCOL[version] } as RequestInit;
182
+ }
183
+
152
184
  export function providerFetch(
153
185
  provider: OcxProviderConfig,
154
186
  runtime: BunRuntimeGateInput = currentBunRuntimeIdentity(),
@@ -162,7 +194,7 @@ export function providerFetch(
162
194
  if (typeof input === "string" && init && shouldUseCodexWsUpstream(input, init, runtime)) {
163
195
  return codexWsUpstreamFetch(input, init, base, runtime);
164
196
  }
165
- return base(input, init);
197
+ return base(input, withUpstreamHttpVersion(input, init, provider));
166
198
  };
167
199
  const waitForPacing = (signal?: AbortSignal) => options.providerName
168
200
  ? waitForProviderRequestSlot(options.providerName, provider, options.modelId, signal)
@@ -10,7 +10,7 @@
10
10
  * catches the pathological case and stays out of the way otherwise. Every uncertainty
11
11
  * resolves toward admitting.
12
12
  */
13
- import { nativeOpenAiContextWindow } from "../../codex/catalog/metadata";
13
+ import { nativeOpenAiContextWindow, nativeOpenAiMaxInputTokens } from "../../codex/catalog/metadata";
14
14
  import { estimateTokens } from "../../lib/token-estimate";
15
15
  import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
16
16
  import type { OcxContentPart, OcxParsedRequest, OcxProviderConfig } from "../../types";
@@ -129,6 +129,9 @@ export function resolveInputCeiling(
129
129
  provider: OcxProviderConfig,
130
130
  providerName: string,
131
131
  modelId: string,
132
+ // Operator cap for the canonical native provider. Passed in rather than read from a
133
+ // config here so this stays pure: no filesystem, no catalog, no registry scan.
134
+ nativeContextCap?: number,
132
135
  ): number | null {
133
136
  const configured = positive(provider.modelContextWindows?.[modelId]) ?? positive(provider.contextWindow);
134
137
 
@@ -137,18 +140,22 @@ export function resolveInputCeiling(
137
140
  // transport-mismatched custom provider named "openai" is preserved verbatim by routing
138
141
  // and must not inherit built-in native limits, and a routed `provider/model` id is not a
139
142
  // native slug. Static maps only — no catalog read.
140
- const native = configured === null
141
- && providerName === OPENAI_CODEX_PROVIDER_ID
143
+ const canonicalNativeBare = providerName === OPENAI_CODEX_PROVIDER_ID
142
144
  && isCanonicalOpenAiForwardProvider(provider)
143
- && !modelId.includes("/")
144
- ? positive(nativeOpenAiContextWindow(modelId))
145
+ && !modelId.includes("/");
146
+ const native = configured === null && canonicalNativeBare
147
+ ? positive(nativeOpenAiContextWindow(modelId, nativeContextCap))
145
148
  : null;
149
+ // The model's own measured input ceiling applies even when the operator configured a
150
+ // window: a 1,050,000 override must not raise the gate above the 922,000 the upstream
151
+ // actually accepts. Computed independently of `configured` for exactly that reason.
152
+ const nativeMaxInput = canonicalNativeBare ? positive(nativeOpenAiMaxInputTokens(modelId, nativeContextCap)) : null;
146
153
 
147
154
  const window = configured ?? native;
148
155
  // modelMaxInputTokens is an input-only cap, so it can only tighten the window.
149
- const maxInput = positive(provider.modelMaxInputTokens?.[modelId]);
150
- if (window === null) return maxInput;
151
- return maxInput === null ? window : Math.min(window, maxInput);
156
+ const configuredMaxInput = positive(provider.modelMaxInputTokens?.[modelId]);
157
+ const limits = [window, configuredMaxInput, nativeMaxInput].filter((v): v is number => v !== null);
158
+ return limits.length === 0 ? null : Math.min(...limits);
152
159
  }
153
160
 
154
161
  /**
@@ -161,8 +168,9 @@ export function checkInputAdmission(
161
168
  provider: OcxProviderConfig,
162
169
  providerName: string,
163
170
  modelId: string,
171
+ nativeContextCap?: number,
164
172
  ): InputAdmissionResult {
165
- const ceiling = resolveInputCeiling(provider, providerName, modelId);
173
+ const ceiling = resolveInputCeiling(provider, providerName, modelId, nativeContextCap);
166
174
  if (ceiling === null) return { admitted: true, estimatedTokens: 0, ceiling: null };
167
175
  const estimatedTokens = estimateInputTokens(parsed, modelId);
168
176
  return { admitted: estimatedTokens <= ceiling * ADMISSION_TOLERANCE, estimatedTokens, ceiling };
@@ -0,0 +1,153 @@
1
+ import { namespacedToolName } from "../types";
2
+ import { sseDataPayload, type SseBlockRewrite } from "./sse-payload-rewrite";
3
+
4
+ /**
5
+ * Item types the CLIENT executes by name. Hosted calls (`web_search_call`,
6
+ * `image_generation_call`, `local_shell_call`, `tool_search_call`, …) are run upstream or
7
+ * carry no tool name, so they are never matched against the request catalog.
8
+ */
9
+ const CLIENT_EXECUTED_CALL_TYPES = new Set(["function_call", "custom_tool_call"]);
10
+
11
+ /** An upstream-supplied name reaches the error message; keep it bounded. */
12
+ const MAX_REPORTED_NAME_CHARS = 100;
13
+
14
+ export const UNDECLARED_TOOL_CALL_ERROR_CODE = "undeclared_tool_call";
15
+
16
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
17
+ return !!value && typeof value === "object" && !Array.isArray(value);
18
+ }
19
+
20
+ function addWireToolName(names: Set<string>, tool: unknown, namespace?: string): void {
21
+ if (!isPlainObject(tool) || typeof tool.name !== "string" || tool.name.length === 0) return;
22
+ names.add(tool.name);
23
+ // Codex routes MCP calls by an explicit `namespace` field, so the same tool is reachable
24
+ // as a bare inner name or as the flattened form; accept both rather than guess which
25
+ // coordinate system this provider echoes back.
26
+ if (namespace) names.add(namespacedToolName(namespace, tool.name));
27
+ }
28
+
29
+ function addWireToolSpecs(names: Set<string>, specs: unknown): void {
30
+ if (!Array.isArray(specs)) return;
31
+ for (const spec of specs) {
32
+ if (!isPlainObject(spec)) continue;
33
+ if (spec.type === "namespace" && Array.isArray(spec.tools)) {
34
+ const namespace = typeof spec.name === "string" ? spec.name : undefined;
35
+ for (const inner of spec.tools) addWireToolName(names, inner, namespace);
36
+ continue;
37
+ }
38
+ addWireToolName(names, spec);
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Tool names the OUTBOUND Responses body actually declared.
44
+ *
45
+ * This reads the body that goes upstream rather than the parsed internal tool list: the
46
+ * passthrough forwards wire shapes (namespaced MCP groups, `additional_tools` items carried
47
+ * inside `input`, routed custom-tool rewrites) that the internal list flattens or renames, and
48
+ * only the wire names can be compared against what the provider echoes back.
49
+ */
50
+ export function collectDeclaredWireToolNames(body: unknown): Set<string> {
51
+ const names = new Set<string>();
52
+ if (!isPlainObject(body)) return names;
53
+ addWireToolSpecs(names, body.tools);
54
+ if (Array.isArray(body.input)) {
55
+ for (const item of body.input) {
56
+ if (isPlainObject(item) && item.type === "additional_tools") addWireToolSpecs(names, item.tools);
57
+ }
58
+ }
59
+ return names;
60
+ }
61
+
62
+ function undeclaredNameInItem(item: unknown, declared: ReadonlySet<string>): string | undefined {
63
+ if (!isPlainObject(item)) return undefined;
64
+ if (typeof item.type !== "string" || !CLIENT_EXECUTED_CALL_TYPES.has(item.type)) return undefined;
65
+ const name = item.name;
66
+ if (typeof name !== "string" || name.length === 0) return undefined;
67
+ if (declared.has(name)) return undefined;
68
+ if (typeof item.namespace === "string" && declared.has(namespacedToolName(item.namespace, name))) {
69
+ return undefined;
70
+ }
71
+ return name;
72
+ }
73
+
74
+ /** First undeclared client tool named by a Responses SSE payload, or undefined. */
75
+ export function undeclaredToolCallName(
76
+ payload: unknown,
77
+ declared: ReadonlySet<string>,
78
+ ): string | undefined {
79
+ if (!isPlainObject(payload)) return undefined;
80
+ if (payload.type === "response.output_item.added" || payload.type === "response.output_item.done") {
81
+ return undeclaredNameInItem(payload.item, declared);
82
+ }
83
+ // Sparse gateways skip incremental items and only ever ship the terminal snapshot.
84
+ if (payload.type === "response.completed" || payload.type === "response.incomplete") {
85
+ return undeclaredToolCallNameInResponse(payload.response, declared);
86
+ }
87
+ return undefined;
88
+ }
89
+
90
+ /** First undeclared client tool in a Responses object's `output` array, or undefined. */
91
+ export function undeclaredToolCallNameInResponse(
92
+ response: unknown,
93
+ declared: ReadonlySet<string>,
94
+ ): string | undefined {
95
+ if (!isPlainObject(response) || !Array.isArray(response.output)) return undefined;
96
+ for (const item of response.output) {
97
+ const name = undeclaredNameInItem(item, declared);
98
+ if (name !== undefined) return name;
99
+ }
100
+ return undefined;
101
+ }
102
+
103
+ export function undeclaredToolCallMessage(name: string): string {
104
+ const reported = name.slice(0, MAX_REPORTED_NAME_CHARS);
105
+ return `routed provider emitted undeclared client tool "${reported}"; only request-declared tools may be called`;
106
+ }
107
+
108
+ function failedBlocks(name: string, newline: string): readonly string[] {
109
+ const failure = {
110
+ type: "upstream_error",
111
+ code: UNDECLARED_TOOL_CALL_ERROR_CODE,
112
+ message: undeclaredToolCallMessage(name),
113
+ };
114
+ const payload = JSON.stringify({
115
+ type: "response.failed",
116
+ response: { status: "failed", error: failure, last_error: failure },
117
+ });
118
+ return [`event: response.failed${newline}data: ${payload}`, "data: [DONE]"];
119
+ }
120
+
121
+ /**
122
+ * Fail closed when a routed provider calls a tool the request never declared (#1700).
123
+ *
124
+ * The bridged paths already refuse such a call (`declaredToolNames` in src/bridge.ts), but the
125
+ * native Responses passthrough relayed it verbatim: Codex received a `function_call` for a tool
126
+ * it has no top-level handler for — `apply_patch`, which under code mode exists only as a nested
127
+ * `tools.apply_patch(...)` helper inside `exec` — and the turn surfaced as a bare `aborted` with
128
+ * no output and no explanation. Replacing the offending event with an explicit `response.failed`
129
+ * turns that silent dead end into a compatibility error naming the tool.
130
+ *
131
+ * Everything after the trip is dropped so a later `response.completed` cannot contradict the
132
+ * terminal already sent. Non-JSON and non-item blocks pass through untouched.
133
+ */
134
+ export function createUndeclaredToolCallGuardBlockRewrite(
135
+ declared: ReadonlySet<string>,
136
+ ): SseBlockRewrite {
137
+ let tripped = false;
138
+ return (block: string) => {
139
+ if (tripped) return [];
140
+ const payload = sseDataPayload(block);
141
+ if (payload === null || payload === "[DONE]") return [block];
142
+ let parsed: unknown;
143
+ try {
144
+ parsed = JSON.parse(payload);
145
+ } catch {
146
+ return [block];
147
+ }
148
+ const name = undeclaredToolCallName(parsed, declared);
149
+ if (name === undefined) return [block];
150
+ tripped = true;
151
+ return failedBlocks(name, block.includes("\r\n") ? "\r\n" : "\n");
152
+ };
153
+ }
@@ -7,6 +7,8 @@ import { PROXY_MARKER, defaultAuthDetectDeps, detectClaudeAuth, ownAdmissionToke
7
7
  import { resolveClaudeAuthMode } from "../claude/auth-mode";
8
8
  import type { OcxConfig } from "../types";
9
9
  import { recordOwnedConfigPath } from "../lib/config-ownership";
10
+ import { providerContextCap } from "../providers/context-cap";
11
+ import { OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
10
12
 
11
13
  /**
12
14
  * Does the opencodex dummy marker belong in the system environment?
@@ -237,10 +239,10 @@ async function computeEffectiveModelEnv(config: OcxConfig, auto?: AutoContextMod
237
239
  const windows = await boundedContextWindows(async () => {
238
240
  const { gatherRoutedModels, visibleNativeSlugs } = await import("../codex/catalog");
239
241
  try {
240
- return buildClaudeContextWindows([...visibleNativeSlugs(config)], await gatherRoutedModels(config));
242
+ return buildClaudeContextWindows([...visibleNativeSlugs(config)], await gatherRoutedModels(config), providerContextCap(config, OPENAI_CODEX_PROVIDER_ID));
241
243
  } catch (error) {
242
244
  if (error && typeof error === "object" && (error as { code?: unknown }).code === "catalog_busy") {
243
- return buildClaudeContextWindows([...visibleNativeSlugs(config)], []);
245
+ return buildClaudeContextWindows([...visibleNativeSlugs(config)], [], providerContextCap(config, OPENAI_CODEX_PROVIDER_ID));
244
246
  }
245
247
  throw error;
246
248
  }
package/src/service.ts CHANGED
@@ -9,7 +9,7 @@ import { execFileSync, execSync, spawnSync } from "node:child_process";
9
9
  import { findLiveProxy, proxyIdentityAt, SERVICE_STOP_LIVENESS } from "./server/proxy-liveness";
10
10
  import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmdirSync, unlinkSync, writeFileSync } from "node:fs";
11
11
  import { homedir, tmpdir } from "node:os";
12
- import { dirname, join, resolve, win32 } from "node:path";
12
+ import { dirname, join, posix, resolve, win32 } from "node:path";
13
13
  import { expandUserPath, getConfigDir, readPid, removePid, removeRuntimePort, verifyPidIdentity } from "./config";
14
14
  import { loadConfig } from "./config";
15
15
  import { restoreNativeCodex, restoreNativeCodexAsync } from "./codex/inject";
@@ -113,12 +113,19 @@ function currentCodexSqliteHomeAbsolute(target: "native" | "windows" = "native")
113
113
  const raw = process.env.CODEX_SQLITE_HOME?.trim();
114
114
  if (!raw) return undefined;
115
115
  const expanded = expandUserPath(raw);
116
- // Windows service artifacts can be rendered by cross-platform tests and
117
- // repair tooling. Preserve an already-absolute drive/UNC path instead of
118
- // anchoring it beneath the current POSIX worktree.
119
- return target === "windows" && win32.isAbsolute(expanded)
120
- ? win32.normalize(expanded)
121
- : resolve(expanded);
116
+ // Service artifacts can be rendered by cross-platform tests and repair tooling, so an
117
+ // already-absolute path for the TARGET platform is preserved rather than re-anchored
118
+ // against the writing host. `resolve()` is host-relative in both directions: on a POSIX
119
+ // host it turns `C:\data` into `<cwd>/C:\data`, and on a Windows host it turns `/tmp/x`
120
+ // into `D:\tmp\x` — neither is a path the target can use. A relative value still resolves,
121
+ // because a service unit has no meaningful working directory.
122
+ //
123
+ // CODEX_HOME and OPENCODEX_HOME are carried through literally, so without this the same
124
+ // generated file disagreed with itself about two variables holding the same kind of value.
125
+ if (target === "windows") {
126
+ return win32.isAbsolute(expanded) ? win32.normalize(expanded) : resolve(expanded);
127
+ }
128
+ return posix.isAbsolute(expanded) ? posix.normalize(expanded) : resolve(expanded);
122
129
  }
123
130
 
124
131
  function currentOpenCodexHome(): string {
@@ -1538,6 +1545,14 @@ export function buildWindowsServiceScript(entry = cliEntry(), port = resolveServ
1538
1545
  '>>"%OCX_SERVICE_LOG%" echo opencodex_home="%OPENCODEX_HOME%"',
1539
1546
  '>>"%OCX_SERVICE_LOG%" echo codex_home="%CODEX_HOME%"',
1540
1547
  '>>"%OCX_SERVICE_LOG%" echo token_file="%OCX_API_TOKEN_FILE%"',
1548
+ 'if not exist "%OCX_BUN%" (',
1549
+ ' >>"%OCX_SERVICE_LOG%" echo [%DATE% %TIME%] installation is incomplete: bundled Bun is missing; reinstall opencodex, then run ocx service repair',
1550
+ " exit /b 3",
1551
+ ")",
1552
+ 'if not exist "%OCX_CLI%" (',
1553
+ ' >>"%OCX_SERVICE_LOG%" echo [%DATE% %TIME%] installation is incomplete: CLI entry is missing; reinstall opencodex, then run ocx service repair',
1554
+ " exit /b 3",
1555
+ ")",
1541
1556
  `"%OCX_BUN%" "%OCX_CLI%" start --port ${port} >>"%OCX_SERVICE_LOG%" 2>&1`,
1542
1557
  "if %ERRORLEVEL% NEQ 0 (",
1543
1558
  ' >>"%OCX_SERVICE_LOG%" echo [%DATE% %TIME%] child exited with code %ERRORLEVEL%; restarting in 5s',
package/src/types.ts CHANGED
@@ -457,6 +457,17 @@ export interface OcxClaudeCodeConfig {
457
457
  smallFastModel?: string;
458
458
  /** Inbound model id remaps: exact id first, then date-stripped (`-\d{8}$`). */
459
459
  modelMap?: Record<string, string>;
460
+ /**
461
+ * Explicit classifier model for Claude Code Auto Mode safety checks (e.g. "RelayA/claude-opus-5").
462
+ * When unset, bare classifier requests check modelMap, then same-provider affinity from
463
+ * `claudeCode.model`, then compatible Anthropic-adapter providers, and finally fallbacks.
464
+ */
465
+ classifierModel?: string;
466
+ /**
467
+ * Ordered fallback candidates for Claude Code Auto Mode classifier routing when the primary
468
+ * classifier route is not available.
469
+ */
470
+ classifierFallbacks?: string[];
460
471
  /**
461
472
  * Inject ANTHROPIC_BASE_URL etc. into the macOS user domain via `launchctl setenv`
462
473
  * so plain `claude` commands route through the proxy without `ocx claude`. Reverted
@@ -514,7 +525,7 @@ export interface OcxClaudeCodeConfig {
514
525
  * (the legacy DISABLE_COMPACT pair takes rule-1 precedence in the CLI).
515
526
  */
516
527
  autoContext?: boolean;
517
- /** Compact-window tokens for auto-context. Default 350_000. */
528
+ /** Compact-window tokens for auto-context. Default 829_800 (AUTO_COMPACT_WINDOW_DEFAULT). */
518
529
  autoCompactWindow?: number;
519
530
  /**
520
531
  * Bundled-skill content elision for ROUTED (non-Anthropic) models (devlog 260712
@@ -1375,6 +1386,14 @@ export interface OcxProviderConfig {
1375
1386
  * link-local, or unique-local upstreams. Metadata endpoints remain blocked.
1376
1387
  */
1377
1388
  allowPrivateNetwork?: boolean;
1389
+ /**
1390
+ * Pin the HTTP version used for upstream provider requests. Bun's fetch negotiates
1391
+ * HTTP/2 via TLS ALPN by default; some Cloudflare-fronted SSE endpoints hang on
1392
+ * HTTP/2 streaming responses (issue #1668). "http1.1" / "h1" forces HTTP/1.1,
1393
+ * "http2" / "h2" forces HTTP/2. Absent or "auto" keeps Bun's default negotiation
1394
+ * (current behavior unchanged). Only meaningful for https: base URLs.
1395
+ */
1396
+ upstreamHttpVersion?: UpstreamHttpVersion;
1378
1397
  /** Keep provider settings on disk but exclude it from routing and model/catalog listings. */
1379
1398
  disabled?: boolean;
1380
1399
  /**
@@ -1668,6 +1687,21 @@ export interface OcxProviderConfig {
1668
1687
  nativeLocalExec?: "off" | "codex-sandbox" | "on";
1669
1688
  }
1670
1689
 
1690
+ /**
1691
+ * Accepted values for the per-provider upstream HTTP-version pin (#1668). Shared by the
1692
+ * zod load schema, the management write boundary (POST/PATCH), and the fetch runtime, so
1693
+ * a value that one boundary accepts can never be rejected by another.
1694
+ */
1695
+ export const UPSTREAM_HTTP_VERSION_VALUES = [
1696
+ "auto",
1697
+ "http1.1",
1698
+ "h1",
1699
+ "http2",
1700
+ "h2",
1701
+ ] as const;
1702
+
1703
+ export type UpstreamHttpVersion = (typeof UPSTREAM_HTTP_VERSION_VALUES)[number];
1704
+
1671
1705
  export const REASONING_SUMMARY_DELIVERY_VALUES = [
1672
1706
  "sequential",
1673
1707
  "sequential_cutoff",