@bitkyc08/opencodex 2.7.23 → 2.7.24

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 (55) hide show
  1. package/README.ko.md +37 -7
  2. package/README.md +52 -11
  3. package/README.zh-CN.md +36 -7
  4. package/bin/ocx.mjs +5 -3
  5. package/gui/dist/assets/index-BzhyTAco.js +40 -0
  6. package/gui/dist/assets/index-Dq3eZ1cU.css +1 -0
  7. package/gui/dist/index.html +2 -2
  8. package/gui/dist/provider-icons/opencode.svg +1 -1
  9. package/package.json +5 -2
  10. package/src/adapters/anthropic-image-normalize.ts +70 -29
  11. package/src/adapters/cursor/transport-retry.ts +20 -1
  12. package/src/adapters/run-turn-queue.ts +40 -0
  13. package/src/codex/auth-api.ts +10 -1
  14. package/src/codex/auth-context.ts +33 -7
  15. package/src/codex/catalog.ts +357 -23
  16. package/src/codex/routing.ts +10 -4
  17. package/src/combos/failover.ts +102 -0
  18. package/src/combos/index.ts +37 -0
  19. package/src/combos/request.ts +31 -0
  20. package/src/combos/resolve.ts +171 -0
  21. package/src/combos/types.ts +203 -0
  22. package/src/config.ts +280 -11
  23. package/src/lib/errors.ts +86 -24
  24. package/src/lib/upstream-retry.ts +8 -4
  25. package/src/oauth/index.ts +7 -1
  26. package/src/oauth/key-providers.ts +2 -32
  27. package/src/oauth/login-cli.ts +4 -3
  28. package/src/oauth/token-guardian.ts +38 -3
  29. package/src/providers/derive.ts +27 -2
  30. package/src/providers/kiro-models.ts +8 -3
  31. package/src/providers/label.ts +3 -1
  32. package/src/providers/openai-sidecar.ts +94 -0
  33. package/src/providers/openai-tier-startup.ts +27 -0
  34. package/src/providers/openai-tiers.ts +283 -0
  35. package/src/providers/openai-virtual-models.ts +82 -0
  36. package/src/providers/quota.ts +344 -24
  37. package/src/providers/registry.ts +112 -20
  38. package/src/reasoning-effort.ts +12 -11
  39. package/src/router.ts +80 -36
  40. package/src/server/auth-cors.ts +85 -9
  41. package/src/server/images.ts +31 -75
  42. package/src/server/index.ts +45 -86
  43. package/src/server/management-api.ts +273 -21
  44. package/src/server/request-log.ts +221 -20
  45. package/src/server/responses.ts +594 -75
  46. package/src/server/search.ts +22 -37
  47. package/src/types.ts +49 -1
  48. package/src/update/index.ts +50 -6
  49. package/src/update/job.ts +21 -4
  50. package/src/usage/log.ts +124 -1
  51. package/src/usage/summary.ts +147 -56
  52. package/src/vision/index.ts +20 -19
  53. package/src/web-search/index.ts +15 -17
  54. package/gui/dist/assets/index-Bk_GgFrh.css +0 -1
  55. package/gui/dist/assets/index-DQjt6Hly.js +0 -40
@@ -9,10 +9,24 @@ import { buildCompactV1Output, COMPACT_PROMPT, decodeCompactionSummary, extractC
9
9
  import { FORWARD_HEADERS } from "../adapters/openai-responses";
10
10
  import { expandPreviousResponseInput, previousResponseConversationId, rememberResponseState } from "../responses/state";
11
11
  import { routeModel } from "../router";
12
+ import {
13
+ advanceComboAfterFailure,
14
+ comboDefaultEffort,
15
+ comboFailureDecision,
16
+ comboIdFromRawBody,
17
+ concreteComboRequestBody,
18
+ getCombo,
19
+ isComboTargetInCooldown,
20
+ NoAvailableComboTargetsError,
21
+ noteComboSuccess,
22
+ parseRetryAfterMs,
23
+ pickComboTarget,
24
+ targetKey,
25
+ } from "../combos";
12
26
  import { isInjectionDebugEnabled } from "../lib/debug-settings";
13
27
  import { injectionDebugLog } from "../lib/injection-debug-log";
14
28
  import { modelInList, namespacedToolName } from "../types";
15
- import type { AdapterEvent, OcxConfig, OcxParsedRequest, OcxProviderConfig } from "../types";
29
+ import type { AdapterEvent, OcxConfig, OcxParsedRequest, OcxProviderConfig, OcxUsage } from "../types";
16
30
  import {
17
31
  forceRefreshOAuthAccessSnapshot,
18
32
  getOAuthCredentialProjectId,
@@ -20,13 +34,15 @@ import {
20
34
  type OAuthAccessSnapshot,
21
35
  UnsupportedOAuthProviderError,
22
36
  } from "../oauth";
23
- import { buildWebSearchTool, planWebSearch, runWithWebSearch } from "../web-search";
24
- import { describeImagesInPlace, planVisionSidecar, stripImagesInPlace } from "../vision";
25
- import { createAdapterEventQueue } from "../adapters/run-turn-queue";
37
+ import { buildWebSearchTool, planWebSearch, runWithWebSearch, shouldResolveOpenAiWebSearchSidecar } from "../web-search";
38
+ import { describeImagesInPlace, planVisionSidecar, shouldResolveOpenAiVisionSidecar, stripImagesInPlace } from "../vision";
39
+ import { createAdapterEventQueue, preflightAdapterEvents } from "../adapters/run-turn-queue";
26
40
  import {
27
41
  applyCodexAuthContextToProvider,
28
42
  CodexAccountCooldownError,
29
43
  CodexAuthContextError,
44
+ CodexDirectAuthenticationError,
45
+ CodexPoolAuthenticationError,
30
46
  CodexThreadAffinityExpiredError,
31
47
  headersForCodexAuthContext,
32
48
  isCodexAuthContextUsable,
@@ -39,6 +55,9 @@ import {
39
55
  type CodexUpstreamOutcome,
40
56
  } from "../codex/routing";
41
57
  import { fetchWithResetRetry, fetchWithTransientRetry } from "../lib/upstream-retry";
58
+ import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential } from "./auth-cors";
59
+ import { listOpenAiForwardSidecarCandidates, resolveFirstUsableOpenAiSidecar, type ResolvedOpenAiForwardSidecar } from "../providers/openai-sidecar";
60
+ import { applyOpenAiVirtualModel, resolveOpenAiCompactModel } from "../providers/openai-virtual-models";
42
61
  import { isUsageDebugEnabled } from "../usage/debug";
43
62
  import { readJsonRequestBody, DecompressedBodyTooLargeError, UnsupportedContentEncodingError } from "./request-decompress";
44
63
  import { resolveAdapter, resolveWireProtocolOverride } from "./adapter-resolve";
@@ -48,13 +67,20 @@ import { resolveProviderTransport } from "../providers/xai-transport";
48
67
  import type { WsData } from "./ws-bridge";
49
68
  import { registerTurn, trackStreamLifetime, unregisterTurn } from "./lifecycle";
50
69
  import { redactSecretString } from "../lib/redact";
70
+ import { readBoundedResponseBody } from "../lib/bounded-body";
51
71
  import {
72
+ beginRequestAttempt,
52
73
  catalogModelSupportsServiceTier,
74
+ finishRequestAttempt,
53
75
  inspectResponseLogJson,
76
+ noteAttemptSend,
54
77
  readConfiguredCodexServiceTier,
55
78
  requestLogSpeedLabel,
79
+ sealRequestAttemptIdentity,
80
+ usageFromResponsesPayload,
56
81
  type RequestLogContext,
57
82
  } from "./request-log";
83
+ import type { AttemptRecoveryKind } from "../usage/log";
58
84
  import {
59
85
  consumeForInspection,
60
86
  consumeForResponseLogMetadata,
@@ -414,20 +440,305 @@ export function decodeRequestErrorResponse(err: unknown, label: string): Respons
414
440
  return formatErrorResponse(400, "invalid_request_error", "Invalid JSON body");
415
441
  }
416
442
 
443
+ function comboUnavailableResponse(message: string): Response {
444
+ return new Response(
445
+ JSON.stringify({
446
+ error: { message, type: "server_error", code: "combo_unavailable" },
447
+ }),
448
+ { status: 503, headers: { "Content-Type": "application/json" } },
449
+ );
450
+ }
451
+
452
+ interface ConsumedComboFailure {
453
+ response: Response;
454
+ classificationText: string;
455
+ /** Valid numeric/date value used only for cooldown calculation. */
456
+ retryAfter?: string;
457
+ /** Reserved for 040 usage attribution without adding another body read. */
458
+ usage?: OcxUsage;
459
+ }
460
+
461
+ interface HandleResponsesOptions {
462
+ forceEmptyResponseId?: boolean;
463
+ abortSignal?: AbortSignal;
464
+ onCodexAuthContextResolved?: (context: CodexAuthContext | undefined) => void;
465
+ recordTerminalOutcomes?: boolean;
466
+ setTerminalOutcomeRecorder?: (recorder: ((status: ResponsesTerminalStatus) => void) | undefined) => void;
467
+ onNativePassthroughTerminal?: (status: ResponsesTerminalStatus) => void;
468
+ onNativePassthroughCancel?: () => void;
469
+ /** Internal recursion guard; callers outside this module must not set it. */
470
+ comboAttempt?: boolean;
471
+ /** 030-owned handoff when a child consumed the original failure under bounds. */
472
+ onConsumedComboFailure?: (failure: ConsumedComboFailure) => void;
473
+ }
474
+
475
+ function clientCancelledResponse(): Response {
476
+ return formatErrorResponse(499, "client_cancelled", "Client cancelled request");
477
+ }
478
+
479
+ function sanitizedRetryAfter(value: string | null, now: number): string | undefined {
480
+ const trimmed = value?.trim();
481
+ if (!trimmed || trimmed.length > 128) return undefined;
482
+ return parseRetryAfterMs(trimmed, now) !== undefined ? trimmed : undefined;
483
+ }
484
+
485
+ async function consumeComboFailure(
486
+ response: Response,
487
+ signal?: AbortSignal,
488
+ now = Date.now(),
489
+ ): Promise<ConsumedComboFailure> {
490
+ const fallback = `Provider error ${response.status}`;
491
+ let classificationText = fallback;
492
+ let usage: OcxUsage | undefined;
493
+ try {
494
+ const body = await readBoundedResponseBody(response, { signal });
495
+ usage = usageFromComboFailureText(body.text);
496
+ if (body.displaySafe) {
497
+ const safeText = redactSecretString(body.text).slice(0, 500);
498
+ if (safeText) classificationText = safeText;
499
+ }
500
+ } catch (error) {
501
+ if (signal?.aborted) throw error;
502
+ classificationText = fallback;
503
+ }
504
+ const message = classificationText === fallback
505
+ ? fallback
506
+ : `${fallback}: ${classificationText}`;
507
+ const retryAfter = sanitizedRetryAfter(response.headers.get("retry-after"), now);
508
+ return {
509
+ response: formatErrorResponse(response.status, "upstream_error", message),
510
+ classificationText,
511
+ ...(retryAfter !== undefined ? { retryAfter } : {}),
512
+ ...(usage ? { usage } : {}),
513
+ };
514
+ }
515
+
516
+ function usageFromComboFailureText(text: string): OcxUsage | undefined {
517
+ try {
518
+ const payload = JSON.parse(text) as Record<string, unknown>;
519
+ const nested = payload.response;
520
+ const source = nested && typeof nested === "object" && !Array.isArray(nested)
521
+ ? nested as Record<string, unknown>
522
+ : payload;
523
+ return usageFromResponsesPayload(source.usage);
524
+ } catch {
525
+ return undefined;
526
+ }
527
+ }
528
+
529
+ function createChildPassthroughCallbackGate(options: HandleResponsesOptions) {
530
+ type Pending =
531
+ | { kind: "terminal"; status: ResponsesTerminalStatus }
532
+ | { kind: "cancel" };
533
+ let state: "pending" | "committed" | "discarded" = "pending";
534
+ let pending: Pending | undefined;
535
+ let accepted = false;
536
+ const publish = (value: Pending): void => {
537
+ if (value.kind === "terminal") options.onNativePassthroughTerminal?.(value.status);
538
+ else options.onNativePassthroughCancel?.();
539
+ };
540
+ const receive = (value: Pending): void => {
541
+ if (state === "discarded" || accepted) return;
542
+ accepted = true;
543
+ if (state === "committed") return publish(value);
544
+ pending ??= value;
545
+ };
546
+ return {
547
+ onTerminal: (status: ResponsesTerminalStatus) => receive({ kind: "terminal", status }),
548
+ onCancel: () => receive({ kind: "cancel" }),
549
+ commit: () => {
550
+ if (state !== "pending") return;
551
+ state = "committed";
552
+ if (pending) publish(pending);
553
+ pending = undefined;
554
+ },
555
+ discard: () => {
556
+ state = "discarded";
557
+ pending = undefined;
558
+ },
559
+ };
560
+ }
561
+
562
+ async function handleComboResponses(
563
+ req: Request,
564
+ rawBody: unknown,
565
+ comboId: string,
566
+ config: OcxConfig,
567
+ logCtx: RequestLogContext,
568
+ options: HandleResponsesOptions,
569
+ ): Promise<Response> {
570
+ Object.assign(logCtx, {
571
+ requestedModel: `combo/${comboId}`,
572
+ model: `combo/${comboId}`,
573
+ provider: "combo",
574
+ });
575
+ const combo = getCombo(config, comboId);
576
+ if (!combo) {
577
+ return formatErrorResponse(404, "invalid_request_error", `Unknown combo: ${comboId}`);
578
+ }
579
+
580
+ const initialNow = Date.now();
581
+ let pick = pickComboTarget(config, comboId, {
582
+ eligible: target => !isComboTargetInCooldown(comboId, target, initialNow),
583
+ });
584
+ if (!pick) {
585
+ return comboUnavailableResponse(`No available targets for combo: ${comboId}`);
586
+ }
587
+
588
+ let lastFailure: Response | null = null;
589
+ while (pick) {
590
+ if (options.abortSignal?.aborted) return clientCancelledResponse();
591
+ const childLog: RequestLogContext = {
592
+ model: pick.target.model,
593
+ provider: pick.target.provider,
594
+ };
595
+ const childBody = concreteComboRequestBody(
596
+ rawBody,
597
+ pick.target,
598
+ comboDefaultEffort(config, comboId),
599
+ );
600
+ const childHeaders = new Headers(req.headers);
601
+ childHeaders.delete("content-length");
602
+ const childRequest = new Request(req.url, {
603
+ method: req.method,
604
+ headers: childHeaders,
605
+ body: JSON.stringify(childBody),
606
+ });
607
+ let resolvedAuth: CodexAuthContext | undefined;
608
+ let terminalRecorder: ((status: ResponsesTerminalStatus) => void) | undefined;
609
+ const started = Date.now();
610
+ const attempt = beginRequestAttempt(
611
+ (logCtx.attempts?.length ?? 0) + 1,
612
+ pick.target.provider,
613
+ pick.target.model,
614
+ config.providers[pick.target.provider]!.adapter,
615
+ );
616
+ childLog.activeAttempt = attempt;
617
+ let attemptRetained = false;
618
+ const retainCancelledAttempt = (): void => {
619
+ if (attemptRetained) return;
620
+ sealRequestAttemptIdentity(
621
+ attempt,
622
+ childLog.provider,
623
+ childLog.providerAdapter ?? attempt.adapter,
624
+ );
625
+ finishRequestAttempt(attempt, 499, Date.now() - started, childLog.usage);
626
+ (logCtx.attempts ??= []).push(attempt);
627
+ attemptRetained = true;
628
+ };
629
+ let consumedChildFailure: ConsumedComboFailure | undefined;
630
+ const callbackGate = createChildPassthroughCallbackGate(options);
631
+ let response: Response;
632
+ try {
633
+ response = await handleResponses(childRequest, config, childLog, {
634
+ ...options,
635
+ comboAttempt: true,
636
+ onCodexAuthContextResolved: value => { resolvedAuth = value; },
637
+ setTerminalOutcomeRecorder: value => { terminalRecorder = value; },
638
+ onConsumedComboFailure: value => { consumedChildFailure = value; },
639
+ onNativePassthroughTerminal: callbackGate.onTerminal,
640
+ onNativePassthroughCancel: callbackGate.onCancel,
641
+ });
642
+ } catch (error) {
643
+ callbackGate.discard();
644
+ if (options.abortSignal?.aborted) {
645
+ retainCancelledAttempt();
646
+ return clientCancelledResponse();
647
+ }
648
+ throw error;
649
+ }
650
+
651
+ if (options.abortSignal?.aborted) {
652
+ callbackGate.discard();
653
+ retainCancelledAttempt();
654
+ return clientCancelledResponse();
655
+ }
656
+
657
+ if (response.ok) {
658
+ sealRequestAttemptIdentity(
659
+ attempt,
660
+ childLog.provider,
661
+ childLog.providerAdapter ?? attempt.adapter,
662
+ );
663
+ (logCtx.attempts ??= []).push(attempt);
664
+ attemptRetained = true;
665
+ noteComboSuccess(comboId, combo, pick.target);
666
+ Object.assign(logCtx, childLog, {
667
+ requestedModel: `combo/${comboId}`,
668
+ model: `combo/${comboId}`,
669
+ provider: "combo",
670
+ attempts: logCtx.attempts,
671
+ activeAttempt: attempt,
672
+ activeAttemptStartedAt: started,
673
+ resolvedModel: childLog.resolvedModel ?? childLog.model,
674
+ });
675
+ options.onCodexAuthContextResolved?.(resolvedAuth);
676
+ options.setTerminalOutcomeRecorder?.(terminalRecorder);
677
+ callbackGate.commit();
678
+ return response;
679
+ }
680
+
681
+ callbackGate.discard();
682
+ if (response.status === 499) {
683
+ retainCancelledAttempt();
684
+ return clientCancelledResponse();
685
+ }
686
+ let failure: ConsumedComboFailure;
687
+ try {
688
+ failure = consumedChildFailure
689
+ ?? await consumeComboFailure(response, options.abortSignal);
690
+ } catch (error) {
691
+ if (options.abortSignal?.aborted) {
692
+ retainCancelledAttempt();
693
+ return clientCancelledResponse();
694
+ }
695
+ throw error;
696
+ }
697
+ if (options.abortSignal?.aborted) {
698
+ retainCancelledAttempt();
699
+ return clientCancelledResponse();
700
+ }
701
+ sealRequestAttemptIdentity(
702
+ attempt,
703
+ childLog.provider,
704
+ childLog.providerAdapter ?? attempt.adapter,
705
+ );
706
+ finishRequestAttempt(
707
+ attempt,
708
+ response.status,
709
+ Date.now() - started,
710
+ failure.usage,
711
+ );
712
+ (logCtx.attempts ??= []).push(attempt);
713
+ attemptRetained = true;
714
+ lastFailure = failure.response;
715
+ if (comboFailureDecision(response.status, failure.classificationText) === "stop") {
716
+ Object.assign(logCtx, childLog, {
717
+ requestedModel: `combo/${comboId}`,
718
+ model: `combo/${comboId}`,
719
+ provider: "combo",
720
+ attempts: logCtx.attempts,
721
+ activeAttempt: undefined,
722
+ activeAttemptStartedAt: undefined,
723
+ });
724
+ return lastFailure;
725
+ }
726
+ console.warn(
727
+ `[combo] ${comboId}: ${targetKey(pick.target)} failed with ${response.status} after ${Date.now() - started}ms`,
728
+ );
729
+ pick = advanceComboAfterFailure(config, pick, {
730
+ retryAfter: failure.retryAfter,
731
+ now: Date.now(),
732
+ });
733
+ }
734
+ return lastFailure!;
735
+ }
736
+
417
737
  export async function handleResponses(
418
738
  req: Request,
419
739
  config: OcxConfig,
420
740
  logCtx: RequestLogContext,
421
- options: {
422
- forceEmptyResponseId?: boolean;
423
- abortSignal?: AbortSignal;
424
- authContext?: CodexAuthContext;
425
- selectedForwardHeaders?: Headers;
426
- recordTerminalOutcomes?: boolean;
427
- setTerminalOutcomeRecorder?: (recorder: ((status: ResponsesTerminalStatus) => void) | undefined) => void;
428
- onNativePassthroughTerminal?: (status: ResponsesTerminalStatus) => void;
429
- onNativePassthroughCancel?: () => void;
430
- } = {},
741
+ options: HandleResponsesOptions = {},
431
742
  ): Promise<Response> {
432
743
  let body: unknown;
433
744
  try {
@@ -435,6 +746,10 @@ export async function handleResponses(
435
746
  } catch (err) {
436
747
  return decodeRequestErrorResponse(err, "responses");
437
748
  }
749
+ const comboId = !options.comboAttempt ? comboIdFromRawBody(body) : null;
750
+ if (comboId && Object.hasOwn(config.combos ?? {}, comboId)) {
751
+ return handleComboResponses(req, body, comboId, config, logCtx, options);
752
+ }
438
753
  const originalBody = body;
439
754
  body = expandPreviousResponseInput(body);
440
755
  const previousResponseInputExpanded = body !== originalBody;
@@ -489,6 +804,9 @@ export async function handleResponses(
489
804
  try {
490
805
  route = routeModel(config, parsed.modelId);
491
806
  } catch (err) {
807
+ if (err instanceof NoAvailableComboTargetsError) {
808
+ return comboUnavailableResponse(err.message);
809
+ }
492
810
  return formatErrorResponse(404, "invalid_request_error", err instanceof Error ? err.message : String(err));
493
811
  }
494
812
 
@@ -505,6 +823,10 @@ export async function handleResponses(
505
823
  logCtx.provider = route.providerName;
506
824
  logCtx.providerAdapter = route.provider.adapter;
507
825
 
826
+ // Virtual model rewriting: Pro aliases → base model + reasoning.mode="pro".
827
+ // Must run before effort caps/native clamps so the base model gets correct limits.
828
+ applyOpenAiVirtualModel(parsed, route, logCtx);
829
+
508
830
  // Fast mode override: when config.fastMode is explicitly set, inject or strip
509
831
  // service_tier for OpenAI-routed models. Undefined = passthrough (client decides).
510
832
  if (config.fastMode !== undefined && route.provider.adapter === "openai-responses") {
@@ -586,11 +908,17 @@ export async function handleResponses(
586
908
  logCtx.requestedServiceTier ?? logCtx.configuredServiceTier,
587
909
  );
588
910
 
589
- let authCtx: CodexAuthContext;
911
+ let authCtx: CodexAuthContext = { kind: "main", accountId: null };
590
912
  let selectedForwardHeaders: Headers;
591
913
  try {
592
- authCtx = options.authContext ?? await resolveCodexAuthContext(req.headers, config);
593
- selectedForwardHeaders = options.selectedForwardHeaders ?? headersForCodexAuthContext(req.headers, authCtx);
914
+ if (route.codexAccountMode === "direct") validateForwardAdmissionCredential(req.headers, config);
915
+ if (route.codexAccountMode) {
916
+ authCtx = await resolveCodexAuthContext(req.headers, config, route.codexAccountMode);
917
+ options.onCodexAuthContextResolved?.(authCtx);
918
+ } else {
919
+ options.onCodexAuthContextResolved?.(undefined);
920
+ }
921
+ selectedForwardHeaders = headersForCodexAuthContext(req.headers, authCtx);
594
922
  } catch (err) {
595
923
  if (err instanceof CodexAccountCooldownError) {
596
924
  return formatErrorResponse(429, "rate_limit_error", "Selected Codex account is cooling down");
@@ -603,12 +931,21 @@ export async function handleResponses(
603
931
  console.error(`[codex-auth] Pool account ${safeAccountLabel} token failed; reauthentication required`);
604
932
  return formatErrorResponse(401, "authentication_error", "Selected Codex account needs reauthentication");
605
933
  }
934
+ if (err instanceof CodexPoolAuthenticationError) {
935
+ return formatErrorResponse(401, "authentication_error", err.message);
936
+ }
937
+ if (err instanceof CodexDirectAuthenticationError) {
938
+ return formatErrorResponse(401, "authentication_error", err.message);
939
+ }
940
+ if (err instanceof ForwardAdmissionCredentialError) {
941
+ return formatErrorResponse(401, "authentication_error", err.message);
942
+ }
606
943
  throw err;
607
944
  }
608
945
  if (!isCodexAuthContextUsable(authCtx, config)) {
609
946
  return formatErrorResponse(401, "authentication_error", "Selected Codex account needs reauthentication");
610
947
  }
611
- route.provider = applyCodexAuthContextToProvider(route.provider, authCtx);
948
+ route.provider = applyCodexAuthContextToProvider(route.provider, authCtx, route.codexAccountMode);
612
949
  logCtx.provider = formatCodexProviderForLog(route.providerName, codexLogAccountId(authCtx), config);
613
950
 
614
951
  // OAuth providers: swap in a fresh access token (auto-refreshed) as the Bearer key, so the
@@ -638,22 +975,48 @@ export async function handleResponses(
638
975
  }
639
976
  }
640
977
  route.provider = resolveProviderTransport(route.providerName, route.provider, parsed.options.promptCacheKey);
978
+ const adapterProvider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider);
979
+ const adapter = resolveAdapter(adapterProvider, config.cacheRetention);
980
+ logCtx.providerAdapter = adapter.name;
981
+ sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, adapter.name);
982
+ const isPassthrough = "passthrough" in adapter && !!adapter.passthrough;
983
+
984
+ let openAiSidecar: ResolvedOpenAiForwardSidecar | undefined;
985
+ const needsOpenAiVision = shouldResolveOpenAiVisionSidecar(config, route.provider, route.modelId, parsed);
986
+ const needsOpenAiSearch = shouldResolveOpenAiWebSearchSidecar(config, parsed, isPassthrough);
987
+ if (needsOpenAiVision || needsOpenAiSearch) {
988
+ try {
989
+ openAiSidecar = await resolveFirstUsableOpenAiSidecar(
990
+ listOpenAiForwardSidecarCandidates(config),
991
+ req.headers,
992
+ config,
993
+ );
994
+ } catch (err) {
995
+ // Sidecars are optional helpers for an otherwise independent routed turn.
996
+ // An unavailable/cooling/expired Multi credential disables the helper; it
997
+ // must not turn a valid routed-provider request into a Codex-auth failure.
998
+ if (
999
+ !(err instanceof CodexPoolAuthenticationError)
1000
+ && !(err instanceof CodexAuthContextError)
1001
+ && !(err instanceof CodexAccountCooldownError)
1002
+ && !(err instanceof CodexThreadAffinityExpiredError)
1003
+ ) throw err;
1004
+ }
1005
+ }
641
1006
 
642
1007
  // Vision sidecar: the routed model can't see images (provider.noVisionModels). Describe each
643
1008
  // attached image through the selected sidecar backend and replace it with text BEFORE the main
644
1009
  // call, so the text-only model can reason about it.
645
- const visionPlan = planVisionSidecar(config, route.provider, route.modelId, parsed, selectedForwardHeaders, authCtx);
646
- const recordSidecarOutcome = sidecarOutcomeRecorder(config, authCtx);
1010
+ const visionPlan = planVisionSidecar(config, route.provider, route.modelId, parsed, openAiSidecar);
1011
+ const recordSidecarOutcome = openAiSidecar?.recordOutcome;
647
1012
  if (visionPlan) {
648
- await describeImagesInPlace(parsed, visionPlan, selectedForwardHeaders, options.abortSignal, recordSidecarOutcome);
1013
+ await describeImagesInPlace(parsed, visionPlan, openAiSidecar?.headers ?? selectedForwardHeaders, options.abortSignal, recordSidecarOutcome);
649
1014
  } else if (modelInList(route.provider.noVisionModels, route.modelId)) {
650
1015
  // Sidecar-covered model but NO plan (no forward provider / missing forwarded auth / sidecar
651
1016
  // disabled): fail closed — never forward raw images to a text-only upstream.
652
1017
  stripImagesInPlace(parsed);
653
1018
  }
654
1019
 
655
- const adapterProvider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider);
656
- const adapter = resolveAdapter(adapterProvider, config.cacheRetention);
657
1020
  const recordTerminalOutcomes = options.recordTerminalOutcomes !== false;
658
1021
 
659
1022
  // Remote compaction v2 on a ROUTED model: Codex sent `compaction_trigger` and requires exactly
@@ -692,6 +1055,12 @@ export async function handleResponses(
692
1055
  );
693
1056
  }
694
1057
  const request = await adapter.buildRequest(parsed, { headers: selectedForwardHeaders });
1058
+ const passthroughEstimate = typeof request.usageLog?.inputTokens === "number"
1059
+ ? request.usageLog.inputTokens
1060
+ : undefined;
1061
+ if (passthroughEstimate !== undefined) {
1062
+ logCtx.usageLogInputTokens = passthroughEstimate;
1063
+ }
695
1064
  // Abort the upstream if the client disconnects. A directly-relayed body does not propagate the
696
1065
  // consumer's cancel to a signalled fetch, so we pass the signal and relay through relayWithAbort,
697
1066
  // whose cancel() aborts the upstream — preventing leaked connections (RC2, passthrough path).
@@ -704,15 +1073,19 @@ export async function handleResponses(
704
1073
  // the ChatGPT backend emits transient 502/520s that an immediate retry absorbs.
705
1074
  // Body is a replayable string; nothing has streamed to the client yet.
706
1075
  upstreamResponse = await fetchWithTransientRetry(
707
- () => fetchWithHeaderTimeout(request.url, {
708
- method: request.method,
709
- headers: request.headers,
710
- body: request.body,
711
- }, upstream.signal, connectMs, parsed.stream, providerFetch(route.provider)),
1076
+ recovery => {
1077
+ noteAttemptSend(logCtx.activeAttempt, passthroughEstimate, recovery);
1078
+ return fetchWithHeaderTimeout(request.url, {
1079
+ method: request.method,
1080
+ headers: request.headers,
1081
+ body: request.body,
1082
+ }, upstream.signal, connectMs, parsed.stream, providerFetch(route.provider));
1083
+ },
712
1084
  { abortSignal: upstream.signal, label: safeHostLabel(request.url) },
713
1085
  );
714
1086
  } catch (err) {
715
1087
  upstream.abort();
1088
+ if (options.abortSignal?.aborted) return clientCancelledResponse();
716
1089
  const outcome = err instanceof Error && err.name === "TimeoutError" ? "timeout" : "connect_error";
717
1090
  if (usesCodexForwardPoolAuth(authCtx, route.provider)) recordCodexUpstreamOutcome(config, authCtx.accountId, outcome);
718
1091
  const msg = outcome === "timeout"
@@ -774,7 +1147,7 @@ export async function handleResponses(
774
1147
  // async-pull segfault on Windows. Branch[0] goes directly to the Response (Bun
775
1148
  // native relay, never enters JS Sink.write); branch[1] is consumed in the
776
1149
  // background for terminal-outcome/quota inspection only.
777
- if (isEventStream && upstreamResponse.body) {
1150
+ if (upstreamResponse.ok && isEventStream && upstreamResponse.body) {
778
1151
  const [nativeBody, inspectBody] = upstreamResponse.body.tee();
779
1152
  const turnAc = new AbortController();
780
1153
  linkAbortSignal(upstream, turnAc.signal);
@@ -813,6 +1186,11 @@ export async function handleResponses(
813
1186
  }));
814
1187
  }
815
1188
  if (headers.get("content-type")?.toLowerCase().includes("application/json")) {
1189
+ if (!upstreamResponse.ok && options.comboAttempt) {
1190
+ const failure = await consumeComboFailure(upstreamResponse, options.abortSignal);
1191
+ options.onConsumedComboFailure?.(failure);
1192
+ return failure.response;
1193
+ }
816
1194
  const text = await upstreamResponse.text();
817
1195
  inspectResponseLogJson(logCtx, text);
818
1196
  if (upstreamResponse.ok && rememberPassthroughResponse) {
@@ -841,6 +1219,7 @@ export async function handleResponses(
841
1219
  const queue = createAdapterEventQueue();
842
1220
  const runTurn = async (): Promise<void> => {
843
1221
  try {
1222
+ noteAttemptSend(logCtx.activeAttempt, logCtx.usageLogInputTokens);
844
1223
  await adapter.runTurn?.(
845
1224
  parsed,
846
1225
  { headers: selectedForwardHeaders, abortSignal: runTurnAbort.signal },
@@ -859,8 +1238,19 @@ export async function handleResponses(
859
1238
  const { toolNsMap, freeformToolNames, toolSearchToolNames } = buildToolBridgeMaps(parsed);
860
1239
  if (parsed.stream) {
861
1240
  void runTurn();
1241
+ let eventSource: AsyncIterable<AdapterEvent> = queue.stream();
1242
+ if (options.comboAttempt) {
1243
+ const preflight = await preflightAdapterEvents(eventSource);
1244
+ if (preflight.error || preflight.empty) {
1245
+ runTurnAbort.abort();
1246
+ queue.close();
1247
+ const message = preflight.error?.message ?? "Adapter ended before producing a response";
1248
+ return formatErrorResponse(502, "upstream_error", redactSecretString(message));
1249
+ }
1250
+ eventSource = preflight.stream;
1251
+ }
862
1252
  const sseStream = bridgeToResponsesSSE(
863
- queue.stream(), parsed.modelId, toolNsMap, freeformToolNames, toolSearchToolNames,
1253
+ eventSource, parsed.modelId, toolNsMap, freeformToolNames, toolSearchToolNames,
864
1254
  () => {
865
1255
  runTurnAbort.abort();
866
1256
  queue.close();
@@ -882,6 +1272,15 @@ export async function handleResponses(
882
1272
 
883
1273
  await runTurn();
884
1274
  const events = await queue.collect();
1275
+ if (options.comboAttempt) {
1276
+ const firstMeaningful = events.find(event => event.type !== "heartbeat");
1277
+ if (!firstMeaningful || firstMeaningful.type === "error") {
1278
+ const message = firstMeaningful?.type === "error"
1279
+ ? firstMeaningful.message
1280
+ : "Adapter ended before producing a response";
1281
+ return formatErrorResponse(502, "upstream_error", redactSecretString(message));
1282
+ }
1283
+ }
885
1284
  const json = buildResponseJSON(events, parsed.modelId, {
886
1285
  hideThinkingSummary: parsed.options.hideThinkingSummary,
887
1286
  toolNsMap,
@@ -896,21 +1295,22 @@ export async function handleResponses(
896
1295
  // Web-search sidecar: Codex enabled web_search but this is a routed (non-OpenAI) model that can't
897
1296
  // run it server-side. Expose web_search as a function tool and run searches via the gpt-mini sidecar
898
1297
  // through the ChatGPT passthrough, looping until the model answers. Otherwise take the normal path.
899
- const wsPlan = planWebSearch(config, parsed, false, selectedForwardHeaders, route.provider, route.modelId, authCtx);
1298
+ const wsPlan = planWebSearch(config, parsed, false, route.provider, route.modelId, openAiSidecar);
900
1299
  if (wsPlan) {
901
1300
  parsed.context.tools = [...(parsed.context.tools ?? []), buildWebSearchTool()];
1301
+ noteAttemptSend(logCtx.activeAttempt, logCtx.usageLogInputTokens);
902
1302
  const wsResponse = await runWithWebSearch({
903
1303
  parsed, adapter,
904
1304
  backend: wsPlan.backend,
905
- forwardProvider: wsPlan.forwardProvider,
1305
+ forwardProvider: wsPlan.forwardSidecar?.provider,
906
1306
  anthropicSidecar: wsPlan.anthropicSidecar,
907
1307
  hostedTool: wsPlan.hostedTool,
908
- selectedForwardHeaders,
1308
+ selectedForwardHeaders: wsPlan.forwardSidecar?.headers ?? selectedForwardHeaders,
909
1309
  settings: wsPlan.settings,
910
1310
  maxSearches: wsPlan.maxSearches,
911
1311
  forceEmptyResponseId: true,
912
1312
  abortSignal: options.abortSignal,
913
- recordSidecarOutcome,
1313
+ recordSidecarOutcome: wsPlan.forwardSidecar?.recordOutcome,
914
1314
  connectTimeoutMs: config.connectTimeoutMs ?? 200_000,
915
1315
  routedModelStallTimeoutMs: wsPlan.routedModelStallTimeoutMs,
916
1316
  stallTimeoutSec: wsPlan.stallTimeoutSec,
@@ -946,22 +1346,34 @@ export async function handleResponses(
946
1346
  const connectMs = config.connectTimeoutMs ?? 200_000;
947
1347
 
948
1348
  const request = await adapter.buildRequest(parsed, { headers: selectedForwardHeaders });
949
- if (typeof request.usageLog?.inputTokens === "number") {
950
- logCtx.usageLogInputTokens = request.usageLog.inputTokens;
951
- }
1349
+ const inputTokenEstimate = typeof request.usageLog?.inputTokens === "number"
1350
+ ? request.usageLog.inputTokens
1351
+ : undefined;
1352
+ if (inputTokenEstimate !== undefined) logCtx.usageLogInputTokens = inputTokenEstimate;
952
1353
  let upstreamResponse: Response;
953
1354
  try {
954
- upstreamResponse = adapter.fetchResponse
955
- ? await adapter.fetchResponse(request, { abortSignal: upstream.signal, timeoutMs: connectMs, stream: parsed.stream })
956
- : await fetchWithResetRetry(
957
- () => fetchWithHeaderTimeout(request.url, {
1355
+ if (adapter.fetchResponse) {
1356
+ noteAttemptSend(logCtx.activeAttempt, inputTokenEstimate);
1357
+ upstreamResponse = await adapter.fetchResponse(request, {
1358
+ abortSignal: upstream.signal,
1359
+ timeoutMs: connectMs,
1360
+ stream: parsed.stream,
1361
+ });
1362
+ } else {
1363
+ upstreamResponse = await fetchWithResetRetry(
1364
+ recovery => {
1365
+ noteAttemptSend(logCtx.activeAttempt, inputTokenEstimate, recovery);
1366
+ return fetchWithHeaderTimeout(request.url, {
958
1367
  method: request.method, headers: request.headers, body: request.body,
959
- }, upstream.signal, connectMs, parsed.stream, providerFetch(route.provider)),
960
- { abortSignal: upstream.signal, label: safeHostLabel(request.url) },
961
- );
1368
+ }, upstream.signal, connectMs, parsed.stream, providerFetch(route.provider));
1369
+ },
1370
+ { abortSignal: upstream.signal, label: safeHostLabel(request.url) },
1371
+ );
1372
+ }
962
1373
  } catch (err) {
963
1374
  cleanupUpstreamAbort();
964
1375
  upstream.abort();
1376
+ if (options.abortSignal?.aborted) return clientCancelledResponse();
965
1377
  const msg = err instanceof Error && err.name === "TimeoutError"
966
1378
  ? `Provider connect timeout after ${connectMs}ms`
967
1379
  : `Provider unreachable: ${err instanceof Error ? err.message : String(err)}`;
@@ -978,11 +1390,20 @@ export async function handleResponses(
978
1390
  let imageTierBias = 0;
979
1391
  let imageRetryAttempted = false;
980
1392
  let oauth401ReplayAttempted = false;
981
- const rebuildAndRefetch = async (): Promise<Response | { failed: Response }> => {
1393
+ const rebuildAndRefetch = async (
1394
+ recovery: AttemptRecoveryKind,
1395
+ ): Promise<Response | { failed: Response }> => {
982
1396
  const retryRequest = await activeAdapter.buildRequest(parsed, {
983
1397
  headers: selectedForwardHeaders,
984
1398
  ...(imageTierBias > 0 ? { imageTierBias } : {}),
985
1399
  });
1400
+ const retryEstimate = typeof retryRequest.usageLog?.inputTokens === "number"
1401
+ ? retryRequest.usageLog.inputTokens
1402
+ : undefined;
1403
+ if (retryEstimate !== undefined) logCtx.usageLogInputTokens = retryEstimate;
1404
+ logCtx.providerAdapter = activeAdapter.name;
1405
+ sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name);
1406
+ noteAttemptSend(logCtx.activeAttempt, retryEstimate, recovery);
986
1407
  try {
987
1408
  return activeAdapter.fetchResponse
988
1409
  ? await activeAdapter.fetchResponse(retryRequest, { abortSignal: upstream.signal, timeoutMs: connectMs, stream: parsed.stream })
@@ -992,6 +1413,9 @@ export async function handleResponses(
992
1413
  } catch (err) {
993
1414
  cleanupUpstreamAbort();
994
1415
  upstream.abort();
1416
+ if (options.abortSignal?.aborted) {
1417
+ return { failed: clientCancelledResponse() };
1418
+ }
995
1419
  const msg = err instanceof Error && err.name === "TimeoutError"
996
1420
  ? `Provider connect timeout after ${connectMs}ms`
997
1421
  : `Provider unreachable: ${err instanceof Error ? err.message : String(err)}`;
@@ -1025,7 +1449,7 @@ export async function handleResponses(
1025
1449
  resolveWireProtocolOverride(route.providerName, route.modelId, refreshedProvider),
1026
1450
  config.cacheRetention,
1027
1451
  );
1028
- const result = await rebuildAndRefetch();
1452
+ const result = await rebuildAndRefetch("oauth-401");
1029
1453
  if ("failed" in result) return result.failed;
1030
1454
  upstreamResponse = result;
1031
1455
  continue recovery;
@@ -1050,7 +1474,7 @@ export async function handleResponses(
1050
1474
  resolveWireProtocolOverride(route.providerName, route.modelId, route.provider),
1051
1475
  config.cacheRetention,
1052
1476
  );
1053
- const result = await rebuildAndRefetch();
1477
+ const result = await rebuildAndRefetch("key-429");
1054
1478
  if ("failed" in result) return result.failed;
1055
1479
  upstreamResponse = result;
1056
1480
  }
@@ -1065,7 +1489,7 @@ export async function handleResponses(
1065
1489
  imageRetryAttempted = true;
1066
1490
  imageTierBias = 1;
1067
1491
  try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
1068
- const result = await rebuildAndRefetch();
1492
+ const result = await rebuildAndRefetch("image-413");
1069
1493
  if ("failed" in result) return result.failed;
1070
1494
  upstreamResponse = result;
1071
1495
  continue recovery;
@@ -1073,6 +1497,12 @@ export async function handleResponses(
1073
1497
  break;
1074
1498
  }
1075
1499
  if (!upstreamResponse.ok) {
1500
+ if (options.comboAttempt) {
1501
+ const failure = await consumeComboFailure(upstreamResponse, options.abortSignal)
1502
+ .finally(cleanupUpstreamAbort);
1503
+ options.onConsumedComboFailure?.(failure);
1504
+ return failure.response;
1505
+ }
1076
1506
  const errorText = await upstreamResponse.text().catch(() => "unknown error");
1077
1507
  cleanupUpstreamAbort();
1078
1508
  // Upstreams occasionally echo request details in error bodies — scrub token-shaped
@@ -1146,7 +1576,62 @@ export function linkAbortSignal(upstream: AbortController, signal?: AbortSignal)
1146
1576
  * REPLACEMENT history (compact_remote.rs). Passthrough forwards to the real ChatGPT backend;
1147
1577
  * routed models run the same summarizer used for v2 and convert the summary to v1 history items.
1148
1578
  */
1149
- export async function handleResponsesCompact(req: Request, config: OcxConfig): Promise<Response> {
1579
+ export const COMPACT_RESPONSE_MAX_BYTES = 32 * 1024 * 1024;
1580
+
1581
+ function compactResponseTooLargeError(): Response {
1582
+ return new Response(JSON.stringify({
1583
+ error: {
1584
+ message: "Compact response exceeded 32 MiB",
1585
+ type: "compact_response_too_large",
1586
+ code: "compact_response_too_large",
1587
+ },
1588
+ }), { status: 502, headers: { "Content-Type": "application/json" } });
1589
+ }
1590
+
1591
+ async function bufferCompactResponse(upstream: Response, signal: AbortSignal): Promise<Response> {
1592
+ const reader = upstream.body?.getReader();
1593
+ const contentType = upstream.headers.get("content-type") ?? "application/json";
1594
+ if (!reader) return new Response(null, { status: upstream.status, headers: { "Content-Type": contentType } });
1595
+ const declaredLength = Number(upstream.headers.get("content-length"));
1596
+ if (Number.isFinite(declaredLength) && declaredLength > COMPACT_RESPONSE_MAX_BYTES) {
1597
+ await reader.cancel("compact_response_too_large").catch(() => undefined);
1598
+ return compactResponseTooLargeError();
1599
+ }
1600
+ const chunks: Uint8Array[] = [];
1601
+ let total = 0;
1602
+ try {
1603
+ while (true) {
1604
+ if (signal.aborted) {
1605
+ await reader.cancel(signal.reason).catch(() => undefined);
1606
+ return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
1607
+ }
1608
+ const { done, value } = await reader.read();
1609
+ if (done) break;
1610
+ total += value.byteLength;
1611
+ if (total > COMPACT_RESPONSE_MAX_BYTES) {
1612
+ await reader.cancel("compact_response_too_large").catch(() => undefined);
1613
+ return compactResponseTooLargeError();
1614
+ }
1615
+ chunks.push(value);
1616
+ }
1617
+ } catch {
1618
+ if (signal.aborted) return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
1619
+ return formatErrorResponse(502, "upstream_error", "Failed to read compact response");
1620
+ }
1621
+ const body = new Uint8Array(total);
1622
+ let offset = 0;
1623
+ for (const chunk of chunks) {
1624
+ body.set(chunk, offset);
1625
+ offset += chunk.byteLength;
1626
+ }
1627
+ return new Response(body, { status: upstream.status, headers: { "Content-Type": contentType } });
1628
+ }
1629
+
1630
+ export async function handleResponsesCompact(
1631
+ req: Request,
1632
+ config: OcxConfig,
1633
+ logCtx: RequestLogContext,
1634
+ ): Promise<Response> {
1150
1635
  let body: unknown;
1151
1636
  try {
1152
1637
  body = await readJsonRequestBody(req);
@@ -1167,6 +1652,27 @@ export async function handleResponsesCompact(req: Request, config: OcxConfig): P
1167
1652
  } catch (err) {
1168
1653
  return formatErrorResponse(404, "invalid_request_error", err instanceof Error ? err.message : String(err));
1169
1654
  }
1655
+ const selectedModelId = route.modelId;
1656
+ logCtx.requestedModel = raw.model;
1657
+ logCtx.model = selectedModelId;
1658
+ logCtx.provider = route.providerName;
1659
+ logCtx.providerAdapter = route.provider.adapter;
1660
+ const virtual = resolveOpenAiCompactModel(route.providerName, selectedModelId);
1661
+ if (virtual) {
1662
+ route.modelId = virtual.wireModelId;
1663
+ logCtx.model = virtual.selectedModelId;
1664
+ logCtx.resolvedModel = virtual.wireModelId;
1665
+ } else {
1666
+ logCtx.resolvedModel = route.modelId;
1667
+ }
1668
+
1669
+ if (route.codexAccountMode === "direct") {
1670
+ try { validateForwardAdmissionCredential(req.headers, config); }
1671
+ catch (err) {
1672
+ if (err instanceof ForwardAdmissionCredentialError) return formatErrorResponse(401, "authentication_error", err.message);
1673
+ throw err;
1674
+ }
1675
+ }
1170
1676
 
1171
1677
  if (route.provider.adapter === "openai-responses") {
1172
1678
  // Native ChatGPT/OpenAI model: forward the compact request verbatim to the real backend.
@@ -1176,37 +1682,51 @@ export async function handleResponsesCompact(req: Request, config: OcxConfig): P
1176
1682
  let compactProvider = route.provider;
1177
1683
  const headers = new Headers({ "content-type": "application/json" });
1178
1684
  try {
1179
- const authCtx = await resolveCodexAuthContext(req.headers, config);
1180
- const selected = headersForCodexAuthContext(req.headers, authCtx);
1181
- compactProvider = applyCodexAuthContextToProvider(route.provider, authCtx);
1182
- for (const name of FORWARD_HEADERS) {
1183
- const value = selected.get(name);
1184
- if (value) headers.set(name, value);
1685
+ if (route.codexAccountMode) {
1686
+ const authCtx = await resolveCodexAuthContext(req.headers, config, route.codexAccountMode);
1687
+ const selected = headersForCodexAuthContext(req.headers, authCtx);
1688
+ compactProvider = applyCodexAuthContextToProvider(route.provider, authCtx, route.codexAccountMode);
1689
+ for (const name of FORWARD_HEADERS) {
1690
+ const value = selected.get(name);
1691
+ if (value) headers.set(name, value);
1692
+ }
1693
+ const override = (compactProvider as { _codexAccountOverride?: { accessToken: string; chatgptAccountId: string } })._codexAccountOverride;
1694
+ if (override) {
1695
+ headers.set("authorization", `Bearer ${override.accessToken}`);
1696
+ headers.set("chatgpt-account-id", override.chatgptAccountId);
1697
+ }
1698
+ }
1699
+ } catch (err) {
1700
+ if (err instanceof CodexAccountCooldownError) {
1701
+ return formatErrorResponse(429, "rate_limit_error", "Selected Codex account is cooling down");
1185
1702
  }
1186
- const override = (compactProvider as { _codexAccountOverride?: { accessToken: string; chatgptAccountId: string } })._codexAccountOverride;
1187
- if (override) {
1188
- headers.set("authorization", `Bearer ${override.accessToken}`);
1189
- headers.set("chatgpt-account-id", override.chatgptAccountId);
1703
+ if (err instanceof CodexThreadAffinityExpiredError) {
1704
+ return formatErrorResponse(409, "invalid_request_error", "Codex thread account affinity expired; start a new session");
1190
1705
  }
1191
- } catch {
1192
- // Auth-context failures degrade to raw forwarded headers (pre-existing behavior) rather
1193
- // than failing the compact turn outright — codex-rs treats compact errors as session-fatal.
1194
- for (const name of FORWARD_HEADERS) {
1195
- const value = req.headers.get(name);
1196
- if (value) headers.set(name, value);
1706
+ if (err instanceof CodexAuthContextError) {
1707
+ return formatErrorResponse(401, "authentication_error", "Selected Codex account needs reauthentication");
1197
1708
  }
1709
+ if (err instanceof CodexPoolAuthenticationError || err instanceof CodexDirectAuthenticationError) {
1710
+ return formatErrorResponse(401, "authentication_error", err.message);
1711
+ }
1712
+ throw err;
1198
1713
  }
1199
1714
  const base = (compactProvider.baseUrl ?? "").replace(/\/$/, "");
1200
1715
  if (compactProvider.apiKey) headers.set("authorization", `Bearer ${resolveEnvValue(compactProvider.apiKey)}`);
1201
- const upstream = await fetch(`${base}/responses/compact`, {
1202
- method: "POST",
1203
- headers,
1204
- body: JSON.stringify({ ...raw, model: route.modelId }),
1205
- });
1206
- return new Response(upstream.body, {
1207
- status: upstream.status,
1208
- headers: { "Content-Type": upstream.headers.get("content-type") ?? "application/json" },
1209
- });
1716
+ const { reasoning: _reasoning, ...compactBody } = raw as typeof raw & { reasoning?: unknown };
1717
+ let upstream: Response;
1718
+ try {
1719
+ upstream = await fetch(`${base}/responses/compact`, {
1720
+ method: "POST",
1721
+ headers,
1722
+ body: JSON.stringify({ ...compactBody, model: route.modelId }),
1723
+ signal: req.signal,
1724
+ });
1725
+ } catch {
1726
+ if (req.signal.aborted) return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
1727
+ return formatErrorResponse(502, "upstream_error", "Failed to connect to compact upstream");
1728
+ }
1729
+ return bufferCompactResponse(upstream, req.signal);
1210
1730
  }
1211
1731
 
1212
1732
  // ROUTED model: run the v2 synthetic-compaction turn internally (appends COMPACT_PROMPT, no
@@ -1227,7 +1747,6 @@ export async function handleResponsesCompact(req: Request, config: OcxConfig): P
1227
1747
  headers: internalHeaders,
1228
1748
  body: JSON.stringify(internalBody),
1229
1749
  });
1230
- const logCtx: RequestLogContext = { model: route.modelId, provider: route.providerName };
1231
1750
  const response = await handleResponses(internalReq, config, logCtx, { abortSignal: req.signal });
1232
1751
  if (!response.ok) return response;
1233
1752
  let json: { output?: unknown[] };