@bitkyc08/opencodex 2.7.40 → 2.7.41

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 (110) hide show
  1. package/README.md +63 -12
  2. package/assets/claude-code-models.gif +0 -0
  3. package/gui/dist/assets/index-B2J4t3te.css +1 -0
  4. package/gui/dist/assets/index-BmvM6wRb.js +65 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +3 -2
  7. package/src/adapters/google.ts +48 -9
  8. package/src/adapters/kiro-events.ts +15 -3
  9. package/src/adapters/kiro.ts +292 -28
  10. package/src/adapters/openai-chat.ts +63 -15
  11. package/src/adapters/openai-responses.ts +18 -0
  12. package/src/bridge.ts +76 -21
  13. package/src/chat/outbound.ts +66 -34
  14. package/src/claude/auth-detect.ts +229 -0
  15. package/src/claude/auth-mode-migration.ts +32 -0
  16. package/src/claude/auth-mode.ts +62 -0
  17. package/src/claude/desktop-3p-guard.ts +35 -0
  18. package/src/claude/desktop-3p.ts +121 -21
  19. package/src/claude/desktop-health.ts +26 -0
  20. package/src/claude/desktop-profile.ts +263 -0
  21. package/src/claude/inbound-debug.ts +4 -0
  22. package/src/claude/model-info.ts +9 -3
  23. package/src/cli/account-extended.ts +34 -0
  24. package/src/cli/account.ts +3 -1
  25. package/src/cli/claude-desktop.ts +152 -0
  26. package/src/cli/claude.ts +37 -3
  27. package/src/cli/doctor.ts +187 -6
  28. package/src/cli/help.ts +13 -1
  29. package/src/cli/index.ts +151 -57
  30. package/src/cli/status-oauth.ts +68 -0
  31. package/src/cli/status.ts +4 -0
  32. package/src/codex/account-lifecycle.ts +31 -0
  33. package/src/codex/auth-api.ts +133 -26
  34. package/src/codex/auth-collision.ts +55 -11
  35. package/src/codex/auth-context.ts +50 -6
  36. package/src/codex/catalog/provider-fetch.ts +6 -2
  37. package/src/codex/home.ts +61 -1
  38. package/src/codex/inject.ts +28 -66
  39. package/src/codex/injected-marker.ts +72 -0
  40. package/src/codex/journal.ts +39 -3
  41. package/src/codex/main-account-cache.ts +25 -0
  42. package/src/codex/model-cache.ts +20 -1
  43. package/src/codex/paths.ts +5 -0
  44. package/src/codex/routing.ts +138 -11
  45. package/src/codex/subagent-model-fallback.ts +455 -0
  46. package/src/codex/sync.ts +17 -0
  47. package/src/combos/failover.ts +10 -2
  48. package/src/combos/index.ts +1 -0
  49. package/src/combos/types.ts +9 -0
  50. package/src/config.ts +117 -0
  51. package/src/grok/inject.ts +339 -0
  52. package/src/grok/status.ts +88 -0
  53. package/src/grok/sync.ts +66 -0
  54. package/src/lib/destination-policy.ts +13 -0
  55. package/src/lib/errors.ts +59 -0
  56. package/src/lib/privacy.ts +9 -0
  57. package/src/lib/process-control.ts +57 -4
  58. package/src/oauth/health.ts +375 -0
  59. package/src/oauth/index.ts +69 -22
  60. package/src/oauth/kiro.ts +5 -3
  61. package/src/oauth/log.ts +48 -0
  62. package/src/oauth/store.ts +55 -6
  63. package/src/providers/alibaba-region-backup.ts +75 -0
  64. package/src/providers/alibaba-region-migration.ts +143 -0
  65. package/src/providers/alibaba-region-startup.ts +36 -0
  66. package/src/providers/api-keys.ts +5 -5
  67. package/src/providers/derive.ts +22 -1
  68. package/src/providers/free-directory.ts +181 -0
  69. package/src/providers/key-failover.ts +2 -2
  70. package/src/providers/kiro-models.ts +3 -2
  71. package/src/providers/openai-tiers.ts +1 -1
  72. package/src/providers/provider-id-rewrite.ts +150 -0
  73. package/src/providers/registry.ts +49 -2
  74. package/src/responses/parser.ts +49 -20
  75. package/src/responses/state.ts +156 -2
  76. package/src/router.ts +73 -5
  77. package/src/server/chat-completions.ts +78 -22
  78. package/src/server/claude-messages.ts +8 -0
  79. package/src/server/images.ts +2 -1
  80. package/src/server/index.ts +50 -30
  81. package/src/server/live.ts +2 -1
  82. package/src/server/management/agent-settings-routes.ts +337 -18
  83. package/src/server/management/api-access.ts +141 -0
  84. package/src/server/management/combo-routes.ts +3 -3
  85. package/src/server/management/config-routes.ts +4 -4
  86. package/src/server/management/logs-usage-routes.ts +64 -4
  87. package/src/server/management/model-routes.ts +114 -8
  88. package/src/server/management/oauth-account-routes.ts +31 -5
  89. package/src/server/management/provider-routes.ts +42 -11
  90. package/src/server/management/shared.ts +81 -2
  91. package/src/server/management/system-routes.ts +6 -1
  92. package/src/server/management-api.ts +24 -7
  93. package/src/server/port-reclaim.ts +261 -0
  94. package/src/server/request-log.ts +11 -5
  95. package/src/server/responses/collaboration.ts +11 -6
  96. package/src/server/responses/compact.ts +2 -1
  97. package/src/server/responses/core.ts +358 -155
  98. package/src/server/responses/passthrough-error.ts +53 -0
  99. package/src/server/search.ts +2 -1
  100. package/src/server/system-env.ts +23 -6
  101. package/src/server/windows-tcp-drop.ts +174 -0
  102. package/src/service.ts +43 -3
  103. package/src/types.ts +74 -6
  104. package/src/update/index.ts +30 -19
  105. package/src/update/job.ts +30 -15
  106. package/src/usage/log.ts +158 -2
  107. package/src/usage/summary.ts +8 -4
  108. package/src/web-search/loop.ts +4 -1
  109. package/gui/dist/assets/index-CMip1DzF.css +0 -1
  110. package/gui/dist/assets/index-cydcmbzC.js +0 -52
@@ -1,5 +1,6 @@
1
1
  import type { Server } from "bun";
2
2
  import { bridgeToResponsesSSE, buildResponseJSON, formatErrorResponse, type ResponsesTerminalStatus } from "../../bridge";
3
+ import { formatPassthroughUpstreamError } from "./passthrough-error";
3
4
  import {
4
5
  getConfigPath,
5
6
  multiAgentGuidanceEnabled,
@@ -9,7 +10,7 @@ import { parseRequest } from "../../responses/parser";
9
10
  import { buildCompactV1Output, COMPACT_PROMPT, decodeCompactionSummary, extractCompactUserMessages } from "../../responses/compaction";
10
11
  import { FORWARD_HEADERS, sanitizeReasoningInputContent } from "../../adapters/openai-responses";
11
12
  import { expandPreviousResponseInput, previousResponseProviderState, rememberResponseState } from "../../responses/state";
12
- import { routeModel } from "../../router";
13
+ import { routeModel, type RouteResult } from "../../router";
13
14
  import {
14
15
  advanceComboAfterFailure,
15
16
  comboDefaultEffort,
@@ -42,6 +43,7 @@ import { createAdapterEventQueue, preflightAdapterEvents } from "../../adapters/
42
43
  import {
43
44
  applyCodexAuthContextToProvider,
44
45
  CodexAccountCooldownError,
46
+ cooldownErrorResponse,
45
47
  CodexAuthContextError,
46
48
  CodexDirectAuthenticationError,
47
49
  CodexPoolAuthenticationError,
@@ -56,6 +58,7 @@ import {
56
58
  } from "../../codex/auth-context";
57
59
  import {
58
60
  formatCodexProviderForLog,
61
+ previewCodexAccountForRequest,
59
62
  recordCodexUpstreamOutcome,
60
63
  type CodexUpstreamOutcome,
61
64
  } from "../../codex/routing";
@@ -76,6 +79,12 @@ import { registerTurn, trackStreamLifetime, unregisterTurn } from "../lifecycle"
76
79
  import { redactSecretString } from "../../lib/redact";
77
80
  import { readBoundedResponseBody } from "../../lib/bounded-body";
78
81
  import { supportedLadderFor } from "../effort-policy";
82
+ import { isThreadSpawnRequest } from "../effort-policy";
83
+ import {
84
+ applySubagentModelFallback,
85
+ maybePrimeSubagentQuota,
86
+ recordSubagentQuotaFailureForThreadSpawn,
87
+ } from "../../codex/subagent-model-fallback";
79
88
  import {
80
89
  beginRequestAttempt,
81
90
  catalogModelSupportsServiceTier,
@@ -266,6 +275,8 @@ export function comboUnavailableResponse(message: string): Response {
266
275
  export interface ConsumedComboFailure {
267
276
  response: Response;
268
277
  classificationText: string;
278
+ /** Structured upstream `error.code` when present in the failure body. */
279
+ upstreamCode?: string;
269
280
  /** Valid numeric/date value used only for cooldown calculation. */
270
281
  retryAfter?: string;
271
282
  /** Reserved for 040 usage attribution without adding another body read. */
@@ -314,12 +325,20 @@ export async function consumeComboFailure(
314
325
  const fallback = `Provider error ${response.status}`;
315
326
  let classificationText = fallback;
316
327
  let usage: OcxUsage | undefined;
328
+ let upstreamCode: string | undefined;
317
329
  try {
318
330
  const body = await readBoundedResponseBody(response, { signal });
319
331
  usage = usageFromComboFailureText(body.text);
320
332
  if (body.displaySafe) {
321
333
  const safeText = redactSecretString(body.text).slice(0, 500);
322
334
  if (safeText) classificationText = safeText;
335
+ try {
336
+ const parsed = JSON.parse(body.text) as { error?: { code?: unknown } | string };
337
+ const nested = typeof parsed?.error === "object" && parsed.error ? parsed.error.code : undefined;
338
+ if (typeof nested === "string" && nested.length > 0) upstreamCode = nested;
339
+ } catch {
340
+ /* non-JSON upstream body — message-only classification */
341
+ }
323
342
  }
324
343
  } catch (error) {
325
344
  if (signal?.aborted) throw error;
@@ -330,8 +349,11 @@ export async function consumeComboFailure(
330
349
  : `${fallback}: ${classificationText}`;
331
350
  const retryAfter = sanitizedRetryAfter(response.headers.get("retry-after"), now);
332
351
  return {
333
- response: formatErrorResponse(response.status, "upstream_error", message),
352
+ response: formatErrorResponse(response.status, "upstream_error", message, {
353
+ ...(upstreamCode !== undefined ? { code: upstreamCode } : {}),
354
+ }),
334
355
  classificationText,
356
+ ...(upstreamCode !== undefined ? { upstreamCode } : {}),
335
357
  ...(retryAfter !== undefined ? { retryAfter } : {}),
336
358
  ...(usage ? { usage } : {}),
337
359
  };
@@ -414,6 +436,169 @@ function unreadableEncryptedAgentTaskResponse(): Response {
414
436
  );
415
437
  }
416
438
 
439
+ type ResponsesAuthResolution =
440
+ | { ok: true; authCtx: CodexAuthContext; headers: Headers }
441
+ | { ok: false; response: Response };
442
+
443
+ /**
444
+ * Resolve Codex auth for a route. On unusable contexts, releases any probe lease
445
+ * before returning the 401 (nothing reaches upstream).
446
+ */
447
+ async function resolveResponsesCodexAuth(
448
+ req: Request,
449
+ config: OcxConfig,
450
+ route: RouteResult,
451
+ options: HandleResponsesOptions,
452
+ ): Promise<ResponsesAuthResolution> {
453
+ try {
454
+ if (route.codexAccountMode === "direct") validateForwardAdmissionCredential(req.headers, config);
455
+ let authCtx: CodexAuthContext;
456
+ if (route.codexAccountMode) {
457
+ authCtx = await resolveCodexAuthContext(req.headers, config, route.codexAccountMode);
458
+ options.onCodexAuthContextResolved?.(authCtx);
459
+ } else {
460
+ authCtx = { kind: "main", accountId: null };
461
+ options.onCodexAuthContextResolved?.(undefined);
462
+ }
463
+ if (!isCodexAuthContextUsable(authCtx, config)) {
464
+ releaseCodexAuthContextProbeLease(authCtx);
465
+ return {
466
+ ok: false,
467
+ response: formatErrorResponse(401, "authentication_error", "Selected Codex account needs reauthentication"),
468
+ };
469
+ }
470
+ return {
471
+ ok: true,
472
+ authCtx,
473
+ headers: headersForCodexAuthContext(req.headers, authCtx),
474
+ };
475
+ } catch (err) {
476
+ if (err instanceof CodexAccountCooldownError) {
477
+ return { ok: false, response: cooldownErrorResponse(err) };
478
+ }
479
+ if (err instanceof CodexThreadAffinityExpiredError) {
480
+ return {
481
+ ok: false,
482
+ response: formatErrorResponse(409, "invalid_request_error", "Codex thread account affinity expired; start a new session"),
483
+ };
484
+ }
485
+ if (err instanceof CodexAuthContextError) {
486
+ const safeAccountLabel = formatCodexProviderForLog(route.providerName, err.accountId, config);
487
+ console.error(`[codex-auth] Pool account ${safeAccountLabel} token failed; reauthentication required`);
488
+ return {
489
+ ok: false,
490
+ response: formatErrorResponse(401, "authentication_error", "Selected Codex account needs reauthentication"),
491
+ };
492
+ }
493
+ if (err instanceof CodexPoolAuthenticationError) {
494
+ return { ok: false, response: formatErrorResponse(401, "authentication_error", err.message) };
495
+ }
496
+ if (err instanceof CodexDirectAuthenticationError) {
497
+ return { ok: false, response: formatErrorResponse(401, "authentication_error", err.message) };
498
+ }
499
+ if (err instanceof ForwardAdmissionCredentialError) {
500
+ return { ok: false, response: formatErrorResponse(401, "authentication_error", err.message) };
501
+ }
502
+ throw err;
503
+ }
504
+ }
505
+
506
+ /**
507
+ * Apply every route-dependent request mutation against the final selected route.
508
+ * Must run only after subagent fallback has settled the model/provider.
509
+ */
510
+ async function applyFinalRouteRequestNormalization(args: {
511
+ parsed: OcxParsedRequest;
512
+ route: RouteResult;
513
+ config: OcxConfig;
514
+ req: Request;
515
+ logCtx: RequestLogContext;
516
+ }): Promise<void> {
517
+ const { parsed, route, config, req, logCtx } = args;
518
+
519
+ // Apply the routed model id upstream: routing may strip a "<provider>/" namespace.
520
+ if (route.modelId !== parsed.modelId) {
521
+ if (parsed._rawBody && typeof parsed._rawBody === "object") {
522
+ (parsed._rawBody as { model?: string }).model = route.modelId;
523
+ }
524
+ parsed.modelId = route.modelId;
525
+ }
526
+ // Settle the wire once so logging, fast-mode, auth, and sidecars read the adapter
527
+ // this request will actually use (#404).
528
+ route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider);
529
+ logCtx.model = route.modelId;
530
+ logCtx.provider = route.providerName;
531
+ logCtx.providerAdapter = route.provider.adapter;
532
+
533
+ // Final selected model before virtual wire-model rewriting (Pro aliases).
534
+ const finalSelectedModelId = route.modelId;
535
+
536
+ // Virtual model rewriting: Pro aliases → base model + reasoning.mode="pro".
537
+ applyOpenAiVirtualModel(parsed, route, logCtx);
538
+
539
+ // Fast mode override for OpenAI-routed models.
540
+ if (config.fastMode !== undefined && route.provider.adapter === "openai-responses") {
541
+ const tier = config.fastMode ? "priority" : undefined;
542
+ if (parsed._rawBody && typeof parsed._rawBody === "object") {
543
+ if (tier) (parsed._rawBody as Record<string, unknown>).service_tier = tier;
544
+ else delete (parsed._rawBody as Record<string, unknown>).service_tier;
545
+ }
546
+ parsed.options.serviceTier = tier;
547
+ }
548
+
549
+ {
550
+ const guidance = await multiAgentGuidanceText(parsed, {
551
+ multiAgentGuidanceEnabled: config.multiAgentGuidanceEnabled,
552
+ injectionModel: config.injectionModel,
553
+ injectionEffort: config.injectionEffort,
554
+ subagentModels: config.subagentModels,
555
+ subagentModelFallback: config.subagentModelFallback,
556
+ injectionPrompt: config.injectionPrompt,
557
+ });
558
+ if (guidance) {
559
+ injectDeveloperMessage(parsed, guidance);
560
+ if (isInjectionDebugEnabled()) {
561
+ injectionDebugLog(`[opencodex] ${route.modelId}: multi-agent guidance injected (surface=${collabSurface(parsed)}, guidanceEnabled=${multiAgentGuidanceEnabled(config)}, ${guidance.length} chars)`);
562
+ }
563
+ } else if (isInjectionDebugEnabled() && collabSurface(parsed) !== null) {
564
+ injectionDebugLog(`[opencodex] ${route.modelId}: collab surface=${collabSurface(parsed)}, guidance silent (effort=${parsed.options.reasoning ?? "unset"}, injectionModel=${config.injectionModel ?? "unset"})`);
565
+ }
566
+ }
567
+
568
+ {
569
+ const { applyEffortCap, effortCapAppliesTo, supportedLadderFor } = await import("../effort-policy");
570
+ const surface = collabSurface(parsed);
571
+ if (effortCapAppliesTo(surface, req.headers, config, parsed._compactionRequest === true)) {
572
+ const capped = applyEffortCap(parsed, req.headers, config, supportedLadderFor(route));
573
+ if (capped) {
574
+ logCtx.requestedEffort = `${capped.from}->${capped.to}`;
575
+ if (isInjectionDebugEnabled()) {
576
+ injectionDebugLog(`[opencodex] ${route.modelId}: effort cap applied (${capped.from} -> ${capped.to}, ${capped.subagent ? "sub-agent" : "main"} turn)`);
577
+ }
578
+ }
579
+ } else if (isInjectionDebugEnabled() && (config.effortCap || config.subagentEffortCap)) {
580
+ injectionDebugLog(`[opencodex] ${route.modelId}: effort cap skipped (surface=${surface ?? "none"}, v2 feature only)`);
581
+ }
582
+ }
583
+
584
+ {
585
+ const { nativeEffortClamp, shouldApplyNativeEffortClamp } = await import("../../codex/catalog");
586
+ const clamped = shouldApplyNativeEffortClamp(route.providerName, route.provider, finalSelectedModelId)
587
+ ? nativeEffortClamp(route.modelId, parsed.options.reasoning)
588
+ : null;
589
+ if (clamped) {
590
+ parsed.options.reasoning = clamped;
591
+ const raw = parsed._rawBody as { reasoning?: { effort?: string } } | undefined;
592
+ if (raw?.reasoning && typeof raw.reasoning === "object") raw.reasoning.effort = clamped;
593
+ logCtx.requestedEffort = `${logCtx.requestedEffort ?? "max"}->${clamped}`;
594
+ }
595
+ }
596
+ logCtx.modelSupportsServiceTier = catalogModelSupportsServiceTier(
597
+ route.modelId,
598
+ logCtx.requestedServiceTier ?? logCtx.configuredServiceTier,
599
+ );
600
+ }
601
+
417
602
 
418
603
 
419
604
  export async function handleComboResponses(
@@ -604,7 +789,9 @@ export async function handleComboResponses(
604
789
  (logCtx.attempts ??= []).push(attempt);
605
790
  attemptRetained = true;
606
791
  lastFailure = failure.response;
607
- if (comboFailureDecision(response.status, failure.classificationText) === "stop") {
792
+ if (comboFailureDecision(failure.response.status, failure.classificationText, {
793
+ code: failure.upstreamCode,
794
+ }) === "stop") {
608
795
  Object.assign(logCtx, childLog, {
609
796
  requestedModel,
610
797
  model: requestedModel,
@@ -706,7 +893,11 @@ export async function handleResponses(
706
893
  }
707
894
  if (parsed._compactionRequest === true) parsed._cursorIsolateConversation = true;
708
895
 
709
- let route;
896
+ if (isThreadSpawnRequest(req.headers)) {
897
+ await maybePrimeSubagentQuota(config);
898
+ }
899
+
900
+ let route: RouteResult;
710
901
  try {
711
902
  route = routeModel(config, parsed.modelId);
712
903
  } catch (err) {
@@ -716,167 +907,72 @@ export async function handleResponses(
716
907
  return formatErrorResponse(404, "invalid_request_error", err instanceof Error ? err.message : String(err));
717
908
  }
718
909
 
719
- // The canonical ChatGPT backend can decrypt its V2 Fernet task tokens; routed
720
- // providers cannot. Reject the raw-input classification before adapter construction
721
- // or provider dispatch so an unreadable worker task cannot trigger a cost storm.
722
- if (!isCanonicalOpenAiForwardProvider(route.provider) && unreadableEncryptedAgentTask) {
723
- return unreadableEncryptedAgentTaskResponse();
724
- }
725
-
726
- // Apply the routed model id upstream: routing may strip a "<provider>/" namespace
727
- // (e.g. "opencode-go/deepseek-v4-pro" "deepseek-v4-pro"). Adapters read parsed.modelId,
728
- // and the passthrough adapter serializes _rawBody, so rewrite both.
729
- if (route.modelId !== parsed.modelId) {
730
- if (parsed._rawBody && typeof parsed._rawBody === "object") {
731
- (parsed._rawBody as { model?: string }).model = route.modelId;
910
+ let authCtx: CodexAuthContext = { kind: "main", accountId: null };
911
+ let selectedForwardHeaders = req.headers;
912
+ let subagentFallbackAccountId = config.activeCodexAccountId ?? null;
913
+ let subagentQuotaFailureModel = parsed.modelId;
914
+
915
+ // Subagent fallback must settle the final model/provider BEFORE route-dependent
916
+ // normalization (virtual models, effort caps, service tier, wire protocol).
917
+ // Preview the preferred Codex account without acquiring a probe lease or refreshing
918
+ // tokens auth is resolved only after the final route is selected.
919
+ if (isThreadSpawnRequest(req.headers) && !options.comboAttempt) {
920
+ const threadId = req.headers.get("x-codex-parent-thread-id");
921
+ const previewAccountId = previewCodexAccountForRequest(threadId, config);
922
+ subagentFallbackAccountId = previewAccountId ?? config.activeCodexAccountId ?? null;
923
+ const fallback = applySubagentModelFallback(
924
+ parsed,
925
+ req.headers,
926
+ config,
927
+ previewAccountId,
928
+ Date.now(),
929
+ unreadableEncryptedAgentTask,
930
+ );
931
+ if (fallback) {
932
+ (logCtx as unknown as Record<string, unknown>).subagentModelFallbackFrom = fallback.from;
933
+ (logCtx as unknown as Record<string, unknown>).subagentModelFallbackTo = fallback.to;
934
+ if (isInjectionDebugEnabled()) {
935
+ injectionDebugLog(`[opencodex] subagent model fallback ${fallback.from} -> ${fallback.to}`);
936
+ }
732
937
  }
733
- parsed.modelId = route.modelId;
734
- }
735
- // Settle the wire once, right after the native model id is known, so logging,
736
- // fast-mode injection, auth, and sidecar decisions all read the adapter this
737
- // request will actually use rather than the provider-wide default (#404).
738
- route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider);
739
- logCtx.model = route.modelId;
740
- logCtx.provider = route.providerName;
741
- logCtx.providerAdapter = route.provider.adapter;
742
-
743
- // Virtual model rewriting: Pro aliases → base model + reasoning.mode="pro".
744
- // Must run before effort caps/native clamps so the base model gets correct limits.
745
- applyOpenAiVirtualModel(parsed, route, logCtx);
938
+ subagentQuotaFailureModel = fallback?.to ?? parsed.modelId;
746
939
 
747
- // Fast mode override: when config.fastMode is explicitly set, inject or strip
748
- // service_tier for OpenAI-routed models. Undefined = passthrough (client decides).
749
- if (config.fastMode !== undefined && route.provider.adapter === "openai-responses") {
750
- const tier = config.fastMode ? "priority" : undefined;
751
- if (parsed._rawBody && typeof parsed._rawBody === "object") {
752
- if (tier) (parsed._rawBody as Record<string, unknown>).service_tier = tier;
753
- else delete (parsed._rawBody as Record<string, unknown>).service_tier;
940
+ if (fallback?.to && !slugsEquivalent(fallback.to, route.modelId)) {
941
+ try {
942
+ route = routeModel(config, fallback.to);
943
+ } catch (err) {
944
+ if (err instanceof NoAvailableComboTargetsError) {
945
+ return comboUnavailableResponse(err.message);
946
+ }
947
+ return formatErrorResponse(404, "invalid_request_error", err instanceof Error ? err.message : String(err));
948
+ }
754
949
  }
755
- parsed.options.serviceTier = tier;
756
950
  }
757
951
 
758
- // Multi-agent guidance shim: codex-rs emits its Proactive delegation developer
759
- // message only on the v2 surface. The proxy fills the gaps: the Proactive text
760
- // for v1 collab surfaces at the top tier (no model designation on v1), and the
761
- // sub-agent model/roster designation plus fork_turns override rules on v2.
762
- // The surface is judged from the request's own tool list. Runs BEFORE the
763
- // mock-max clamp below so the synthetic top tier (ultra arrives as max on the
764
- // codex wire) is still visible. Both request shapes are rewritten.
765
- {
766
- const guidance = await multiAgentGuidanceText(parsed, {
767
- multiAgentGuidanceEnabled: config.multiAgentGuidanceEnabled,
768
- injectionModel: config.injectionModel,
769
- injectionEffort: config.injectionEffort,
770
- subagentModels: config.subagentModels,
771
- injectionPrompt: config.injectionPrompt,
772
- });
773
- if (guidance) {
774
- injectDeveloperMessage(parsed, guidance);
775
- if (isInjectionDebugEnabled()) injectionDebugLog(`[opencodex] ${route.modelId}: multi-agent guidance injected (surface=${collabSurface(parsed)}, guidanceEnabled=${multiAgentGuidanceEnabled(config)}, ${guidance.length} chars)`);
776
- } else if (isInjectionDebugEnabled() && collabSurface(parsed) !== null) {
777
- injectionDebugLog(`[opencodex] ${route.modelId}: collab surface=${collabSurface(parsed)}, guidance silent (effort=${parsed.options.reasoning ?? "unset"}, injectionModel=${config.injectionModel ?? "unset"})`);
778
- }
952
+ // Encrypted child tasks may only reach the canonical native backend. This check
953
+ // runs against the FINAL route so native-only fallback can rescue a routed primary.
954
+ if (!isCanonicalOpenAiForwardProvider(route.provider) && unreadableEncryptedAgentTask) {
955
+ return unreadableEncryptedAgentTaskResponse();
779
956
  }
780
957
 
781
- // Hard effort caps (effortCap / subagentEffortCap): enforcement companion to the advisory
782
- // injection above — spawn-arg prompting cannot stop codex-rs from inheriting the parent's
783
- // ultra-tier default on bare spawns (see src/server/effort-policy.ts). Runs BEFORE the
784
- // mock-max clamp so a capped effort is what nativeness clamping then validates; rewrites
785
- // both request shapes (same dual-write contract as the clamp below).
786
- // GATE: v2 feature only (effortCapAppliesTo) — v2-surface main turns plus header-marked
787
- // child turns admitted regardless of tool surface (depth-limited leaves carry no collab
788
- // tools while shallower children do, so tool sniffing alone would cap siblings
789
- // inconsistently); multiAgentMode "v1" disables caps entirely; compaction turns bypass
790
- // caps so routed compaction matches native /v1/responses/compact (which never enters
791
- // handleResponses).
792
- {
793
- const { applyEffortCap, effortCapAppliesTo, supportedLadderFor } = await import("../effort-policy");
794
- const surface = collabSurface(parsed);
795
- if (effortCapAppliesTo(surface, req.headers, config, parsed._compactionRequest === true)) {
796
- const capped = applyEffortCap(parsed, req.headers, config, supportedLadderFor(route));
797
- if (capped) {
798
- logCtx.requestedEffort = `${capped.from}->${capped.to}`;
799
- if (isInjectionDebugEnabled()) {
800
- injectionDebugLog(`[opencodex] ${route.modelId}: effort cap applied (${capped.from} -> ${capped.to}, ${capped.subagent ? "sub-agent" : "main"} turn)`);
801
- }
802
- }
803
- } else if (isInjectionDebugEnabled() && (config.effortCap || config.subagentEffortCap)) {
804
- injectionDebugLog(`[opencodex] ${route.modelId}: effort cap skipped (surface=${surface ?? "none"}, v2 feature only)`);
805
- }
806
- }
958
+ await applyFinalRouteRequestNormalization({ parsed, route, config, req, logCtx });
807
959
 
808
- // Mock-max clamp: native models whose real ladder stops below max (gpt-5.5/5.4/…)
809
- // receive `max` when the user picks Ultra (codex converts ultra->max client-side).
810
- // Clamp to the model's highest real effort BEFORE any adapter — the ChatGPT
811
- // passthrough serializes _rawBody verbatim, so both shapes must be rewritten.
812
- // GUARD: judge nativeness by BOTH the originally requested id (logCtx.requestedModel)
813
- // and the resolved provider identity. Routing strips the "<provider>/" namespace, and
814
- // some third-party providers expose bare `defaultModel` selectors, so route.modelId
815
- // alone can make a routed model masquerade as an off-snapshot native. Only the
816
- // canonical built-in ChatGPT forward provider should receive the native clamp.
817
960
  {
818
- const requestedModelId = logCtx.requestedModel ?? route.modelId;
819
- const { nativeEffortClamp, shouldApplyNativeEffortClamp } = await import("../../codex/catalog");
820
- const clamped = shouldApplyNativeEffortClamp(route.providerName, route.provider, requestedModelId)
821
- ? nativeEffortClamp(route.modelId, parsed.options.reasoning)
822
- : null;
823
- if (clamped) {
824
- parsed.options.reasoning = clamped;
825
- const raw = parsed._rawBody as { reasoning?: { effort?: string } } | undefined;
826
- if (raw?.reasoning && typeof raw.reasoning === "object") raw.reasoning.effort = clamped;
827
- logCtx.requestedEffort = `${logCtx.requestedEffort ?? "max"}->${clamped}`;
828
- }
961
+ const finalAuth = await resolveResponsesCodexAuth(req, config, route, options);
962
+ if (!finalAuth.ok) return finalAuth.response;
963
+ authCtx = finalAuth.authCtx;
964
+ selectedForwardHeaders = finalAuth.headers;
829
965
  }
830
- logCtx.modelSupportsServiceTier = catalogModelSupportsServiceTier(
831
- route.modelId,
832
- logCtx.requestedServiceTier ?? logCtx.configuredServiceTier,
833
- );
834
966
 
835
- let authCtx: CodexAuthContext = { kind: "main", accountId: null };
836
- let selectedForwardHeaders: Headers;
837
- try {
838
- if (route.codexAccountMode === "direct") validateForwardAdmissionCredential(req.headers, config);
839
- if (route.codexAccountMode) {
840
- authCtx = await resolveCodexAuthContext(req.headers, config, route.codexAccountMode);
841
- options.onCodexAuthContextResolved?.(authCtx);
842
- } else {
843
- options.onCodexAuthContextResolved?.(undefined);
844
- }
845
- selectedForwardHeaders = headersForCodexAuthContext(req.headers, authCtx);
846
- } catch (err) {
847
- if (err instanceof CodexAccountCooldownError) {
848
- return formatErrorResponse(429, "rate_limit_error", "Selected Codex account is cooling down");
849
- }
850
- if (err instanceof CodexThreadAffinityExpiredError) {
851
- return formatErrorResponse(409, "invalid_request_error", "Codex thread account affinity expired; start a new session");
852
- }
853
- if (err instanceof CodexAuthContextError) {
854
- const safeAccountLabel = formatCodexProviderForLog(route.providerName, err.accountId, config);
855
- console.error(`[codex-auth] Pool account ${safeAccountLabel} token failed; reauthentication required`);
856
- return formatErrorResponse(401, "authentication_error", "Selected Codex account needs reauthentication");
857
- }
858
- if (err instanceof CodexPoolAuthenticationError) {
859
- return formatErrorResponse(401, "authentication_error", err.message);
860
- }
861
- if (err instanceof CodexDirectAuthenticationError) {
862
- return formatErrorResponse(401, "authentication_error", err.message);
863
- }
864
- if (err instanceof ForwardAdmissionCredentialError) {
865
- return formatErrorResponse(401, "authentication_error", err.message);
866
- }
867
- throw err;
868
- }
869
- if (!isCodexAuthContextUsable(authCtx, config)) {
870
- // Nothing reaches upstream on this path, so give the probe back.
871
- releaseCodexAuthContextProbeLease(authCtx);
872
- return formatErrorResponse(401, "authentication_error", "Selected Codex account needs reauthentication");
873
- }
874
967
  route.provider = applyCodexAuthContextToProvider(route.provider, authCtx, route.codexAccountMode);
875
968
  logCtx.provider = formatCodexProviderForLog(route.providerName, codexLogAccountId(authCtx), config);
876
969
  // Prefer Codex pool account as the Cursor thread namespace when present. Cursor routes without
877
970
  // codexAccountMode still get a credential-derived scope inside the Cursor adapter.
878
971
  const identityScope = codexLogAccountId(authCtx);
879
972
  if (identityScope) parsed._cursorIdentityScope = identityScope;
973
+ subagentFallbackAccountId = authCtx.kind === "pool" || authCtx.kind === "main-pool"
974
+ ? authCtx.accountId
975
+ : config.activeCodexAccountId ?? null;
880
976
 
881
977
  // OAuth providers: swap in a fresh access token (auto-refreshed) as the Bearer key, so the
882
978
  // existing openai-chat / anthropic adapters authenticate with no change.
@@ -1178,6 +1274,22 @@ export async function handleResponses(
1178
1274
  if (terminalBodyWillRecord) {
1179
1275
  options.setTerminalOutcomeRecorder?.((status, httpStatusOverride) => {
1180
1276
  terminalRecorder(status, httpStatusOverride);
1277
+ if (status === "failed") {
1278
+ const quotaFailureMessage = httpStatusOverride === 429 || httpStatusOverride === 402
1279
+ || logCtx.terminalHttpStatus === 429
1280
+ || logCtx.terminalHttpStatus === 402
1281
+ ? (httpStatusOverride ?? logCtx.terminalHttpStatus)
1282
+ : undefined;
1283
+ if (quotaFailureMessage !== undefined) {
1284
+ recordSubagentQuotaFailureForThreadSpawn(
1285
+ req.headers,
1286
+ subagentQuotaFailureModel,
1287
+ quotaFailureMessage,
1288
+ config,
1289
+ subagentFallbackAccountId,
1290
+ );
1291
+ }
1292
+ }
1181
1293
  options.onNativePassthroughTerminal?.(status);
1182
1294
  });
1183
1295
  } else {
@@ -1194,6 +1306,23 @@ export async function handleResponses(
1194
1306
  }
1195
1307
  }
1196
1308
 
1309
+ // Non-2xx passthrough failures must never reach Codex as an empty body —
1310
+ // Codex renders that as the opaque "Unknown error" (#452). Combo attempts
1311
+ // keep their typed failure envelope. Non-empty bodies are relayed verbatim
1312
+ // (headers included) so pool-retry Activation B/D and client diagnostics stay intact.
1313
+ if (!upstreamResponse.ok) {
1314
+ if (options.comboAttempt) {
1315
+ const failure = await consumeComboFailure(upstreamResponse, options.abortSignal);
1316
+ options.onConsumedComboFailure?.(failure);
1317
+ return failure.response;
1318
+ }
1319
+ const errorText = await upstreamResponse.text().catch(() => "");
1320
+ return formatPassthroughUpstreamError(upstreamResponse.status, errorText, {
1321
+ statusText: upstreamResponse.statusText,
1322
+ headers,
1323
+ });
1324
+ }
1325
+
1197
1326
  // Bun#32111 workaround: passthrough SSE uses tee()+native relay to avoid the
1198
1327
  // async-pull segfault on Windows. Branch[0] goes directly to the Response (Bun
1199
1328
  // native relay, never enters JS Sink.write); branch[1] is consumed in the
@@ -1204,7 +1333,7 @@ export async function handleResponses(
1204
1333
  // bounded reader with inline inspection (see src/server/relay-eager.ts and
1205
1334
  // devlog/_plan/260723_win_mem_safestream/020). Default on the bundled
1206
1335
  // known-bad runtime remains the tee path below.
1207
- if (upstreamResponse.ok && isEventStream && upstreamResponse.body) {
1336
+ if (isEventStream && upstreamResponse.body) {
1208
1337
  const repairConfig = route.provider.responsesItemIdRepair;
1209
1338
  const winNoRepair = process.platform === "win32" && !hasResponsesItemIdRepair(repairConfig);
1210
1339
  const eagerDecision = winNoRepair ? decideEagerRelay(config.streamMode ?? "auto") : null;
@@ -1215,6 +1344,22 @@ export async function handleResponses(
1215
1344
  const reportNativeTerminal = recordTerminalOutcomes
1216
1345
  ? (status: ResponsesTerminalStatus, httpStatusOverride?: number) => {
1217
1346
  terminalRecorder?.(status, httpStatusOverride);
1347
+ if (status === "failed") {
1348
+ const quotaFailureMessage = httpStatusOverride === 429 || httpStatusOverride === 402
1349
+ || logCtx.terminalHttpStatus === 429
1350
+ || logCtx.terminalHttpStatus === 402
1351
+ ? (httpStatusOverride ?? logCtx.terminalHttpStatus)
1352
+ : undefined;
1353
+ if (quotaFailureMessage !== undefined) {
1354
+ recordSubagentQuotaFailureForThreadSpawn(
1355
+ req.headers,
1356
+ subagentQuotaFailureModel,
1357
+ quotaFailureMessage,
1358
+ config,
1359
+ subagentFallbackAccountId,
1360
+ );
1361
+ }
1362
+ }
1218
1363
  options.onNativePassthroughTerminal?.(status);
1219
1364
  }
1220
1365
  : undefined;
@@ -1259,6 +1404,22 @@ export async function handleResponses(
1259
1404
  // client-cancel (no terminal seen) is finalized separately via consumeForInspection's onCancel.
1260
1405
  const reportNativeTerminal = (status: ResponsesTerminalStatus, httpStatusOverride?: number) => {
1261
1406
  terminalRecorder?.(status, httpStatusOverride);
1407
+ if (status === "failed") {
1408
+ const quotaFailureMessage = httpStatusOverride === 429 || httpStatusOverride === 402
1409
+ || logCtx.terminalHttpStatus === 429
1410
+ || logCtx.terminalHttpStatus === 402
1411
+ ? (httpStatusOverride ?? logCtx.terminalHttpStatus)
1412
+ : undefined;
1413
+ if (quotaFailureMessage !== undefined) {
1414
+ recordSubagentQuotaFailureForThreadSpawn(
1415
+ req.headers,
1416
+ subagentQuotaFailureModel,
1417
+ quotaFailureMessage,
1418
+ config,
1419
+ subagentFallbackAccountId,
1420
+ );
1421
+ }
1422
+ }
1262
1423
  options.onNativePassthroughTerminal?.(status);
1263
1424
  };
1264
1425
  consumeForInspection(
@@ -1297,14 +1458,9 @@ export async function handleResponses(
1297
1458
  }));
1298
1459
  }
1299
1460
  if (headers.get("content-type")?.toLowerCase().includes("application/json")) {
1300
- if (!upstreamResponse.ok && options.comboAttempt) {
1301
- const failure = await consumeComboFailure(upstreamResponse, options.abortSignal);
1302
- options.onConsumedComboFailure?.(failure);
1303
- return failure.response;
1304
- }
1305
1461
  const text = await upstreamResponse.text();
1306
1462
  inspectResponseLogJson(logCtx, text);
1307
- if (upstreamResponse.ok && rememberPassthroughResponse) {
1463
+ if (rememberPassthroughResponse) {
1308
1464
  try {
1309
1465
  rememberPassthroughResponse(JSON.parse(text) as { id?: unknown; output?: unknown; status?: unknown });
1310
1466
  } catch { /* non-JSON despite content-type; recording is best-effort */ }
@@ -1374,6 +1530,15 @@ export async function handleResponses(
1374
1530
  hideThinkingSummary: parsed.options.hideThinkingSummary,
1375
1531
  ...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
1376
1532
  ...(routedCompaction ? { compaction: true } : {}),
1533
+ onUsage: usage => {
1534
+ // Raw adapter usage, pre wire-normalization: the bridged SSE now always carries
1535
+ // zero-default detail objects, so provenance must come from here (cache_detail_missing).
1536
+ logCtx.usageFromBridge = true;
1537
+ if (usage) {
1538
+ logCtx.usage = usage;
1539
+ if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
1540
+ }
1541
+ },
1377
1542
  ...(routedCompaction ? {} : {
1378
1543
  onCompletedResponse: (response: Record<string, unknown>, providerState?: OcxProviderContinuationState) =>
1379
1544
  rememberResponseState(
@@ -1411,6 +1576,13 @@ export async function handleResponses(
1411
1576
  toolSearchToolNames,
1412
1577
  ...(routedCompaction ? { compaction: true } : {}),
1413
1578
  onProviderState: state => { providerState = state; },
1579
+ onUsage: usage => {
1580
+ logCtx.usageFromBridge = true;
1581
+ if (usage) {
1582
+ logCtx.usage = usage;
1583
+ if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
1584
+ }
1585
+ },
1414
1586
  });
1415
1587
  if (!routedCompaction) {
1416
1588
  rememberResponseState(
@@ -1442,6 +1614,13 @@ export async function handleResponses(
1442
1614
  forceEmptyResponseId: true,
1443
1615
  abortSignal: options.abortSignal,
1444
1616
  ...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
1617
+ onUsage: usage => {
1618
+ logCtx.usageFromBridge = true;
1619
+ if (usage) {
1620
+ logCtx.usage = usage;
1621
+ if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
1622
+ }
1623
+ },
1445
1624
  recordSidecarOutcome: wsPlan.forwardSidecar?.recordOutcome,
1446
1625
  connectTimeoutMs: config.connectTimeoutMs ?? 200_000,
1447
1626
  routedModelStallTimeoutMs: wsPlan.routedModelStallTimeoutMs,
@@ -1640,6 +1819,15 @@ export async function handleResponses(
1640
1819
  }
1641
1820
  const errorText = await upstreamResponse.text().catch(() => "unknown error");
1642
1821
  cleanupUpstreamAbort();
1822
+ recordSubagentQuotaFailureForThreadSpawn(
1823
+ req.headers,
1824
+ subagentQuotaFailureModel,
1825
+ upstreamResponse.status === 429 || upstreamResponse.status === 402
1826
+ ? upstreamResponse.status
1827
+ : `Provider error ${upstreamResponse.status}: ${redactSecretString(errorText.slice(0, 500))}`,
1828
+ config,
1829
+ subagentFallbackAccountId,
1830
+ );
1643
1831
  // Upstreams occasionally echo request details in error bodies — scrub token-shaped
1644
1832
  // material before it reaches the client-facing error surface.
1645
1833
  return formatErrorResponse(upstreamResponse.status, "upstream_error", `Provider error ${upstreamResponse.status}: ${redactSecretString(errorText.slice(0, 500))}`);
@@ -1788,6 +1976,14 @@ export async function handleResponses(
1788
1976
  hideThinkingSummary: parsed.options.hideThinkingSummary,
1789
1977
  ...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
1790
1978
  ...(routedCompaction ? { compaction: true } : {}),
1979
+ onUsage: usage => {
1980
+ // Raw adapter usage, pre wire-normalization (see the runTurn branch above).
1981
+ logCtx.usageFromBridge = true;
1982
+ if (usage) {
1983
+ logCtx.usage = usage;
1984
+ if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
1985
+ }
1986
+ },
1791
1987
  // Compaction turns must NOT enter the continuation cache: _rawBody still holds the full
1792
1988
  // PRE-compaction history, and a later previous_response_id expansion would rehydrate the
1793
1989
  // giant stale chain Codex just replaced.
@@ -1837,6 +2033,13 @@ export async function handleResponses(
1837
2033
  toolSearchToolNames,
1838
2034
  ...(routedCompaction ? { compaction: true } : {}),
1839
2035
  onProviderState: state => { providerState = state; },
2036
+ onUsage: usage => {
2037
+ logCtx.usageFromBridge = true;
2038
+ if (usage) {
2039
+ logCtx.usage = usage;
2040
+ if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
2041
+ }
2042
+ },
1840
2043
  });
1841
2044
  // See the streaming branch: compaction turns skip the continuation cache.
1842
2045
  if (!routedCompaction) {