@bitkyc08/opencodex 2.55.0 → 2.56.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 (167) hide show
  1. package/gui/dist/assets/{index-VuoiWj9J.js → index-D4zuyIxQ.js} +1 -1
  2. package/gui/dist/index.html +1 -1
  3. package/package.json +2 -1
  4. package/src/adapters/base.ts +21 -0
  5. package/src/adapters/cursor/transport-retry.ts +46 -1
  6. package/src/adapters/cursor.ts +4 -0
  7. package/src/adapters/kiro/adapter.ts +42 -1
  8. package/src/adapters/kiro-retry.ts +23 -4
  9. package/src/adapters/openai-chat/errors.ts +116 -0
  10. package/src/adapters/openai-chat/messages.ts +346 -0
  11. package/src/adapters/openai-chat/passthrough.ts +146 -0
  12. package/src/adapters/openai-chat/response-events.ts +117 -0
  13. package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
  14. package/src/adapters/openai-chat/tool-schema.ts +477 -0
  15. package/src/adapters/openai-chat/wire.ts +50 -0
  16. package/src/adapters/openai-chat.ts +33 -1445
  17. package/src/adapters/openai-responses/canonical-forward.ts +202 -0
  18. package/src/adapters/openai-responses/image-gen.ts +406 -0
  19. package/src/adapters/openai-responses/internal.ts +3 -0
  20. package/src/adapters/openai-responses/passthrough.ts +611 -0
  21. package/src/adapters/openai-responses/prompt-cache.ts +83 -0
  22. package/src/adapters/openai-responses/reasoning.ts +220 -0
  23. package/src/adapters/openai-responses/request-strips.ts +185 -0
  24. package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
  25. package/src/adapters/openai-responses/tool-schema.ts +293 -0
  26. package/src/adapters/openai-responses/web-search.ts +156 -0
  27. package/src/adapters/openai-responses.ts +4 -2625
  28. package/src/bridge/errors.ts +34 -0
  29. package/src/bridge/internal.ts +174 -0
  30. package/src/bridge/response-json.ts +624 -0
  31. package/src/bridge/sse.ts +1444 -0
  32. package/src/bridge.ts +5 -2204
  33. package/src/chat/inbound.ts +12 -1
  34. package/src/codex/account-lifecycle.ts +3 -0
  35. package/src/codex/account-store.ts +71 -9
  36. package/src/codex/auth-api/account-list.ts +507 -0
  37. package/src/codex/auth-api/http.ts +32 -0
  38. package/src/codex/auth-api/login-flow.ts +554 -0
  39. package/src/codex/auth-api/login-state.ts +64 -0
  40. package/src/codex/auth-api/main-account-probe.ts +331 -0
  41. package/src/codex/auth-api/pool-mode-gate.ts +274 -0
  42. package/src/codex/auth-api/pool-quota-probe.ts +512 -0
  43. package/src/codex/auth-api/reset-credit-service.ts +422 -0
  44. package/src/codex/auth-api/routes.ts +425 -0
  45. package/src/codex/auth-api/runtime-config.ts +48 -0
  46. package/src/codex/auth-api.ts +27 -3118
  47. package/src/codex/auth-context.ts +95 -28
  48. package/src/codex/catalog/auto-review.ts +507 -0
  49. package/src/codex/catalog/build-entries.ts +981 -0
  50. package/src/codex/catalog/combo-member.ts +375 -0
  51. package/src/codex/catalog/derive-entry.ts +229 -0
  52. package/src/codex/catalog/effort.ts +0 -1
  53. package/src/codex/catalog/gated-native-warn.ts +63 -0
  54. package/src/codex/catalog/gather-capture.ts +533 -0
  55. package/src/codex/catalog/model-hints.ts +691 -0
  56. package/src/codex/catalog/model-visibility.ts +304 -0
  57. package/src/codex/catalog/provider-fetch.ts +52 -2942
  58. package/src/codex/catalog/provider-models.ts +685 -0
  59. package/src/codex/catalog/restore.ts +132 -0
  60. package/src/codex/catalog/retained-sync.ts +706 -0
  61. package/src/codex/catalog/routed-gather.ts +858 -0
  62. package/src/codex/catalog/subagent-roster.ts +176 -0
  63. package/src/codex/catalog/sync.ts +52 -2698
  64. package/src/codex/inject/config-toml.ts +563 -0
  65. package/src/codex/inject/remove.ts +192 -0
  66. package/src/codex/inject/restore.ts +540 -0
  67. package/src/codex/inject/routing-classify.ts +109 -0
  68. package/src/codex/inject/routing-target.ts +125 -0
  69. package/src/codex/inject.ts +81 -1436
  70. package/src/codex/lineage.ts +458 -0
  71. package/src/codex/pool-refresh-backoff.ts +152 -0
  72. package/src/codex/routing/active-account.ts +194 -0
  73. package/src/codex/routing/cooldown-math.ts +275 -0
  74. package/src/codex/routing/health-store.ts +402 -0
  75. package/src/codex/routing/probe-lease.ts +358 -0
  76. package/src/codex/routing/selection.ts +703 -0
  77. package/src/codex/routing/thread-affinity.ts +538 -0
  78. package/src/codex/routing.ts +353 -2234
  79. package/src/codex/shim-fingerprint.ts +223 -0
  80. package/src/codex/shim-inspect.ts +175 -0
  81. package/src/codex/shim-probe.ts +367 -0
  82. package/src/codex/shim-restore-lock.ts +169 -0
  83. package/src/codex/shim-state-file.ts +151 -0
  84. package/src/codex/shim-templates.ts +265 -0
  85. package/src/codex/shim.ts +48 -1268
  86. package/src/config/diagnostics.ts +705 -0
  87. package/src/config/feature-flags.ts +55 -0
  88. package/src/config/live-reconcile.ts +403 -0
  89. package/src/config/load-degrade.ts +880 -0
  90. package/src/config/mutation-lock.ts +244 -0
  91. package/src/config/openai-tier-backup.ts +268 -0
  92. package/src/config/persist-unlocked.ts +92 -0
  93. package/src/config/proxy-env.ts +188 -0
  94. package/src/config/salvage.ts +244 -0
  95. package/src/config/schema/config-schema.ts +640 -0
  96. package/src/config/schema/leaf-validators.ts +855 -0
  97. package/src/config/warn-memo.ts +28 -0
  98. package/src/config.ts +234 -4481
  99. package/src/generated/compatibility-version.json +539 -39
  100. package/src/lib/request-execution-budget.ts +69 -20
  101. package/src/lib/spend-reservation-ledger.ts +940 -0
  102. package/src/lib/upstream-retry.ts +55 -11
  103. package/src/lib/workflow-budget.ts +553 -30
  104. package/src/providers/quota/account-cache.ts +441 -0
  105. package/src/providers/quota/antigravity.ts +295 -0
  106. package/src/providers/quota/report-cache.ts +320 -0
  107. package/src/providers/quota/vendor-probes-key.ts +1243 -0
  108. package/src/providers/quota/vendor-probes-oauth.ts +590 -0
  109. package/src/providers/quota.ts +324 -3079
  110. package/src/providers/registry/entries-core.ts +1221 -0
  111. package/src/providers/registry/entries-extended.ts +1204 -0
  112. package/src/providers/registry/model-seeds.ts +908 -0
  113. package/src/providers/registry/types.ts +352 -0
  114. package/src/providers/registry.ts +24 -3536
  115. package/src/responses/continuation-ownership.ts +29 -0
  116. package/src/responses/state/replay-fingerprint.ts +80 -0
  117. package/src/responses/state/snapshot-codec.ts +104 -0
  118. package/src/responses/state/spill-failure.ts +118 -0
  119. package/src/responses/state/spill-queue.ts +665 -0
  120. package/src/responses/state/temp-recovery.ts +257 -0
  121. package/src/responses/state.ts +82 -1143
  122. package/src/routing/identity-domains.ts +449 -0
  123. package/src/routing/probe-lease.ts +511 -0
  124. package/src/server/index/bounded-request.ts +88 -0
  125. package/src/server/index/live-sideband.ts +565 -0
  126. package/src/server/index/serve-options.ts +1766 -0
  127. package/src/server/index/startup-warnings.ts +213 -0
  128. package/src/server/index/websocket-handler.ts +335 -0
  129. package/src/server/index.ts +40 -2547
  130. package/src/server/management/route-registry.ts +26 -23
  131. package/src/server/management/shared.ts +8 -5
  132. package/src/server/management/workflow-budget-routes.ts +133 -0
  133. package/src/server/management-api.ts +12 -0
  134. package/src/server/request-log-conversation.ts +9 -7
  135. package/src/server/request-log.ts +245 -1
  136. package/src/server/responses/account-change-state.ts +233 -0
  137. package/src/server/responses/adapter-continuation.ts +514 -0
  138. package/src/server/responses/adapter-delivery.ts +214 -0
  139. package/src/server/responses/adapter-dispatch.ts +971 -0
  140. package/src/server/responses/compact.ts +59 -4
  141. package/src/server/responses/completion-policy.ts +33 -0
  142. package/src/server/responses/core-auth.ts +527 -0
  143. package/src/server/responses/core-codex-account.ts +859 -0
  144. package/src/server/responses/core-combo-failure.ts +210 -0
  145. package/src/server/responses/core-combo.ts +707 -0
  146. package/src/server/responses/core-errors.ts +152 -0
  147. package/src/server/responses/core-lifetime.ts +95 -0
  148. package/src/server/responses/core-normalize.ts +350 -0
  149. package/src/server/responses/core-opaque-recovery.ts +380 -0
  150. package/src/server/responses/core-options.ts +159 -0
  151. package/src/server/responses/core-replay.ts +225 -0
  152. package/src/server/responses/core.ts +192 -8893
  153. package/src/server/responses/passthrough-delivery.ts +856 -0
  154. package/src/server/responses/passthrough-dispatch.ts +1476 -0
  155. package/src/server/responses/passthrough-execution.ts +54 -0
  156. package/src/server/responses/request-prepare.ts +970 -0
  157. package/src/server/responses/request-send-budget.ts +164 -0
  158. package/src/server/responses/request-sidecar-auth.ts +149 -0
  159. package/src/server/responses/request-transport.ts +744 -0
  160. package/src/server/responses/response-effects.ts +157 -0
  161. package/src/server/responses/run-turn-execution.ts +448 -0
  162. package/src/server/responses/sidecar-execution.ts +469 -0
  163. package/src/server/responses-image-gen-repair.ts +1 -1
  164. package/src/server/workflow-refusal.ts +84 -0
  165. package/src/types/config.ts +30 -0
  166. package/src/usage/log.ts +146 -0
  167. package/src/usage/summary.ts +171 -21
@@ -0,0 +1,164 @@
1
+ import type { ResponsesRequestContext } from "./core-options";
2
+ import { createRequestExecutionBudget, isRequestExecutionBudget } from "../../lib/request-execution-budget";
3
+ import { chargeWorkflowSends, workflowSendCeilingReached } from "../../lib/workflow-budget";
4
+ import { workflowRefusalResponse } from "../workflow-refusal";
5
+ import type { AttemptRecoveryKind } from "../../usage/log";
6
+ import { noteAttemptSend } from "../request-log";
7
+ import { TRANSIENT_RETRY_MAX_ATTEMPTS } from "../../lib/upstream-retry";
8
+ import type { SingleUseDispatchPermit, SendClass } from "../../lib/request-execution-budget";
9
+
10
+ /** Owns the shared request send counter and recovery permits. */
11
+ export function createResponsesSendBudget(
12
+ requestContext: Pick<ResponsesRequestContext, "options" | "req" | "logCtx">,
13
+ ) {
14
+ const { options, req, logCtx } = requestContext;
15
+
16
+
17
+ // One transient-retry budget for the whole LOGICAL request, read ABOVE the passthrough branch
18
+ // so that branch shares it too. It used to be a local declared below, which put it in the
19
+ // temporal dead zone for the passthrough sends and left each recovery leg taking the helper's
20
+ // fresh default of 3. It is now a holder carried on options, so a combo child inherits the
21
+ // parent's spend instead of starting over per target -- both halves of the measured
22
+ // amplification in #4546.
23
+ const sendBudget = options.sendBudget ?? createRequestExecutionBudget();
24
+ // The root workflow is the user-visible task. A per-request cap cannot bound a fan-out that
25
+ // sends once per child seven hundred times, so every send charged to the request is charged
26
+ // to the root as well (#4546).
27
+ const workflowRootId = req.headers.get("x-codex-parent-thread-id")?.trim() || undefined;
28
+ const noteTransientSends = (used: number): void => {
29
+ const charged = Math.max(0, used);
30
+ sendBudget.used += charged;
31
+ chargeWorkflowSends(workflowRootId, charged);
32
+ };
33
+ // Refused before any dispatch, and deliberately not by evicting the root's ledger entry:
34
+ // dropping the record to make room would hand the fan-out a fresh allowance, which is the
35
+ // laundering this ceiling exists to stop. The client is told the task needs a new grant
36
+ // rather than being given a synthetic upstream error.
37
+ if (workflowSendCeilingReached(workflowRootId)) {
38
+ // A log context exists here, unlike at HTTP admission, so the row this request writes is
39
+ // marked synthetic rather than reading as a request that vanished with zero sends.
40
+ return workflowRefusalResponse("workflow-sends-exhausted", logCtx, undefined, workflowRootId);
41
+ }
42
+ // No floor. Math.max(1, ...) meant an exhausted request still funded one send on every
43
+ // recovery leg, so a bounded per-leg allowance never became a bounded per-request one.
44
+ const remainingTransientSendBudget = (budget: number): number =>
45
+ isRequestExecutionBudget(sendBudget)
46
+ ? sendBudget.remainingBaseSends(budget)
47
+ : Math.max(0, budget - sendBudget.used);
48
+ // The adapter contract needs the full budget, not just the counter. options.sendBudget is
49
+ // typed as the narrow holder so a caller that predates this can still pass one, so narrow it
50
+ // once here rather than asserting at each adapter call site.
51
+ const adapterSendBudget = isRequestExecutionBudget(sendBudget) ? sendBudget : undefined;
52
+ /**
53
+ * Records an adapter's OWN inner retries against this attempt.
54
+ *
55
+ * Ordinal 1 is the send each call site already recorded through `noteAttemptSend`, so only
56
+ * the extra physical sends are added here and an adapter that does not retry internally
57
+ * leaves its log byte-for-byte as it was. Kiro reaches roughly eighteen sends per call and
58
+ * Cursor re-sends a whole turn, and both reported one; a count that cannot be observed
59
+ * cannot be pinned by a regression, which is why the instrumentation precedes the cap.
60
+ */
61
+ const noteAdapterPhysicalSend = (
62
+ inputTokens: number | undefined,
63
+ send: { ordinal: number; recovery?: AttemptRecoveryKind },
64
+ ): void => {
65
+ if (send.ordinal <= 1) return;
66
+ noteAttemptSend(logCtx.activeAttempt, inputTokens, send.recovery);
67
+ };
68
+ const sendBudgetExhausted = (): boolean =>
69
+ remainingTransientSendBudget(TRANSIENT_RETRY_MAX_ATTEMPTS) === 0;
70
+ /**
71
+ * A credential hop reserves the send its own replay will make, and that replay is a recovery
72
+ * leg. The leg must SPEND the hop's reservation instead of taking a second one: the
73
+ * final-recovery reserve is single, so a rebuild that reserved on top of a hop would be
74
+ * refused and the request would answer with a synthetic 502 in place of the real 429 the hop
75
+ * was recovering from.
76
+ */
77
+ let pendingHopPermit: SingleUseDispatchPermit | undefined;
78
+ /**
79
+ * How many sends a recovery leg may make, and the permit that authorises the last one.
80
+ *
81
+ * The base allowance is spent first. Once it is gone a recovery class may still draw the
82
+ * single shared final-recovery reserve -- which is what keeps the validated sanitized rebuild
83
+ * after a 5xx streak alive at four total sends -- but an account move and a rebuild cannot
84
+ * each take one. `countedExternally` is set because these legs run through the retry helper,
85
+ * which reports the same send again through `onSendsConsumed`.
86
+ */
87
+ const recoverySendAllowance = (
88
+ cap: number,
89
+ sendClass: SendClass,
90
+ targetKey: string,
91
+ ): { attempts: number; permit?: SingleUseDispatchPermit } => {
92
+ const base = remainingTransientSendBudget(cap);
93
+ if (base > 0) return { attempts: base };
94
+ if (pendingHopPermit) {
95
+ const hopPermit = pendingHopPermit;
96
+ pendingHopPermit = undefined;
97
+ return { attempts: 1, permit: hopPermit };
98
+ }
99
+ if (!isRequestExecutionBudget(sendBudget)) return { attempts: 0 };
100
+ const decision = sendBudget.reserveDispatch({ sendClass, targetKey, countedExternally: true });
101
+ return decision.allowed ? { attempts: 1, permit: decision.permit } : { attempts: 0 };
102
+ };
103
+ /**
104
+ * One credential hop of this logical request, admitted by the INTERSECTION of two bounds.
105
+ *
106
+ * `GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST` and `ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST`
107
+ * stay exactly as they are: they bound rotation within one credential roster. What neither
108
+ * can see is everything else this request already sent, so three hops layered on a spent
109
+ * budget still reached upstream three more times. A hop now happens only when its own layer
110
+ * cap AND the shared budget both permit it, and the smaller of the two wins.
111
+ *
112
+ * `countedExternally` is for the hops whose replay goes out through the retry helper, which
113
+ * reports the same physical send through `onSendsConsumed`; the others are charged here and
114
+ * nowhere else. A refusal is not an error: the caller keeps the real upstream response --
115
+ * status, `Retry-After`, quota body -- because return-the-last-answer is the exhaustion
116
+ * contract this unit settled on.
117
+ */
118
+ /**
119
+ * A credential rotation inside ONE provider's roster is "auth-recovery", not
120
+ * "account-failover". The distinction is load-bearing: "account-failover" sets
121
+ * `isAlternateTarget` unconditionally, so under `maxAlternateTargetSends: 1` the first
122
+ * rotation would refuse every later one AND consume the single slot a genuine cross-pool
123
+ * move needs -- a roster whose first two accounts are both 429'd would return the 429
124
+ * while a free third account sat unused. The roster cap bounds how far rotation walks;
125
+ * the shared total bounds how many sends the request makes. Reserve "account-failover"
126
+ * for a real move between pools.
127
+ */
128
+ const reserveCredentialHop = (
129
+ sendClass: SendClass,
130
+ targetKey: string,
131
+ countedExternally = false,
132
+ ): { allowed: boolean; permit?: SingleUseDispatchPermit } => {
133
+ if (!isRequestExecutionBudget(sendBudget)) return { allowed: true };
134
+ const decision = sendBudget.reserveDispatch({ sendClass, targetKey, countedExternally });
135
+ return decision.allowed ? { allowed: true, permit: decision.permit } : { allowed: false };
136
+ };
137
+ /**
138
+ * Both classes share the one reserve, so this only changes what the decision is called --
139
+ * but a recovery event that says "repair" when a credential refresh drove it is the kind of
140
+ * mislabelled evidence #4592 existed to stop.
141
+ */
142
+ const recoveryClassFor = (recovery: AttemptRecoveryKind): SendClass =>
143
+ /401|429|oauth|rate-limit|key/.test(recovery) ? "auth-recovery" : "repair";
144
+
145
+ return {
146
+ workflowRootId,
147
+ noteTransientSends,
148
+ remainingTransientSendBudget,
149
+ adapterSendBudget,
150
+ noteAdapterPhysicalSend,
151
+ sendBudgetExhausted,
152
+ get pendingHopPermit(): SingleUseDispatchPermit | undefined {
153
+ return pendingHopPermit;
154
+ },
155
+ set pendingHopPermit(value: SingleUseDispatchPermit | undefined) {
156
+ pendingHopPermit = value;
157
+ },
158
+ recoverySendAllowance,
159
+ reserveCredentialHop,
160
+ recoveryClassFor,
161
+ };
162
+ }
163
+
164
+ export type ResponsesSendBudget = Exclude<ReturnType<typeof createResponsesSendBudget>, Response>;
@@ -0,0 +1,149 @@
1
+ import type { ResponsesRequestContext } from "./core-options";
2
+ import type { PreparedResponsesRequest } from "./request-prepare";
3
+ import type { ResponsesTransport } from "./request-transport";
4
+ import type { ResolvedOpenAiForwardSidecar } from "../../providers/openai-sidecar";
5
+ import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
6
+ import {
7
+ shouldResolveOpenAiVisionSidecar,
8
+ resolveOpenAiVisionModel,
9
+ planVisionSidecar,
10
+ describeImagesInPlace,
11
+ requiresVisionPreprocessing,
12
+ stripImagesInPlace,
13
+ } from "../../vision";
14
+ import { shouldResolveOpenAiWebSearchSidecar } from "../../web-search";
15
+ import { shouldResolveOpenAiPassthroughWebSearchBridge } from "../../web-search/passthrough-bridge";
16
+ import {
17
+ listOpenAiForwardSidecarCandidates,
18
+ captureExplicitOpenAiCallerAuth,
19
+ resolveFirstUsableOpenAiSidecar,
20
+ } from "../../providers/openai-sidecar";
21
+ import {
22
+ tryClaimNativeMainProfileForTurn as tryClaimStoredSidecarMainProfile,
23
+ } from "../../codex/native-main-admission";
24
+ import { codexAccountSelectionForTurn } from "../lifecycle";
25
+ import {
26
+ CodexPoolAuthenticationError,
27
+ CodexAuthContextError,
28
+ CodexAccountCooldownError,
29
+ CodexThreadAffinityExpiredError,
30
+ CodexMainProfileDrainingError,
31
+ } from "../../codex/auth-context";
32
+
33
+ /** One responsibility of the Responses request pipeline; state owners are explicit. */
34
+ export async function prepareResponsesSidecarAuth(
35
+ requestContext: Pick<ResponsesRequestContext, "options" | "config" | "req">,
36
+ requestState: Pick<
37
+ PreparedResponsesRequest,
38
+ | "parsed"
39
+ | "route"
40
+ | "selectedForwardHeaders"
41
+ | "translatorBudget"
42
+ >,
43
+ transportState: Pick<ResponsesTransport, "adapter" | "isPassthrough">,
44
+ ) {
45
+ const { options, config, req } = requestContext;
46
+ const { parsed, route, translatorBudget } = requestState;
47
+ const { isPassthrough } = transportState;
48
+
49
+
50
+ let openAiSidecar: ResolvedOpenAiForwardSidecar | undefined;
51
+ const visionDescribeTerminal = options.visionDescribeTerminal === true;
52
+ const routedCompaction = parsed._compactionRequest === true
53
+ && !isCanonicalOpenAiForwardProvider(route.provider);
54
+ const needsOpenAiVision = !visionDescribeTerminal
55
+ && shouldResolveOpenAiVisionSidecar(config, route.provider, route.modelId, parsed, route.providerName);
56
+ const needsOpenAiSearch = !routedCompaction && !transportState.adapter.runTurn
57
+ && (shouldResolveOpenAiWebSearchSidecar(config, parsed, isPassthrough)
58
+ || shouldResolveOpenAiPassthroughWebSearchBridge(route.provider, parsed, isPassthrough));
59
+ if (needsOpenAiVision || needsOpenAiSearch) {
60
+ try {
61
+ const candidates = listOpenAiForwardSidecarCandidates(config);
62
+ let sidecarAuth = options.openAiSidecarAuth;
63
+ if (!sidecarAuth && options.allowStoredOpenAiSidecarAuth === true
64
+ && route.codexAccountId === undefined
65
+ && candidates.some(candidate => candidate.accountMode === "direct")
66
+ && tryClaimStoredSidecarMainProfile(options.turnAdmissionLease)) {
67
+ // Request-local helper authority only: never promote this pair to caller, primary,
68
+ // or retry credentials. Claim before reading so profile switches remain fenced.
69
+ try {
70
+ const { getMainAccountToken } = await import("../../codex/main-account");
71
+ const token = getMainAccountToken();
72
+ if (token) sidecarAuth = captureExplicitOpenAiCallerAuth(new Headers({
73
+ authorization: `Bearer ${token.accessToken}`, "chatgpt-account-id": token.chatgptAccountId,
74
+ }), config);
75
+ } catch { /* stored enrichment is optional */ }
76
+ }
77
+ // Preserve explicit OpenAI helper auth across route changes without returning it to
78
+ // primary-provider headers or alternate-main retry. The resolver revalidates scope.
79
+ const sidecarHeaders = new Headers(req.headers);
80
+ sidecarHeaders.delete("authorization");
81
+ sidecarHeaders.delete("chatgpt-account-id");
82
+ if (sidecarAuth) {
83
+ sidecarHeaders.set("authorization", sidecarAuth.authorization);
84
+ sidecarHeaders.set("chatgpt-account-id", sidecarAuth.chatgptAccountId);
85
+ }
86
+ openAiSidecar = await resolveFirstUsableOpenAiSidecar(
87
+ candidates,
88
+ sidecarHeaders,
89
+ config,
90
+ {
91
+ admission: options.admission,
92
+ codexAuthPolicy: options.codexAuthPolicy,
93
+ // Account-qualified native routes are passthrough, so their in-turn helper is vision.
94
+ // Scope its cooldown and outcome to the helper model, not the routed text model.
95
+ ...(route.codexAccountId !== undefined
96
+ ? { exactAccount: { accountId: route.codexAccountId, modelId: resolveOpenAiVisionModel(config) } }
97
+ : {}),
98
+ beginCodexAccountSelection: codexAccountSelectionForTurn(options.turnAdmissionLease),
99
+ },
100
+ );
101
+ } catch (err) {
102
+ // Sidecars are optional helpers for an otherwise independent routed turn.
103
+ // An unavailable/cooling/expired Multi credential disables the helper; it
104
+ // must not turn a valid routed-provider request into a Codex-auth failure.
105
+ if (
106
+ !(err instanceof CodexPoolAuthenticationError)
107
+ && !(err instanceof CodexAuthContextError)
108
+ && !(err instanceof CodexAccountCooldownError)
109
+ && !(err instanceof CodexThreadAffinityExpiredError)
110
+ && !(err instanceof CodexMainProfileDrainingError)
111
+ ) throw err;
112
+ }
113
+ }
114
+
115
+ // Vision sidecar: the routed model can't see images (provider.noVisionModels). Describe each
116
+ // attached image through the selected sidecar backend and replace it with text BEFORE the main
117
+ // call, so the text-only model can reason about it.
118
+ // Terminal describe fence (roadmap 180): the sidecar's OWN loopback describe
119
+ // call must never plan another describe. The flag arrives from the Chat
120
+ // surface (whose bridge rebuilds headers) or as the raw header for native
121
+ // Responses callers. Marked + text-only routed model → strip, depth cap 1.
122
+ const visionPlan = visionDescribeTerminal
123
+ ? undefined
124
+ : planVisionSidecar(config, route.provider, route.modelId, parsed, openAiSidecar, {
125
+ admission: options.admission, codexAuthPolicy: options.codexAuthPolicy, providerName: route.providerName,
126
+ });
127
+ const recordSidecarOutcome = openAiSidecar?.recordOutcome;
128
+ if (visionPlan) {
129
+ await describeImagesInPlace(
130
+ parsed,
131
+ visionPlan,
132
+ openAiSidecar?.headers ?? requestState.selectedForwardHeaders,
133
+ options.abortSignal,
134
+ recordSidecarOutcome,
135
+ translatorBudget,
136
+ );
137
+ } else if (requiresVisionPreprocessing(config, route.provider, route.modelId, route.providerName)) {
138
+ // Image capability is not positively proven but no sidecar plan is dispatchable: fail closed.
139
+ // Never forward raw image bytes to an unverified upstream.
140
+ stripImagesInPlace(parsed, translatorBudget);
141
+ }
142
+
143
+ return {
144
+ openAiSidecar,
145
+ routedCompaction,
146
+ };
147
+ }
148
+
149
+ export type ResponsesSidecarAuth = Exclude<Awaited<ReturnType<typeof prepareResponsesSidecarAuth>>, Response>;