@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.
- package/gui/dist/assets/{index-VuoiWj9J.js → index-D4zuyIxQ.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +2 -1
- package/src/adapters/base.ts +21 -0
- package/src/adapters/cursor/transport-retry.ts +46 -1
- package/src/adapters/cursor.ts +4 -0
- package/src/adapters/kiro/adapter.ts +42 -1
- package/src/adapters/kiro-retry.ts +23 -4
- package/src/adapters/openai-chat/errors.ts +116 -0
- package/src/adapters/openai-chat/messages.ts +346 -0
- package/src/adapters/openai-chat/passthrough.ts +146 -0
- package/src/adapters/openai-chat/response-events.ts +117 -0
- package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
- package/src/adapters/openai-chat/tool-schema.ts +477 -0
- package/src/adapters/openai-chat/wire.ts +50 -0
- package/src/adapters/openai-chat.ts +33 -1445
- package/src/adapters/openai-responses/canonical-forward.ts +202 -0
- package/src/adapters/openai-responses/image-gen.ts +406 -0
- package/src/adapters/openai-responses/internal.ts +3 -0
- package/src/adapters/openai-responses/passthrough.ts +611 -0
- package/src/adapters/openai-responses/prompt-cache.ts +83 -0
- package/src/adapters/openai-responses/reasoning.ts +220 -0
- package/src/adapters/openai-responses/request-strips.ts +185 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
- package/src/adapters/openai-responses/tool-schema.ts +293 -0
- package/src/adapters/openai-responses/web-search.ts +156 -0
- package/src/adapters/openai-responses.ts +4 -2625
- package/src/bridge/errors.ts +34 -0
- package/src/bridge/internal.ts +174 -0
- package/src/bridge/response-json.ts +624 -0
- package/src/bridge/sse.ts +1444 -0
- package/src/bridge.ts +5 -2204
- package/src/chat/inbound.ts +12 -1
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-store.ts +71 -9
- package/src/codex/auth-api/account-list.ts +507 -0
- package/src/codex/auth-api/http.ts +32 -0
- package/src/codex/auth-api/login-flow.ts +554 -0
- package/src/codex/auth-api/login-state.ts +64 -0
- package/src/codex/auth-api/main-account-probe.ts +331 -0
- package/src/codex/auth-api/pool-mode-gate.ts +274 -0
- package/src/codex/auth-api/pool-quota-probe.ts +512 -0
- package/src/codex/auth-api/reset-credit-service.ts +422 -0
- package/src/codex/auth-api/routes.ts +425 -0
- package/src/codex/auth-api/runtime-config.ts +48 -0
- package/src/codex/auth-api.ts +27 -3118
- package/src/codex/auth-context.ts +95 -28
- package/src/codex/catalog/auto-review.ts +507 -0
- package/src/codex/catalog/build-entries.ts +981 -0
- package/src/codex/catalog/combo-member.ts +375 -0
- package/src/codex/catalog/derive-entry.ts +229 -0
- package/src/codex/catalog/effort.ts +0 -1
- package/src/codex/catalog/gated-native-warn.ts +63 -0
- package/src/codex/catalog/gather-capture.ts +533 -0
- package/src/codex/catalog/model-hints.ts +691 -0
- package/src/codex/catalog/model-visibility.ts +304 -0
- package/src/codex/catalog/provider-fetch.ts +52 -2942
- package/src/codex/catalog/provider-models.ts +685 -0
- package/src/codex/catalog/restore.ts +132 -0
- package/src/codex/catalog/retained-sync.ts +706 -0
- package/src/codex/catalog/routed-gather.ts +858 -0
- package/src/codex/catalog/subagent-roster.ts +176 -0
- package/src/codex/catalog/sync.ts +52 -2698
- package/src/codex/inject/config-toml.ts +563 -0
- package/src/codex/inject/remove.ts +192 -0
- package/src/codex/inject/restore.ts +540 -0
- package/src/codex/inject/routing-classify.ts +109 -0
- package/src/codex/inject/routing-target.ts +125 -0
- package/src/codex/inject.ts +81 -1436
- package/src/codex/lineage.ts +458 -0
- package/src/codex/pool-refresh-backoff.ts +152 -0
- package/src/codex/routing/active-account.ts +194 -0
- package/src/codex/routing/cooldown-math.ts +275 -0
- package/src/codex/routing/health-store.ts +402 -0
- package/src/codex/routing/probe-lease.ts +358 -0
- package/src/codex/routing/selection.ts +703 -0
- package/src/codex/routing/thread-affinity.ts +538 -0
- package/src/codex/routing.ts +353 -2234
- package/src/codex/shim-fingerprint.ts +223 -0
- package/src/codex/shim-inspect.ts +175 -0
- package/src/codex/shim-probe.ts +367 -0
- package/src/codex/shim-restore-lock.ts +169 -0
- package/src/codex/shim-state-file.ts +151 -0
- package/src/codex/shim-templates.ts +265 -0
- package/src/codex/shim.ts +48 -1268
- package/src/config/diagnostics.ts +705 -0
- package/src/config/feature-flags.ts +55 -0
- package/src/config/live-reconcile.ts +403 -0
- package/src/config/load-degrade.ts +880 -0
- package/src/config/mutation-lock.ts +244 -0
- package/src/config/openai-tier-backup.ts +268 -0
- package/src/config/persist-unlocked.ts +92 -0
- package/src/config/proxy-env.ts +188 -0
- package/src/config/salvage.ts +244 -0
- package/src/config/schema/config-schema.ts +640 -0
- package/src/config/schema/leaf-validators.ts +855 -0
- package/src/config/warn-memo.ts +28 -0
- package/src/config.ts +234 -4481
- package/src/generated/compatibility-version.json +539 -39
- package/src/lib/request-execution-budget.ts +69 -20
- package/src/lib/spend-reservation-ledger.ts +940 -0
- package/src/lib/upstream-retry.ts +55 -11
- package/src/lib/workflow-budget.ts +553 -30
- package/src/providers/quota/account-cache.ts +441 -0
- package/src/providers/quota/antigravity.ts +295 -0
- package/src/providers/quota/report-cache.ts +320 -0
- package/src/providers/quota/vendor-probes-key.ts +1243 -0
- package/src/providers/quota/vendor-probes-oauth.ts +590 -0
- package/src/providers/quota.ts +324 -3079
- package/src/providers/registry/entries-core.ts +1221 -0
- package/src/providers/registry/entries-extended.ts +1204 -0
- package/src/providers/registry/model-seeds.ts +908 -0
- package/src/providers/registry/types.ts +352 -0
- package/src/providers/registry.ts +24 -3536
- package/src/responses/continuation-ownership.ts +29 -0
- package/src/responses/state/replay-fingerprint.ts +80 -0
- package/src/responses/state/snapshot-codec.ts +104 -0
- package/src/responses/state/spill-failure.ts +118 -0
- package/src/responses/state/spill-queue.ts +665 -0
- package/src/responses/state/temp-recovery.ts +257 -0
- package/src/responses/state.ts +82 -1143
- package/src/routing/identity-domains.ts +449 -0
- package/src/routing/probe-lease.ts +511 -0
- package/src/server/index/bounded-request.ts +88 -0
- package/src/server/index/live-sideband.ts +565 -0
- package/src/server/index/serve-options.ts +1766 -0
- package/src/server/index/startup-warnings.ts +213 -0
- package/src/server/index/websocket-handler.ts +335 -0
- package/src/server/index.ts +40 -2547
- package/src/server/management/route-registry.ts +26 -23
- package/src/server/management/shared.ts +8 -5
- package/src/server/management/workflow-budget-routes.ts +133 -0
- package/src/server/management-api.ts +12 -0
- package/src/server/request-log-conversation.ts +9 -7
- package/src/server/request-log.ts +245 -1
- package/src/server/responses/account-change-state.ts +233 -0
- package/src/server/responses/adapter-continuation.ts +514 -0
- package/src/server/responses/adapter-delivery.ts +214 -0
- package/src/server/responses/adapter-dispatch.ts +971 -0
- package/src/server/responses/compact.ts +59 -4
- package/src/server/responses/completion-policy.ts +33 -0
- package/src/server/responses/core-auth.ts +527 -0
- package/src/server/responses/core-codex-account.ts +859 -0
- package/src/server/responses/core-combo-failure.ts +210 -0
- package/src/server/responses/core-combo.ts +707 -0
- package/src/server/responses/core-errors.ts +152 -0
- package/src/server/responses/core-lifetime.ts +95 -0
- package/src/server/responses/core-normalize.ts +350 -0
- package/src/server/responses/core-opaque-recovery.ts +380 -0
- package/src/server/responses/core-options.ts +159 -0
- package/src/server/responses/core-replay.ts +225 -0
- package/src/server/responses/core.ts +192 -8893
- package/src/server/responses/passthrough-delivery.ts +856 -0
- package/src/server/responses/passthrough-dispatch.ts +1476 -0
- package/src/server/responses/passthrough-execution.ts +54 -0
- package/src/server/responses/request-prepare.ts +970 -0
- package/src/server/responses/request-send-budget.ts +164 -0
- package/src/server/responses/request-sidecar-auth.ts +149 -0
- package/src/server/responses/request-transport.ts +744 -0
- package/src/server/responses/response-effects.ts +157 -0
- package/src/server/responses/run-turn-execution.ts +448 -0
- package/src/server/responses/sidecar-execution.ts +469 -0
- package/src/server/responses-image-gen-repair.ts +1 -1
- package/src/server/workflow-refusal.ts +84 -0
- package/src/types/config.ts +30 -0
- package/src/usage/log.ts +146 -0
- 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>;
|