@yansigit/opencodex 2.31.3 → 2.33.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 (176) hide show
  1. package/README.md +2 -2
  2. package/bin/ocx.mjs +99 -70
  3. package/gui/dist/assets/index-DKLr4LTE.js +102 -0
  4. package/gui/dist/assets/index-DrSQdTRd.css +1 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +6 -5
  7. package/src/adapters/anthropic.ts +25 -13
  8. package/src/adapters/azure.ts +20 -4
  9. package/src/adapters/base.ts +5 -1
  10. package/src/adapters/command-code.ts +42 -10
  11. package/src/adapters/cursor/live-models.ts +8 -0
  12. package/src/adapters/cursor/live-transport.ts +1 -1
  13. package/src/adapters/cursor/native-exec-desktop.ts +16 -0
  14. package/src/adapters/cursor/protobuf-events.ts +158 -7
  15. package/src/adapters/cursor/protobuf-request.ts +33 -15
  16. package/src/adapters/cursor/request-builder.ts +4 -3
  17. package/src/adapters/cursor/tool-definitions.ts +27 -1
  18. package/src/adapters/cursor/types.ts +4 -3
  19. package/src/adapters/cursor.ts +9 -0
  20. package/src/adapters/google-antigravity-replay.ts +2 -2
  21. package/src/adapters/google-antigravity-wire.ts +7 -0
  22. package/src/adapters/google-errors.ts +6 -2
  23. package/src/adapters/google-http.ts +30 -7
  24. package/src/adapters/google-truncation.ts +5 -0
  25. package/src/adapters/google-wire-compiler.ts +38 -6
  26. package/src/adapters/google.ts +154 -33
  27. package/src/adapters/kiro-tools.ts +20 -9
  28. package/src/adapters/kiro.ts +0 -3
  29. package/src/adapters/openai-chat.ts +9 -0
  30. package/src/adapters/openai-responses.ts +4 -1
  31. package/src/adapters/tool-catalog-nudge.ts +1 -1
  32. package/src/adapters/xai-web-search.ts +7 -2
  33. package/src/bridge.ts +133 -24
  34. package/src/claude/context-windows.ts +16 -9
  35. package/src/cli/dispatch.ts +50 -2
  36. package/src/cli/doctor.ts +26 -13
  37. package/src/cli/help.ts +4 -3
  38. package/src/cli/index.ts +20 -6
  39. package/src/cli/models.ts +13 -3
  40. package/src/cli/observe.ts +20 -5
  41. package/src/cli/provider.ts +8 -1
  42. package/src/cli/registry.ts +7 -5
  43. package/src/cli/status.ts +25 -1
  44. package/src/cli/system-restart-client.ts +1 -1
  45. package/src/cli/usage-report.ts +134 -0
  46. package/src/codex/app-server-processes.ts +3 -1
  47. package/src/codex/auth-api.ts +4 -2
  48. package/src/codex/autostart-health.ts +16 -0
  49. package/src/codex/catalog/aggregation.ts +12 -0
  50. package/src/codex/catalog/effort.ts +42 -12
  51. package/src/codex/catalog/metadata.ts +27 -1
  52. package/src/codex/catalog/model-metadata.ts +593 -0
  53. package/src/codex/catalog/parsing.ts +71 -27
  54. package/src/codex/catalog/provider-fetch.ts +189 -33
  55. package/src/codex/catalog/sync.ts +6 -5
  56. package/src/codex/convergence.ts +5 -0
  57. package/src/codex/desktop-app-restart.ts +342 -0
  58. package/src/codex/history-job.ts +32 -3
  59. package/src/codex/history-manifest.ts +112 -0
  60. package/src/codex/history-migration-guardian.ts +5 -5
  61. package/src/codex/history-provider.ts +825 -247
  62. package/src/codex/history-worker.ts +8 -5
  63. package/src/codex/inject.ts +49 -21
  64. package/src/codex/injected-marker.ts +1 -1
  65. package/src/codex/internal/history-writer.ts +4 -3
  66. package/src/codex/native-profile-startup.ts +157 -27
  67. package/src/codex/native-residue.ts +26 -33
  68. package/src/codex/shim.ts +56 -3
  69. package/src/combos/failover.ts +27 -0
  70. package/src/compatibility/index.ts +26 -0
  71. package/src/compatibility/manifest.ts +253 -0
  72. package/src/compatibility/openai-responses.ts +81 -0
  73. package/src/config/atomic-write.ts +219 -0
  74. package/src/config/paths.ts +40 -0
  75. package/src/config/process-state.ts +308 -0
  76. package/src/config/provider-validation.ts +214 -0
  77. package/src/config.ts +153 -814
  78. package/src/generated/compatibility-version.json +232 -148
  79. package/src/images/loop.ts +37 -6
  80. package/src/images/plan.ts +5 -4
  81. package/src/integrations/ownership-policy.ts +141 -0
  82. package/src/integrations/ownership.ts +10 -0
  83. package/src/integrations/state.ts +44 -5
  84. package/src/integrations/writer.ts +6 -0
  85. package/src/lib/azure-identity.ts +154 -0
  86. package/src/lib/bounded-body.ts +14 -2
  87. package/src/lib/debug.ts +42 -0
  88. package/src/lib/errors.ts +14 -0
  89. package/src/lib/process-control.ts +2 -1
  90. package/src/lib/provider-outbound.ts +45 -33
  91. package/src/lib/provider-tls-profile.ts +309 -0
  92. package/src/lib/proxy-env.ts +49 -0
  93. package/src/lib/redact.ts +10 -1
  94. package/src/lib/state-store-registrations.ts +2 -0
  95. package/src/lib/tool-argument-integers.ts +56 -5
  96. package/src/oauth/antigravity-routing.ts +282 -236
  97. package/src/oauth/callback-server.ts +22 -2
  98. package/src/oauth/command-code.ts +5 -16
  99. package/src/oauth/google-antigravity.ts +42 -5
  100. package/src/oauth/health.ts +1 -1
  101. package/src/oauth/index.ts +15 -3
  102. package/src/oauth/kimi.ts +9 -1
  103. package/src/oauth/open-browser-choice.ts +26 -0
  104. package/src/oauth/store.ts +6 -0
  105. package/src/providers/antigravity-quota.ts +3 -1
  106. package/src/providers/api-keys.ts +2 -1
  107. package/src/providers/auto-compact-budget.ts +65 -0
  108. package/src/providers/derive.ts +4 -0
  109. package/src/providers/key-failover.ts +5 -1
  110. package/src/providers/openai-tiers.ts +5 -0
  111. package/src/providers/provider-id-rewrite.ts +1 -0
  112. package/src/providers/quota.ts +59 -13
  113. package/src/providers/registry.ts +4 -2
  114. package/src/providers/request-pacing.ts +33 -6
  115. package/src/providers/xai-transport.ts +21 -0
  116. package/src/reasoning-effort.ts +19 -2
  117. package/src/responses/apply-patch-envelope.ts +63 -0
  118. package/src/responses/custom-tool-compat.ts +132 -38
  119. package/src/responses/google-provider-options.ts +36 -0
  120. package/src/responses/namespace-tool-compat.ts +84 -4
  121. package/src/responses/parser.ts +14 -2
  122. package/src/responses/provider-opaque-metadata.ts +3 -3
  123. package/src/responses/reasoning-replay-cache.ts +81 -3
  124. package/src/responses/schema.ts +37 -0
  125. package/src/responses/state.ts +94 -4
  126. package/src/router.ts +8 -2
  127. package/src/server/auth-cors.ts +37 -7
  128. package/src/server/images.ts +19 -35
  129. package/src/server/index.ts +102 -21
  130. package/src/server/local-management-read-client.ts +1 -1
  131. package/src/server/local-provider-reload-client.ts +1 -1
  132. package/src/server/management/agent-settings-routes.ts +206 -16
  133. package/src/server/management/combo-routes.ts +6 -0
  134. package/src/server/management/config-routes.ts +35 -6
  135. package/src/server/management/context.ts +1 -1
  136. package/src/server/management/logs-usage-routes.ts +27 -6
  137. package/src/server/management/model-routes.ts +8 -4
  138. package/src/server/management/model-rows.ts +4 -0
  139. package/src/server/management/native-integration-routes.ts +2 -1
  140. package/src/server/management/oauth-account-routes.ts +25 -4
  141. package/src/server/management/provider-capability-config.ts +1 -1
  142. package/src/server/management/provider-routes.ts +113 -15
  143. package/src/server/management/routing-profile-routes.ts +3 -0
  144. package/src/server/management/system-restart.ts +1 -1
  145. package/src/server/port-reclaim.ts +1 -1
  146. package/src/server/proxy-liveness.ts +2 -1
  147. package/src/server/request-log-conversation.ts +30 -0
  148. package/src/server/request-log.ts +21 -0
  149. package/src/server/responses/agent-task-recovery.ts +1 -1
  150. package/src/server/responses/codex-auth-error.ts +55 -0
  151. package/src/server/responses/combo-stream-preflight.ts +171 -0
  152. package/src/server/responses/compact.ts +36 -22
  153. package/src/server/responses/core.ts +584 -247
  154. package/src/server/responses/empty-completion-guard.ts +35 -6
  155. package/src/server/responses/fetch-helpers.ts +20 -102
  156. package/src/server/responses/v2-native-parent-override.ts +59 -0
  157. package/src/server/responses/ws-upstream.ts +75 -2
  158. package/src/server/responses-custom-tool-repair.ts +41 -5
  159. package/src/server/responses-undeclared-tool-guard.ts +241 -18
  160. package/src/service.ts +9 -5
  161. package/src/types/config.ts +16 -1
  162. package/src/types/provider.ts +16 -0
  163. package/src/types/request.ts +34 -1
  164. package/src/types/tools.ts +114 -11
  165. package/src/types.ts +7 -1
  166. package/src/update/index.ts +5 -4
  167. package/src/update/job.ts +3 -1
  168. package/src/update/transactional-install.mjs +8 -1
  169. package/src/usage/log.ts +16 -8
  170. package/src/usage/summary.ts +201 -8
  171. package/src/vision/describe.ts +18 -13
  172. package/src/web-search/executor.ts +10 -3
  173. package/src/web-search/gemini-executor.ts +6 -4
  174. package/src/web-search/loop.ts +42 -6
  175. package/gui/dist/assets/index-CGoDO3uO.css +0 -1
  176. package/gui/dist/assets/index-Cxt5fZMP.js +0 -102
package/src/bridge.ts CHANGED
@@ -8,6 +8,7 @@ import type {
8
8
  } from "./types";
9
9
  import { coerceIntegerToolArguments } from "./lib/tool-argument-integers";
10
10
  import { adapterFailureFromMessage, classifyError, CYBER_POLICY_ERROR_CODE, isCyberPolicyCode, type OcxErrorPayload } from "./lib/errors";
11
+ import { repairFreeformToolInput } from "./responses/apply-patch-envelope";
11
12
  import { encodeCompactionSummary } from "./responses/compaction";
12
13
  import { isTruncatedStopReason, truncationReasonFor } from "./responses/truncated-stop-reason";
13
14
  import { encodeReasoningEnvelope, type ReasoningEnvelope } from "./responses/reasoning-envelope";
@@ -18,6 +19,7 @@ import {
18
19
  awaitThoughtSignatureDurability,
19
20
  } from "./responses/thought-signature-replay";
20
21
  import { resolveStallTimeoutSec } from "./stall-timeout";
22
+ import { normalizeDeclaredToolName } from "./types";
21
23
  import { usageDisplayTotalTokens } from "./usage/totals";
22
24
  import { appendSafeWebSearchSource, safeWebSearchSources } from "./web-search/sources";
23
25
  import {
@@ -27,6 +29,7 @@ import {
27
29
  type TranslatorBudget,
28
30
  type TranslatorBufferKind,
29
31
  } from "./lib/translator-budget";
32
+ import { debugFingerprint, debugStreamDiagnostic, type DebugStreamDiagnosticContext } from "./lib/debug";
30
33
 
31
34
  function uuid(): string {
32
35
  return crypto.randomUUID().replace(/-/g, "");
@@ -117,7 +120,9 @@ function adapterFailureFromEvent(event: Extract<AdapterEvent, { type: "error" }>
117
120
  }
118
121
  const fallback = adapterFailureFromMessage(event.message);
119
122
  let httpStatus = event.status ?? fallback.httpStatus;
120
- const error = classifyError(httpStatus, event.errorType ?? fallback.error.type, event.message);
123
+ const error = httpStatus === 400 && event.errorType === "upstream_error"
124
+ ? { message: event.message, type: "upstream_error", code: event.code ?? null }
125
+ : classifyError(httpStatus, event.errorType ?? fallback.error.type, event.message);
121
126
  if (event.errorType !== undefined) error.type = event.errorType;
122
127
  if (event.code !== undefined) error.code = event.code;
123
128
  // Codex maps cyber_policy on HTTP 400 (body) or mid-stream code; never leave it as 502.
@@ -165,6 +170,88 @@ interface OutputItem {
165
170
 
166
171
  export type ResponsesTerminalStatus = "completed" | "failed" | "incomplete";
167
172
 
173
+ export interface BridgeDiagnosticSequence { value: number }
174
+
175
+ export interface BridgeDiagnosticContext extends DebugStreamDiagnosticContext {
176
+ sequence?: BridgeDiagnosticSequence;
177
+ }
178
+
179
+ export function adapterEventDiagnosticDetails(event: AdapterEvent): Record<string, unknown> {
180
+ switch (event.type) {
181
+ case "text_delta":
182
+ return { byteLength: Buffer.byteLength(event.text), fingerprint: debugFingerprint(event.text) };
183
+ case "thinking_delta":
184
+ return { byteLength: Buffer.byteLength(event.thinking), fingerprint: debugFingerprint(event.thinking) };
185
+ case "reasoning_raw_delta":
186
+ return { byteLength: Buffer.byteLength(event.text), fingerprint: debugFingerprint(event.text) };
187
+ case "thinking_signature":
188
+ case "redacted_thinking":
189
+ case "kiro_redacted_reasoning": {
190
+ const content = event.type === "thinking_signature" ? event.signature : event.data;
191
+ return { byteLength: Buffer.byteLength(content), fingerprint: debugFingerprint(content) };
192
+ }
193
+ case "tool_call_delta":
194
+ return { byteLength: Buffer.byteLength(event.arguments), fingerprint: debugFingerprint(event.arguments) };
195
+ case "tool_call_start":
196
+ return {
197
+ idByteLength: Buffer.byteLength(event.id),
198
+ idFingerprint: debugFingerprint(event.id),
199
+ nameByteLength: Buffer.byteLength(event.name),
200
+ nameFingerprint: debugFingerprint(event.name),
201
+ };
202
+ case "web_search_call_begin":
203
+ return { idByteLength: Buffer.byteLength(event.id), idFingerprint: debugFingerprint(event.id) };
204
+ case "web_search_call_end": {
205
+ const queries = JSON.stringify(event.queries);
206
+ return {
207
+ idByteLength: Buffer.byteLength(event.id),
208
+ idFingerprint: debugFingerprint(event.id),
209
+ byteLength: Buffer.byteLength(queries),
210
+ fingerprint: debugFingerprint(queries),
211
+ status: event.status,
212
+ };
213
+ }
214
+ case "error":
215
+ return {
216
+ byteLength: Buffer.byteLength(event.message),
217
+ fingerprint: debugFingerprint(event.message),
218
+ ...(event.status !== undefined ? { status: event.status } : {}),
219
+ ...(event.code !== undefined
220
+ ? { codeByteLength: Buffer.byteLength(event.code), codeFingerprint: debugFingerprint(event.code) }
221
+ : {}),
222
+ ...(event.retryable !== undefined ? { retryable: event.retryable } : {}),
223
+ };
224
+ case "incomplete":
225
+ return {
226
+ ...(event.message !== undefined ? { byteLength: Buffer.byteLength(event.message), fingerprint: debugFingerprint(event.message) } : {}),
227
+ reasonByteLength: Buffer.byteLength(event.reason),
228
+ reasonFingerprint: debugFingerprint(event.reason),
229
+ ...(event.retryable !== undefined ? { retryable: event.retryable } : {}),
230
+ };
231
+ case "done":
232
+ return {
233
+ ...(event.stopReason !== undefined
234
+ ? { stopReasonByteLength: Buffer.byteLength(event.stopReason), stopReasonFingerprint: debugFingerprint(event.stopReason) }
235
+ : {}),
236
+ ...(event.endTurn !== undefined ? { endTurn: event.endTurn } : {}),
237
+ };
238
+ default:
239
+ return {};
240
+ }
241
+ }
242
+
243
+ /** Emit one adapter-stage diagnostic while preserving one sequence across sidecar iterations. */
244
+ export function diagnoseAdapterEvent(context: BridgeDiagnosticContext, event: AdapterEvent): void {
245
+ const sequence = context.sequence ??= { value: 0 };
246
+ debugStreamDiagnostic(
247
+ context,
248
+ "adapter",
249
+ ++sequence.value,
250
+ event.type,
251
+ adapterEventDiagnosticDetails(event),
252
+ );
253
+ }
254
+
168
255
  export function bridgeToResponsesSSE(
169
256
  events: AsyncIterable<AdapterEvent>,
170
257
  modelId: string,
@@ -225,17 +312,19 @@ export function bridgeToResponsesSSE(
225
312
  setInterval: (handler: () => void, ms: number) => unknown;
226
313
  clearInterval: (id: unknown) => void;
227
314
  };
315
+ /** Internal, opt-in structural stream diagnostics. */
316
+ diagnostic?: BridgeDiagnosticContext;
228
317
  },
229
318
  ): ReadableStream<Uint8Array> {
230
319
  const replayCacheScope = options?.replayCacheScope;
231
320
  const setBeatInterval = options?.timers?.setInterval ?? ((handler: () => void, ms: number) => setInterval(handler, ms));
232
321
  const clearBeatInterval = options?.timers?.clearInterval ?? ((id: unknown) => clearInterval(id as ReturnType<typeof setInterval>));
233
- // Freeform/custom tools (apply_patch) carry their body in `input`; the model is given a
234
- // function with `{input:string}`, so unwrap it here when relaying back as a custom_tool_call.
235
- const freeformInput = (args: string): string => {
236
- try { const o = JSON.parse(args); if (o && typeof o.input === "string") return o.input; } catch { /* raw */ }
237
- return args;
238
- };
322
+ // Freeform/custom tools (apply_patch, code-mode exec) carry their body in `input`; the
323
+ // model is given a function with `{input:string}`, so unwrap it here when relaying back
324
+ // as a custom_tool_call. Decorated apply_patch envelopes are repaired at this boundary.
325
+ const freeformInput = (args: string, toolName: string, namespace?: string): string => (
326
+ repairFreeformToolInput(args, toolName, namespace)
327
+ );
239
328
  // Best-effort unwrap of a PARTIAL freeform arg buffer for live input streaming
240
329
  // (`response.custom_tool_call_input.delta` — codex-rs uses it for UI preview only;
241
330
  // the completed custom_tool_call item stays authoritative). Compact `{"input":"...`
@@ -325,6 +414,7 @@ export function bridgeToResponsesSSE(
325
414
  };
326
415
  const responseId = options?.responseId ?? `resp_${uuid()}`;
327
416
  let seq = 0;
417
+ let diagnosticSequence = 0;
328
418
  // Set once the client is gone (cancel) or an enqueue throws on a torn-down controller, so we
329
419
  // never enqueue again and never throw a second time inside start() — the RC2 double-throw that
330
420
  // otherwise surfaced as proxy-side stream noise on every client disconnect.
@@ -626,6 +716,7 @@ export function bridgeToResponsesSSE(
626
716
  const argsStr = coerceIntegerToolArguments(
627
717
  currentToolCall.args || "{}",
628
718
  options?.toolParameterSchemas?.get(currentToolCall.name),
719
+ currentToolCall.namespace === undefined ? currentToolCall.name : undefined,
629
720
  );
630
721
  // Finalize streamed function-call arguments so Codex commits the call (incl. MCP / computer_use).
631
722
  if (!currentToolCall.freeform && !currentToolCall.toolSearch) {
@@ -636,7 +727,8 @@ export function bridgeToResponsesSSE(
636
727
  if (currentToolCall.freeform) {
637
728
  emit("response.custom_tool_call_input.done", {
638
729
  item_id: currentToolCall.itemId, output_index: currentToolCall.outputIndex,
639
- input: freeformInput(currentToolCall.args),
730
+ ...(currentToolCall.namespace ? { namespace: currentToolCall.namespace } : {}),
731
+ input: freeformInput(currentToolCall.args, currentToolCall.name, currentToolCall.namespace),
640
732
  });
641
733
  }
642
734
  // Freeform tools serialize as custom_tool_call without extra_content; remember the
@@ -652,7 +744,8 @@ export function bridgeToResponsesSSE(
652
744
  ? {
653
745
  type: "custom_tool_call", id: currentToolCall.itemId,
654
746
  call_id: currentToolCall.callId, name: currentToolCall.name,
655
- input: freeformInput(currentToolCall.args), status: "completed",
747
+ ...(currentToolCall.namespace ? { namespace: currentToolCall.namespace } : {}),
748
+ input: freeformInput(currentToolCall.args, currentToolCall.name, currentToolCall.namespace), status: "completed",
656
749
  }
657
750
  : {
658
751
  type: "function_call", id: currentToolCall.itemId,
@@ -691,7 +784,8 @@ export function bridgeToResponsesSSE(
691
784
  ? {
692
785
  type: "custom_tool_call", id: currentToolCall.itemId,
693
786
  call_id: currentToolCall.callId, name: currentToolCall.name,
694
- input: freeformInput(currentToolCall.args), status: "incomplete",
787
+ ...(currentToolCall.namespace ? { namespace: currentToolCall.namespace } : {}),
788
+ input: freeformInput(currentToolCall.args, currentToolCall.name, currentToolCall.namespace), status: "incomplete",
695
789
  }
696
790
  : {
697
791
  type: "function_call", id: currentToolCall.itemId,
@@ -853,6 +947,17 @@ export function bridgeToResponsesSSE(
853
947
  }
854
948
  if (next.done) { upstreamDone = true; break; }
855
949
  const event = next.value;
950
+ if (options?.diagnostic) {
951
+ debugStreamDiagnostic(
952
+ options.diagnostic,
953
+ "bridge",
954
+ options.diagnostic.sequence
955
+ ? ++options.diagnostic.sequence.value
956
+ : ++diagnosticSequence,
957
+ event.type,
958
+ adapterEventDiagnosticDetails(event),
959
+ );
960
+ }
856
961
  let terminalEvent = false;
857
962
  // Invisible adapter heartbeats (and buffered web-search progress) count as upstream
858
963
  // liveness only — they must not suppress wire keepalives that re-arm Codex idle timers.
@@ -1036,13 +1141,14 @@ export function bridgeToResponsesSSE(
1036
1141
  rememberReasoningForCall(event.id, rawReasoningForNextToolCall, replayCacheScope);
1037
1142
  }
1038
1143
  if (currentToolCall) closeCurrentToolCall();
1039
- const mapped = toolNsMap?.get(event.name);
1040
- const realName = mapped?.name ?? event.name;
1041
- if (options?.declaredToolNames && !options.declaredToolNames.has(event.name)) {
1144
+ const effectiveName = normalizeDeclaredToolName(event.name, options?.declaredToolNames);
1145
+ const mapped = toolNsMap?.get(effectiveName);
1146
+ const realName = mapped?.name ?? effectiveName;
1147
+ if (options?.declaredToolNames && !options.declaredToolNames.has(effectiveName)) {
1042
1148
  const failure = responseError(
1043
1149
  502,
1044
1150
  "upstream_error",
1045
- `routed provider emitted undeclared client tool "${event.name}"; only request-declared tools may be called`,
1151
+ `routed provider emitted undeclared client tool "${effectiveName}"; only request-declared tools may be called`,
1046
1152
  );
1047
1153
  emit("response.failed", {
1048
1154
  response: {
@@ -1064,7 +1170,7 @@ export function bridgeToResponsesSSE(
1064
1170
  const item = toolSearch
1065
1171
  ? { type: "tool_search_call", id: itemId, call_id: event.id, execution: "client", arguments: {}, status: "in_progress" }
1066
1172
  : freeform
1067
- ? { type: "custom_tool_call", id: itemId, call_id: event.id, name: realName, input: "", status: "in_progress" }
1173
+ ? { type: "custom_tool_call", id: itemId, call_id: event.id, name: realName, ...(ns ? { namespace: ns } : {}), input: "", status: "in_progress" }
1068
1174
  : { type: "function_call", id: itemId, call_id: event.id, name: realName, arguments: "", status: "in_progress", ...(ns ? { namespace: ns } : {}) };
1069
1175
  emit("response.output_item.added", { output_index: outputIndex, item });
1070
1176
  currentToolCall = { itemId, outputIndex, callId: event.id, name: realName, args: "", argsBytes: 0, namespace: ns, freeform, toolSearch, providerMetadata: event.providerMetadata };
@@ -1567,10 +1673,9 @@ function buildResponseJSONWithBudget(
1567
1673
  // Web-search citations awaiting the next assistant message (attached as url_citation annotations).
1568
1674
  let pendingWebSources: { url: string; title?: string }[] = [];
1569
1675
 
1570
- const freeformInput = (args: string): string => {
1571
- try { const o = JSON.parse(args); if (o && typeof o.input === "string") return o.input; } catch { /* raw */ }
1572
- return args;
1573
- };
1676
+ const freeformInput = (args: string, toolName: string, namespace?: string): string => (
1677
+ repairFreeformToolInput(args, toolName, namespace)
1678
+ );
1574
1679
  const parseArgsObj = (args: string): Record<string, unknown> => {
1575
1680
  try { const o = JSON.parse(args); return o && typeof o === "object" ? o : {}; } catch { return {}; }
1576
1681
  };
@@ -1656,6 +1761,7 @@ function buildResponseJSONWithBudget(
1656
1761
  const coercedArgs = coerceIntegerToolArguments(
1657
1762
  currentToolCallArgs,
1658
1763
  options?.toolParameterSchemas?.get(currentToolCallName),
1764
+ ns === undefined ? realName : undefined,
1659
1765
  );
1660
1766
  // Freeform tools serialize as custom_tool_call without extra_content; remember the
1661
1767
  // signature server-side regardless so the replayed call can be re-signed (#1735).
@@ -1670,7 +1776,8 @@ function buildResponseJSONWithBudget(
1670
1776
  pushOutput({
1671
1777
  type: "custom_tool_call", id: `ctc_${uuid()}`,
1672
1778
  call_id: currentToolCallId, name: realName,
1673
- input: freeformInput(currentToolCallArgs), status,
1779
+ ...(ns ? { namespace: ns } : {}),
1780
+ input: freeformInput(currentToolCallArgs, realName, ns), status,
1674
1781
  });
1675
1782
  } else {
1676
1783
  pushOutput({
@@ -1777,7 +1884,7 @@ function buildResponseJSONWithBudget(
1777
1884
  ));
1778
1885
  }
1779
1886
  break;
1780
- case "tool_call_start":
1887
+ case "tool_call_start": {
1781
1888
  if (currentText) flushText("commentary");
1782
1889
  if (currentSummaryReasoning) flushSummaryReasoning();
1783
1890
  if (currentRawReasoning) flushRawReasoning();
@@ -1785,10 +1892,11 @@ function buildResponseJSONWithBudget(
1785
1892
  rememberReasoningForCall(e.id, rawReasoningForNextToolCall, replayCacheScope);
1786
1893
  }
1787
1894
  flushToolCall();
1788
- if (options?.declaredToolNames && !options.declaredToolNames.has(e.name)) {
1895
+ const effectiveName = normalizeDeclaredToolName(e.name, options?.declaredToolNames);
1896
+ if (options?.declaredToolNames && !options.declaredToolNames.has(effectiveName)) {
1789
1897
  errorEvent = {
1790
1898
  type: "error",
1791
- message: `routed provider emitted undeclared client tool "${e.name}"; only request-declared tools may be called`,
1899
+ message: `routed provider emitted undeclared client tool "${effectiveName}"; only request-declared tools may be called`,
1792
1900
  status: 502,
1793
1901
  errorType: "upstream_error",
1794
1902
  };
@@ -1796,11 +1904,12 @@ function buildResponseJSONWithBudget(
1796
1904
  }
1797
1905
  currentToolCallId = e.id;
1798
1906
  budget?.openCall(e.id);
1799
- currentToolCallName = e.name;
1907
+ currentToolCallName = effectiveName;
1800
1908
  currentToolCallArgs = "";
1801
1909
  currentToolCallArgsBytes = 0;
1802
1910
  currentToolCallProviderMetadata = e.providerMetadata;
1803
1911
  break;
1912
+ }
1804
1913
  case "tool_call_delta":
1805
1914
  {
1806
1915
  ({ value: currentToolCallArgs, bytes: currentToolCallArgsBytes } = appendBatchString(
@@ -118,17 +118,24 @@ export function buildClaudeContextWindows(
118
118
  put(desktop3pAlias("native", slug), window);
119
119
  put(aliasForNative(slug), window);
120
120
  }
121
+ // Anthropic passthrough guard (audit 021 #3): canonical claude ids ride the
122
+ // subscription passthrough — marking a sub-1M one would strap [1m]/1M-beta onto
123
+ // a model that cannot host it. Register anthropic rows only at >=1M.
124
+ const registrable = routedModels.filter(
125
+ m =>
126
+ typeof m.contextWindow === "number" &&
127
+ m.contextWindow > 0 &&
128
+ !(m.provider === "anthropic" && m.contextWindow < ONE_MILLION),
129
+ );
121
130
  // Bare routed ids are registered only when unambiguous across providers (audit
122
- // 021 #5) — natives are registered first, so a native slug always wins the bare key.
131
+ // 021 #5) — natives are registered first, so a native slug always wins the bare
132
+ // key. Counted over the rows that can actually claim the key: a row this loop
133
+ // skips contributes no window, so letting it veto the bare key withholds an
134
+ // answer that was never in doubt.
123
135
  const bareCounts = new Map<string, number>();
124
- for (const m of routedModels) bareCounts.set(m.id, (bareCounts.get(m.id) ?? 0) + 1);
125
- for (const m of routedModels) {
126
- const window = m.contextWindow;
127
- if (typeof window !== "number" || window <= 0) continue;
128
- // Anthropic passthrough guard (audit 021 #3): canonical claude ids ride the
129
- // subscription passthrough — marking a sub-1M one would strap [1m]/1M-beta onto
130
- // a model that cannot host it. Register anthropic rows only at >=1M.
131
- if (m.provider === "anthropic" && window < ONE_MILLION) continue;
136
+ for (const m of registrable) bareCounts.set(m.id, (bareCounts.get(m.id) ?? 0) + 1);
137
+ for (const m of registrable) {
138
+ const window = m.contextWindow as number;
132
139
  put(`${m.provider}/${m.id}`, window);
133
140
  put(desktop3pAlias(m.provider, m.id), window);
134
141
  put(aliasForRoute(m.provider, m.id), window);
@@ -203,7 +203,11 @@ const commandRunners: Record<string, CommandRunner> = {
203
203
  return 0;
204
204
  },
205
205
  sync: async deps => {
206
- const restartCodex = deps.args.slice(1).includes("--restart-codex");
206
+ const syncArgs = deps.args.slice(1);
207
+ const restartCodex = syncArgs.includes("--restart-codex");
208
+ // Separate flag on purpose: --restart-codex promises app-server-only scope,
209
+ // and quitting the desktop app ends live conversations.
210
+ const restartDesktopApp = syncArgs.includes("--restart-desktop-app");
207
211
  const live = await deps.findLiveProxy();
208
212
  const synced = await syncModelsToCodex(
209
213
  live?.port,
@@ -229,6 +233,7 @@ const commandRunners: Record<string, CommandRunner> = {
229
233
  // exactly when a long-lived app-server is holding the stale list.
230
234
  if (synced.catalogWritten || synced.cacheSynced) {
231
235
  afterCatalogWriteHandleAppServers({ restart: restartCodex, log: console });
236
+ if (restartDesktopApp) await handleDesktopAppRestart(console);
232
237
  }
233
238
  // `ocx sync` is a direct CLI path; it does not call the management
234
239
  // `/api/sync` route. Refresh the already-connected MCode block here too,
@@ -259,7 +264,9 @@ const commandRunners: Record<string, CommandRunner> = {
259
264
  return await cmdV2(deps.args.slice(1), {}, async () => (await deps.findLiveProxy())?.port);
260
265
  },
261
266
  "sync-cache": async deps => {
262
- const restartCodex = deps.args.slice(1).includes("--restart-codex");
267
+ const cacheArgs = deps.args.slice(1);
268
+ const restartCodex = cacheArgs.includes("--restart-codex");
269
+ const restartDesktopApp = cacheArgs.includes("--restart-desktop-app");
263
270
  const { withCatalogWriteSerialization } = await import("../codex/catalog-write-serialization");
264
271
  const { invalidateCodexModelsCacheWithPermit } = await import("../codex/catalog/sync");
265
272
  const { getCodexHome } = await import("../codex/paths");
@@ -270,6 +277,7 @@ const commandRunners: Record<string, CommandRunner> = {
270
277
  // Only warn/restart when models_cache was actually rewritten from a readable catalog.
271
278
  if (invalidated.kind === "completed" && invalidated.value) {
272
279
  afterCatalogWriteHandleAppServers({ restart: restartCodex, log: console });
280
+ if (restartDesktopApp) await handleDesktopAppRestart(console);
273
281
  } else if (desiredDisabled) {
274
282
  console.log("Codex integration is OFF; cache sync skipped (no catalog or cache write).");
275
283
  }
@@ -583,3 +591,43 @@ export async function dispatchCommand(head: CliHead, deps: CliDispatchDeps): Pro
583
591
  }
584
592
  return await runner(deps);
585
593
  }
594
+
595
+ /**
596
+ * Report the outcome of an opt-in desktop-app restart. Kept next to the two
597
+ * callers so `sync` and `sync-cache` cannot drift in what they tell the user.
598
+ */
599
+ async function handleDesktopAppRestart(log: Pick<Console, "log" | "error">): Promise<void> {
600
+ const { restartCodexDesktopApp } = await import("../codex/desktop-app-restart");
601
+ const result = restartCodexDesktopApp();
602
+ switch (result.reason) {
603
+ case "windows_only":
604
+ log.error("--restart-desktop-app is supported on Windows only; nothing was stopped.");
605
+ return;
606
+ case "package_discovery_failed":
607
+ log.error(
608
+ "Could not identify the installed Codex desktop package. Quit and relaunch the desktop app "
609
+ + "manually to refresh the model picker.",
610
+ );
611
+ return;
612
+ case "self_ancestry":
613
+ log.error(
614
+ "Refusing to restart the desktop app because this command is running inside it. "
615
+ + "Run 'ocx sync --restart-desktop-app' from an external terminal instead.",
616
+ );
617
+ return;
618
+ case "no_targets":
619
+ log.log("Codex desktop app is not running; nothing to restart.");
620
+ return;
621
+ case "targets_survived":
622
+ log.error(
623
+ `Codex desktop app PID(s) ${result.surviving.join(", ")} did not exit, so it was not relaunched. `
624
+ + "Quit the desktop app manually to refresh the model picker.",
625
+ );
626
+ return;
627
+ default:
628
+ if (result.relaunch === "started") {
629
+ log.log("Codex desktop app restarted; its model picker will re-read the catalog.");
630
+ }
631
+ }
632
+ }
633
+
package/src/cli/doctor.ts CHANGED
@@ -10,7 +10,8 @@
10
10
  import { accessSync, constants, existsSync, readFileSync } from "node:fs";
11
11
  import { homedir } from "node:os";
12
12
  import { dirname, join } from "node:path";
13
- import { getConfigDir, getConfigPath, readConfigDiagnostics, readPid, resolveEnvValue } from "../config";
13
+ import { getConfigDir, getConfigPath, readConfigDiagnostics, resolveEnvValue } from "../config";
14
+ import { readPid } from "../config/process-state";
14
15
  import { findLiveProxy, type LiveProxy } from "../server/proxy-liveness";
15
16
  import { BUN_RUNTIME_SOURCES } from "../lib/bun-runtime";
16
17
  import type { BunRuntimeSource } from "../lib/bun-runtime";
@@ -41,7 +42,7 @@ import {
41
42
  resolveEffectiveUserIdentity,
42
43
  } from "../codex/user-identity";
43
44
  import { collectProjectCodexConfigWarnings, formatProjectCodexConfigWarningsForDoctor } from "../codex/project-config-warnings";
44
- import { collectStartupHealth, startupHealthSummary } from "../codex/autostart-health";
45
+ import { collectStartupHealth, formatStartupRoutingDetail, startupHealthSummary } from "../codex/autostart-health";
45
46
  import {
46
47
  displayCodexRuntimePath,
47
48
  loadLastEffortClamp,
@@ -982,7 +983,7 @@ export async function runDoctor(args: string[] = []): Promise<void> {
982
983
  const startup = collectStartupHealth(doctorConfig);
983
984
  console.log("\nCodex restart safety");
984
985
  console.log(` ${startup.rebootSafe ? "ok " : "!! "} ${startupHealthSummary(startup)}`);
985
- console.log(` routing=${startup.routingKind}, service=${startup.serviceViable ? "viable" : startup.serviceInstalled ? "installed-but-unhealthy" : "absent"}, shim=${startup.shimHealthy ? "healthy" : startup.shimInstalled ? "stale" : "absent"}`);
986
+ console.log(` ${formatStartupRoutingDetail(startup)}`);
986
987
 
987
988
  console.log("\nCodex runtime selection");
988
989
  {
@@ -1072,10 +1073,10 @@ export async function runDoctor(args: string[] = []): Promise<void> {
1072
1073
  console.log(` ${probe.ok ? "ok " : "-- "} ${WHAM_USAGE_URL}`);
1073
1074
  console.log(` ${detail}, ${probe.durationMs}ms, ${probe.authenticated ? "authenticated" : "unauthenticated"}`);
1074
1075
 
1075
- // Design B upgrade visibility: threads still tagged opencodex are invisible to the native
1076
- // Codex app until the one-time migration lands. Read-only probe (readonly sqlite, 100ms
1077
- // busy timeout) reports state, never mutates.
1078
- console.log("\nCodex history migration");
1076
+ // Design B upgrade visibility: only the backup manifest authorizes restoring provider
1077
+ // metadata. Bare routed rows have unknown provenance and remain unchanged. This read-only
1078
+ // probe reports manifest work and database readability; it never mutates.
1079
+ console.log("\nCodex history metadata restore");
1079
1080
  // The history failure messages point here; make the visit worthwhile by
1080
1081
  // probing the coordinator namespace the locks live in. The probe exercises
1081
1082
  // identity, runtime-root, and permission checks without taking any lock or
@@ -1096,11 +1097,17 @@ export async function runDoctor(args: string[] = []): Promise<void> {
1096
1097
  for (const line of formatCoordinatorDoctorLines(inspectCodexCoordinator())) console.log(line);
1097
1098
  const pending = countPendingOpencodexHistory();
1098
1099
  if (pending.failed) {
1099
- console.log(" -- state DB locked or unreadable (Codex app open?) — migration state unknown");
1100
+ if (pending.failureReason === "busy") {
1101
+ console.log(" -- history database, backup manifest, or rollout file is busy — exact metadata restore is pending");
1102
+ } else if (pending.failureReason === "permission") {
1103
+ console.log(" -- state DB or backup manifest access was denied — restore state unknown");
1104
+ } else {
1105
+ console.log(" -- backup manifest or restore target failed integrity checks — manual review required");
1106
+ }
1100
1107
  } else if (pending.pendingRows === 0 && pending.backupEntries === 0) {
1101
- console.log(" ok no legacy opencodex-tagged threads pending");
1108
+ console.log(" ok no manifest-backed provider metadata pending; untracked routed history is unchanged");
1102
1109
  } else {
1103
- console.log(` -- ${pending.pendingRows} thread(s) still tagged opencodex, ${pending.backupEntries} backup manifest entr${pending.backupEntries === 1 ? "y" : "ies"}`);
1110
+ console.log(` -- ${pending.backupEntries} backup manifest entr${pending.backupEntries === 1 ? "y" : "ies"} pending exact metadata restore`);
1104
1111
  }
1105
1112
 
1106
1113
  console.log("\nProject Codex configs");
@@ -1148,7 +1155,7 @@ export async function runDoctor(args: string[] = []): Promise<void> {
1148
1155
  const { collectCodexAppServerCatalogState } = await import("../codex/app-server-processes");
1149
1156
  const catalogState = collectCodexAppServerCatalogState();
1150
1157
  if (catalogState.state === "stale") {
1151
- console.log(` [WARN] Codex app-server (PID(s): ${catalogState.processes.map(p => p.pid).join(", ")}) started before the on-disk catalog changed; its in-memory model list disagrees with ocx. Action: restart Codex (or run \`ocx sync --restart-codex\`)`);
1158
+ console.log(` [WARN] Codex app-server (PID(s): ${catalogState.processes.map(p => p.pid).join(", ")}) started before the on-disk catalog changed; its in-memory model list disagrees with ocx. Action: restart Codex (or run \`ocx sync --restart-codex\`; on Windows the desktop app may need \`ocx sync --restart-desktop-app\`)`);
1152
1159
  } else if (catalogState.state === "unknown") {
1153
1160
  console.log(" [WARN] Could not verify whether the running Codex app-server's model catalog is current (start time or catalog unreadable). Action: if the model list looks stale, restart Codex");
1154
1161
  } else if (catalogState.state === "fresh") {
@@ -1185,8 +1192,14 @@ export async function runDoctor(args: string[] = []): Promise<void> {
1185
1192
  }
1186
1193
  }
1187
1194
  }
1188
- if (pending.failed || pending.pendingRows > 0 || pending.backupEntries > 0) {
1189
- hints.push("Legacy chat threads are still tagged opencodex (or the DB was locked). The running proxy retries the migration automatically; to force it now, close the Codex app and run 'ocx sync'.");
1195
+ if (pending.failed && pending.failureReason === "busy") {
1196
+ hints.push("Backed-up history metadata is pending or its state is unreadable. The running proxy retries exact restoration automatically; to force it now, close the Codex app and run 'ocx sync'. Untracked routed history is not relabeled.");
1197
+ } else if (pending.failed && pending.failureReason === "permission") {
1198
+ hints.push("Backed-up history metadata could not be inspected because access was denied. Fix access to the reported Codex history paths, then run 'ocx sync'; repeated retries do not repair permissions.");
1199
+ } else if (pending.failed) {
1200
+ hints.push("The history manifest or its target is invalid or changed. Preserve both, inspect the manifest/database/rollout identity, and do not repeatedly run 'ocx sync' until the mismatch is understood. Untracked routed history is not relabeled.");
1201
+ } else if (pending.backupEntries > 0) {
1202
+ hints.push("Backed-up history metadata is pending. The running proxy retries exact restoration automatically; to force it now, close the Codex app and run 'ocx sync'. Untracked routed history is not relabeled.");
1190
1203
  }
1191
1204
  if (dual.dualInstall && !dual.effectiveIsWindowsMount) {
1192
1205
  hints.push(`Codex is installed on BOTH WSL and Windows. Each side keeps its own ~/.codex (logins, config, catalog are separate); ocx here manages the Linux one. To share a single home, set CODEX_HOME=${dual.windowsCodexHomes[0] ?? `${dual.automountRoot}/c/Users/<you>/.codex`} in WSL (drvfs file locking is less reliable).`);
package/src/cli/help.ts CHANGED
@@ -24,8 +24,8 @@ Usage:
24
24
  ocx stop Stop the proxy AND restore native Codex (plain codex works again)
25
25
  ocx restore Restore native Codex without stopping (alias: eject)
26
26
  ocx restore back Re-point codex at the running proxy (undo restore)
27
- ocx recover-history --legacy-openai
28
- Explicitly recover pre-backup syncResumeHistory rows
27
+ ocx recover-history --legacy-openai --yes
28
+ Force all user-message opencodex rows to OpenAI (legacy recovery)
29
29
  ocx uninstall Remove service/shim/config and restore native Codex (alias: remove)
30
30
  ocx service [sub] Run as a background service (default: install/update/start)
31
31
  ocx codex-shim <sub> Auto-start proxy when \`codex\` launches (install|status|uninstall|remove)
@@ -57,7 +57,8 @@ Usage:
57
57
  ocx observe <sub> Logs, usage, storage, memory, and debug data
58
58
  ocx route <sub> Routing features (combo, policy)
59
59
  ocx logs [filters] Alias of ocx observe logs
60
- ocx usage [--range <7d|30d|all>] Alias of ocx observe usage
60
+ ocx usage [--range <today|1d|7d|30d|all>] [--provider <name>] [--model <id>]
61
+ Token and estimated-cost report (alias of ocx observe usage)
61
62
  ocx storage [--json] Alias of ocx observe storage
62
63
  ocx memory [--json] Alias of ocx observe memory
63
64
  ocx api-key <sub> Alias of ocx access key
package/src/cli/index.ts CHANGED
@@ -12,6 +12,9 @@ import {
12
12
  codexAutoStartEnabled,
13
13
  getConfigDir,
14
14
  loadConfig,
15
+ saveConfig,
16
+ } from "../config";
17
+ import {
15
18
  readPid,
16
19
  readPidFileValue,
17
20
  readRuntimePort,
@@ -19,11 +22,10 @@ import {
19
22
  removePidIfValueIs,
20
23
  removeRuntimePort,
21
24
  removeRuntimePortIfPidIs,
22
- saveConfig,
23
25
  writePid,
24
26
  writeRuntimePort,
25
- } from "../config";
26
- import { collectStatus } from "./status";
27
+ } from "../config/process-state";
28
+ import { collectStatus, unusedProxyWarningLines } from "./status";
27
29
 
28
30
  import {
29
31
  discoverStableProxyForRestart,
@@ -44,7 +46,7 @@ import { runCli } from "./root";
44
46
  import { ProxyOwnershipRefusedError, stopProxy } from "../lib/process-control";
45
47
  import { loadServiceTokenFromFile } from "../lib/service-secrets";
46
48
  import { diagnoseService, isServiceOwnershipError, serviceCommand, serviceEnvironmentOwnedHere, serviceStartableFromTray, serviceStatusSummary, stopServiceIfInstalled, uninstallServiceIfInstalled } from "../service";
47
- import { startupHealthSummary } from "../codex/autostart-health";
49
+ import { formatStartupRoutingDetail, startupHealthSummary } from "../codex/autostart-health";
48
50
  import { drainAndShutdown, isRecyclingForExit, startServer } from "../server";
49
51
  import { injectSystemEnv, reconcileShellHook, revertSystemEnv, uninstallShellHook } from "../server/system-env";
50
52
  import { buildDesktop3pRegistry } from "../claude/desktop-3p";
@@ -847,6 +849,12 @@ async function handleStatus() {
847
849
  console.log(`❌ Proxy: ${status.proxyLabel}`);
848
850
  }
849
851
  console.log(` Health: ${status.healthLabel}`);
852
+ for (const line of unusedProxyWarningLines({
853
+ proxyUp: Boolean(status.json.proxy.pid || status.json.proxy.health.ok),
854
+ routingKind: status.json.startup.routingKind,
855
+ })) {
856
+ console.log(` ${line}`);
857
+ }
850
858
  if (!(status.json.proxy.pid || status.json.proxy.health.ok)) {
851
859
  console.log(" ↳ Not running — Codex/Claude requests will fail with connection errors.");
852
860
  // The service summary a few lines below already tells a registered-but-not-serving
@@ -866,6 +874,7 @@ async function handleStatus() {
866
874
  console.log(` Default provider: ${status.json.defaultProvider}`);
867
875
  console.log(` Codex autostart: ${status.json.codexAutostart ? "enabled" : "disabled"}`);
868
876
  console.log(` Restart safety: ${startupHealthSummary(status.json.startup)}`);
877
+ console.log(` ${formatStartupRoutingDetail(status.json.startup)}`);
869
878
  console.log(` Service: ${status.json.service.summary}`);
870
879
  console.log(` ${status.json.codexShim.summary}`);
871
880
  console.log(` Codex runtime: ${status.json.codexRuntime.path}`);
@@ -906,8 +915,13 @@ async function handleStatus() {
906
915
 
907
916
  async function handleRecoverHistory() {
908
917
  if (args[1] !== "--legacy-openai") {
909
- console.error("Usage: ocx recover-history --legacy-openai");
910
- console.error("Only use this if an older syncResumeHistory build already remapped OpenAI Codex App history to opencodex before backup support existed.");
918
+ console.error("Usage: ocx recover-history --legacy-openai --yes");
919
+ console.error("This force-relabels every user-message opencodex row to OpenAI, including legitimate dedicated-provider history. Back up first and use it only for pre-backup legacy recovery.");
920
+ process.exit(1);
921
+ }
922
+ console.error("WARNING: this force-relabels every user-message opencodex row to OpenAI, normalizes exec to cli, and includes legitimate dedicated-provider history.");
923
+ if (args.length !== 3 || args[2] !== "--yes") {
924
+ console.error("Re-run with explicit confirmation: ocx recover-history --legacy-openai --yes");
911
925
  process.exit(1);
912
926
  }
913
927
  // Manifest-independent legacy ejection, serialized like every other history
package/src/cli/models.ts CHANGED
@@ -5,7 +5,12 @@ import { randomUUID } from "node:crypto";
5
5
  import { createInterface } from "node:readline/promises";
6
6
  import { syncModelsToCodex } from "../codex/sync";
7
7
  import { hasOwnProvider, isValidProviderName, loadConfig, saveConfig } from "../config";
8
- import { canonicalizeReasoningEfforts, isDeclaredReasoningEffort, modelRecordValue } from "../reasoning-effort";
8
+ import {
9
+ canonicalizeReasoningEfforts,
10
+ configuredReasoningEfforts,
11
+ isDeclaredReasoningEffort,
12
+ modelRecordValue,
13
+ } from "../reasoning-effort";
9
14
  import { encodedModelIdCollides, routedSlug, slugEquals } from "../providers/slug-codec";
10
15
  import { knownModelIdsForProvider } from "../router";
11
16
  import { findLiveProxy } from "../server/proxy-liveness";
@@ -91,7 +96,6 @@ function collectModels(config: OcxConfig, providerFilter?: string): ModelEntry[]
91
96
  const seen = new Set<string>();
92
97
  const contextWindows = prov.modelContextWindows ?? {};
93
98
  const inputModalities = prov.modelInputModalities ?? {};
94
- const reasoningEfforts = prov.modelReasoningEfforts ?? {};
95
99
  const globalContext = prov.contextWindow ?? null;
96
100
 
97
101
  const addModel = (model: string, isDefault: boolean) => {
@@ -107,7 +111,13 @@ function collectModels(config: OcxConfig, providerFilter?: string): ModelEntry[]
107
111
  // an exact `gpt-oss:120b` entry that lists "image", and the proxy rejects the image.
108
112
  const noVision = modelInList(prov.noVisionModels, model);
109
113
  const modalities = noVision ? ["text"] : (modelRecordValue(inputModalities, model) ?? null);
110
- const efforts = modelRecordValue(reasoningEfforts, model) ?? prov.reasoningEfforts ?? null;
114
+ // Same reason, for the ladder: `configuredReasoningEfforts` is what the catalog
115
+ // (`provider-fetch`) and the effort cap (`effort-policy`) resolve through, and it
116
+ // does three things this expression did not — it returns [] for a noReasoningModels
117
+ // match, drops levels Codex does not declare, and re-adds tiers the wire map proves
118
+ // the model emits. Restating two of its five lines here reported a ladder the proxy
119
+ // strips, and unsanitized junk as a supported level.
120
+ const efforts = configuredReasoningEfforts(prov, model) ?? null;
111
121
 
112
122
  entries.push({
113
123
  provider: provName,