@bitkyc08/opencodex 2.89.0 → 2.91.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/App-DMps4OLk.css +1 -0
- package/gui/dist/assets/App-nZ34cR5G.js +58 -0
- package/gui/dist/assets/{Tray-7z1COnmz.js → Tray-BuLy7hL1.js} +1 -1
- package/gui/dist/assets/{index-uchD-ngf.js → index-DGXS51sp.js} +15 -15
- package/gui/dist/assets/quota-summary-DO1Sx5TM.js +1 -0
- package/gui/dist/index.html +1 -1
- package/package.json +2 -2
- package/src/adapters/anthropic.ts +2 -5
- package/src/adapters/base.ts +4 -0
- package/src/adapters/claude-cli/adapter.ts +13 -7
- package/src/adapters/claude-cli/usage-admission.ts +28 -2
- package/src/adapters/coding-agent/turn.ts +14 -4
- package/src/adapters/cursor/protobuf-request.ts +3 -0
- package/src/adapters/google-progress-history.ts +19 -1
- package/src/adapters/google.ts +28 -9
- package/src/adapters/image.ts +5 -3
- package/src/adapters/input-media-guard.ts +5 -2
- package/src/adapters/kiro-refusal.ts +41 -1
- package/src/adapters/ollama-native.ts +1 -0
- package/src/adapters/openai-chat/messages.ts +34 -4
- package/src/adapters/openai-chat/passthrough.ts +37 -4
- package/src/adapters/openai-chat/reasoning-replay.ts +98 -13
- package/src/adapters/openai-chat/serialized-tool-call-content.ts +95 -134
- package/src/adapters/openai-chat/serialized-tool-call-markup.ts +108 -0
- package/src/adapters/openai-chat/standalone-bare-tool-calls.ts +136 -0
- package/src/adapters/openai-chat.ts +7 -6
- package/src/adapters/openai-responses/opengateway-input.ts +139 -0
- package/src/adapters/openai-responses/passthrough.ts +7 -2
- package/src/adapters/openai-responses/request-strips.ts +32 -0
- package/src/adapters/openai-responses/transport.ts +19 -1
- package/src/chat/inbound.ts +20 -2
- package/src/chat/outbound.ts +36 -4
- package/src/chat/reasoning-details.ts +137 -0
- package/src/claude/desktop-3p-library.ts +43 -3
- package/src/claude/desktop-3p.ts +20 -10
- package/src/claude/desktop-picker-profile.ts +18 -9
- package/src/claude/desktop-remote-store-io.ts +2 -2
- package/src/cli/capabilities-agents-routing.ts +50 -0
- package/src/cli/capabilities-base.ts +15 -2
- package/src/cli/capabilities-observe-system.ts +4 -4
- package/src/cli/codex-shim-autorestore.ts +5 -0
- package/src/cli/dispatch.ts +30 -0
- package/src/cli/ensure-desktop-cold-start.ts +2 -2
- package/src/cli/fallback.ts +127 -0
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +1 -2
- package/src/cli/link-apply-restart.ts +72 -0
- package/src/cli/link-child.ts +6 -2
- package/src/cli/link-cleanup.ts +95 -0
- package/src/cli/link-enrollment.ts +4 -0
- package/src/cli/link.ts +84 -9
- package/src/cli/provider.ts +44 -2
- package/src/cli/registry.ts +13 -1
- package/src/cli/restart-failure.ts +4 -0
- package/src/cli/runtime-api.ts +1 -1
- package/src/cli/status-codex-settings.ts +56 -0
- package/src/cli/status-fallback.ts +29 -0
- package/src/cli/status-install.ts +52 -0
- package/src/cli/status.ts +20 -1
- package/src/cli/system-restart-client.ts +32 -1
- package/src/cli/update-restart-child.ts +4 -3
- package/src/cli/update-restart-home.ts +20 -8
- package/src/client/connect.ts +82 -6
- package/src/client/home-unreachable.ts +16 -0
- package/src/client/hub-relay.ts +8 -2
- package/src/client/link-home.ts +119 -6
- package/src/client/link-parity-reporter.ts +107 -0
- package/src/client/link-relay-transport.ts +8 -1
- package/src/client/link-relay.ts +27 -3
- package/src/client/machine-listener.ts +83 -1
- package/src/client/runtime-restart.ts +127 -0
- package/src/client/runtime.ts +137 -25
- package/src/codex/account-lifecycle.ts +19 -6
- package/src/codex/account-store.ts +27 -8
- package/src/codex/account-usability.ts +4 -1
- package/src/codex/auth-api/account-list.ts +10 -5
- package/src/codex/auth-api/account-pause-group.ts +2 -22
- package/src/codex/auth-api/login-collision.ts +59 -0
- package/src/codex/auth-api/login-flow.ts +12 -7
- package/src/codex/auth-api/pool-mode-gate.ts +81 -13
- package/src/codex/auth-collision.ts +31 -4
- package/src/codex/auth-context.ts +58 -12
- package/src/codex/catalog/parsing.ts +4 -0
- package/src/codex/cli-update-plan.ts +18 -3
- package/src/codex/collision-identity.ts +78 -0
- package/src/codex/desktop-compatibility/windows-key-protection.ts +18 -6
- package/src/codex/main-account-hard-lock.ts +14 -0
- package/src/codex/quota-history.ts +7 -0
- package/src/codex/quota.ts +10 -0
- package/src/codex/routing/cooldown-math.ts +5 -0
- package/src/codex/routing/failure-window.ts +61 -9
- package/src/codex/routing.ts +4 -3
- package/src/codex/shim-diagnostics.ts +2 -1
- package/src/codex/shim-fingerprint.ts +11 -2
- package/src/codex/shim-templates.ts +16 -5
- package/src/codex/shim-windows-refresh.ts +224 -0
- package/src/codex/shim.ts +45 -21
- package/src/codex/subagent-model-fallback.ts +24 -1
- package/src/combos/failover.ts +8 -1
- package/src/combos/index.ts +1 -0
- package/src/combos/resolve.ts +12 -0
- package/src/config/diagnostics.ts +5 -0
- package/src/config/load-degrade.ts +2 -0
- package/src/config/provider-validation.ts +7 -2
- package/src/config/schema/config-schema.ts +3 -0
- package/src/config/schema/fallback-policy.ts +40 -0
- package/src/generated/compatibility-version.json +360 -192
- package/src/generated/model-metadata.ts +1 -1
- package/src/images/loop.ts +2 -2
- package/src/lib/authorization-scheme.ts +95 -0
- package/src/lib/bun-transpiler-cache-scope.mjs +31 -17
- package/src/lib/provider-admission-wait.ts +14 -0
- package/src/lib/provider-client-headers.ts +1 -0
- package/src/lib/redact.ts +2 -2
- package/src/lib/service-secrets.ts +37 -2
- package/src/lib/system-restart-contract.ts +24 -0
- package/src/lib/test-home-guard.ts +78 -11
- package/src/lib/translator-budget.ts +1 -1
- package/src/lib/upstream-retry.ts +15 -0
- package/src/link/child-parity.ts +124 -0
- package/src/link/codex-settings.ts +37 -0
- package/src/link/compatibility.ts +119 -0
- package/src/link/management-routes.ts +2 -0
- package/src/link/peer-report.ts +100 -0
- package/src/link/remote-platform.ts +78 -0
- package/src/link/ssh-argv.ts +19 -0
- package/src/link/status-projection.ts +24 -2
- package/src/oauth/account-import/google-antigravity-adapter.ts +38 -2
- package/src/oauth/account-import/parser.ts +15 -2
- package/src/oauth/account-import/types.ts +7 -0
- package/src/oauth/chatgpt-shared.ts +10 -1
- package/src/oauth/dsh.ts +45 -2
- package/src/oauth/index.ts +1 -1
- package/src/oauth/kimi.ts +2 -0
- package/src/oauth/kiro.ts +44 -1
- package/src/oauth/login-flow-state.ts +11 -2
- package/src/oauth/nous.ts +4 -1
- package/src/oauth/store.ts +2 -1
- package/src/protocols/codecs/chat.ts +3 -3
- package/src/protocols/encoders/adapter-events.ts +26 -2
- package/src/protocols/encoders/chat.ts +55 -3
- package/src/providers/claude-cli-usage.ts +20 -5
- package/src/providers/initial-model-selection.ts +2 -2
- package/src/providers/key-failover.ts +10 -2
- package/src/providers/key-store.ts +8 -3
- package/src/providers/mistral-transport.ts +240 -0
- package/src/providers/openai-sidecar.ts +5 -0
- package/src/providers/quota/anthropic-account-quota.ts +18 -6
- package/src/providers/quota/antigravity.ts +8 -10
- package/src/providers/quota/failure-codes.ts +18 -0
- package/src/providers/quota/vendor-probes-oauth.ts +12 -3
- package/src/providers/quota-routing-cache.ts +1 -1
- package/src/providers/quota.ts +5 -1
- package/src/providers/registry/entries-extended.ts +6 -1
- package/src/providers/routed-config.ts +1 -1
- package/src/providers/xai-transport.ts +2 -1
- package/src/quota-pool/assemble.ts +326 -0
- package/src/quota-pool/contract.ts +58 -0
- package/src/quota-pool/identity.ts +203 -0
- package/src/remote/features.ts +74 -0
- package/src/remote/protocol.ts +2 -0
- package/src/remote/served-by.ts +57 -0
- package/src/responses/audio.ts +46 -0
- package/src/responses/input-media.ts +37 -15
- package/src/responses/parser-content.ts +22 -27
- package/src/responses/parser.ts +7 -0
- package/src/responses/reasoning-envelope.ts +12 -0
- package/src/responses/reasoning-retention.ts +218 -11
- package/src/responses/schema.ts +3 -4
- package/src/responses/web-search-replay.ts +11 -2
- package/src/routing/fallback-policy.ts +290 -0
- package/src/routing/fallback-state.ts +179 -0
- package/src/server/audio-client.ts +2 -1
- package/src/server/audio-dictation.ts +2 -1
- package/src/server/audio-upstream.ts +2 -1
- package/src/server/auth-cors.ts +6 -5
- package/src/server/background-lifecycle.ts +4 -0
- package/src/server/chat-completions.ts +8 -1
- package/src/server/chat-native.ts +8 -7
- package/src/server/gui-session.ts +2 -1
- package/src/server/index/link-listener.ts +11 -1
- package/src/server/index/link-management.ts +47 -0
- package/src/server/index/link-relay-sessions.ts +11 -2
- package/src/server/index/optional-listeners.ts +7 -2
- package/src/server/index/serve-options.ts +27 -4
- package/src/server/index/websocket-handler.ts +10 -0
- package/src/server/lifecycle.ts +17 -1
- package/src/server/live.ts +5 -0
- package/src/server/management/context.ts +1 -1
- package/src/server/management/fallback-routes.ts +75 -0
- package/src/server/management/link-enrollment.ts +21 -3
- package/src/server/management/link-route-state.ts +2 -0
- package/src/server/management/link-routes.ts +12 -1
- package/src/server/management/oauth-account-routes.ts +2 -1
- package/src/server/management/quota-pool-routes.ts +24 -0
- package/src/server/management/route-registry.ts +3 -0
- package/src/server/management-api.ts +16 -0
- package/src/server/management-auth.ts +2 -1
- package/src/server/messages-native.ts +11 -0
- package/src/server/request-log.ts +16 -7
- package/src/server/responses/adapter-continuation.ts +2 -2
- package/src/server/responses/adapter-dispatch.ts +2 -2
- package/src/server/responses/agent-task-recovery.ts +1 -1
- package/src/server/responses/codex-ws-exchange.ts +3 -0
- package/src/server/responses/codex-ws-metadata.ts +44 -0
- package/src/server/responses/codex-ws-session.ts +21 -1
- package/src/server/responses/combo-stream-preflight.ts +7 -2
- package/src/server/responses/compact.ts +4 -2
- package/src/server/responses/core-auth.ts +31 -1
- package/src/server/responses/core-codex-account.ts +21 -14
- package/src/server/responses/core-combo.ts +75 -1
- package/src/server/responses/core-opaque-recovery.ts +20 -11
- package/src/server/responses/deepseek-image-files.ts +121 -0
- package/src/server/responses/empty-completion-guard.ts +8 -0
- package/src/server/responses/encrypted-payload.ts +3 -3
- package/src/server/responses/fetch-helpers.ts +30 -10
- package/src/server/responses/input-admission.ts +6 -2
- package/src/server/responses/main-hard-lock-routing.ts +85 -8
- package/src/server/responses/opengateway-completion-guard.ts +232 -0
- package/src/server/responses/passthrough-completion.ts +41 -0
- package/src/server/responses/passthrough-delivery.ts +2 -0
- package/src/server/responses/passthrough-dispatch.ts +13 -13
- package/src/server/responses/passthrough-key-rotation.ts +40 -0
- package/src/server/responses/policy-fallback.ts +44 -1
- package/src/server/responses/reasoning-archive-error.ts +21 -0
- package/src/server/responses/request-prepare.ts +19 -1
- package/src/server/responses/request-transport.ts +61 -3
- package/src/server/restart-replacement.ts +50 -2
- package/src/server/ws-bridge.ts +5 -0
- package/src/service/cli.ts +25 -2
- package/src/service/diagnostics.ts +20 -6
- package/src/service/orchestration.ts +17 -0
- package/src/service/status-json.ts +116 -0
- package/src/types/config.ts +7 -0
- package/src/types/fallback.ts +38 -0
- package/src/types/provider.ts +2 -0
- package/src/types/request.ts +16 -1
- package/src/types.ts +1 -0
- package/src/web-search/loop.ts +2 -2
- package/gui/dist/assets/App-BnRSujOq.js +0 -58
- package/gui/dist/assets/App-dnkJ85te.css +0 -1
- package/gui/dist/assets/quota-summary-BY_10IEz.js +0 -1
|
@@ -370,6 +370,9 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
|
|
|
370
370
|
{ method: "GET", path: "/api/provider-context-caps", module: "server/management/provider-routes", mutates: false },
|
|
371
371
|
{ method: "GET", path: "/api/provider-presets", module: "server/management/provider-routes", mutates: false },
|
|
372
372
|
{ method: "GET", path: "/api/provider-quotas", module: "server/management/provider-routes", mutates: false },
|
|
373
|
+
{ method: "GET", path: "/api/quota-pool", module: "server/management/quota-pool-routes", mutates: false, exempt: { reason: "deferred-verb", why: "The ocx.quota-pool/1 read is a machine contract GOAT consumes over management and the link relay; the CLI shows the same caches through ocx provider quota and ocx account.", owner: "ocx-1 quota pool", ownerDoc: "structure/quota-pool.md" } },
|
|
374
|
+
{ method: "GET", path: "/api/fallback", module: "server/management/fallback-routes", mutates: false },
|
|
375
|
+
{ method: "PUT", path: "/api/fallback", module: "server/management/fallback-routes", mutates: true },
|
|
373
376
|
{ method: "GET", path: "/api/provider-request-pacing", module: "server/management/provider-key-pacing-routes", mutates: false },
|
|
374
377
|
{ method: "GET", path: "/api/providers", module: "server/management/provider-routes", mutates: false },
|
|
375
378
|
{ method: "PATCH", path: "/api/providers", module: "server/management/provider-routes", mutates: true },
|
|
@@ -161,6 +161,20 @@ async function handleLowQuotaRoutesOnDemand(ctx: ManagementContext): Promise<Res
|
|
|
161
161
|
return handleLowQuotaRoutes(ctx);
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
/** Lazy for the same reason: the quota-pool assembler pulls every quota cache reader. */
|
|
165
|
+
async function handleQuotaPoolRoutesOnDemand(ctx: ManagementContext): Promise<Response | null> {
|
|
166
|
+
if (!pathInManagementNamespace(ctx.url.pathname, "/api/quota-pool", false)) return null;
|
|
167
|
+
const { handleQuotaPoolRoutes } = await import("./management/quota-pool-routes");
|
|
168
|
+
return handleQuotaPoolRoutes(ctx);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Lazy: the OCX-11 fallback state view and its write live off the dashboard's common path. */
|
|
172
|
+
async function handleFallbackRoutesOnDemand(ctx: ManagementContext): Promise<Response | null> {
|
|
173
|
+
if (!pathInManagementNamespace(ctx.url.pathname, "/api/fallback", false)) return null;
|
|
174
|
+
const { handleFallbackRoutes } = await import("./management/fallback-routes");
|
|
175
|
+
return handleFallbackRoutes(ctx);
|
|
176
|
+
}
|
|
177
|
+
|
|
164
178
|
/**
|
|
165
179
|
* Lazy like the Lab and routing-profile handlers, and for the same recorded reason: this file is
|
|
166
180
|
* mounted for every dashboard request, so a static import would put the workflow-budget ledger
|
|
@@ -351,6 +365,8 @@ export async function handleManagementAPI(
|
|
|
351
365
|
?? (await handleRequestHistoryRoutes(ctx))
|
|
352
366
|
?? (await handleQuotaResetRoutesOnDemand(ctx))
|
|
353
367
|
?? (await handleLowQuotaRoutesOnDemand(ctx))
|
|
368
|
+
?? (await handleQuotaPoolRoutesOnDemand(ctx))
|
|
369
|
+
?? (await handleFallbackRoutesOnDemand(ctx))
|
|
354
370
|
?? (await handleWorkflowBudgetRoutesOnDemand(ctx))
|
|
355
371
|
?? (await handleCodexAgentRoleRoutesOnDemand(ctx))
|
|
356
372
|
?? (await handleProtocolRoutesOnDemand(ctx))
|
|
@@ -73,6 +73,7 @@ import {
|
|
|
73
73
|
} from "./gui-session";
|
|
74
74
|
import { hasLocalDesktopSnapshotCapability } from "./local-desktop-snapshot-auth";
|
|
75
75
|
import { hasLocalAccountSwitchCapability } from "./local-account-switch-auth";
|
|
76
|
+
import { authorizationCredential } from "../lib/authorization-scheme";
|
|
76
77
|
export type { GuiSessionBootstrap, GuiSessionRequestContext } from "./gui-session";
|
|
77
78
|
|
|
78
79
|
const LOCAL_READ_REPLAY_LIMIT = 256;
|
|
@@ -545,7 +546,7 @@ function hasGuiPairCapability(
|
|
|
545
546
|
|
|
546
547
|
function requestManagementCredential(req: Request): string | null {
|
|
547
548
|
return req.headers.get("x-opencodex-api-key")?.trim()
|
|
548
|
-
|| req.headers
|
|
549
|
+
|| authorizationCredential(req.headers)
|
|
549
550
|
|| null;
|
|
550
551
|
}
|
|
551
552
|
|
|
@@ -849,6 +849,17 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
849
849
|
}
|
|
850
850
|
releaseRetainedRequest();
|
|
851
851
|
|
|
852
|
+
// The last pool account refused on spent quota and rotation found no replacement: answer the
|
|
853
|
+
// usage limit with its reset (#6991), as the bridge lane does, not the upstream "try again later".
|
|
854
|
+
const poolUsageLimit = response.status === 429 && oauthBinding
|
|
855
|
+
? anthropicRoutingFor(nativeInstance!).anthropicPoolUsageLimitAfterRefusal(Date.now(), oauthBinding.routeDecision, route.modelId)
|
|
856
|
+
: null;
|
|
857
|
+
if (poolUsageLimit) {
|
|
858
|
+
discard(response);
|
|
859
|
+
cleanupAbort();
|
|
860
|
+
logCtx.errorCode = "usage_limit_reached";
|
|
861
|
+
return retainUpstreamMessagesRequestId(localOAuthFailure(poolUsageLimit)!, response.headers);
|
|
862
|
+
}
|
|
852
863
|
if (!response.ok) {
|
|
853
864
|
let bodyText = "";
|
|
854
865
|
try {
|
|
@@ -1720,8 +1720,10 @@ export function noteProviderAttemptSend(
|
|
|
1720
1720
|
if (attempt && attempt.sendCount > 0 && attemptAccountChanged(previous, next, attempt.provider, logCtx.provider)) {
|
|
1721
1721
|
// An input estimate is not evidence that a failed send used that many tokens.
|
|
1722
1722
|
delete attempt.inputTokenEstimate;
|
|
1723
|
+
// An account-scoped recovery kind names the status the sealed account answered.
|
|
1723
1724
|
finishRequestAttempt(attempt, attempt.status >= 100 ? attempt.status
|
|
1724
|
-
: recovery === "key-401" ? 401 : recovery
|
|
1725
|
+
: recovery === "key-401" || recovery === "oauth-401" ? 401 : recovery === "oauth-account-403" ? 403
|
|
1726
|
+
: recovery === "oauth-account-402" ? 402 : recovery?.includes("429") ? 429 : 502,
|
|
1725
1727
|
Date.now() - (logCtx.activeAttemptStartedAt ?? Date.now()), attempt.usage);
|
|
1726
1728
|
// This attempt is being sealed because a NAMED recovery rejected it, so the recovery kind
|
|
1727
1729
|
// is direct evidence here rather than an inference from history. Without this the sealed
|
|
@@ -1850,16 +1852,20 @@ export function finishRequestAttempt(
|
|
|
1850
1852
|
export function aggregateAttemptUsage(
|
|
1851
1853
|
attempts: readonly PersistedUsageAttempt[],
|
|
1852
1854
|
): FinalizedUsageResult {
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
+
// A usage-less attempt that never reached upstream spent nothing (summary.ts `spentNothing`),
|
|
1856
|
+
// so it cannot leave the row's usage unknown.
|
|
1857
|
+
const measured = attempts.filter(attempt => attempt.usage
|
|
1858
|
+
|| (attempt.sendCount !== 0 && attempt.locallyAnswered !== true));
|
|
1859
|
+
const status: UsageStatus = measured.length > 0
|
|
1860
|
+
&& measured.every(attempt => attempt.usageStatus === "unsupported")
|
|
1855
1861
|
? "unsupported"
|
|
1856
|
-
:
|
|
1862
|
+
: measured.some(attempt => (
|
|
1857
1863
|
attempt.usageStatus === "unreported" || attempt.usageStatus === "unsupported"
|
|
1858
1864
|
))
|
|
1859
1865
|
? "unreported"
|
|
1860
|
-
:
|
|
1866
|
+
: measured.some(attempt => attempt.usageStatus === "estimated")
|
|
1861
1867
|
? "estimated"
|
|
1862
|
-
:
|
|
1868
|
+
: measured.length > 0
|
|
1863
1869
|
? "reported"
|
|
1864
1870
|
: "unreported";
|
|
1865
1871
|
|
|
@@ -1876,7 +1882,10 @@ export function aggregateAttemptUsage(
|
|
|
1876
1882
|
return present.length > 0 ? present.reduce((sum, value) => sum + value, 0) : undefined;
|
|
1877
1883
|
};
|
|
1878
1884
|
const cachedInputTokens = sumOptional("cachedInputTokens");
|
|
1879
|
-
|
|
1885
|
+
// Readers take `cacheReadInputTokens ?? cachedInputTokens` (usage/cost.ts), so once any attempt
|
|
1886
|
+
// reports the read field, an attempt that only reports `cachedInputTokens` must count in it too.
|
|
1887
|
+
const cacheReadInputTokens = sumOptional("cacheReadInputTokens") === undefined ? undefined
|
|
1888
|
+
: usages.reduce((sum, usage) => sum + (usage.cacheReadInputTokens ?? usage.cachedInputTokens ?? 0), 0);
|
|
1880
1889
|
const cacheCreationInputTokens = sumOptional("cacheCreationInputTokens");
|
|
1881
1890
|
const cacheCreation1hInputTokens = sumOptional("cacheCreation1hInputTokens");
|
|
1882
1891
|
const reasoningOutputTokens = sumOptional("reasoningOutputTokens");
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
fetchWithTransientRetry,
|
|
29
29
|
fetchWithResetRetry,
|
|
30
30
|
applyUpstreamRecoveryInit,
|
|
31
|
-
isNonReplayableResponse,
|
|
31
|
+
isNonReplayableResponse, isSameTargetRetryHold,
|
|
32
32
|
prepareSameTarget429Wait,
|
|
33
33
|
SendBudgetExhaustedError,
|
|
34
34
|
} from "../../lib/upstream-retry";
|
|
@@ -349,7 +349,7 @@ export function createAdapterContinuations(
|
|
|
349
349
|
&& !kiroReplaySetupFailed
|
|
350
350
|
// A synthesized replay refusal is not a rate limit; replaying the continuation on
|
|
351
351
|
// it would re-send a turn whose first send may already have been processed.
|
|
352
|
-
&& !isNonReplayableResponse(response)
|
|
352
|
+
&& !isNonReplayableResponse(response) && !isSameTargetRetryHold(response)
|
|
353
353
|
&& rateLimitPolicy !== null
|
|
354
354
|
&& adapterExchange.rateLimitRetries < rateLimitPolicy.attempts
|
|
355
355
|
// The main recovery loop and the passthrough ladder both consult the shared remainder
|
|
@@ -9,7 +9,7 @@ import { rotateAnthropicAccountOnResponseForInstance } from "../../oauth/anthrop
|
|
|
9
9
|
import { authorizeResendForRecovery } from "../../lib/request-resend-gate";
|
|
10
10
|
import { ambiguousResendAllowanceFor, selfContainedResponsesBody } from "./reset-replay";
|
|
11
11
|
import { transientSendCapFor } from "./request-send-budget";
|
|
12
|
-
import { isNonReplayableResponse } from "../../lib/upstream-retry";
|
|
12
|
+
import { isNonReplayableResponse, isSameTargetRetryHold } from "../../lib/upstream-retry";
|
|
13
13
|
import { sanitizeNonReplayableUpstreamError } from "./non-replayable-error";
|
|
14
14
|
import { isLocalUpstream } from "../../lib/local-upstream";
|
|
15
15
|
import type { ResponsesRequestContext, ResponsesAdmissionState } from "./core-options";
|
|
@@ -1239,7 +1239,7 @@ export async function prepareAdapterExchange(
|
|
|
1239
1239
|
// replay is lossless. Runs before key failover so "primary-first" setups keep the same
|
|
1240
1240
|
// key on rate-limit blips; only after the attempts are exhausted does failover run.
|
|
1241
1241
|
while (
|
|
1242
|
-
upstreamResponse.status === 429
|
|
1242
|
+
upstreamResponse.status === 429 && !isSameTargetRetryHold(upstreamResponse)
|
|
1243
1243
|
&& rateLimitPolicy !== null
|
|
1244
1244
|
&& rateLimitRetries < rateLimitPolicy.attempts
|
|
1245
1245
|
&& !sendBudgetExhausted()
|
|
@@ -104,7 +104,7 @@ function findEnvelope(input: unknown): AgentEnvelope | null {
|
|
|
104
104
|
const type = input[itemIndex] && typeof input[itemIndex] === "object"
|
|
105
105
|
? (input[itemIndex] as { type?: unknown }).type
|
|
106
106
|
: undefined;
|
|
107
|
-
if (type !== "compaction_trigger" && type !== "additional_tools") break;
|
|
107
|
+
if (type !== "compaction_trigger" && type !== "additional_tools" && type !== "configuration_update") break;
|
|
108
108
|
itemIndex -= 1;
|
|
109
109
|
}
|
|
110
110
|
|
|
@@ -458,6 +458,9 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
|
|
|
458
458
|
return;
|
|
459
459
|
}
|
|
460
460
|
if (terminal || settledPreOpen || signal?.aborted) return;
|
|
461
|
+
// New sockets have just validated their upgrade; retained sockets carry the
|
|
462
|
+
// same connection signal into a fresh exchange, never its predecessor's frames.
|
|
463
|
+
metadata?.seed(session.upgradeHeaders());
|
|
461
464
|
sent = true;
|
|
462
465
|
try {
|
|
463
466
|
if (nativeControl) {
|
|
@@ -6,11 +6,48 @@ export const CODEX_WS_METADATA_MAX_HEADERS = 128;
|
|
|
6
6
|
export const CODEX_WS_METADATA_MAX_VALUE_BYTES = 4096;
|
|
7
7
|
|
|
8
8
|
export type CodexWsQuotaObserver = (headers: Headers) => void;
|
|
9
|
+
const REASONING_INCLUDED_HEADER = "x-reasoning-included";
|
|
10
|
+
const HTTP_HEADER_TOKEN = /^[!#$%&'*+.^_`|~0-9a-z-]+$/i;
|
|
9
11
|
|
|
10
12
|
function record(value: unknown): value is Record<string, unknown> {
|
|
11
13
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
12
14
|
}
|
|
13
15
|
|
|
16
|
+
/** Bun emits this payload before validating the upgrade; the session must wait for open. */
|
|
17
|
+
export function readCodexWsUpgradeHeaders(payload: unknown): Headers {
|
|
18
|
+
try {
|
|
19
|
+
if (!record(payload) || payload.statusCode !== 101 || !Array.isArray(payload.rawHeaders)
|
|
20
|
+
|| payload.rawHeaders.length % 2 !== 0 || payload.rawHeaders.length > CODEX_WS_METADATA_MAX_HEADERS * 2) return new Headers();
|
|
21
|
+
const headers = new Headers();
|
|
22
|
+
let bytes = 0;
|
|
23
|
+
let connectionNominated = false;
|
|
24
|
+
for (let i = 0; i < payload.rawHeaders.length; i += 2) {
|
|
25
|
+
const name = payload.rawHeaders[i];
|
|
26
|
+
const value = payload.rawHeaders[i + 1];
|
|
27
|
+
if (typeof name !== "string" || typeof value !== "string"
|
|
28
|
+
|| name.length + value.length > CODEX_WS_METADATA_MAX_BYTES - bytes || !HTTP_HEADER_TOKEN.test(name)
|
|
29
|
+
|| /[\r\n\0]/.test(value)) return new Headers();
|
|
30
|
+
bytes += Buffer.byteLength(name) + Buffer.byteLength(value);
|
|
31
|
+
if (bytes > CODEX_WS_METADATA_MAX_BYTES) return new Headers();
|
|
32
|
+
const lower = name.toLowerCase();
|
|
33
|
+
if (lower === "connection" && value.split(",").some(token => token.trim().toLowerCase() === REASONING_INCLUDED_HEADER)) {
|
|
34
|
+
connectionNominated = true;
|
|
35
|
+
}
|
|
36
|
+
// Other allowlisted response headers (quota, request id, turn state, model)
|
|
37
|
+
// describe a response, not a reusable connection. Never retain them here.
|
|
38
|
+
if (lower === REASONING_INCLUDED_HEADER) {
|
|
39
|
+
if (Buffer.byteLength(value) > CODEX_WS_METADATA_MAX_VALUE_BYTES) return new Headers();
|
|
40
|
+
headers.append(lower, value);
|
|
41
|
+
if (Buffer.byteLength(headers.get(lower)!) > CODEX_WS_METADATA_MAX_VALUE_BYTES) return new Headers();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return connectionNominated ? new Headers() : headers;
|
|
45
|
+
} catch {
|
|
46
|
+
// Missing/unsupported or malformed optional metadata cannot authorize a retry.
|
|
47
|
+
return new Headers();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
14
51
|
function finiteNonnegative(value: unknown): value is number {
|
|
15
52
|
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
16
53
|
}
|
|
@@ -91,6 +128,13 @@ export class CodexWsMetadata {
|
|
|
91
128
|
|
|
92
129
|
snapshot(): Headers { return new Headers(this.headers); }
|
|
93
130
|
|
|
131
|
+
/** Seed only the connection-level inclusion signal; per-response frames still win. */
|
|
132
|
+
seed(headers: Headers): void {
|
|
133
|
+
if (this.ended || this.committed || this.headers.has(REASONING_INCLUDED_HEADER)) return;
|
|
134
|
+
const value = headers.get(REASONING_INCLUDED_HEADER);
|
|
135
|
+
if (value !== null) setMetadataHeader(this.headers, REASONING_INCLUDED_HEADER, value);
|
|
136
|
+
}
|
|
137
|
+
|
|
94
138
|
private publishQuota(headers: Headers): void {
|
|
95
139
|
// Observation is auxiliary bookkeeping; a consumer exception cannot turn
|
|
96
140
|
// a valid provider frame into a retryable transport failure.
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readCodexWsUpgradeHeaders } from "./codex-ws-metadata";
|
|
2
|
+
|
|
1
3
|
export const MAX_CODEX_WS_SESSION_EXCHANGES = 32;
|
|
2
4
|
|
|
3
5
|
/** Owns one physical socket; request listeners belong to the exchange, not this object. */
|
|
@@ -8,10 +10,13 @@ export class CodexWsSession {
|
|
|
8
10
|
busy = false;
|
|
9
11
|
private owner?: (reason: Error) => void;
|
|
10
12
|
private readonly completedIds = new Set<string>();
|
|
13
|
+
private pendingUpgradeHeaders?: Headers;
|
|
14
|
+
private connectionHeaders?: Headers;
|
|
11
15
|
|
|
12
16
|
constructor(url: string, headers: Record<string, string>, readonly retainable = false,
|
|
13
17
|
private readonly changed: () => void = () => {}, proxy?: string) {
|
|
14
18
|
this.socket = new WebSocket(url, { headers, ...(proxy ? { proxy } : {}) } as unknown as string[]);
|
|
19
|
+
this.socket.addEventListener("handshake", this.onHandshake);
|
|
15
20
|
this.socket.addEventListener("open", this.onOpen);
|
|
16
21
|
this.socket.addEventListener("message", this.onIdleMessage);
|
|
17
22
|
this.socket.addEventListener("close", this.onClose);
|
|
@@ -20,6 +25,7 @@ export class CodexWsSession {
|
|
|
20
25
|
|
|
21
26
|
get reused(): boolean { return this.completedIds.size > 0; }
|
|
22
27
|
hasCompleted(id: string): boolean { return this.completedIds.has(id); }
|
|
28
|
+
upgradeHeaders(): Headers { return new Headers(this.opened && !this.closed ? this.connectionHeaders : undefined); }
|
|
23
29
|
|
|
24
30
|
reserve(): boolean {
|
|
25
31
|
if (this.closed || this.busy || (this.opened && this.socket.readyState !== undefined && this.socket.readyState !== 1)) return false;
|
|
@@ -71,7 +77,18 @@ export class CodexWsSession {
|
|
|
71
77
|
}
|
|
72
78
|
}
|
|
73
79
|
|
|
74
|
-
private
|
|
80
|
+
private onHandshake = (event: Event): void => {
|
|
81
|
+
if (this.opened || this.closed) return;
|
|
82
|
+
this.pendingUpgradeHeaders = readCodexWsUpgradeHeaders((event as MessageEvent<unknown>).data);
|
|
83
|
+
};
|
|
84
|
+
private onOpen = (): void => {
|
|
85
|
+
if (this.closed) return;
|
|
86
|
+
// A 101 payload alone is not proof that Bun accepted the handshake.
|
|
87
|
+
this.connectionHeaders = this.pendingUpgradeHeaders;
|
|
88
|
+
this.pendingUpgradeHeaders = undefined;
|
|
89
|
+
this.socket.removeEventListener("handshake", this.onHandshake);
|
|
90
|
+
this.opened = true;
|
|
91
|
+
};
|
|
75
92
|
private onIdleMessage = (): void => {
|
|
76
93
|
if (!this.busy) this.dispose(new Error("codex websocket received unsolicited idle data"));
|
|
77
94
|
};
|
|
@@ -85,6 +102,9 @@ export class CodexWsSession {
|
|
|
85
102
|
// The active exchange's close listener retains pre-send fallback semantics.
|
|
86
103
|
};
|
|
87
104
|
private detach(): void {
|
|
105
|
+
this.pendingUpgradeHeaders = undefined;
|
|
106
|
+
this.connectionHeaders = undefined;
|
|
107
|
+
this.socket.removeEventListener("handshake", this.onHandshake);
|
|
88
108
|
this.socket.removeEventListener("open", this.onOpen);
|
|
89
109
|
this.socket.removeEventListener("message", this.onIdleMessage);
|
|
90
110
|
this.socket.removeEventListener("close", this.onClose);
|
|
@@ -224,8 +224,13 @@ function failedTerminalResponse(
|
|
|
224
224
|
: topLevelError && typeof topLevelError === "object" && !Array.isArray(topLevelError)
|
|
225
225
|
? topLevelError as Record<string, unknown>
|
|
226
226
|
: {
|
|
227
|
-
type:
|
|
228
|
-
|
|
227
|
+
// A caller-validated flat error can carry its identity beside type:error.
|
|
228
|
+
// Retain it when that caller established a 400, rather than discarding the
|
|
229
|
+
// rejection code before the single-shot opaque-state recovery sees it.
|
|
230
|
+
type: terminalPayload.type === "error" && logCtx.terminalHttpStatus === 400
|
|
231
|
+
? "invalid_request_error" : "upstream_error",
|
|
232
|
+
code: terminalPayload.type === "error" && logCtx.terminalHttpStatus === 400
|
|
233
|
+
? (typeof terminalPayload.code === "string" ? terminalPayload.code : null) : "upstream_server_error",
|
|
229
234
|
message: typeof terminalPayload.message === "string"
|
|
230
235
|
? terminalPayload.message
|
|
231
236
|
: logCtx.upstreamError ?? "Provider stream failed before producing output",
|
|
@@ -88,6 +88,7 @@ import {
|
|
|
88
88
|
recordCodexUpstreamOutcome,
|
|
89
89
|
type CodexUpstreamOutcome,
|
|
90
90
|
} from "../../codex/routing";
|
|
91
|
+
import { codexFailureWindowOutcomeMeta } from "../../codex/routing/failure-window";
|
|
91
92
|
import {
|
|
92
93
|
applyAccountChangeConversationStateScrub,
|
|
93
94
|
conversationStateBindingFromAuth,
|
|
@@ -134,7 +135,7 @@ import type { DataPlaneAdmission } from "../auth-cors";
|
|
|
134
135
|
import { listOpenAiForwardSidecarCandidates, resolveFirstUsableOpenAiSidecar, type ResolvedOpenAiForwardSidecar } from "../../providers/openai-sidecar";
|
|
135
136
|
import { CODEX_FORWARD_BASE_URL, isCanonicalOpenAiForwardProvider, supportsNativeResponsesCompactEndpoint } from "../../providers/openai-tiers";
|
|
136
137
|
import { buildResponsesTransport, copyResponsesClientMetadata } from "../../adapters/openai-responses/transport";
|
|
137
|
-
import { scrubOcxCompactionItems, stripCanonicalOnlyTopLevelFields, stripInternalChatMessageMetadataPassthrough } from "../../adapters/openai-responses/request-strips";
|
|
138
|
+
import { scrubOcxCompactionItems, stripCanonicalOnlyInputItems, stripCanonicalOnlyTopLevelFields, stripInternalChatMessageMetadataPassthrough } from "../../adapters/openai-responses/request-strips";
|
|
138
139
|
import { dropResponsesReasoningInputItems } from "../../adapters/openai-responses/reasoning";
|
|
139
140
|
import { requiresPlaintextReasoningReplay } from "../../adapters/openai-responses/passthrough";
|
|
140
141
|
import { destinationDecodesNativeCompactionBlob, isOpenAiOperatedResponsesDestination } from "../../providers/openai-tiers-destination";
|
|
@@ -1062,6 +1063,7 @@ export async function handleResponsesCompact(
|
|
|
1062
1063
|
if ((outcome === 401 || outcome === 403) && upstream && wasPermanentAccountRefusal(upstream)) return;
|
|
1063
1064
|
recordCodexUpstreamOutcome(config, ctx.accountId, outcome, {
|
|
1064
1065
|
...meta,
|
|
1066
|
+
...codexFailureWindowOutcomeMeta(ctx),
|
|
1065
1067
|
threadId: ctx.kind === "pool" || ctx.kind === "main-pool" ? ctx.affinityKey : undefined,
|
|
1066
1068
|
fixedAccount: ctx.fixedAccount,
|
|
1067
1069
|
modelId: selectedModelId,
|
|
@@ -1153,7 +1155,7 @@ export async function handleResponsesCompact(
|
|
|
1153
1155
|
replayBody = stripInternalChatMessageMetadataPassthrough(replayBody) as typeof replayBody;
|
|
1154
1156
|
}
|
|
1155
1157
|
return isOpenAiOperatedResponsesDestination(sendProvider)
|
|
1156
|
-
? replayBody : stripCanonicalOnlyTopLevelFields(replayBody);
|
|
1158
|
+
? replayBody : stripCanonicalOnlyInputItems(stripCanonicalOnlyTopLevelFields(replayBody));
|
|
1157
1159
|
};
|
|
1158
1160
|
const doFetch = (upstreamRecovery?: UpstreamSendRecovery) => fetchWithHeaderTimeout(
|
|
1159
1161
|
compactUrl,
|
|
@@ -2,6 +2,7 @@ import type { OcxProviderConfig, OcxConfig } from "../../types";
|
|
|
2
2
|
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
3
3
|
import type { CodexAuthContext } from "../../codex/auth-context";
|
|
4
4
|
import type { RouteResult } from "../../router";
|
|
5
|
+
import { routeConcreteModel } from "../../router";
|
|
5
6
|
import type { HandleResponsesOptions } from "./core-options";
|
|
6
7
|
import {
|
|
7
8
|
hasForwardableCodexBearer,
|
|
@@ -24,6 +25,7 @@ import {
|
|
|
24
25
|
CodexAuthContextError,
|
|
25
26
|
applyCodexAuthContextToProvider,
|
|
26
27
|
stripCodexRuntimeProviderFields,
|
|
28
|
+
mainAccountHardLockCallerProof,
|
|
27
29
|
} from "../../codex/auth-context";
|
|
28
30
|
import { codexAccountSelectionForTurn, tryClaimNativeMainProfileForTurn } from "../lifecycle";
|
|
29
31
|
import { isNativeMainTrafficBlocked } from "../../codex/native-profile-startup";
|
|
@@ -44,6 +46,7 @@ import { forceRefreshMainAccountToken } from "../../codex/main-account";
|
|
|
44
46
|
import { rebindPoolCreditPolicy } from "../../codex/pool-credit-policy";
|
|
45
47
|
import { safeCallerSessionId } from "../caller-session-identity";
|
|
46
48
|
import { copyResponsesClientMetadata } from "../../adapters/openai-responses/transport";
|
|
49
|
+
import { AgentAssertionUnsupportedError, agentAssertionUnsupportedResponse, authorizationCredential, carryAgentAssertionForCallerBearer, hasAgentAssertionAuthorization } from "../../lib/authorization-scheme";
|
|
47
50
|
|
|
48
51
|
/** Normalize caller aliases only on native egress; preserve ingress and fallback identity. */
|
|
49
52
|
export function withClaudeNativeSession(headers: Headers, provider: OcxProviderConfig, sessionId?: string): Headers {
|
|
@@ -203,6 +206,9 @@ export async function resolveResponsesCodexAuth(
|
|
|
203
206
|
retainAccountForUploadedFiles = false,
|
|
204
207
|
): Promise<ResponsesAuthResolution> {
|
|
205
208
|
let authCtx: CodexAuthContext | undefined;
|
|
209
|
+
// Ingress already refuses assertion_only; a later internal hop must not reach a pool or
|
|
210
|
+
// routed credential with it either, because that would be a silent credential swap.
|
|
211
|
+
if (hasAgentAssertionAuthorization(req.headers)) return { ok: false, response: agentAssertionUnsupportedResponse() };
|
|
206
212
|
try {
|
|
207
213
|
let authInputHeaders = codexRouteCredentialDomainHeaders(
|
|
208
214
|
req,
|
|
@@ -330,7 +336,7 @@ export async function resolveResponsesCodexAuth(
|
|
|
330
336
|
} else {
|
|
331
337
|
// Even adapters that ignore caller auth must not retain a proxy secret for
|
|
332
338
|
// a later internal hop or a future transport change.
|
|
333
|
-
const bearer = headers
|
|
339
|
+
const bearer = authorizationCredential(headers);
|
|
334
340
|
if (bearer && isProxyAdmissionSecret(bearer, config)) {
|
|
335
341
|
headers.delete("authorization");
|
|
336
342
|
headers.delete("chatgpt-account-id");
|
|
@@ -341,6 +347,11 @@ export async function resolveResponsesCodexAuth(
|
|
|
341
347
|
if (options.compactionRecoveryKind === "compaction-v1") {
|
|
342
348
|
copyResponsesClientMetadata(headers, authInputHeaders, route.provider);
|
|
343
349
|
}
|
|
350
|
+
// dual_header: the assertion is first-party only and travels only beside the caller's own
|
|
351
|
+
// unchanged Bearer, compared with the ORIGINAL request (injected credentials do not count).
|
|
352
|
+
if (isCanonicalOpenAiForwardProvider(route.provider) && authCtx?.kind === "main" && !substituteMainCredential) {
|
|
353
|
+
carryAgentAssertionForCallerBearer(headers, req.headers);
|
|
354
|
+
}
|
|
344
355
|
return {
|
|
345
356
|
ok: true,
|
|
346
357
|
authCtx,
|
|
@@ -362,6 +373,7 @@ export async function resolveResponsesCodexAuth(
|
|
|
362
373
|
if (err instanceof ForwardAdmissionCredentialError) {
|
|
363
374
|
return { ok: false, response: formatErrorResponse(401, "authentication_error", err.message) };
|
|
364
375
|
}
|
|
376
|
+
if (err instanceof AgentAssertionUnsupportedError) return { ok: false, response: agentAssertionUnsupportedResponse() };
|
|
365
377
|
const response = mapCodexAuthContextErrorToResponse(err, {
|
|
366
378
|
accountSelector: route.codexAccountNamespace,
|
|
367
379
|
now: Date.now(),
|
|
@@ -590,3 +602,21 @@ export async function refreshNativeMainForwardAuth(args: {
|
|
|
590
602
|
}) ?? nativeMainRefreshFailureResponse(error) };
|
|
591
603
|
}
|
|
592
604
|
}
|
|
605
|
+
|
|
606
|
+
/** Inspect the original credential domain, before fallback changes the route or drops headers. */
|
|
607
|
+
export function mainHardLockFallbackCallerCheck(
|
|
608
|
+
req: Request, config: OcxConfig, options: HandleResponsesOptions,
|
|
609
|
+
): (sourceModel: string) => Promise<(() => boolean) | null> {
|
|
610
|
+
return async sourceModel => {
|
|
611
|
+
let sourceRoute: ReturnType<typeof routeConcreteModel>;
|
|
612
|
+
try { sourceRoute = routeConcreteModel(config, sourceModel); }
|
|
613
|
+
// This preview cannot replace the normal routing phase's existing error response.
|
|
614
|
+
catch { return null; }
|
|
615
|
+
const headers = codexRouteCredentialDomainHeaders(req, sourceRoute, options, false, config);
|
|
616
|
+
const { substituteMainCredential } = codexRouteCredentialOwnership(headers, config, sourceRoute, options);
|
|
617
|
+
return mainAccountHardLockCallerProof(headers, options.codexAuthPolicy ?? config, {
|
|
618
|
+
substituteMainCredentialForDirect: substituteMainCredential,
|
|
619
|
+
signal: options.abortSignal ?? req.signal,
|
|
620
|
+
});
|
|
621
|
+
};
|
|
622
|
+
}
|
|
@@ -11,6 +11,7 @@ import { claimMainQuotaDispatchForWs, isMainQuotaDispatchLive, renewMainQuotaDis
|
|
|
11
11
|
import type { CodexWsQuotaObserver } from "./codex-ws-metadata";
|
|
12
12
|
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
13
13
|
import { isCodexAccountGenerationLive } from "../../codex/account-store";
|
|
14
|
+
import { codexFailureWindowOutcomeMeta } from "../../codex/routing/failure-window";
|
|
14
15
|
import { applyAccountQuotaFromUpstreamHeaders as applyCapturedCodexQuota } from "../../codex/quota";
|
|
15
16
|
import type { RouteResult } from "../../router";
|
|
16
17
|
import {
|
|
@@ -93,20 +94,19 @@ export function sidecarOutcomeRecorder(
|
|
|
93
94
|
config: OcxConfig,
|
|
94
95
|
authCtx: CodexAuthContext,
|
|
95
96
|
): ((outcome: CodexUpstreamOutcome) => void) | undefined {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
: undefined;
|
|
97
|
+
if (authCtx.kind !== "pool" && authCtx.kind !== "main-pool") return undefined;
|
|
98
|
+
const windowMeta = codexFailureWindowOutcomeMeta(authCtx);
|
|
99
|
+
return outcome => recordCodexUpstreamOutcome(config, authCtx.accountId, outcome, {
|
|
100
|
+
...windowMeta,
|
|
101
|
+
threadId: authCtx.affinityKey,
|
|
102
|
+
fixedAccount: authCtx.fixedAccount,
|
|
103
|
+
probeLeaseId: authCtx.probeLeaseId,
|
|
104
|
+
probeQuotaScope: authCtx.probeQuotaScope,
|
|
105
|
+
writerGeneration: authCtx.writerGeneration,
|
|
106
|
+
// Quarantine retains its stored-credential fence. main-pool has no stored-record
|
|
107
|
+
// generation; its separate dispatch proof above fences only failure-window samples.
|
|
108
|
+
...(authCtx.kind === "pool" ? { credentialGeneration: authCtx.generation } : {}),
|
|
109
|
+
});
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
|
|
@@ -148,6 +148,7 @@ export function recordCodexPreOutputRefusalOutcome(
|
|
|
148
148
|
if (!usesCodexForwardPoolAuth(authCtx, provider) || wasPermanentAccountRefusal(response)) return;
|
|
149
149
|
noteCodexWsPreOutputRefusalOutcome(response);
|
|
150
150
|
recordCodexUpstreamOutcome(config, authCtx.accountId, response.status, {
|
|
151
|
+
...codexFailureWindowOutcomeMeta(authCtx),
|
|
151
152
|
threadId: authCtx.affinityKey,
|
|
152
153
|
fixedAccount: authCtx.fixedAccount,
|
|
153
154
|
modelId,
|
|
@@ -676,6 +677,7 @@ export async function retryCodexPoolOnAlternateAccount(
|
|
|
676
677
|
if (codexWsPreOutputRefusalOutcomeRecorded(firstResponse)) return;
|
|
677
678
|
noteCodexWsPreOutputRefusalOutcome(firstResponse);
|
|
678
679
|
recordCodexUpstreamOutcome(config, firstAuthCtx.accountId, outcomeStatus, {
|
|
680
|
+
...codexFailureWindowOutcomeMeta(firstAuthCtx),
|
|
679
681
|
threadId: firstAuthCtx.affinityKey,
|
|
680
682
|
fixedAccount: firstAuthCtx.fixedAccount,
|
|
681
683
|
modelId: route.modelId,
|
|
@@ -693,6 +695,7 @@ export async function retryCodexPoolOnAlternateAccount(
|
|
|
693
695
|
if (outcomeStatus === firstResponse.status || (outcomeStatus !== 429 && outcomeStatus !== 402)) return;
|
|
694
696
|
recordCodexUpstreamOutcome(config, firstAuthCtx.accountId, outcomeStatus, {
|
|
695
697
|
...codexQuotaOutcomeMeta(firstResponse),
|
|
698
|
+
...codexFailureWindowOutcomeMeta(firstAuthCtx),
|
|
696
699
|
threadId: firstAuthCtx.affinityKey,
|
|
697
700
|
modelId: route.modelId,
|
|
698
701
|
probeLeaseId: codexProbeLeaseId(firstAuthCtx),
|
|
@@ -862,6 +865,7 @@ export async function retryCodexPoolOnAlternateAccount(
|
|
|
862
865
|
}
|
|
863
866
|
recordCodexUpstreamOutcome(config, firstAuthCtx.accountId, outcomeStatus, {
|
|
864
867
|
...quotaMeta,
|
|
868
|
+
...codexFailureWindowOutcomeMeta(firstAuthCtx),
|
|
865
869
|
threadId: firstAuthCtx.affinityKey,
|
|
866
870
|
modelId: route.modelId,
|
|
867
871
|
probeLeaseId: codexProbeLeaseId(firstAuthCtx),
|
|
@@ -1149,6 +1153,7 @@ export function codexForwardTerminalOutcomeRecorder(
|
|
|
1149
1153
|
logCtx?: RequestLogContext,
|
|
1150
1154
|
): ((status: ResponsesTerminalStatus, httpStatusOverride?: number) => void) | undefined {
|
|
1151
1155
|
if (!usesCodexForwardPoolAuth(authCtx, provider)) return undefined;
|
|
1156
|
+
const windowMeta = codexFailureWindowOutcomeMeta(authCtx);
|
|
1152
1157
|
return (status, httpStatusOverride) => {
|
|
1153
1158
|
const quotaStatus = [httpStatusOverride, logCtx?.terminalHttpStatus]
|
|
1154
1159
|
.find(value => value === 429 || value === 402);
|
|
@@ -1157,6 +1162,7 @@ export function codexForwardTerminalOutcomeRecorder(
|
|
|
1157
1162
|
// request. Don't penalize account health; record success to clear any
|
|
1158
1163
|
// prior soft-avoid so a healthy account isn't stuck avoided.
|
|
1159
1164
|
recordCodexUpstreamOutcome(config, authCtx.accountId, 200, {
|
|
1165
|
+
...windowMeta,
|
|
1160
1166
|
threadId: authCtx.affinityKey,
|
|
1161
1167
|
fixedAccount: authCtx.fixedAccount,
|
|
1162
1168
|
modelId,
|
|
@@ -1180,6 +1186,7 @@ export function codexForwardTerminalOutcomeRecorder(
|
|
|
1180
1186
|
? 200
|
|
1181
1187
|
: (quotaStatus ?? httpStatusOverride ?? logCtx?.terminalHttpStatus ?? 502);
|
|
1182
1188
|
recordCodexUpstreamOutcome(config, authCtx.accountId, outcome, {
|
|
1189
|
+
...windowMeta,
|
|
1183
1190
|
threadId: authCtx.affinityKey,
|
|
1184
1191
|
fixedAccount: authCtx.fixedAccount,
|
|
1185
1192
|
modelId,
|