@bitkyc08/opencodex 2.55.0 → 2.57.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 (256) hide show
  1. package/bin/ocx.mjs +10 -0
  2. package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
  3. package/gui/dist/assets/{index-VuoiWj9J.js → index-Cz7CLdif.js} +21 -21
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +4 -3
  6. package/src/adapters/base.ts +21 -0
  7. package/src/adapters/codebuddy/adapter.ts +2 -1
  8. package/src/adapters/codebuddy/scaffold-guard.ts +248 -0
  9. package/src/adapters/command-code.ts +1 -1
  10. package/src/adapters/cursor/envelope-echo.ts +8 -2
  11. package/src/adapters/cursor/transport-retry.ts +46 -1
  12. package/src/adapters/cursor.ts +4 -0
  13. package/src/adapters/google.ts +7 -7
  14. package/src/adapters/kiro/adapter.ts +42 -1
  15. package/src/adapters/kiro/payload.ts +17 -3
  16. package/src/adapters/kiro/reasoning.ts +70 -7
  17. package/src/adapters/kiro/stream.ts +8 -2
  18. package/src/adapters/kiro/wire.ts +2 -1
  19. package/src/adapters/kiro-events.ts +21 -13
  20. package/src/adapters/kiro-retry.ts +23 -4
  21. package/src/adapters/openai-chat/errors.ts +116 -0
  22. package/src/adapters/openai-chat/messages.ts +346 -0
  23. package/src/adapters/openai-chat/passthrough.ts +146 -0
  24. package/src/adapters/openai-chat/response-events.ts +117 -0
  25. package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
  26. package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
  27. package/src/adapters/openai-chat/tool-schema.ts +495 -0
  28. package/src/adapters/openai-chat/wire.ts +50 -0
  29. package/src/adapters/openai-chat.ts +40 -1452
  30. package/src/adapters/openai-responses/canonical-forward.ts +202 -0
  31. package/src/adapters/openai-responses/image-gen.ts +406 -0
  32. package/src/adapters/openai-responses/internal.ts +3 -0
  33. package/src/adapters/openai-responses/passthrough.ts +642 -0
  34. package/src/adapters/openai-responses/prompt-cache.ts +83 -0
  35. package/src/adapters/openai-responses/reasoning.ts +220 -0
  36. package/src/adapters/openai-responses/request-strips.ts +185 -0
  37. package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
  38. package/src/adapters/openai-responses/tool-schema.ts +293 -0
  39. package/src/adapters/openai-responses/web-search.ts +156 -0
  40. package/src/adapters/openai-responses.ts +4 -2625
  41. package/src/bridge/errors.ts +58 -0
  42. package/src/bridge/internal.ts +174 -0
  43. package/src/bridge/response-json.ts +630 -0
  44. package/src/bridge/sse.ts +1462 -0
  45. package/src/bridge.ts +5 -2204
  46. package/src/chat/inbound.ts +12 -1
  47. package/src/claude/desktop-profile.ts +66 -9
  48. package/src/claude/outbound.ts +18 -0
  49. package/src/cli/account-main.ts +1 -1
  50. package/src/cli/capabilities.ts +2 -2
  51. package/src/cli/combo.ts +10 -1
  52. package/src/cli/index.ts +48 -5
  53. package/src/cli/registry.ts +2 -1
  54. package/src/cli/system-command.ts +4 -4
  55. package/src/clients/config-export.ts +7 -3
  56. package/src/codex/account-label.ts +14 -3
  57. package/src/codex/account-lifecycle.ts +3 -0
  58. package/src/codex/account-store.ts +184 -35
  59. package/src/codex/account-usability.ts +21 -0
  60. package/src/codex/auth-api/account-list.ts +507 -0
  61. package/src/codex/auth-api/http.ts +32 -0
  62. package/src/codex/auth-api/login-flow.ts +566 -0
  63. package/src/codex/auth-api/login-state.ts +64 -0
  64. package/src/codex/auth-api/main-account-probe.ts +331 -0
  65. package/src/codex/auth-api/pool-mode-gate.ts +274 -0
  66. package/src/codex/auth-api/pool-quota-probe.ts +512 -0
  67. package/src/codex/auth-api/reset-credit-service.ts +431 -0
  68. package/src/codex/auth-api/routes.ts +425 -0
  69. package/src/codex/auth-api/runtime-config.ts +48 -0
  70. package/src/codex/auth-api.ts +27 -3118
  71. package/src/codex/auth-context.ts +252 -35
  72. package/src/codex/catalog/aggregation.ts +80 -1
  73. package/src/codex/catalog/auto-review.ts +507 -0
  74. package/src/codex/catalog/build-entries.ts +981 -0
  75. package/src/codex/catalog/combo-member.ts +375 -0
  76. package/src/codex/catalog/derive-entry.ts +229 -0
  77. package/src/codex/catalog/effort.ts +0 -1
  78. package/src/codex/catalog/gated-native-warn.ts +63 -0
  79. package/src/codex/catalog/gather-capture.ts +533 -0
  80. package/src/codex/catalog/model-hints.ts +691 -0
  81. package/src/codex/catalog/model-visibility.ts +305 -0
  82. package/src/codex/catalog/provider-fetch.ts +52 -2942
  83. package/src/codex/catalog/provider-models.ts +685 -0
  84. package/src/codex/catalog/remote.ts +30 -0
  85. package/src/codex/catalog/restore.ts +132 -0
  86. package/src/codex/catalog/retained-sync.ts +714 -0
  87. package/src/codex/catalog/routed-gather.ts +895 -0
  88. package/src/codex/catalog/subagent-roster.ts +176 -0
  89. package/src/codex/catalog/sync.ts +52 -2698
  90. package/src/codex/cli-install-provenance.ts +7 -1
  91. package/src/codex/convergence.ts +7 -2
  92. package/src/codex/desktop-app/types.ts +11 -2
  93. package/src/codex/desktop-app/windows.ts +5 -5
  94. package/src/codex/inject/config-toml.ts +563 -0
  95. package/src/codex/inject/remove.ts +192 -0
  96. package/src/codex/inject/restore.ts +567 -0
  97. package/src/codex/inject/routing-classify.ts +109 -0
  98. package/src/codex/inject/routing-target.ts +125 -0
  99. package/src/codex/inject.ts +89 -1444
  100. package/src/codex/lineage.ts +458 -0
  101. package/src/codex/model-entitlements.ts +152 -15
  102. package/src/codex/pool-refresh-backoff.ts +161 -0
  103. package/src/codex/quota-rejection.ts +104 -15
  104. package/src/codex/routing/active-account.ts +194 -0
  105. package/src/codex/routing/cache-affinity.ts +70 -0
  106. package/src/codex/routing/cooldown-math.ts +285 -0
  107. package/src/codex/routing/health-store.ts +402 -0
  108. package/src/codex/routing/probe-lease.ts +358 -0
  109. package/src/codex/routing/selection.ts +780 -0
  110. package/src/codex/routing/thread-affinity.ts +586 -0
  111. package/src/codex/routing/transient-hold-dispatch.ts +141 -0
  112. package/src/codex/routing.ts +370 -2271
  113. package/src/codex/shim-fingerprint.ts +223 -0
  114. package/src/codex/shim-inspect.ts +175 -0
  115. package/src/codex/shim-probe.ts +367 -0
  116. package/src/codex/shim-restore-lock.ts +169 -0
  117. package/src/codex/shim-state-file.ts +151 -0
  118. package/src/codex/shim-templates.ts +265 -0
  119. package/src/codex/shim.ts +48 -1268
  120. package/src/codex/warmup.ts +1 -1
  121. package/src/combos/failover.ts +85 -0
  122. package/src/combos/request.ts +17 -10
  123. package/src/combos/types.ts +23 -2
  124. package/src/config/diagnostics.ts +705 -0
  125. package/src/config/feature-flags.ts +55 -0
  126. package/src/config/live-reconcile.ts +403 -0
  127. package/src/config/load-degrade.ts +880 -0
  128. package/src/config/mutation-lock.ts +244 -0
  129. package/src/config/openai-tier-backup.ts +268 -0
  130. package/src/config/pending-teardown.ts +31 -0
  131. package/src/config/persist-unlocked.ts +92 -0
  132. package/src/config/proxy-env.ts +188 -0
  133. package/src/config/salvage.ts +244 -0
  134. package/src/config/schema/config-schema.ts +640 -0
  135. package/src/config/schema/leaf-validators.ts +855 -0
  136. package/src/config/warn-memo.ts +28 -0
  137. package/src/config.ts +234 -4481
  138. package/src/generated/compatibility-version.json +649 -121
  139. package/src/images/loop.ts +1 -1
  140. package/src/lib/errors.ts +17 -0
  141. package/src/lib/request-execution-budget.ts +198 -23
  142. package/src/lib/spend-reservation-ledger.ts +958 -0
  143. package/src/lib/state-store-registrations.ts +6 -2
  144. package/src/lib/test-home-guard.ts +85 -1
  145. package/src/lib/upstream-retry.ts +132 -21
  146. package/src/lib/windows-elevation.ts +76 -14
  147. package/src/lib/workflow-budget.ts +553 -30
  148. package/src/oauth/index.ts +2 -2
  149. package/src/oauth/key-providers.ts +2 -2
  150. package/src/providers/kiro-models.ts +4 -3
  151. package/src/providers/label.ts +19 -1
  152. package/src/providers/model-discovery.ts +16 -0
  153. package/src/providers/quota/account-cache.ts +441 -0
  154. package/src/providers/quota/antigravity.ts +295 -0
  155. package/src/providers/quota/report-cache.ts +320 -0
  156. package/src/providers/quota/vendor-probes-key.ts +1243 -0
  157. package/src/providers/quota/vendor-probes-oauth.ts +590 -0
  158. package/src/providers/quota.ts +324 -3079
  159. package/src/providers/registry/entries-core.ts +1228 -0
  160. package/src/providers/registry/entries-extended.ts +1213 -0
  161. package/src/providers/registry/model-seeds.ts +912 -0
  162. package/src/providers/registry/types.ts +352 -0
  163. package/src/providers/registry.ts +24 -3536
  164. package/src/responses/continuation-ownership.ts +29 -0
  165. package/src/responses/reasoning-envelope.ts +6 -3
  166. package/src/responses/state/replay-fingerprint.ts +80 -0
  167. package/src/responses/state/snapshot-codec.ts +104 -0
  168. package/src/responses/state/spill-failure.ts +118 -0
  169. package/src/responses/state/spill-queue.ts +665 -0
  170. package/src/responses/state/temp-recovery.ts +257 -0
  171. package/src/responses/state.ts +82 -1143
  172. package/src/routing/identity-domains.ts +456 -0
  173. package/src/routing/probe-lease.ts +613 -0
  174. package/src/server/chat-completions.ts +3 -1
  175. package/src/server/chat-native.ts +37 -9
  176. package/src/server/index/bounded-request.ts +88 -0
  177. package/src/server/index/live-sideband.ts +601 -0
  178. package/src/server/index/serve-options.ts +1766 -0
  179. package/src/server/index/startup-warnings.ts +213 -0
  180. package/src/server/index/websocket-handler.ts +339 -0
  181. package/src/server/index.ts +45 -2552
  182. package/src/server/inspection-tee.ts +107 -0
  183. package/src/server/live.ts +46 -1
  184. package/src/server/management/combo-routes.ts +10 -1
  185. package/src/server/management/route-registry.ts +26 -23
  186. package/src/server/management/shared.ts +8 -5
  187. package/src/server/management/workflow-budget-routes.ts +133 -0
  188. package/src/server/management-api.ts +12 -0
  189. package/src/server/relay-eager.ts +2 -0
  190. package/src/server/relay.ts +14 -19
  191. package/src/server/request-log-conversation.ts +9 -7
  192. package/src/server/request-log.ts +372 -4
  193. package/src/server/response-log-body.ts +153 -0
  194. package/src/server/responses/account-change-state.ts +307 -0
  195. package/src/server/responses/adapter-continuation.ts +540 -0
  196. package/src/server/responses/adapter-delivery.ts +208 -0
  197. package/src/server/responses/adapter-dispatch.ts +1042 -0
  198. package/src/server/responses/codex-ws-wire.ts +5 -0
  199. package/src/server/responses/collaboration.ts +74 -4
  200. package/src/server/responses/combo-session-recall.ts +68 -8
  201. package/src/server/responses/compact.ts +113 -17
  202. package/src/server/responses/completion-policy.ts +33 -0
  203. package/src/server/responses/core-auth.ts +529 -0
  204. package/src/server/responses/core-codex-account.ts +907 -0
  205. package/src/server/responses/core-combo-failure.ts +210 -0
  206. package/src/server/responses/core-combo.ts +787 -0
  207. package/src/server/responses/core-errors.ts +170 -0
  208. package/src/server/responses/core-lifetime.ts +95 -0
  209. package/src/server/responses/core-normalize.ts +350 -0
  210. package/src/server/responses/core-opaque-recovery.ts +380 -0
  211. package/src/server/responses/core-options.ts +159 -0
  212. package/src/server/responses/core-replay.ts +298 -0
  213. package/src/server/responses/core.ts +192 -8893
  214. package/src/server/responses/encrypted-payload.ts +0 -1
  215. package/src/server/responses/input-admission.ts +126 -6
  216. package/src/server/responses/passthrough-delivery.ts +869 -0
  217. package/src/server/responses/passthrough-dispatch.ts +1494 -0
  218. package/src/server/responses/passthrough-error.ts +38 -2
  219. package/src/server/responses/passthrough-execution.ts +54 -0
  220. package/src/server/responses/request-prepare.ts +1080 -0
  221. package/src/server/responses/request-send-budget.ts +259 -0
  222. package/src/server/responses/request-sidecar-auth.ts +149 -0
  223. package/src/server/responses/request-spend.ts +147 -0
  224. package/src/server/responses/request-transport.ts +803 -0
  225. package/src/server/responses/response-effects.ts +157 -0
  226. package/src/server/responses/run-turn-execution.ts +476 -0
  227. package/src/server/responses/sidecar-execution.ts +463 -0
  228. package/src/server/responses/terminal-guard.ts +65 -4
  229. package/src/server/responses-image-gen-repair.ts +1 -1
  230. package/src/server/responses-undeclared-tool-guard.ts +9 -5
  231. package/src/server/workflow-refusal.ts +84 -0
  232. package/src/service/windows-ops.ts +210 -16
  233. package/src/service/windows-scheduler.ts +28 -21
  234. package/src/service.ts +1 -1
  235. package/src/types/config.ts +34 -1
  236. package/src/types/request.ts +8 -5
  237. package/src/types/tools.ts +24 -0
  238. package/src/types.ts +2 -0
  239. package/src/update/index.ts +10 -0
  240. package/src/update/stop-contract.d.mts +1 -0
  241. package/src/update/stop-contract.mjs +19 -0
  242. package/src/update/stop-decision.d.mts +1 -1
  243. package/src/update/stop-decision.mjs +12 -3
  244. package/src/usage/log.ts +147 -1
  245. package/src/usage/summary.ts +171 -21
  246. package/src/vision/anthropic-describe.ts +1 -1
  247. package/src/vision/describe.ts +5 -5
  248. package/src/web-search/anthropic-executor.ts +1 -1
  249. package/src/web-search/exa-executor.ts +1 -1
  250. package/src/web-search/executor.ts +1 -1
  251. package/src/web-search/gemini-executor.ts +1 -1
  252. package/src/web-search/loop.ts +1 -1
  253. package/src/web-search/ollama-executor.ts +1 -1
  254. package/src/web-search/parse.ts +67 -14
  255. package/src/web-search/passthrough-bridge.ts +64 -31
  256. package/src/web-search/xai-executor.ts +1 -1
@@ -44,7 +44,7 @@ async function drainErrorBody(res: Response, signal: AbortSignal): Promise<void>
44
44
  fatalUtf8: true,
45
45
  });
46
46
  } catch (error) {
47
- if (signal.aborted) {
47
+ if (signal.aborted && res.status !== 429) {
48
48
  throw new CodexWarmupError("transport", "Codex warmup request failed", {
49
49
  cause: error,
50
50
  });
@@ -401,11 +401,15 @@ export function comboFailureCooldownScope(
401
401
  ): ComboFailureCooldownScope {
402
402
  const code = normalizedFailureCode(options?.code);
403
403
  // Request-shape refusals first: an oversized request must not cool a healthy target.
404
+ // A native transport can surface a zero-output model overflow as a generic
405
+ // upstream_server_error carrying precise context-window prose, so consult the bounded
406
+ // message classifier too: that target is healthy, the turn was simply too large for it.
404
407
  if (
405
408
  status === 413
406
409
  || REQUEST_SHAPE_FAILURE_CODES.has(code)
407
410
  || isRequestLocalFreePromptCap(status, message, options?.code)
408
411
  || isProviderTargetContextOverflow(status, message, options?.code)
412
+ || isDefiniteContextOverflow(status, message)
409
413
  || isRequestLocalTargetIncompatibility(status, message, options?.code)
410
414
  ) return "none";
411
415
  if (isProviderScopedQuotaCap(status, message, options?.code)) return "provider";
@@ -451,6 +455,74 @@ function isProviderTargetContextOverflow(
451
455
  && /\bprompt\s+\d+\s*>\s*\d+\s+maximum context length\b/i.test(message);
452
456
  }
453
457
 
458
+ /** A status can carry a verdict about the REQUEST; 401/403/429 speak about the credential. */
459
+ const CONTEXT_VERDICT_STATUSES: ReadonlySet<number> = new Set([400, 413, 422]);
460
+
461
+ /**
462
+ * Phrases a provider emits when the INPUT does not fit this model's context window. Matched
463
+ * against the innermost provider message only, so an unrelated refusal that merely quotes one
464
+ * of these tokens in a code field cannot authorize a replay.
465
+ */
466
+ const DEFINITE_CONTEXT_OVERFLOW_PHRASES = [
467
+ "exceeds the context window",
468
+ "exceed the context window",
469
+ "context window exceeded",
470
+ "context length exceeded",
471
+ "maximum context length",
472
+ "maximum context window",
473
+ "too many tokens",
474
+ ];
475
+
476
+ /** Wrapper envelopes unwrapped before the leaf message is read. */
477
+ const MAX_CONTEXT_OVERFLOW_ENVELOPES = 4;
478
+
479
+ function isDefiniteContextOverflowMessage(text: string): boolean {
480
+ const normalized = text.toLowerCase();
481
+ return normalized === "context_length_exceeded"
482
+ || DEFINITE_CONTEXT_OVERFLOW_PHRASES.some(phrase => normalized.includes(phrase));
483
+ }
484
+
485
+ /**
486
+ * Confirm a context overflow from the provider MESSAGE rather than from a code token that
487
+ * merely appears somewhere in the envelope. An upstream controls both fields and can emit a
488
+ * contradictory pair -- `context_length_exceeded` beside `Unsupported parameter: user` -- and
489
+ * that is not evidence the turn is too large for this model. `classifyError` reads the whole
490
+ * blob, which is exactly the looseness this must not inherit.
491
+ *
492
+ * A JSON-shaped body that fails to parse is truncated or corrupt, not prose: `classificationText`
493
+ * is capped at 500 characters by `normalizeUpstreamErrorText` before it reaches this function, so
494
+ * a long envelope arrives here as a JSON prefix. Reading that prefix as plain text would let an
495
+ * arbitrary field that happens to sit in the first 500 bytes authorize a hop, so it fails closed.
496
+ *
497
+ * Only the exact proxy wrapper is unwrapped, within a fixed envelope budget and 16,384 characters.
498
+ */
499
+ function isDefiniteContextOverflow(status: number, message: string): boolean {
500
+ if (!CONTEXT_VERDICT_STATUSES.has(status) && status < 500) return false;
501
+ if (message.length > 16_384) return false;
502
+ let text = message.trim();
503
+ // One pass per unwrapped envelope, plus one for the leaf the last envelope yields.
504
+ for (let unwrapped = 0; unwrapped <= MAX_CONTEXT_OVERFLOW_ENVELOPES; unwrapped += 1) {
505
+ const providerPrefix = /^Provider error \d{3}:\s*/.exec(text);
506
+ if (providerPrefix) text = text.slice(providerPrefix[0].length).trim();
507
+ if (!text.startsWith("{")) return isDefiniteContextOverflowMessage(text);
508
+ if (unwrapped === MAX_CONTEXT_OVERFLOW_ENVELOPES) return false;
509
+ let payload: unknown;
510
+ try { payload = JSON.parse(text); } catch { return false; }
511
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
512
+ const record = payload as Record<string, unknown>;
513
+ const response = record.response && typeof record.response === "object" && !Array.isArray(record.response)
514
+ ? record.response as Record<string, unknown>
515
+ : undefined;
516
+ const source = [record.error, response?.error, response?.last_error, record.last_error, record]
517
+ .find((candidate): candidate is Record<string, unknown> =>
518
+ !!candidate && typeof candidate === "object" && !Array.isArray(candidate)
519
+ && typeof (candidate as Record<string, unknown>).message === "string");
520
+ if (!source) return false;
521
+ text = (source.message as string).trim();
522
+ }
523
+ return false;
524
+ }
525
+
454
526
  export function comboFailureDecision(
455
527
  status: number,
456
528
  message: string,
@@ -458,6 +530,10 @@ export function comboFailureDecision(
458
530
  ): ComboFailureDecision {
459
531
  if (status === 499) return "stop";
460
532
  if (message.toLowerCase().includes("origin_rejected")) return "stop";
533
+ // Structured form of the same hard refusal. The prose test above misses it when the origin
534
+ // reports the code out of band, and every hop rule below -- including the context-overflow
535
+ // one -- must stay subordinate to it.
536
+ if (normalizedFailureCode(options?.code) === "origin_rejected") return "stop";
461
537
  // The origin may already be executing this turn (the Codex WebSocket relay sent the create
462
538
  // frame and never saw a response event). Hopping would send the same request to a second
463
539
  // target while the first may still be generating; the honest status goes to the client.
@@ -476,6 +552,15 @@ export function comboFailureDecision(
476
552
  // (for example 5059 + invalid_request_prompt_too_long). That is evidence that this
477
553
  // target is too small, not that every later combo target is incapable of serving it.
478
554
  if (isProviderTargetContextOverflow(status, message, options?.code)) return "hop";
555
+ // A definite context-window refusal is target-local inside a heterogeneous combo: this model
556
+ // cannot hold the turn, but a later target may have a larger window. Two boundaries keep this
557
+ // safe. It is reached only after cancellation, structured origin/cyber refusals and
558
+ // non-replayable post-send codes have already stopped. And it only ever classifies a failure
559
+ // the combo stream preflight already proved emitted no output: `comboStreamPayloadCommitsOutput`
560
+ // commits the child on any text, tool call or unknown event, and only a zero-output terminal
561
+ // becomes a failure response at all, so a turn whose text the client already saw is never
562
+ // reclassified here.
563
+ if (isDefiniteContextOverflow(status, message)) return "hop";
479
564
  // A local input-admission refusal (#1524) says "this candidate cannot fit the request",
480
565
  // not "the request is impossible": the next candidate may have a larger context window.
481
566
  //
@@ -1,5 +1,5 @@
1
- import type { OcxComboDefaultEffort, OcxComboReasoningEffortMode, OcxComboTarget, OcxConfig } from "../types";
2
- import { resolveEffortAtOrBelow } from "../reasoning-effort";
1
+ import type { OcxComboDefaultEffort, OcxComboDefaultEffortMode, OcxComboReasoningEffortMode, OcxComboTarget, OcxConfig } from "../types";
2
+ import { isCodexReasoningEffort, resolveEffortAtOrBelow } from "../reasoning-effort";
3
3
  import { resolveComboId } from "./types";
4
4
 
5
5
  const warnedUnsupportedDefaults = new Set<string>();
@@ -60,22 +60,29 @@ export function concreteComboRequestBody(
60
60
  defaultEffort: OcxComboDefaultEffort | null,
61
61
  targetReasoningEfforts: readonly string[] | undefined,
62
62
  reasoningEffortMode: OcxComboReasoningEffortMode = "strict",
63
+ defaultEffortMode: OcxComboDefaultEffortMode = "fallback",
63
64
  ): Record<string, unknown> {
64
65
  const clone = structuredClone(body) as Record<string, unknown>;
65
66
  clone.model = `${target.provider}/${target.model}`;
67
+ if (defaultEffortMode === "force" && (!defaultEffort || !isCodexReasoningEffort(defaultEffort))) {
68
+ throw new Error("force combo default effort requires a valid defaultEffort");
69
+ }
66
70
  if (targetReasoningEfforts?.length === 0
67
71
  || (reasoningEffortMode === "adaptive" && targetReasoningEfforts === undefined)) {
68
72
  stripUnsupportedReasoningControls(clone);
69
73
  }
70
- if (!defaultEffort) return clone;
74
+ if (!defaultEffort || !isCodexReasoningEffort(defaultEffort)) return clone;
71
75
  const reasoning = clone.reasoning;
72
- const needsDefault = reasoning === undefined || (
73
- reasoning
74
- && typeof reasoning === "object"
75
- && !Array.isArray(reasoning)
76
- && !Object.prototype.hasOwnProperty.call(reasoning, "effort")
77
- );
78
- if (!needsDefault) return clone;
76
+ const reasoningRecord = reasoning && typeof reasoning === "object" && !Array.isArray(reasoning)
77
+ ? reasoning as Record<string, unknown>
78
+ : undefined;
79
+ const hasEffort = reasoningRecord !== undefined
80
+ && Object.prototype.hasOwnProperty.call(reasoningRecord, "effort");
81
+ const callerEffort = reasoningRecord?.effort;
82
+ const validCallerEffort = typeof callerEffort === "string" && isCodexReasoningEffort(callerEffort);
83
+ const needsDefault = reasoning === undefined || (reasoningRecord !== undefined && !hasEffort);
84
+ const shouldForce = defaultEffortMode === "force" && validCallerEffort;
85
+ if (!needsDefault && !shouldForce) return clone;
79
86
  // Picker availability treats an unknown ladder as a wildcard, but runtime
80
87
  // injection stays fail-closed until this concrete target advertises support.
81
88
  //
@@ -1,6 +1,6 @@
1
1
  import { isCodexReasoningEffort } from "../reasoning-effort";
2
2
  import { SUPPORTED_NATIVE_OPENAI_SLUGS } from "../codex/catalog/native-models";
3
- import type { OcxComboConfig, OcxComboDefaultEffort, OcxComboReasoningEffortMode, OcxComboStrategy, OcxComboTarget, OcxProviderConfig } from "../types";
3
+ import type { OcxComboConfig, OcxComboDefaultEffort, OcxComboDefaultEffortMode, OcxComboReasoningEffortMode, OcxComboStrategy, OcxComboTarget, OcxProviderConfig } from "../types";
4
4
  import { COMBO_NAMESPACE, isValidComboId, targetKey } from "./identifiers";
5
5
 
6
6
  export const COMBO_DEFAULT_WAIT_FOR_COOLDOWN_MS = 0;
@@ -26,6 +26,8 @@ export interface NormalizedComboConfig {
26
26
  cooldownMs?: number;
27
27
  waitForCooldownMs: number;
28
28
  defaultEffort: OcxComboDefaultEffort | null;
29
+ /** Client-precedence policy; `fallback` preserves legacy behavior. */
30
+ defaultEffortMode: OcxComboDefaultEffortMode;
29
31
  /** Picker-ladder derivation policy; `strict` preserves the legacy intersection rule. */
30
32
  reasoningEffortMode: OcxComboReasoningEffortMode;
31
33
  /** Disable image input; `auto` preserves the intersection derived from all targets. */
@@ -167,6 +169,21 @@ export function comboConfigIssues(
167
169
  message: "defaultEffort must be one of: low, medium, high, xhigh, max, ultra",
168
170
  });
169
171
  }
172
+ if (body.defaultEffortMode !== undefined
173
+ && body.defaultEffortMode !== "fallback"
174
+ && body.defaultEffortMode !== "force") {
175
+ issues.push({
176
+ path: ["defaultEffortMode"],
177
+ message: 'defaultEffortMode must be "fallback" or "force"',
178
+ });
179
+ }
180
+ if (body.defaultEffortMode === "force"
181
+ && (typeof body.defaultEffort !== "string" || !isCodexReasoningEffort(body.defaultEffort))) {
182
+ issues.push({
183
+ path: ["defaultEffort"],
184
+ message: "defaultEffort is required when defaultEffortMode is force",
185
+ });
186
+ }
170
187
  if (body.imageInput !== undefined && body.imageInput !== "auto" && body.imageInput !== "disabled") {
171
188
  issues.push({ path: ["imageInput"], message: 'imageInput must be "auto" or "disabled"' });
172
189
  }
@@ -293,12 +310,16 @@ export function comboConfigError(
293
310
  export function normalizeComboConfig(raw: OcxComboConfig): NormalizedComboConfig {
294
311
  const alias = typeof raw.alias === "string" ? raw.alias.trim() : "";
295
312
  const displayName = typeof raw.displayName === "string" ? raw.displayName.trim() : "";
313
+ const defaultEffort = typeof raw.defaultEffort === "string" && isCodexReasoningEffort(raw.defaultEffort)
314
+ ? raw.defaultEffort
315
+ : null;
296
316
  return {
297
317
  strategy: raw.strategy ?? "failover",
298
318
  stickyLimit: raw.stickyLimit ?? 1,
299
319
  cooldownMs: raw.cooldownMs,
300
320
  waitForCooldownMs: raw.waitForCooldownMs ?? COMBO_DEFAULT_WAIT_FOR_COOLDOWN_MS,
301
- defaultEffort: raw.defaultEffort ?? null,
321
+ defaultEffort,
322
+ defaultEffortMode: raw.defaultEffortMode === "force" && defaultEffort !== null ? "force" : "fallback",
302
323
  reasoningEffortMode: raw.reasoningEffortMode === "adaptive" ? "adaptive" : "strict",
303
324
  imageInput: raw.imageInput === "disabled" ? "disabled" : "auto",
304
325
  alias: alias || null,