@bitkyc08/opencodex 2.63.0-preview.20260923 → 2.64.0-preview.20260923
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/AGENTS_INSTALL.md +4 -3
- package/README.md +46 -33
- package/assets/download-linux.svg +10 -0
- package/assets/download-macos.svg +10 -0
- package/assets/download-windows.svg +10 -0
- package/bin/ocx.mjs +12 -4
- package/gui/dist/assets/App-D5eN1TID.js +50 -0
- package/gui/dist/assets/App-I5AnaSLh.css +1 -0
- package/gui/dist/assets/{Tray-B03pW-Uf.js → Tray-Bh_ErQDh.js} +1 -1
- package/gui/dist/assets/index-BEq4OCOz.js +86 -0
- package/gui/dist/assets/index-DiBRuK-d.css +1 -0
- package/gui/dist/assets/usage-companion-chart-DBG_37kQ.js +1 -0
- package/gui/dist/index.html +2 -2
- package/native/remote-workspace-helper/src/protocol.rs +5 -0
- package/package.json +4 -1
- package/src/adapters/anthropic.ts +66 -7
- package/src/adapters/codebuddy/adapter.ts +112 -16
- package/src/adapters/codebuddy/mcp-server.ts +180 -0
- package/src/adapters/codebuddy/scaffold-guard.ts +25 -8
- package/src/adapters/codebuddy/tool-bridge.ts +597 -0
- package/src/adapters/coding-agent/protocol.ts +123 -10
- package/src/adapters/coding-agent/turn.ts +324 -2
- package/src/adapters/command-code-restored-schema.ts +112 -0
- package/src/adapters/command-code-tool-text.ts +589 -0
- package/src/adapters/command-code.ts +80 -6
- package/src/adapters/cursor/catalog.ts +12 -0
- package/src/adapters/cursor/current-request.ts +46 -0
- package/src/adapters/cursor/discovery.ts +1 -1
- package/src/adapters/cursor/effort-map.ts +4 -0
- package/src/adapters/cursor/native-exec.ts +15 -0
- package/src/adapters/cursor/protobuf-events.ts +40 -12
- package/src/adapters/cursor/protobuf-request.ts +83 -18
- package/src/adapters/cursor/request-builder.ts +5 -4
- package/src/adapters/cursor/tool-guidance.ts +1 -1
- package/src/adapters/devin/live-models.ts +7 -0
- package/src/adapters/inline-think-tags.ts +251 -0
- package/src/adapters/kiro/adapter.ts +1 -0
- package/src/adapters/kiro/stream.ts +5 -3
- package/src/adapters/kiro/usage.ts +4 -3
- package/src/adapters/mimo-free.ts +37 -18
- package/src/adapters/openai-chat/messages.ts +5 -5
- package/src/adapters/openai-chat/tool-schema.ts +124 -2
- package/src/adapters/openai-chat.ts +13 -3
- package/src/adapters/openai-responses/passthrough.ts +15 -2
- package/src/adapters/openai-responses/reasoning.ts +14 -1
- package/src/adapters/openai-responses/request-strips.ts +29 -3
- package/src/adapters/openai-responses/tool-output-recovery.ts +9 -3
- package/src/adapters/qoder/adapter.ts +15 -12
- package/src/adapters/qoder/scaffold-guard.ts +45 -13
- package/src/bridge/internal.ts +20 -0
- package/src/bridge/sse.ts +14 -5
- package/src/claude/agents-inject.ts +2 -1
- package/src/claude/desktop-applied-marker.ts +44 -0
- package/src/claude/desktop-profile.ts +22 -12
- package/src/claude/inbound.ts +8 -3
- package/src/cli/access.ts +22 -5
- package/src/cli/account-api.ts +6 -0
- package/src/cli/account-auth.ts +5 -2
- package/src/cli/account.ts +5 -6
- package/src/cli/aside-profiles.ts +61 -1
- package/src/cli/claude-desktop.ts +3 -2
- package/src/cli/claude.ts +3 -1
- package/src/cli/codex-cli-update.ts +2 -2
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/dispatch.ts +12 -4
- package/src/cli/index.ts +98 -10
- package/src/cli/registry.ts +1 -1
- package/src/cli/resolve.ts +113 -1
- package/src/cli/root.ts +5 -0
- package/src/cli/stop-approval.ts +186 -0
- package/src/cli/stop-report.ts +1 -1
- package/src/cli/system-restart-client.ts +19 -4
- package/src/client/hub-client.ts +25 -18
- package/src/clients/config-export.ts +12 -4
- package/src/codex/account-auto-switch.ts +59 -0
- package/src/codex/account-lifecycle.ts +7 -0
- package/src/codex/account-priority.ts +10 -0
- package/src/codex/account-usability.ts +9 -0
- package/src/codex/auth-api/account-list.ts +5 -0
- package/src/codex/auth-api/login-flow.ts +15 -5
- package/src/codex/auth-api/login-state.ts +5 -16
- package/src/codex/auth-api/pool-mode-gate.ts +30 -7
- package/src/codex/auth-api/routes.ts +39 -3
- package/src/codex/auth-context.ts +145 -17
- package/src/codex/catalog/build-entries.ts +2 -1
- package/src/codex/catalog/effort.ts +8 -2
- package/src/codex/catalog/metadata.ts +33 -7
- package/src/codex/catalog/native-models.ts +102 -5
- package/src/codex/catalog/parsing.ts +2 -0
- package/src/codex/catalog/provider-models.ts +19 -6
- package/src/codex/cli-installation-targets.ts +44 -40
- package/src/codex/codex-write-lock.ts +32 -2
- package/src/codex/inject/multi-agent-v2.ts +90 -0
- package/src/codex/inject/plan.ts +365 -0
- package/src/codex/inject.ts +237 -362
- package/src/codex/log-guard/maintenance.ts +30 -13
- package/src/codex/project-config-warnings.ts +47 -7
- package/src/codex/prompt-layers/encoding.ts +41 -0
- package/src/codex/prompt-layers/toml-read.ts +1 -1
- package/src/codex/prompt-layers.ts +17 -13
- package/src/codex/prompt-text-probe.ts +20 -10
- package/src/codex/quota-observation-freshness.ts +34 -0
- package/src/codex/quota-rejection.ts +8 -4
- package/src/codex/quota.ts +4 -2
- package/src/codex/routing/selection.ts +32 -9
- package/src/codex/routing.ts +53 -11
- package/src/codex/subagent-model-fallback.ts +4 -3
- package/src/codex/sync.ts +11 -0
- package/src/codex/windows-installation-files.ts +33 -11
- package/src/codex/write-coordination.ts +4 -0
- package/src/combos/request.ts +6 -2
- package/src/companion/settings.ts +6 -1
- package/src/config/atomic-write.ts +12 -1
- package/src/config/derived-registries.ts +29 -0
- package/src/config/diagnostics.ts +17 -0
- package/src/config/live-reconcile.ts +11 -2
- package/src/config/load-degrade.ts +9 -2
- package/src/config/persist-unlocked.ts +4 -3
- package/src/config/persisted-mutation.ts +94 -0
- package/src/config/provider-validation.ts +1 -1
- package/src/config/proxy-env.ts +62 -12
- package/src/config/rebase-provenance.ts +80 -1
- package/src/config/schema/config-schema.ts +5 -0
- package/src/config/schema/leaf-validators.ts +50 -1
- package/src/config/subagent-models.ts +41 -15
- package/src/config.ts +18 -95
- package/src/generated/compatibility-version.json +330 -218
- package/src/generated/model-metadata.ts +8 -5
- package/src/github/star-state.ts +46 -6
- package/src/grok/inject.ts +74 -40
- package/src/grok/status.ts +11 -4
- package/src/integrations/cursor-effort-table.ts +50 -13
- package/src/integrations/raycast-detect.ts +19 -4
- package/src/integrations/serialize.ts +11 -1
- package/src/lib/bounded-body.ts +30 -0
- package/src/lib/crash-guard.ts +52 -4
- package/src/lib/local-aside-sync-contract.ts +41 -0
- package/src/lib/package-tree-integrity.ts +181 -9
- package/src/lib/proxy-env.ts +18 -1
- package/src/lib/request-failure-attribution.ts +1 -0
- package/src/lib/request-failure-model.ts +3 -0
- package/src/lib/service-secrets.ts +99 -2
- package/src/lib/socks5-fetch.ts +43 -14
- package/src/lib/token-estimate.ts +17 -2
- package/src/oauth/command-code.ts +3 -2
- package/src/oauth/generic-account-failover.ts +29 -1
- package/src/oauth/index.ts +24 -16
- package/src/oauth/login-flow-state.ts +5 -5
- package/src/oauth/meta-muse-device.ts +49 -7
- package/src/oauth/store.ts +74 -9
- package/src/providers/alibaba-region-backup.ts +16 -1
- package/src/providers/anthropic-fast.ts +89 -0
- package/src/providers/anthropic-reset-grant-ledger.ts +288 -0
- package/src/providers/anthropic-reset-grants.ts +329 -0
- package/src/providers/claude-cli-identity.ts +12 -0
- package/src/providers/command-code-efforts.ts +184 -102
- package/src/providers/derive.ts +11 -3
- package/src/providers/fastwire.ts +16 -3
- package/src/providers/label.ts +8 -3
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/openai-virtual-models.ts +1 -0
- package/src/providers/quota/vendor-probes-oauth.ts +2 -1
- package/src/providers/reasoning-metadata.ts +38 -15
- package/src/providers/registry/entries-core.ts +95 -7
- package/src/providers/registry/entries-extended.ts +21 -10
- package/src/providers/registry/model-ids.ts +1 -0
- package/src/providers/registry/model-seeds.ts +31 -3
- package/src/providers/registry/types.ts +3 -1
- package/src/providers/resolved-model-policy-merge.ts +38 -8
- package/src/providers/resolved-model-policy.ts +4 -2
- package/src/providers/service-tier.ts +3 -1
- package/src/reasoning-effort.ts +6 -5
- package/src/responses/code-mode-helper-compat.ts +14 -4
- package/src/responses/code-mode-shell-input.ts +54 -0
- package/src/responses/custom-tool-compat.ts +173 -5
- package/src/responses/parser.ts +38 -2
- package/src/responses/reasoning-replay-cache.ts +26 -0
- package/src/router.ts +11 -1
- package/src/routing/history/indexer.ts +7 -2
- package/src/routing/history/schema.ts +3 -1
- package/src/server/adapter-resolve.ts +2 -2
- package/src/server/auth-cors.ts +3 -0
- package/src/server/chat-completions.ts +14 -1
- package/src/server/chat-native.ts +17 -0
- package/src/server/claude-messages.ts +4 -3
- package/src/server/direct-local-http.ts +45 -19
- package/src/server/gui-session.ts +6 -15
- package/src/server/index/package-tree-guard.ts +53 -0
- package/src/server/index/serve-options.ts +17 -3
- package/src/server/index/startup-warnings.ts +17 -0
- package/src/server/index.ts +15 -17
- package/src/server/lifecycle.ts +8 -0
- package/src/server/live.ts +54 -4
- package/src/server/management/agent-settings-routes.ts +44 -8
- package/src/server/management/anthropic-reset-grant-routes.ts +252 -0
- package/src/server/management/codex-prompt-routes.ts +5 -1
- package/src/server/management/config-routes.ts +14 -4
- package/src/server/management/oauth-account-routes.ts +17 -1
- package/src/server/management/provider-overwrite-carry.ts +164 -0
- package/src/server/management/provider-routes.ts +36 -6
- package/src/server/management/remote-workspace-routes.ts +2 -2
- package/src/server/management/route-registry.ts +2 -0
- package/src/server/management/shadow-call-validation.ts +39 -0
- package/src/server/management/system-restart.ts +72 -6
- package/src/server/management-api.ts +16 -2
- package/src/server/management-auth.ts +52 -8
- package/src/server/proxy-liveness.ts +110 -4
- package/src/server/request-log.ts +26 -9
- package/src/server/request-metrics.ts +5 -0
- package/src/server/responses/adapter-continuation.ts +6 -3
- package/src/server/responses/adapter-dispatch.ts +65 -1
- package/src/server/responses/compact.ts +31 -1
- package/src/server/responses/compaction-routing.ts +28 -3
- package/src/server/responses/core-codex-account.ts +85 -26
- package/src/server/responses/core-combo-failure.ts +16 -15
- package/src/server/responses/core-normalize.ts +5 -1
- package/src/server/responses/core-opaque-recovery.ts +50 -2
- package/src/server/responses/core-options.ts +2 -0
- package/src/server/responses/core-replay.ts +7 -0
- package/src/server/responses/core.ts +1 -1
- package/src/server/responses/encrypted-payload.ts +23 -9
- package/src/server/responses/passthrough-delivery.ts +55 -35
- package/src/server/responses/passthrough-dispatch.ts +12 -13
- package/src/server/responses/policy-fallback.ts +12 -1
- package/src/server/responses/request-prepare.ts +46 -7
- package/src/server/responses/request-spend.ts +31 -15
- package/src/server/responses/run-turn-execution.ts +6 -5
- package/src/server/responses/shadow-target-availability.ts +61 -0
- package/src/server/responses-custom-tool-repair.ts +2 -0
- package/src/service/claim.ts +185 -0
- package/src/service/cli.ts +10 -1
- package/src/service/guarded-manager-target.ts +151 -0
- package/src/service/guards.ts +47 -48
- package/src/service/managing-cli.ts +170 -0
- package/src/service/orchestration.ts +3 -1
- package/src/service/state.ts +4 -0
- package/src/service/systemd.ts +16 -1
- package/src/service.ts +1 -1
- package/src/types/config.ts +8 -0
- package/src/types/provider.ts +16 -0
- package/src/types/request.ts +10 -0
- package/src/types/tools.ts +9 -1
- package/src/types/wire.ts +68 -4
- package/src/types.ts +1 -0
- package/src/update/job.ts +10 -16
- package/src/update/npm-invocation.mjs +17 -16
- package/src/update/transactional-install.d.mts +22 -1
- package/src/update/transactional-install.mjs +176 -19
- package/src/update/update-failure-guidance.d.mts +8 -0
- package/src/update/update-failure-guidance.mjs +47 -0
- package/src/usage/expected-prices.ts +54 -12
- package/src/usage/log.ts +117 -3
- package/src/usage/telemetry-contract.ts +1 -0
- package/src/usage/timeline.ts +34 -6
- package/src/usage/user-cost-overlay-reconciler.ts +3 -3
- package/src/vision/reasoning.ts +2 -4
- package/src/web-search/xai-executor.ts +14 -4
- package/gui/dist/assets/App-EJxiUFMq.js +0 -50
- package/gui/dist/assets/index-BmJwNBHL.js +0 -86
- package/gui/dist/assets/index-DdDunwDb.css +0 -1
- package/gui/dist/assets/usage-companion-chart-IftE60UK.js +0 -1
- package/src/adapters/kiro-thinking.ts +0 -112
|
@@ -41,6 +41,7 @@ import { isNativeMainTrafficBlocked } from "../../codex/native-profile-startup";
|
|
|
41
41
|
import { MAIN_CODEX_ACCOUNT_ID } from "../../codex/main-account";
|
|
42
42
|
import { slugsEquivalent } from "../../providers/slug-codec";
|
|
43
43
|
import {
|
|
44
|
+
callerCodexWorkspaceAccountId,
|
|
44
45
|
codexProbeLeaseId,
|
|
45
46
|
codexTransientProbeGrant,
|
|
46
47
|
codexProbeQuotaScope,
|
|
@@ -173,8 +174,9 @@ export function normalizeCodexUnsupportedModelDetail(value: string): string {
|
|
|
173
174
|
* that comparison fail for the one model that is still account-gated, which silently disabled
|
|
174
175
|
* both the alternate-account retry and the same-account ladder built for exactly that case.
|
|
175
176
|
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
177
|
+
* Accept the HTTP `detail` envelope and the `error.message` envelope emitted by the
|
|
178
|
+
* WebSocket refused-create projection. Both must match the whole sentence, whitespace-collapsed
|
|
179
|
+
* and case-folded, with nothing before or after it. Competing envelopes are ambiguous. No prose is
|
|
178
180
|
* inferred and no other 400 shape is admitted, because a 400 is also what a malformed request
|
|
179
181
|
* earns and that must never read as an entitlement fact.
|
|
180
182
|
*/
|
|
@@ -186,7 +188,18 @@ export function codexUnsupportedModelFromDetail(
|
|
|
186
188
|
try {
|
|
187
189
|
const payload = JSON.parse(bodyText) as unknown;
|
|
188
190
|
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return undefined;
|
|
189
|
-
const
|
|
191
|
+
const record = payload as Record<string, unknown>;
|
|
192
|
+
const hasDetail = Object.hasOwn(record, "detail");
|
|
193
|
+
const hasError = Object.hasOwn(record, "error");
|
|
194
|
+
if (hasDetail === hasError) return undefined;
|
|
195
|
+
let detail: unknown = record.detail;
|
|
196
|
+
if (hasError) {
|
|
197
|
+
const error = record.error;
|
|
198
|
+
if (!error || typeof error !== "object" || Array.isArray(error)) return undefined;
|
|
199
|
+
const fields = error as Record<string, unknown>;
|
|
200
|
+
if ([fields.type, fields.code].some(value => value != null && typeof value !== "string")) return undefined;
|
|
201
|
+
detail = fields.message;
|
|
202
|
+
}
|
|
190
203
|
if (typeof detail !== "string") return undefined;
|
|
191
204
|
const matched = /^the '([^']{1,256})' model is not supported when using codex with a chatgpt account\.$/u
|
|
192
205
|
.exec(normalizeCodexUnsupportedModelDetail(detail));
|
|
@@ -280,15 +293,11 @@ export async function shouldRetryCodexPoolAccountQuota(
|
|
|
280
293
|
// body carries no quota evidence either, but the marker is the contract, not the prose.
|
|
281
294
|
if (isNonReplayableResponse(response)) return false;
|
|
282
295
|
if (response.status === 402 || response.status === 429) {
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
// ambiguous body keeps the broad #584 behaviour unchanged, and `rate_limit_exceeded`,
|
|
289
|
-
// `slow_down` and plan-level exhaustion still rotate exactly as before.
|
|
290
|
-
const { codexScopedExhaustionCode } = await import("../../codex/quota-rejection");
|
|
291
|
-
return await codexScopedExhaustionCode(response, { signal }) === undefined;
|
|
296
|
+
// The response does not identify the organization or project whose quota was exhausted.
|
|
297
|
+
// Resolve the alternate before deciding whether its known workspace identity proves that an
|
|
298
|
+
// organization-scoped retry would be futile. Until then, preserve the broad #584 behaviour.
|
|
299
|
+
void signal;
|
|
300
|
+
return true;
|
|
292
301
|
}
|
|
293
302
|
if (response.status < 500 || response.status >= 600) return false;
|
|
294
303
|
try {
|
|
@@ -305,6 +314,20 @@ export async function shouldRetryCodexPoolAccountQuota(
|
|
|
305
314
|
}
|
|
306
315
|
|
|
307
316
|
|
|
317
|
+
export async function shouldRetryCodexScopedQuotaOnAlternate(
|
|
318
|
+
response: Response,
|
|
319
|
+
firstWorkspaceAccountId: string,
|
|
320
|
+
alternateWorkspaceAccountId: string | undefined,
|
|
321
|
+
signal?: AbortSignal,
|
|
322
|
+
): Promise<boolean> {
|
|
323
|
+
if (!firstWorkspaceAccountId || firstWorkspaceAccountId !== alternateWorkspaceAccountId) return true;
|
|
324
|
+
const { codexScopedExhaustionCode } = await import("../../codex/quota-rejection");
|
|
325
|
+
const code = await codexScopedExhaustionCode(response, { signal });
|
|
326
|
+
// Workspace identity binds organization-level limits, but the response supplies no project id.
|
|
327
|
+
return code === undefined || code === "project_spend_limit_exceeded";
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
|
|
308
331
|
/**
|
|
309
332
|
* A pre-stream upstream 5xx another Codex account may still be able to serve.
|
|
310
333
|
*
|
|
@@ -448,6 +471,7 @@ export function applyCodexAccountGatedWireNormalization(parsed: OcxParsedRequest
|
|
|
448
471
|
if (logCtx) {
|
|
449
472
|
logCtx.preserveResolvedModelFromRoute = true;
|
|
450
473
|
delete logCtx.resolvedModel;
|
|
474
|
+
logCtx.wireModel = wireModel;
|
|
451
475
|
}
|
|
452
476
|
parsed.modelId = wireModel;
|
|
453
477
|
if (!parsed._rawBody || typeof parsed._rawBody !== "object") return;
|
|
@@ -532,6 +556,22 @@ export async function retryCodexPoolOnAlternateAccount(
|
|
|
532
556
|
writerGeneration: firstAuthCtx.writerGeneration,
|
|
533
557
|
});
|
|
534
558
|
};
|
|
559
|
+
// A body-confirmed quota response may arrive under HTTP 5xx. A path that returns the
|
|
560
|
+
// first response without a move must still record the NORMALIZED outcome: the ordinary
|
|
561
|
+
// terminal recorder sees only that wire status and would misclassify it as transient,
|
|
562
|
+
// leaving the exhausted account immediately selectable next turn.
|
|
563
|
+
const recordWrappedQuotaOutcome = (): void => {
|
|
564
|
+
if (outcomeStatus === firstResponse.status || (outcomeStatus !== 429 && outcomeStatus !== 402)) return;
|
|
565
|
+
recordCodexUpstreamOutcome(config, firstAuthCtx.accountId, outcomeStatus, {
|
|
566
|
+
...codexQuotaOutcomeMeta(firstResponse),
|
|
567
|
+
threadId: firstAuthCtx.affinityKey,
|
|
568
|
+
modelId: route.modelId,
|
|
569
|
+
probeLeaseId: codexProbeLeaseId(firstAuthCtx),
|
|
570
|
+
probeQuotaScope: codexProbeQuotaScope(firstAuthCtx),
|
|
571
|
+
transientProbe: codexTransientProbeGrant(firstAuthCtx),
|
|
572
|
+
writerGeneration: firstAuthCtx.writerGeneration,
|
|
573
|
+
});
|
|
574
|
+
};
|
|
535
575
|
if (outcomeStatus === 400 && ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(route.modelId)) {
|
|
536
576
|
invalidateCodexModelEntitlementsForAccount(firstAuthCtx.accountId);
|
|
537
577
|
let refreshed;
|
|
@@ -628,26 +668,45 @@ export async function retryCodexPoolOnAlternateAccount(
|
|
|
628
668
|
&& retryAuthCtx?.kind !== "main-pool"
|
|
629
669
|
&& retryAuthCtx?.kind !== "main"
|
|
630
670
|
) {
|
|
631
|
-
|
|
632
|
-
// the ordinary terminal recorder sees only that wire status and would misclassify it
|
|
633
|
-
// as transient, leaving the exhausted account immediately selectable next turn.
|
|
634
|
-
if (outcomeStatus !== firstResponse.status && (outcomeStatus === 429 || outcomeStatus === 402)) {
|
|
635
|
-
recordCodexUpstreamOutcome(config, firstAuthCtx.accountId, outcomeStatus, {
|
|
636
|
-
...codexQuotaOutcomeMeta(firstResponse),
|
|
637
|
-
threadId: firstAuthCtx.affinityKey,
|
|
638
|
-
modelId: route.modelId,
|
|
639
|
-
probeLeaseId: codexProbeLeaseId(firstAuthCtx),
|
|
640
|
-
probeQuotaScope: codexProbeQuotaScope(firstAuthCtx),
|
|
641
|
-
transientProbe: codexTransientProbeGrant(firstAuthCtx),
|
|
642
|
-
writerGeneration: firstAuthCtx.writerGeneration,
|
|
643
|
-
});
|
|
644
|
-
}
|
|
671
|
+
recordWrappedQuotaOutcome();
|
|
645
672
|
// No usable alternate was resolved, so the reserved move never becomes a send.
|
|
646
673
|
accountMovePermit?.release();
|
|
647
674
|
recordUnmovedTransientOutcome();
|
|
648
675
|
return { kind: "no-alternate" };
|
|
649
676
|
}
|
|
650
677
|
|
|
678
|
+
if (
|
|
679
|
+
(outcomeStatus === 429 || outcomeStatus === 402)
|
|
680
|
+
&& !await shouldRetryCodexScopedQuotaOnAlternate(
|
|
681
|
+
firstResponse,
|
|
682
|
+
firstAuthCtx.chatgptAccountId,
|
|
683
|
+
retryAuthCtx.kind === "pool" || retryAuthCtx.kind === "main-pool"
|
|
684
|
+
? retryAuthCtx.chatgptAccountId
|
|
685
|
+
// A request-owned `main` alternate has no stored account id; its workspace
|
|
686
|
+
// identity is what the caller's own credential materializes upstream.
|
|
687
|
+
: callerCodexWorkspaceAccountId(callerAuthHeaders),
|
|
688
|
+
options.abortSignal,
|
|
689
|
+
)
|
|
690
|
+
) {
|
|
691
|
+
// Suppressing the move is not suppressing the evidence: a same-workspace refusal
|
|
692
|
+
// still records its normalized quota outcome on the account that produced it.
|
|
693
|
+
recordWrappedQuotaOutcome();
|
|
694
|
+
accountMovePermit?.release();
|
|
695
|
+
releaseCodexAuthContextProbeLease(retryAuthCtx);
|
|
696
|
+
return { kind: "no-alternate" };
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// The scope classification above reads the rejection body asynchronously, so the
|
|
700
|
+
// request may have been cancelled while it ran. Re-check before the send below
|
|
701
|
+
// mutates routing state or spends the alternate on a caller that is gone.
|
|
702
|
+
if (options.abortSignal?.aborted) {
|
|
703
|
+
recordWrappedQuotaOutcome();
|
|
704
|
+
recordUnmovedTransientOutcome();
|
|
705
|
+
accountMovePermit?.release();
|
|
706
|
+
releaseCodexAuthContextProbeLease(retryAuthCtx);
|
|
707
|
+
return { kind: "no-alternate" };
|
|
708
|
+
}
|
|
709
|
+
|
|
651
710
|
const quotaMeta = { ...codexQuotaOutcomeMeta(firstResponse), ...(await codexDenialOutcomeMeta(firstResponse)) };
|
|
652
711
|
if (outcomeStatus === 429 || outcomeStatus === 402) {
|
|
653
712
|
const { applyAccountQuotaFromUpstreamHeaders } = await import("../../codex/auth-api");
|
|
@@ -45,27 +45,28 @@ export async function consumeComboFailure(
|
|
|
45
45
|
// a second body read, so this mirrors its normalization: raw 402/429, or a 5xx whose intact,
|
|
46
46
|
// display-safe body carries a recognized quota message.
|
|
47
47
|
let quotaConfirmedByBody = false;
|
|
48
|
+
const serverError = response.status >= 500 && response.status < 600;
|
|
48
49
|
try {
|
|
49
|
-
const body = await readBoundedResponseBody(response, {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (
|
|
56
|
-
|
|
57
|
-
&& body.displaySafe && !body.truncated
|
|
58
|
-
) {
|
|
50
|
+
const body = await readBoundedResponseBody(response, { signal, reportUtf8Validity: serverError });
|
|
51
|
+
// A 5xx body counts as quota or classification evidence only when it decoded as valid
|
|
52
|
+
// UTF-8, matching shouldRetryCodexPoolAccountQuota. A malformed byte keeps the status-only
|
|
53
|
+
// fallback, with one exception: a cyber-policy refusal must still stop the combo, so the
|
|
54
|
+
// replacement-decoded text may carry that verdict and nothing else.
|
|
55
|
+
const utf8Trusted = !serverError || body.utf8Valid === true;
|
|
56
|
+
if (utf8Trusted) usage = usageFromComboFailureText(body.text);
|
|
57
|
+
if (serverError && utf8Trusted && body.displaySafe && !body.truncated) {
|
|
59
58
|
const quotaMessage = codexQuotaFailureMessage(body.text);
|
|
60
59
|
quotaConfirmedByBody = quotaMessage !== undefined
|
|
61
60
|
&& isRateLimitOrQuotaFailureMessage(quotaMessage);
|
|
62
61
|
}
|
|
63
|
-
if (body.displaySafe) {
|
|
62
|
+
if (body.displaySafe && !body.truncated) {
|
|
64
63
|
const normalized = normalizeUpstreamErrorText(body.text, fallback);
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
if (utf8Trusted || isCyberPolicyCode(normalized.code) || isCyberPolicyMessage(normalized.safeText)) {
|
|
65
|
+
classificationText = normalized.safeText;
|
|
66
|
+
upstreamCode = normalized.code;
|
|
67
|
+
upstreamMessage = normalized.message;
|
|
68
|
+
upstreamType = normalized.type;
|
|
69
|
+
}
|
|
69
70
|
}
|
|
70
71
|
} catch (error) {
|
|
71
72
|
if (signal?.aborted) throw error;
|
|
@@ -16,6 +16,7 @@ import { prepareEffortNormalization } from "../effort-policy";
|
|
|
16
16
|
import { resolveOpenCodeGoTransport } from "../../providers/opencode-go-transport";
|
|
17
17
|
import { getOrAllocateRequestSessionLane } from "../request-log-conversation";
|
|
18
18
|
import { shouldPreparePlaintextV2AgentMessages } from "../../responses/plaintext-v2-agent-messages";
|
|
19
|
+
import { hasValidatedActiveReasoningEffort } from "../../responses/parser";
|
|
19
20
|
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
20
21
|
import { applyOpenAiVirtualModel } from "../../providers/openai-virtual-models";
|
|
21
22
|
import {
|
|
@@ -164,7 +165,8 @@ export async function applyFinalRouteRequestNormalization(args: {
|
|
|
164
165
|
if (inboundWire === "responses" && parsed._rawBody) {
|
|
165
166
|
const summary = (parsed._rawBody as { reasoning?: { summary?: unknown } }).reasoning?.summary;
|
|
166
167
|
parsed.options.hideThinkingSummary = summary === "none"
|
|
167
|
-
|| (!summary &&
|
|
168
|
+
|| (!summary && !hasValidatedActiveReasoningEffort(parsed.options)
|
|
169
|
+
&& route.provider.showThinkingSummary !== true);
|
|
168
170
|
}
|
|
169
171
|
if (preserveAnthropicResponseModel) parsed._responseModelId = responseModelId;
|
|
170
172
|
logCtx.model = virtualModel?.selectedModelId ?? route.modelId;
|
|
@@ -195,6 +197,8 @@ export async function applyFinalRouteRequestNormalization(args: {
|
|
|
195
197
|
|
|
196
198
|
if (parsed._responseModelId !== undefined && parsed._responseModelId !== parsed.modelId) {
|
|
197
199
|
logCtx.resolvedModel = route.modelId;
|
|
200
|
+
logCtx.wireModel = route.modelId;
|
|
201
|
+
logCtx.responseModelEcho = parsed._responseModelId;
|
|
198
202
|
logCtx.preserveResolvedModelFromRoute = true;
|
|
199
203
|
}
|
|
200
204
|
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { ENCRYPTED_FUNCTION_OUTPUT_REJECTION, upstreamErrorMessageFromPayload } from "../../lib/errors";
|
|
2
2
|
import { readBoundedResponseBody } from "../../lib/bounded-body";
|
|
3
3
|
import { isReasoningEffortRejection } from "../../providers/reasoning-metadata";
|
|
4
|
+
import { isAnthropicFastRefusal } from "../../providers/anthropic-fast";
|
|
5
|
+
import type { AdapterRequest } from "../../adapters/base";
|
|
4
6
|
import { isNonReplayableResponse } from "../../lib/upstream-retry";
|
|
5
7
|
import type { OcxParsedRequest } from "../../types";
|
|
6
8
|
import type { RequestLogContext } from "../request-log";
|
|
7
9
|
import type { AttemptRecoveryKind } from "../../usage/log";
|
|
8
10
|
import { rememberReasoningReplayOpaqueBlobRejection } from "../../responses/reasoning-replay-cache";
|
|
11
|
+
import { resolvedAdapterWire } from "../../responses/continuation-ownership";
|
|
9
12
|
|
|
10
13
|
export const OPAQUE_RESPONSES_INPUT_TYPES = new Set([
|
|
11
14
|
"reasoning",
|
|
@@ -22,6 +25,21 @@ export const FUNCTION_OUTPUT_TYPES = new Set(["function_call_output", "custom_to
|
|
|
22
25
|
export const AGENT_MESSAGE_TYPE = "agent_message";
|
|
23
26
|
|
|
24
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Whether the adapter serving this send speaks the Responses wire, as the adapter registry
|
|
30
|
+
* declares it.
|
|
31
|
+
*
|
|
32
|
+
* Recovery used to compare the adapter name with `"openai-responses"`. Azure OpenAI wraps the same
|
|
33
|
+
* passthrough under its own name, so an `invalid_encrypted_content` after moving a conversation
|
|
34
|
+
* onto Azure never recovered (#5583). The registry already records that relationship as
|
|
35
|
+
* `contractParent: "openai-responses"`, so reading the resolved wire covers Azure and any later
|
|
36
|
+
* wrapper of the same contract by construction, while every translated wire stays excluded.
|
|
37
|
+
*/
|
|
38
|
+
export function adapterSpeaksResponsesWire(adapterName: string): boolean {
|
|
39
|
+
return resolvedAdapterWire(adapterName) === "openai-responses";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
25
43
|
export function encryptedFunctionOutputParts(output: unknown): boolean {
|
|
26
44
|
return Array.isArray(output) && output.some(part => (
|
|
27
45
|
part !== null
|
|
@@ -282,7 +300,7 @@ export function shouldAttemptOpaqueBlobRecovery(args: {
|
|
|
282
300
|
&& isEncryptedFunctionOutputRejection(args.errorBody)
|
|
283
301
|
);
|
|
284
302
|
return acceptedStatus
|
|
285
|
-
&& args.adapterName
|
|
303
|
+
&& adapterSpeaksResponsesWire(args.adapterName)
|
|
286
304
|
&& !args.alreadyAttempted
|
|
287
305
|
&& outboundResponsesBodyCarriesOpaqueBlob(args.outboundBody)
|
|
288
306
|
&& isSelfIdentifiedOpaqueBlobRejection(args.errorBody);
|
|
@@ -312,6 +330,33 @@ export async function reasoningEffortRejectionText(
|
|
|
312
330
|
}
|
|
313
331
|
|
|
314
332
|
|
|
333
|
+
/**
|
|
334
|
+
* Whether an Anthropic response refused the fast lane of a request that actually sent it.
|
|
335
|
+
*
|
|
336
|
+
* "Actually sent" is read from the adapter's own tier record for that exact request, never from
|
|
337
|
+
* the route: a request built without `speed` can be refused for other reasons and must not be
|
|
338
|
+
* downgraded. The body is read from a clone, so a refusal that is not recovered (or whose resend
|
|
339
|
+
* is not admitted) still reaches the caller intact.
|
|
340
|
+
*/
|
|
341
|
+
export async function anthropicFastRefused(
|
|
342
|
+
response: Response,
|
|
343
|
+
sentRequest: AdapterRequest | undefined,
|
|
344
|
+
adapterName: string,
|
|
345
|
+
alreadyAttempted: boolean,
|
|
346
|
+
signal: AbortSignal,
|
|
347
|
+
): Promise<boolean> {
|
|
348
|
+
if (alreadyAttempted || adapterName !== "anthropic") return false;
|
|
349
|
+
if (response.status !== 400 && response.status !== 429) return false;
|
|
350
|
+
const outcome = sentRequest?.tierLog?.outcome;
|
|
351
|
+
if (outcome?.wireKind !== "anthropic-speed" || typeof outcome.wireValue !== "string") return false;
|
|
352
|
+
try {
|
|
353
|
+
const body = await readBoundedResponseBody(response.clone(), { signal });
|
|
354
|
+
return isAnthropicFastRefusal(response.status, response.headers, body.truncated ? undefined : body.text);
|
|
355
|
+
} catch {
|
|
356
|
+
return isAnthropicFastRefusal(response.status, response.headers, undefined);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
315
360
|
export async function opaqueBlobRejectionBodyForRecovery(
|
|
316
361
|
response: Response,
|
|
317
362
|
outboundBody: string | undefined,
|
|
@@ -323,7 +368,7 @@ export async function opaqueBlobRejectionBodyForRecovery(
|
|
|
323
368
|
isNonReplayableResponse(response)
|
|
324
369
|
|| response.status < 400
|
|
325
370
|
|| (response.status >= 500 && response.status !== 502)
|
|
326
|
-
|| adapterName
|
|
371
|
+
|| !adapterSpeaksResponsesWire(adapterName)
|
|
327
372
|
|| alreadyAttempted
|
|
328
373
|
|| !outboundResponsesBodyCarriesOpaqueBlob(outboundBody)
|
|
329
374
|
) return undefined;
|
|
@@ -365,6 +410,9 @@ export async function consoleGoUploadRejectionBody(
|
|
|
365
410
|
|
|
366
411
|
export function prepareOpaqueBlobRecovery(parsed: OcxParsedRequest): void {
|
|
367
412
|
parsed._stripReasoningEncryptedContent = true;
|
|
413
|
+
// The destination rejected state another serving identity minted. A reasoning item's id names
|
|
414
|
+
// an item in that identity's store, so it goes with the blob.
|
|
415
|
+
parsed._dropForeignReasoningItemIds = true;
|
|
368
416
|
const rawBody = parsed._rawBody;
|
|
369
417
|
if (!rawBody || typeof rawBody !== "object" || Array.isArray(rawBody)) return;
|
|
370
418
|
const input = (rawBody as { input?: unknown }).input;
|
|
@@ -114,6 +114,8 @@ export interface HandleResponsesOptions {
|
|
|
114
114
|
callerDirectAuth?: CallerDirectAuth | null;
|
|
115
115
|
/** Internal recursion guard; callers outside this module must not set it. */
|
|
116
116
|
comboAttempt?: boolean;
|
|
117
|
+
/** Internal handoff: this combo was selected by shadow-call interception. */
|
|
118
|
+
shadowCallIntercepted?: boolean;
|
|
117
119
|
compactionRoutingOverride?: CompactionRoutingOverride | null;
|
|
118
120
|
/** Internal combo handoff for one parent-validated continuation snapshot. */
|
|
119
121
|
comboReplaySnapshot?: {
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
durableReplayDestinationIdentity,
|
|
22
22
|
bindReasoningReplayScope,
|
|
23
23
|
reasoningReplayServingIdentityChanged,
|
|
24
|
+
reasoningReplayItemStoreChanged,
|
|
24
25
|
reasoningReplayOpaqueBlobRejectionMemoized,
|
|
25
26
|
} from "../../responses/reasoning-replay-cache";
|
|
26
27
|
import type { OAuthAccessSnapshot } from "../../oauth";
|
|
@@ -137,9 +138,15 @@ export function bindRouteReasoningReplayScope(args: {
|
|
|
137
138
|
// after the first mismatch, but it cannot make history minted by the prior route decodable.
|
|
138
139
|
if (reasoningReplayServingIdentityChanged(parsed._reasoningReplayScope)) {
|
|
139
140
|
parsed._stripReasoningEncryptedContent = true;
|
|
141
|
+
// Only a different destination or credential makes the replayed item ids unresolvable; a
|
|
142
|
+
// model change on the same store keeps them.
|
|
143
|
+
if (reasoningReplayItemStoreChanged(parsed._reasoningReplayScope)) {
|
|
144
|
+
parsed._dropForeignReasoningItemIds = true;
|
|
145
|
+
}
|
|
140
146
|
}
|
|
141
147
|
if (reasoningReplayOpaqueBlobRejectionMemoized(parsed._reasoningReplayScope)) {
|
|
142
148
|
parsed._stripReasoningEncryptedContent = true;
|
|
149
|
+
parsed._dropForeignReasoningItemIds = true;
|
|
143
150
|
}
|
|
144
151
|
bindProviderContinuationForRoute(parsed, continuationOwner);
|
|
145
152
|
}
|
|
@@ -189,7 +189,7 @@ export { readDisplaySafeErrorText } from "./core-errors";
|
|
|
189
189
|
export { usesCodexForwardPoolAuth } from "./core-codex-account";
|
|
190
190
|
export { preAuthUpstreamHostCircuitKey } from "./core-codex-account";
|
|
191
191
|
export { upstreamHostCircuitOpenResponse } from "./core-codex-account";
|
|
192
|
-
export { shouldRetryCodexPoolAccountQuota } from "./core-codex-account";
|
|
192
|
+
export { shouldRetryCodexPoolAccountQuota, shouldRetryCodexScopedQuotaOnAlternate } from "./core-codex-account";
|
|
193
193
|
export { shouldRetryCodexPoolAccountTransient } from "./core-codex-account";
|
|
194
194
|
export { codexAccountGatedCanonicalWireModel } from "./core-codex-account";
|
|
195
195
|
export { codexForwardTerminalOutcomeRecorder } from "./core-codex-account";
|
|
@@ -123,11 +123,18 @@ function looksLikeUnknownOpaqueSlot(payload: string): boolean {
|
|
|
123
123
|
*/
|
|
124
124
|
const FERNET_TOKEN_CANDIDATE = /g[A-Za-z0-9_-]{97,}={0,2}/g;
|
|
125
125
|
const FERNET_TOKEN_BOUNDARY_CHAR = /[A-Za-z0-9_=-]/;
|
|
126
|
+
// A normal mixed agent task contains one encrypted body. Keep pathological slots
|
|
127
|
+
// from amplifying into an attacker-controlled number of request parts.
|
|
128
|
+
const MAX_EMBEDDED_FERNET_RUNS_PER_SLOT = 64;
|
|
126
129
|
|
|
127
130
|
interface FernetTokenRun {
|
|
128
131
|
index: number;
|
|
129
132
|
token: string;
|
|
130
133
|
}
|
|
134
|
+
interface FernetRunScan {
|
|
135
|
+
runs: FernetTokenRun[];
|
|
136
|
+
overflow: boolean;
|
|
137
|
+
}
|
|
131
138
|
|
|
132
139
|
/**
|
|
133
140
|
* Validate only the key-independent Fernet wire structure. Authenticity cannot be
|
|
@@ -158,11 +165,11 @@ function isStructurallyValidFernetToken(token: string): boolean {
|
|
|
158
165
|
}
|
|
159
166
|
|
|
160
167
|
export function structurallyValidFernetTokens(payload: string): string[] {
|
|
161
|
-
return fernetTokenRuns(payload).map(run => run.token);
|
|
168
|
+
return fernetTokenRuns(payload).runs.map(run => run.token);
|
|
162
169
|
}
|
|
163
170
|
|
|
164
171
|
/** Maximal, boundary-delimited and structurally valid Fernet runs embedded in a slot. */
|
|
165
|
-
function fernetTokenRuns(payload: string):
|
|
172
|
+
function fernetTokenRuns(payload: string): FernetRunScan {
|
|
166
173
|
const runs: FernetTokenRun[] = [];
|
|
167
174
|
for (const match of payload.matchAll(FERNET_TOKEN_CANDIDATE)) {
|
|
168
175
|
const index = match.index ?? 0;
|
|
@@ -172,9 +179,10 @@ function fernetTokenRuns(payload: string): FernetTokenRun[] {
|
|
|
172
179
|
if (before && FERNET_TOKEN_BOUNDARY_CHAR.test(before)) continue;
|
|
173
180
|
if (after && FERNET_TOKEN_BOUNDARY_CHAR.test(after)) continue;
|
|
174
181
|
if (!isStructurallyValidFernetToken(token)) continue;
|
|
182
|
+
if (runs.length === MAX_EMBEDDED_FERNET_RUNS_PER_SLOT) return { runs, overflow: true };
|
|
175
183
|
runs.push({ index, token });
|
|
176
184
|
}
|
|
177
|
-
return runs;
|
|
185
|
+
return { runs, overflow: false };
|
|
178
186
|
}
|
|
179
187
|
|
|
180
188
|
function textWithoutFernetRuns(payload: string, runs: readonly FernetTokenRun[]): string {
|
|
@@ -291,7 +299,9 @@ export function hasUnreadableEncryptedAgentTask(input: unknown): boolean {
|
|
|
291
299
|
}
|
|
292
300
|
|
|
293
301
|
if (fragmentParts.has(part)) continue;
|
|
294
|
-
const
|
|
302
|
+
const scan = fernetTokenRuns(record.encrypted_content);
|
|
303
|
+
if (scan.overflow) return true;
|
|
304
|
+
const { runs } = scan;
|
|
295
305
|
if (runs.length > 0) hasFernetTask = true;
|
|
296
306
|
readableParts.push(textWithoutFernetRuns(record.encrypted_content, runs));
|
|
297
307
|
}
|
|
@@ -308,9 +318,11 @@ export function hasUnreadableEncryptedAgentTask(input: unknown): boolean {
|
|
|
308
318
|
|
|
309
319
|
|
|
310
320
|
export function encryptedSlotParts(payload: string): Array<Record<string, string>> {
|
|
321
|
+
const scan = fernetTokenRuns(payload);
|
|
322
|
+
if (scan.overflow) return [{ type: "input_text", text: OMITTED_ENCRYPTED_CONTENT_TEXT }];
|
|
311
323
|
const parts: Array<Record<string, string>> = [];
|
|
312
324
|
let last = 0;
|
|
313
|
-
for (const run of
|
|
325
|
+
for (const run of scan.runs) {
|
|
314
326
|
const before = payload.slice(last, run.index);
|
|
315
327
|
if (before.trim().length > 0) parts.push({ type: "input_text", text: before });
|
|
316
328
|
parts.push({ type: "encrypted_content", encrypted_content: run.token });
|
|
@@ -420,7 +432,9 @@ export function stripAgentMessageCiphertextInPlace(input: unknown): number {
|
|
|
420
432
|
|
|
421
433
|
/** Free text: drop embedded token runs, and replace a slot that is nothing but a token. */
|
|
422
434
|
function textWithoutCiphertext(text: string): string {
|
|
423
|
-
const
|
|
435
|
+
const scan = fernetTokenRuns(text);
|
|
436
|
+
if (scan.overflow) return OMITTED_ENCRYPTED_CONTENT_TEXT;
|
|
437
|
+
const { runs } = scan;
|
|
424
438
|
if (runs.length > 0) return textWithRunsOmitted(text, runs);
|
|
425
439
|
return looksLikeFernetToken(text.trim()) ? OMITTED_ENCRYPTED_CONTENT_TEXT : text;
|
|
426
440
|
}
|
|
@@ -473,11 +487,11 @@ function contentWithoutCiphertext(content: unknown[]): unknown[] {
|
|
|
473
487
|
changed = true;
|
|
474
488
|
// Keep whatever plaintext a recognizable slot carries around its token; a slot this
|
|
475
489
|
// cannot parse is replaced whole rather than forwarded on the chance that it is benign.
|
|
476
|
-
const
|
|
490
|
+
const scan = fernetTokenRuns(record.encrypted_content);
|
|
477
491
|
return {
|
|
478
492
|
type: "input_text",
|
|
479
|
-
text: runs.length > 0
|
|
480
|
-
? textWithRunsOmitted(record.encrypted_content, runs)
|
|
493
|
+
text: scan.overflow ? OMITTED_ENCRYPTED_CONTENT_TEXT : scan.runs.length > 0
|
|
494
|
+
? textWithRunsOmitted(record.encrypted_content, scan.runs)
|
|
481
495
|
: OMITTED_ENCRYPTED_CONTENT_TEXT,
|
|
482
496
|
};
|
|
483
497
|
}
|
|
@@ -207,7 +207,10 @@ export async function deliverPassthroughResponse(
|
|
|
207
207
|
|
|
208
208
|
const headers = sanitizePassthroughHeaders(upstreamResponse.headers, codexSafetyBufferingOptions);
|
|
209
209
|
const resolvedModel = headers.get("openai-model")?.trim();
|
|
210
|
-
if (resolvedModel
|
|
210
|
+
if (resolvedModel) {
|
|
211
|
+
logCtx.servedModel = resolvedModel;
|
|
212
|
+
if (!logCtx.preserveResolvedModelFromRoute) logCtx.resolvedModel = resolvedModel;
|
|
213
|
+
}
|
|
211
214
|
if (isUsageDebugEnabled()) {
|
|
212
215
|
const upstreamContentType = upstreamResponse.headers.get("content-type");
|
|
213
216
|
if (upstreamContentType) logCtx.usageDebugContentType = upstreamContentType;
|
|
@@ -380,36 +383,62 @@ export async function deliverPassthroughResponse(
|
|
|
380
383
|
});
|
|
381
384
|
// Capture the binding that actually served the first leg, after its permitted reselection.
|
|
382
385
|
const webSearchBridgeBinding = requestBindings.get(nativeExchange.request);
|
|
383
|
-
//
|
|
384
|
-
//
|
|
385
|
-
|
|
386
|
+
// Repair must observe the raw first leg before the bridge suppresses an intercepted search
|
|
387
|
+
// lifecycle. Otherwise a provider that leaves that complete call open never arms repair's
|
|
388
|
+
// grace timer, so the bridge cannot execute the search or begin its continuation.
|
|
389
|
+
let passthroughSseBody = terminalRepairPolicy
|
|
390
|
+
? relayResponsesSseWithTerminalRepair(
|
|
391
|
+
upstreamResponse.body,
|
|
392
|
+
upstream,
|
|
393
|
+
terminalRepairPolicy,
|
|
394
|
+
translatorBudget,
|
|
395
|
+
options.responsesTerminalRepairScheduler,
|
|
396
|
+
)
|
|
397
|
+
: upstreamResponse.body;
|
|
398
|
+
passthroughSseBody = webSearchBridgePlan
|
|
386
399
|
? createPassthroughWebSearchBridgeStream({
|
|
387
400
|
plan: webSearchBridgePlan,
|
|
388
|
-
firstLeg:
|
|
401
|
+
firstLeg: passthroughSseBody,
|
|
389
402
|
requestBody: nativeExchange.request.body,
|
|
390
403
|
// Continuation legs replay the same built request with the executed search appended.
|
|
391
404
|
// The first leg already passed the recovery ladder, the outbound size ceiling, and the
|
|
392
405
|
// host circuit; a KEY-auth destination has no OAuth refresh to replay on a later leg.
|
|
393
|
-
send: (continuationBody: string) =>
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
406
|
+
send: async (continuationBody: string) => {
|
|
407
|
+
const continuation = await fetchWithHeaderTimeout(
|
|
408
|
+
nativeExchange.request.url,
|
|
409
|
+
{ method: nativeExchange.request.method, headers: nativeExchange.request.headers, body: continuationBody },
|
|
410
|
+
upstream.signal,
|
|
411
|
+
connectMs,
|
|
412
|
+
true,
|
|
413
|
+
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
414
|
+
// Pacing can outlive a manual selection change. A continuation must retain the
|
|
415
|
+
// first leg's key and appended search result, never rebuild from the original turn.
|
|
416
|
+
beforeDispatch: () => {
|
|
417
|
+
if (webSearchBridgeBinding?.kind !== "api-key"
|
|
418
|
+
|| !providerApiKeySelectionIsCurrent(config, route.providerName, webSearchBridgeBinding.provider)) {
|
|
419
|
+
throw new Error("API key selection changed during a web-search continuation");
|
|
420
|
+
}
|
|
421
|
+
},
|
|
422
|
+
providerName: route.providerName,
|
|
423
|
+
modelId: route.modelId,
|
|
424
|
+
}),
|
|
425
|
+
false,
|
|
426
|
+
);
|
|
427
|
+
// The same provider can leave a complete continuation open without a terminal, which
|
|
428
|
+
// stalls the bridge's decide loop exactly like the first leg — so every leg gets the
|
|
429
|
+
// same repair, not only the intercepted first one.
|
|
430
|
+
if (!terminalRepairPolicy || !continuation.ok || !continuation.body) return continuation;
|
|
431
|
+
return new Response(
|
|
432
|
+
relayResponsesSseWithTerminalRepair(
|
|
433
|
+
continuation.body,
|
|
434
|
+
upstream,
|
|
435
|
+
terminalRepairPolicy,
|
|
436
|
+
translatorBudget,
|
|
437
|
+
options.responsesTerminalRepairScheduler,
|
|
438
|
+
),
|
|
439
|
+
continuation,
|
|
440
|
+
);
|
|
441
|
+
},
|
|
413
442
|
execute: createPassthroughWebSearchBridgeExecutor(webSearchBridgePlan, {
|
|
414
443
|
providerApiKey: route.provider.apiKey ?? "",
|
|
415
444
|
auth: webSearchBridgeAuth,
|
|
@@ -432,16 +461,7 @@ export async function deliverPassthroughResponse(
|
|
|
432
461
|
onFinalize: () => releaseCodexAuthContextProbeLease(openAiSidecar?.authContext),
|
|
433
462
|
signal: upstream.signal,
|
|
434
463
|
})
|
|
435
|
-
:
|
|
436
|
-
const passthroughSseBody = terminalRepairPolicy
|
|
437
|
-
? relayResponsesSseWithTerminalRepair(
|
|
438
|
-
upstreamSseBody,
|
|
439
|
-
upstream,
|
|
440
|
-
terminalRepairPolicy,
|
|
441
|
-
translatorBudget,
|
|
442
|
-
options.responsesTerminalRepairScheduler,
|
|
443
|
-
)
|
|
444
|
-
: upstreamSseBody;
|
|
464
|
+
: passthroughSseBody;
|
|
445
465
|
const repairConfig = route.provider.responsesItemIdRepair;
|
|
446
466
|
// Grok Build renders deltas live but reconstructs its durable assistant
|
|
447
467
|
// turn from the completed response snapshot. Native Responses streams
|