@bitkyc08/opencodex 2.87.0 → 2.89.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/README.md +11 -1
- package/bin/ocx.mjs +15 -4
- package/gui/dist/assets/App-BnRSujOq.js +58 -0
- package/gui/dist/assets/App-dnkJ85te.css +1 -0
- package/gui/dist/assets/{Tray-DCd-A1az.js → Tray-7z1COnmz.js} +1 -1
- package/gui/dist/assets/index-uchD-ngf.js +94 -0
- package/gui/dist/assets/{quota-summary-ChvmKGOk.js → quota-summary-BY_10IEz.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic/cache-retention.ts +117 -0
- package/src/adapters/anthropic/passthrough.ts +7 -4
- package/src/adapters/anthropic-rejection-memo.ts +71 -0
- package/src/adapters/anthropic-replay.ts +135 -0
- package/src/adapters/anthropic-tool-catalog.ts +11 -0
- package/src/adapters/anthropic.ts +86 -48
- package/src/adapters/coding-agent/protocol.ts +6 -0
- package/src/adapters/google-http.ts +3 -2
- package/src/adapters/google-progress-history.ts +53 -0
- package/src/adapters/google-vertex-endpoint.ts +67 -0
- package/src/adapters/google.ts +52 -30
- package/src/adapters/openai-responses/compaction-search-history.ts +27 -0
- package/src/adapters/openai-responses/passthrough.ts +17 -12
- package/src/adapters/openai-responses/reasoning.ts +68 -1
- package/src/adapters/registry.ts +1 -1
- package/src/adapters/upstream-http-error.ts +3 -2
- package/src/adapters/zed.ts +7 -4
- package/src/bridge/internal.ts +17 -3
- package/src/bridge/response-json.ts +4 -4
- package/src/bridge/sse.ts +17 -10
- package/src/claude/inbound-content-options.ts +66 -0
- package/src/claude/inbound.ts +12 -26
- package/src/claude/outbound.ts +18 -5
- package/src/cli/account-api.ts +13 -1
- package/src/cli/account-extended.ts +17 -5
- package/src/cli/account-key-quota.ts +4 -1
- package/src/cli/account.ts +1 -1
- package/src/cli/capabilities-agents-routing.ts +45 -8
- package/src/cli/capabilities-base.ts +5 -5
- package/src/cli/capabilities-provider-models.ts +5 -4
- package/src/cli/combo-result.ts +8 -2
- package/src/cli/combo.ts +17 -0
- package/src/cli/cross-home-owner.ts +16 -2
- package/src/cli/help.ts +1 -1
- package/src/cli/loopback-family.ts +31 -0
- package/src/cli/message-command.ts +4 -1
- package/src/cli/message-remote-args.ts +69 -0
- package/src/cli/message-remote-command.ts +77 -0
- package/src/cli/provider-runtime.ts +69 -1
- package/src/cli/registry.ts +3 -3
- package/src/codex/account-pause.ts +6 -1
- package/src/codex/account-runtime-state.ts +5 -0
- package/src/codex/account-store.ts +30 -3
- package/src/codex/auth-api/account-list.ts +3 -0
- package/src/codex/auth-api/main-account-probe.ts +3 -2
- package/src/codex/auth-context.ts +9 -4
- package/src/codex/catalog/aggregation.ts +15 -0
- package/src/codex/catalog/auto-review.ts +2 -1
- package/src/codex/catalog/build-entries.ts +17 -7
- package/src/codex/catalog/derive-entry.ts +3 -1
- package/src/codex/catalog/metadata.ts +2 -0
- package/src/codex/catalog/model-visibility.ts +0 -2
- package/src/codex/catalog/ownership.ts +71 -0
- package/src/codex/catalog/parsing.ts +12 -3
- package/src/codex/catalog/retained-sync.ts +2 -1
- package/src/codex/catalog/routed-gather.ts +3 -1
- package/src/codex/catalog/routed-removal.ts +4 -4
- package/src/codex/convergence.ts +2 -1
- package/src/codex/low-quota-desktop-notifier.ts +125 -0
- package/src/codex/low-quota-protection.ts +57 -31
- package/src/codex/main-account-cache.ts +77 -3
- package/src/codex/main-account.ts +6 -2
- package/src/codex/native-residue.ts +2 -3
- package/src/codex/quota.ts +26 -2
- package/src/codex/websocket-activity.ts +161 -0
- package/src/codex/websocket-registry.ts +25 -1
- package/src/combos/failover.ts +2 -1
- package/src/combos/jev-model-backend.ts +30 -7
- package/src/combos/jev-model-request.ts +20 -0
- package/src/combos/jev-quota-activation.ts +153 -0
- package/src/combos/jev-quota-config.ts +62 -0
- package/src/combos/jev-quota-route.ts +26 -0
- package/src/combos/jev-quota.ts +88 -0
- package/src/combos/jev.ts +25 -12
- package/src/combos/types.ts +27 -0
- package/src/compatibility/index.ts +3 -0
- package/src/compatibility/openai-responses.ts +8 -1
- package/src/compatibility/opengateway-responses.ts +61 -0
- package/src/config/account-auto-pause-revisions.ts +136 -0
- package/src/config/diagnostics.ts +13 -0
- package/src/config/live-reconcile.ts +9 -0
- package/src/config/opencode-zen-upgrade.ts +9 -8
- package/src/config/persist-unlocked.ts +10 -1
- package/src/config/removed-opencode-free.ts +135 -0
- package/src/config/schema/account-auto-pause.ts +14 -0
- package/src/config/schema/config-schema.ts +15 -0
- package/src/config/schema/leaf-validators.ts +7 -0
- package/src/config.ts +10 -3
- package/src/generated/compatibility-version.json +411 -175
- package/src/images/loop.ts +101 -27
- package/src/lib/account-auto-pause-response.ts +16 -0
- package/src/lib/bun-runtime.ts +18 -1
- package/src/lib/bun-transpiler-cache-scope.d.mts +8 -0
- package/src/lib/bun-transpiler-cache-scope.mjs +68 -0
- package/src/lib/decision-quota-sink.ts +76 -0
- package/src/lib/request-failure-model.ts +2 -0
- package/src/lib/state-store-registrations.ts +25 -0
- package/src/link/ssh-argv.ts +3 -0
- package/src/messaging/envelope.ts +6 -3
- package/src/messaging/remote-auth.ts +83 -0
- package/src/messaging/remote-bridge.ts +270 -0
- package/src/messaging/remote-contract.ts +108 -0
- package/src/messaging/remote-enrollment-recovery.ts +59 -0
- package/src/messaging/remote-enrollment.ts +244 -0
- package/src/messaging/remote-envelope.ts +40 -0
- package/src/messaging/remote-files.ts +117 -0
- package/src/messaging/remote-owner.ts +169 -0
- package/src/messaging/remote-ports.ts +56 -0
- package/src/messaging/remote-process.ts +131 -0
- package/src/messaging/remote-replies.ts +98 -0
- package/src/messaging/remote-rpc-admission.ts +32 -0
- package/src/messaging/remote-send.ts +72 -0
- package/src/messaging/remote-store.ts +111 -0
- package/src/messaging/remote-tunnels.ts +65 -0
- package/src/messaging/rpc.ts +13 -1
- package/src/oauth/account-auto-pause-policy.ts +66 -0
- package/src/oauth/account-auto-pause.ts +171 -0
- package/src/oauth/account-quota-rank.ts +2 -1
- package/src/oauth/anthropic-account-refusal.ts +6 -0
- package/src/oauth/anthropic-model-family.ts +8 -0
- package/src/oauth/anthropic-model-quota.ts +2 -6
- package/src/oauth/api-key-pool-preserve.ts +37 -0
- package/src/oauth/generic-account-failover.ts +7 -4
- package/src/oauth/index.ts +5 -35
- package/src/oauth/store.ts +35 -8
- package/src/oauth/types.ts +2 -0
- package/src/plugins/upstream-hooks.ts +14 -1
- package/src/protocols/encoders/adapter-events.ts +5 -5
- package/src/protocols/encoders/messages.ts +2 -2
- package/src/providers/api-key-pause.ts +32 -0
- package/src/providers/api-key-resolve.ts +10 -4
- package/src/providers/api-key-selection.ts +6 -1
- package/src/providers/api-keys.ts +8 -4
- package/src/providers/claude-cli-usage.ts +5 -0
- package/src/providers/key-failover.ts +11 -11
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/opencode-zen-rate-limit.ts +12 -73
- package/src/providers/quota/account-cache.ts +15 -1
- package/src/providers/quota/anthropic-account-quota.ts +2 -1
- package/src/providers/quota/anthropic-cooldown-recovery.ts +22 -4
- package/src/providers/quota/anthropic-decision-windows.ts +47 -0
- package/src/providers/quota/antigravity.ts +39 -6
- package/src/providers/quota/report-cache.ts +14 -0
- package/src/providers/quota/vendor-probes-oauth.ts +3 -3
- package/src/providers/quota-decision-publication.ts +77 -0
- package/src/providers/quota-decision-snapshot.ts +145 -0
- package/src/providers/quota-routing-cache.ts +3 -0
- package/src/providers/quota-types.ts +6 -1
- package/src/providers/quota.ts +3 -2
- package/src/providers/registry/entries-extended.ts +8 -52
- package/src/providers/registry/model-seeds.ts +1 -1
- package/src/providers/registry.ts +1 -2
- package/src/providers/stale-vision-classification-migration.ts +1 -1
- package/src/responses/parser.ts +18 -10
- package/src/responses/reasoning-replay-cache.ts +50 -4
- package/src/responses/web-search-replay.ts +138 -0
- package/src/routing/analytics.ts +7 -15
- package/src/routing/compatibility/behavior.ts +4 -1
- package/src/server/auth-cors.ts +7 -0
- package/src/server/background-lifecycle.ts +2 -1
- package/src/server/chat-native.ts +28 -13
- package/src/server/claude-messages.ts +4 -2
- package/src/server/index/live-sideband.ts +3 -2
- package/src/server/index/optional-listeners.ts +6 -0
- package/src/server/index/serve-options.ts +11 -5
- package/src/server/index/websocket-handler.ts +18 -4
- package/src/server/index.ts +7 -7
- package/src/server/lifecycle.ts +13 -1
- package/src/server/management/combo-routes.ts +17 -2
- package/src/server/management/oauth-account-routes.ts +10 -1
- package/src/server/management/provider-completion-test.ts +289 -0
- package/src/server/management/provider-routes.ts +9 -1
- package/src/server/management/route-registry.ts +2 -0
- package/src/server/messages-native.ts +34 -7
- package/src/server/messages-shared.ts +4 -1
- package/src/server/request-log.ts +7 -2
- package/src/server/responses/account-auto-pause-recovery.ts +26 -0
- package/src/server/responses/adapter-continuation.ts +34 -18
- package/src/server/responses/adapter-delivery.ts +53 -6
- package/src/server/responses/adapter-dispatch.ts +104 -29
- package/src/server/responses/anthropic-thinking-recovery.ts +153 -0
- package/src/server/responses/combo-session-recall.ts +25 -0
- package/src/server/responses/combo-stream-preflight.ts +6 -3
- package/src/server/responses/combo-tool-loop-pin.ts +70 -0
- package/src/server/responses/compact.ts +8 -5
- package/src/server/responses/compaction-recovery.ts +2 -0
- package/src/server/responses/context-overflow.ts +141 -2
- package/src/server/responses/core-codex-account.ts +9 -3
- package/src/server/responses/core-combo.ts +34 -7
- package/src/server/responses/core-lifetime.ts +3 -2
- package/src/server/responses/core-options.ts +2 -0
- package/src/server/responses/core-replay.ts +5 -1
- package/src/server/responses/core.ts +12 -9
- package/src/server/responses/empty-completion-guard.ts +2 -0
- package/src/server/responses/fetch-helpers.ts +35 -4
- package/src/server/responses/jev-model-invoke.ts +13 -14
- package/src/server/responses/native-steering-log.ts +2 -2
- package/src/server/responses/passthrough-delivery.ts +3 -2
- package/src/server/responses/passthrough-dispatch.ts +30 -33
- package/src/server/responses/passthrough-execution.ts +3 -2
- package/src/server/responses/passthrough-observations.ts +38 -0
- package/src/server/responses/request-compression.ts +98 -0
- package/src/server/responses/request-prepare.ts +1 -0
- package/src/server/responses/request-transport.ts +32 -3
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/response-tool-ownership.ts +21 -0
- package/src/server/responses/run-turn-execution.ts +2 -0
- package/src/server/responses/sidecar-execution.ts +59 -10
- package/src/server/responses/sidecar-send-budget.ts +2 -2
- package/src/server/responses/terminal-guard.ts +2 -0
- package/src/server/responses-undeclared-tool-guard.ts +1 -1
- package/src/server/startup-action-control.ts +2 -2
- package/src/server/startup-health-cache.ts +2 -2
- package/src/server/windows-tray-control.ts +2 -2
- package/src/server/ws-bridge.ts +2 -0
- package/src/service/definition.ts +9 -1
- package/src/types/account-auto-pause.ts +15 -0
- package/src/types/config.ts +23 -1
- package/src/types/opencode-zen.ts +9 -3
- package/src/types/provider.ts +9 -5
- package/src/types/request.ts +30 -4
- package/src/types/wire.ts +4 -2
- package/src/types.ts +2 -0
- package/src/usage/cost.ts +17 -1
- package/src/usage/expected-prices.ts +3 -1
- package/src/usage/jev-stats.ts +4 -0
- package/src/usage/log.ts +2 -0
- package/src/usage/summary.ts +16 -3
- package/src/usage/telemetry-contract.ts +1 -0
- package/src/web-search/anthropic-history.ts +59 -0
- package/src/web-search/loop.ts +212 -73
- package/src/web-search/progress-stream.ts +14 -1
- package/src/web-search/run-turn-loop.ts +9 -3
- package/src/web-search/sidecar-search-replay.ts +96 -0
- package/gui/dist/assets/App-ZfYbLHkB.js +0 -58
- package/gui/dist/assets/App-wT3mXqFz.css +0 -1
- package/gui/dist/assets/index-CfYDMj34.js +0 -94
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
} from "../../combos";
|
|
46
46
|
import { formatErrorResponse } from "../../bridge";
|
|
47
47
|
import { SEND_BUDGET_EXHAUSTED_CODE } from "../../lib/errors";
|
|
48
|
+
import { redactSecretString, sanitizeLogMetadataString } from "../../lib/redact";
|
|
48
49
|
import {
|
|
49
50
|
expandPreviousResponseInput,
|
|
50
51
|
previousResponseReplayFailure,
|
|
@@ -98,6 +99,7 @@ import type { CodexAuthContext } from "../../codex/auth-context";
|
|
|
98
99
|
import type { ResponsesTerminalStatus } from "../../bridge";
|
|
99
100
|
import { beginRequestAttempt, sealRequestAttemptIdentity, finishRequestAttempt } from "../request-log";
|
|
100
101
|
import { rememberComboForLane } from "./combo-session-recall";
|
|
102
|
+
import { COMBO_TOOL_LOOP_PIN_REASON, toolLoopPinnedPick } from "./combo-tool-loop-pin";
|
|
101
103
|
import { runTurnAdapterSseResponses } from "./core-lifetime";
|
|
102
104
|
import { normalizePersistedJevDecision } from "../../usage/jev-stats";
|
|
103
105
|
import {
|
|
@@ -144,7 +146,8 @@ function cooledPoolAccountLabel(config: OcxConfig, providerName: string, modelId
|
|
|
144
146
|
* through `accountLogLabel`, but the per-request account budget can end the pool's own rotation
|
|
145
147
|
* before the last refused account is recorded. Hand that account to the pool's rotator, which
|
|
146
148
|
* cools it under the pool's own Retry-After policy, and spare the target only when the pool still
|
|
147
|
-
* has an eligible sibling.
|
|
149
|
+
* has an eligible sibling. The rotator only peeks round-robin here: nothing is sent to that sibling.
|
|
150
|
+
* A lone account or an all-cooled pool returns null and cools the target.
|
|
148
151
|
*/
|
|
149
152
|
function cooledGenericOAuthAccountLabel(
|
|
150
153
|
config: OcxConfig, providerName: string, modelId: string, label: string | undefined, retryAfter: string | undefined,
|
|
@@ -155,7 +158,7 @@ function cooledGenericOAuthAccountLabel(
|
|
|
155
158
|
const matches = getAccountSet(providerName)?.accounts.filter(account =>
|
|
156
159
|
oauthAccountLogLabel(account.id, base) === label) ?? [];
|
|
157
160
|
return matches.length === 1
|
|
158
|
-
&& rotateGenericOAuthAccountOn429(config, providerName, matches[0]!.id, retryAfter, Date.now(), modelId)
|
|
161
|
+
&& rotateGenericOAuthAccountOn429(config, providerName, matches[0]!.id, retryAfter, Date.now(), modelId, false)
|
|
159
162
|
? label : undefined;
|
|
160
163
|
}
|
|
161
164
|
|
|
@@ -588,7 +591,19 @@ export async function executeComboResponses(
|
|
|
588
591
|
waitForCooldownMs: combo.waitForCooldownMs,
|
|
589
592
|
abortSignal: options.abortSignal,
|
|
590
593
|
});
|
|
591
|
-
|
|
594
|
+
// An in-flight tool loop stays on the target that issued its calls while that target is still
|
|
595
|
+
// selectable (#7298). Override routes never record a lane target, so they never pin either.
|
|
596
|
+
const toolLoopPin = options.compactionRoutingOverride || options.mainHardLockFallbackOverride
|
|
597
|
+
? undefined
|
|
598
|
+
: toolLoopPinnedPick({
|
|
599
|
+
config,
|
|
600
|
+
comboId,
|
|
601
|
+
lane: sessionLaneIdFromRequest(req.headers),
|
|
602
|
+
body,
|
|
603
|
+
eligible: target => (meteredAllowed || !target.metered) && targetEligible(target),
|
|
604
|
+
now: initialNow,
|
|
605
|
+
});
|
|
606
|
+
let pick = toolLoopPin ?? await pickWithWait({
|
|
592
607
|
eligible: targetEligible,
|
|
593
608
|
now: initialNow,
|
|
594
609
|
});
|
|
@@ -624,7 +639,7 @@ export async function executeComboResponses(
|
|
|
624
639
|
: comboUnavailable(comboId, undefined, comboExclusionReasons(config, comboId, targetEligible));
|
|
625
640
|
}
|
|
626
641
|
let jevDecision: JevDecision | undefined;
|
|
627
|
-
if (combo.strategy === "jev") {
|
|
642
|
+
if (combo.strategy === "jev" && !toolLoopPin) {
|
|
628
643
|
const choices = eligibleJevComboChoices(config, comboId, targetEligible, Date.now());
|
|
629
644
|
const first = choices[0];
|
|
630
645
|
if (!first) {
|
|
@@ -652,6 +667,7 @@ export async function executeComboResponses(
|
|
|
652
667
|
isDestinationAllowed: (providerName, modelId) => routeAllowedByScope(
|
|
653
668
|
resolveAdmissionModelScope(config, options.admission), { providerName, modelId },
|
|
654
669
|
),
|
|
670
|
+
...(combo.decisionQuotaSignals === true ? { decisionQuotaSignals: true, decisionQuotaTiers: combo.decisionQuotaTiers } : {}),
|
|
655
671
|
...(combo.decisionProvider ? { decisionProvider: combo.decisionProvider } : {}),
|
|
656
672
|
...(combo.decisionModel
|
|
657
673
|
? {
|
|
@@ -695,6 +711,7 @@ export async function executeComboResponses(
|
|
|
695
711
|
? { chosenProbability: decision.chosenProbability }
|
|
696
712
|
: {}),
|
|
697
713
|
...(decision.usage ? { usage: decision.usage } : {}),
|
|
714
|
+
...(decision.quota ? { quota: decision.quota } : {}),
|
|
698
715
|
});
|
|
699
716
|
console.debug("[combo] JEV decision", {
|
|
700
717
|
backend: decision.backend,
|
|
@@ -712,8 +729,11 @@ export async function executeComboResponses(
|
|
|
712
729
|
// One immutable combo selection trace, before any child dispatch; child
|
|
713
730
|
// adoption below must never replace it with a concrete child route trace.
|
|
714
731
|
const decision = comboRouteDecisionTrace(config, comboId, pick, requestedModel);
|
|
715
|
-
|
|
716
|
-
?
|
|
732
|
+
const selectedReason = options.memoryModelPhase
|
|
733
|
+
? memoryModelRouteReason(options.memoryModelPhase)
|
|
734
|
+
: toolLoopPin ? COMBO_TOOL_LOOP_PIN_REASON : undefined;
|
|
735
|
+
logCtx.routeDecision = selectedReason
|
|
736
|
+
? { ...decision, selected: { ...decision.selected, reason: selectedReason } }
|
|
717
737
|
: decision;
|
|
718
738
|
|
|
719
739
|
const originalReasoning = body && typeof body === "object" && !Array.isArray(body)
|
|
@@ -1192,8 +1212,15 @@ export async function executeComboResponses(
|
|
|
1192
1212
|
}
|
|
1193
1213
|
return lastFailure;
|
|
1194
1214
|
}
|
|
1215
|
+
// A bare status made a revoked credential read as a provider outage. Log only the parsed
|
|
1216
|
+
// code and message, never the raw envelope, as one redacted, bounded line.
|
|
1217
|
+
const failureReason = sanitizeLogMetadataString(
|
|
1218
|
+
redactSecretString([failure.upstreamCode, failure.upstreamMessage].filter(Boolean).join(": ")).replace(/\s+/g, " "),
|
|
1219
|
+
200,
|
|
1220
|
+
);
|
|
1195
1221
|
console.warn(
|
|
1196
|
-
`[combo] ${comboId}: ${targetKey(pick.target)} failed with ${failure.response.status}
|
|
1222
|
+
`[combo] ${comboId}: ${targetKey(pick.target)} failed with ${failure.response.status}`
|
|
1223
|
+
+ ` after ${Date.now() - started}ms${failureReason ? `: ${failureReason}` : ""}`,
|
|
1197
1224
|
);
|
|
1198
1225
|
const failureNow = Date.now();
|
|
1199
1226
|
const attemptedTargets = pick.attempted;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { TranslatorBudget } from "../../lib/translator-budget";
|
|
2
|
+
import { carryResponseToolOwnership } from "./response-tool-ownership";
|
|
2
3
|
import { finalizeTranslatorBudgetResponse } from "../../lib/translator-budget";
|
|
3
4
|
import { isNonReplayableResponse, markResponseNonReplayable } from "../../lib/upstream-retry";
|
|
4
5
|
import {
|
|
@@ -50,7 +51,7 @@ export function finalizeOwnedTranslatorBudget(response: Response, budget: Transl
|
|
|
50
51
|
if (isPreinspectedJsonResponse(response)) markPreinspectedJsonResponse(finalizedResponse);
|
|
51
52
|
if (isNonReplayableResponse(response)) markResponseNonReplayable(finalizedResponse);
|
|
52
53
|
if (runTurnAdapterSseResponses.has(response)) runTurnAdapterSseResponses.add(finalizedResponse);
|
|
53
|
-
return finalizedResponse;
|
|
54
|
+
return carryResponseToolOwnership(response, finalizedResponse);
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
/** Release a serving-account slot when the client body ends, errors or is cancelled. */
|
|
@@ -75,7 +76,7 @@ export function finalizeAccountLease(response: Response, release: () => void, on
|
|
|
75
76
|
if (isEagerRelaySseResponse(response)) markEagerRelaySseResponse(wrapped);
|
|
76
77
|
if (isPreinspectedJsonResponse(response)) markPreinspectedJsonResponse(wrapped);
|
|
77
78
|
if (runTurnAdapterSseResponses.has(response)) runTurnAdapterSseResponses.add(wrapped);
|
|
78
|
-
return wrapped;
|
|
79
|
+
return carryResponseToolOwnership(response, wrapped);
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
|
|
@@ -177,6 +177,8 @@ export interface HandleResponsesOptions {
|
|
|
177
177
|
nativeReasoningMint?: { owner?: NativeReasoningOwner };
|
|
178
178
|
/** The Claude client's typed `web_search_*` / `web_fetch_*` declarations; kept only for `inboundWire: "anthropic"`. */
|
|
179
179
|
anthropicServerTools?: readonly Record<string, unknown>[];
|
|
180
|
+
/** The Claude client's replayable server-tool exchanges (`replayedAnthropicServerBlocks`); same scope. */
|
|
181
|
+
anthropicServerReplay?: ReadonlyMap<string, readonly Record<string, unknown>[]>;
|
|
180
182
|
/** Droid's per-request effort default; each concrete combo or policy target applies it only if its ladder allows it. */
|
|
181
183
|
droidDefaultEffort?: string;
|
|
182
184
|
/** PF-07: the Chat source a combo child may send natively; set only by the Chat ingress. */
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { bindAnthropicRejectionPolicy } from "../../adapters/anthropic-rejection-memo";
|
|
2
|
+
import { getReasoningReplayOpaqueBlobRejection } from "../../responses/reasoning-replay-cache";
|
|
1
3
|
import type {
|
|
2
4
|
OcxProviderContinuationOwner,
|
|
3
5
|
OcxProviderContinuationState,
|
|
@@ -177,8 +179,10 @@ export function bindRouteReasoningReplayScope(args: {
|
|
|
177
179
|
parsed._dropForeignReasoningItemIds = true;
|
|
178
180
|
}
|
|
179
181
|
}
|
|
182
|
+
const rejectionMemo = getReasoningReplayOpaqueBlobRejection(parsed._reasoningReplayScope);
|
|
183
|
+
bindAnthropicRejectionPolicy(parsed, adapterName === "anthropic" ? rejectionMemo : undefined);
|
|
180
184
|
if (reasoningReplayOpaqueBlobRejectionMemoized(parsed._reasoningReplayScope)) {
|
|
181
|
-
parsed._stripReasoningEncryptedContent = true;
|
|
185
|
+
if (adapterName !== "anthropic" || rejectionMemo?.mode !== "selective") parsed._stripReasoningEncryptedContent = true;
|
|
182
186
|
parsed._stripNativeCompactionEncryptedContent = true;
|
|
183
187
|
parsed._dropForeignReasoningItemIds = true;
|
|
184
188
|
}
|
|
@@ -12,6 +12,7 @@ import { createInferenceSendBudget } from "../inference/context";
|
|
|
12
12
|
import { finalizeOwnedTranslatorBudget, finalizeAccountLease } from "./core-lifetime";
|
|
13
13
|
import type { TranslatorBudget } from "../../lib/translator-budget";
|
|
14
14
|
import { executeComboResponses } from "./core-combo";
|
|
15
|
+
import { registerTurn, unregisterTurn } from "../lifecycle";
|
|
15
16
|
import { runWithCompactionRecovery } from "./compaction-recovery";
|
|
16
17
|
/**
|
|
17
18
|
* Route one `/v1/responses` request through the adapter pipeline: recovery loop, passthrough
|
|
@@ -67,15 +68,17 @@ export async function handleComboResponses(
|
|
|
67
68
|
logCtx: RequestLogContext,
|
|
68
69
|
options: HandleResponsesOptions & { translatorBudget: TranslatorBudget },
|
|
69
70
|
): Promise<Response> {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
requestDispatchers
|
|
78
|
-
|
|
71
|
+
// Each child relay binds the turn's lease and unbinds it when its stream ends; the lease is
|
|
72
|
+
// released once nothing is bound. Hold it for the whole ladder so a refused child's relay
|
|
73
|
+
// cannot settle the turn the next target still runs in ("turn already settled"). An accepted
|
|
74
|
+
// child's stream is bound before this returns and keeps the lease until it ends.
|
|
75
|
+
const ladder = new AbortController();
|
|
76
|
+
registerTurn(ladder, options.turnAdmissionLease);
|
|
77
|
+
try {
|
|
78
|
+
return await executeComboResponses(req, rawBody, comboId, config, logCtx, options, requestDispatchers);
|
|
79
|
+
} finally {
|
|
80
|
+
if (options.turnAdmissionLease && "bindAbortController" in options.turnAdmissionLease) unregisterTurn(ladder);
|
|
81
|
+
}
|
|
79
82
|
}
|
|
80
83
|
const requestDispatchers: ResponsesDispatchers = { handleResponses, handleComboResponses };
|
|
81
84
|
export { adapterNeedsForcedContinuation } from "./core-replay";
|
|
@@ -172,6 +172,7 @@ export function mergeUsage(
|
|
|
172
172
|
const cachedInputTokens = sumOptional("cachedInputTokens");
|
|
173
173
|
const cacheReadInputTokens = sumOptional("cacheReadInputTokens");
|
|
174
174
|
const cacheCreationInputTokens = sumOptional("cacheCreationInputTokens");
|
|
175
|
+
const cacheCreation1hInputTokens = sumOptional("cacheCreation1hInputTokens");
|
|
175
176
|
const reasoningOutputTokens = sumOptional("reasoningOutputTokens");
|
|
176
177
|
const providerCredits = sumOptional("providerCredits");
|
|
177
178
|
const contextTotalTokens = second.contextTotalTokens ?? first.contextTotalTokens;
|
|
@@ -188,6 +189,7 @@ export function mergeUsage(
|
|
|
188
189
|
...(cachedInputTokens !== undefined ? { cachedInputTokens } : {}),
|
|
189
190
|
...(cacheReadInputTokens !== undefined ? { cacheReadInputTokens } : {}),
|
|
190
191
|
...(cacheCreationInputTokens !== undefined ? { cacheCreationInputTokens } : {}),
|
|
192
|
+
...(cacheCreation1hInputTokens !== undefined ? { cacheCreation1hInputTokens } : {}),
|
|
191
193
|
...(reasoningOutputTokens !== undefined ? { reasoningOutputTokens } : {}),
|
|
192
194
|
...(providerCredits !== undefined ? { providerCredits } : {}),
|
|
193
195
|
...(first.estimated || second.estimated ? { estimated: true } : {}),
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
} from "../../lib/provider-egress";
|
|
32
32
|
import { redactSecretString } from "../../lib/redact";
|
|
33
33
|
import { outboundCredentials } from "../../lib/outbound-credential-registry";
|
|
34
|
+
import { compressUpstreamRequestBody, isCanonicalCodexResponsesUrl } from "./request-compression";
|
|
34
35
|
|
|
35
36
|
export { withUpstreamHttpVersion };
|
|
36
37
|
|
|
@@ -48,6 +49,17 @@ const EGRESS_DECIDED = Symbol.for("opencodex.provider-egress.decided");
|
|
|
48
49
|
const UPSTREAM_REWRITTEN = Symbol.for("opencodex.plugins.upstream-rewritten");
|
|
49
50
|
// Survives queued credential rebuilds and executor replacement without importing budget owners.
|
|
50
51
|
const PHYSICAL_DISPATCH = Symbol.for("opencodex.physical-dispatch");
|
|
52
|
+
// Final destination and headers after plugin rewrites and queued rebuilds, for observers that
|
|
53
|
+
// must attribute a response to the credential that actually reached the wire.
|
|
54
|
+
const PHYSICAL_SEND_OBSERVER = Symbol.for("opencodex.physical-send-observer");
|
|
55
|
+
export type AccountAutoPauseObserver = ((response: Response) => Promise<Response>) & {
|
|
56
|
+
physical?: (sameDestination: boolean, headers: Headers) => void;
|
|
57
|
+
};
|
|
58
|
+
/** Init fragment that reports the final send of the observer's attempt; empty when nothing observes. */
|
|
59
|
+
export function physicalSendObserverInit(observer: AccountAutoPauseObserver | undefined, target: string): Record<symbol, unknown> {
|
|
60
|
+
const physical = observer?.physical;
|
|
61
|
+
return physical ? { [PHYSICAL_SEND_OBSERVER]: (destination: string, headers: Headers) => physical(destination === target, headers) } : {};
|
|
62
|
+
}
|
|
51
63
|
// One WS attempt and its HTTP fallback are one logical physical-send receipt.
|
|
52
64
|
const WS_FALLBACK_RECEIPT = Symbol.for("opencodex.ws-fallback-receipt");
|
|
53
65
|
// Replacement admission waits on the client signal, outside the response-header deadline.
|
|
@@ -206,18 +218,27 @@ export function sendWithConnectionPolicy(
|
|
|
206
218
|
// Bun can reset large native Codex string uploads before receiving headers while
|
|
207
219
|
// the identical UTF-8 buffer succeeds. Convert only at the final HTTP send so
|
|
208
220
|
// WebSocket selection still sees the serialized string and no retry is added.
|
|
221
|
+
// Opt-in compression (#7247) takes precedence at the same boundary and also yields bytes. Every
|
|
222
|
+
// retry and HTTP fallback re-enters here with the caller's original string, so each send derives
|
|
223
|
+
// its own coded bytes; `headers` is already this send's private copy.
|
|
209
224
|
const target = input instanceof Request ? input.url : String(input);
|
|
210
225
|
const body = init?.body;
|
|
211
|
-
const
|
|
212
|
-
|
|
226
|
+
const compressed = compressUpstreamRequestBody(target, body, headers);
|
|
227
|
+
if (compressed) {
|
|
228
|
+
headers.set("Content-Encoding", compressed.encoding);
|
|
229
|
+
headers.delete("Content-Length");
|
|
230
|
+
}
|
|
231
|
+
const largeCodexBody = !compressed && typeof body === "string"
|
|
232
|
+
&& isCanonicalCodexResponsesUrl(target)
|
|
213
233
|
&& Buffer.byteLength(body, "utf8") >= 1024 * 1024;
|
|
214
234
|
(init as Record<symbol, (() => void) | undefined> | undefined)?.[PHYSICAL_DISPATCH]?.();
|
|
235
|
+
(init as Record<symbol, ((destination: string, headers: Headers) => void) | undefined> | undefined)?.[PHYSICAL_SEND_OBSERVER]?.(target, headers);
|
|
215
236
|
// Final headers, after plugin rewrites: an upstream can only echo what it received here, so the
|
|
216
237
|
// request log refuses any diagnostic that repeats one of these values (see request-log-terminal-status).
|
|
217
238
|
outboundCredentials().remember(headers);
|
|
218
239
|
return physicalFetch(input, {
|
|
219
240
|
...init,
|
|
220
|
-
...(largeCodexBody ? { body: Buffer.from(body, "utf8") } : {}),
|
|
241
|
+
...(compressed ? { body: compressed.body } : largeCodexBody ? { body: Buffer.from(body, "utf8") } : {}),
|
|
221
242
|
headers,
|
|
222
243
|
redirect: "manual",
|
|
223
244
|
...(fresh ? { keepalive: false } : {}),
|
|
@@ -228,6 +249,7 @@ export function sendWithConnectionPolicy(
|
|
|
228
249
|
}
|
|
229
250
|
|
|
230
251
|
export interface ProviderFetchOptions {
|
|
252
|
+
accountAutoPause?: (name: string, provider: OcxProviderConfig, headers: Headers) => AccountAutoPauseObserver | undefined;
|
|
231
253
|
/**
|
|
232
254
|
* Keep this send on HTTP even where the WebSocket upstream would normally be selected.
|
|
233
255
|
*
|
|
@@ -344,7 +366,7 @@ export function providerFetch(
|
|
|
344
366
|
// ChatGPT Codex backend: streaming turns ride the responses_websockets
|
|
345
367
|
// transport (measured ~3s faster TTFT than the SSE POST queue); everything
|
|
346
368
|
// else keeps the provider's HTTP fetch. See ws-upstream.ts for the details.
|
|
347
|
-
const
|
|
369
|
+
const unobserved = async (input: Parameters<typeof globalThis.fetch>[0], init?: RequestInit,
|
|
348
370
|
reacquirePacing?: (provider: OcxProviderConfig, modelId?: string) => Promise<void>) => {
|
|
349
371
|
const upstreamWebsocket = provider.upstreamWebsocket;
|
|
350
372
|
if (!options.httpOnly && typeof input === "string" && init
|
|
@@ -381,6 +403,15 @@ export function providerFetch(
|
|
|
381
403
|
}
|
|
382
404
|
return httpFetch(input, init, reacquirePacing);
|
|
383
405
|
};
|
|
406
|
+
const unpaced = async (input: Parameters<typeof globalThis.fetch>[0], init?: RequestInit,
|
|
407
|
+
reacquirePacing?: (provider: OcxProviderConfig, modelId?: string) => Promise<void>) => {
|
|
408
|
+
const observe = options.accountAutoPause?.(providerName, provider,
|
|
409
|
+
new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined)));
|
|
410
|
+
const observedInit = observe?.physical
|
|
411
|
+
? { ...init, ...physicalSendObserverInit(observe, input instanceof Request ? input.url : String(input)) } : init;
|
|
412
|
+
const response = await unobserved(input, observedInit, reacquirePacing);
|
|
413
|
+
return observe ? observe(response) : response;
|
|
414
|
+
};
|
|
384
415
|
let pacingSlotAcquired = options.pacingSlotAcquired === true;
|
|
385
416
|
const waitForPacing = async (signal?: AbortSignal): Promise<ProviderRequestSlot | undefined> => {
|
|
386
417
|
if (pacingSlotAcquired) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readBoundedResponseBytes } from "../../lib/bounded-body";
|
|
2
2
|
import { JevModelInvokeError, type JevModelInvoke, type JevModelInvokeResult } from "../../combos/jev-model-backend";
|
|
3
3
|
import { JEV_MAX_REQUEST_BYTES, JEV_MAX_RESPONSE_BYTES } from "../../combos/jev";
|
|
4
|
+
import { serializeJevModelRequest } from "../../combos/jev-model-request";
|
|
4
5
|
import type { OcxConfig } from "../../types";
|
|
5
6
|
import { codexEffortRank, isCodexReasoningEffort } from "../../reasoning-effort";
|
|
6
7
|
import { routeConcreteModel } from "../../router";
|
|
@@ -92,7 +93,7 @@ function decodeSse(bytes: Uint8Array): JevModelInvokeResult {
|
|
|
92
93
|
* bounds billed output (including reasoning) and gives the spend reservation a nonzero ceiling;
|
|
93
94
|
* a truncated answer is malformed and fails open like any other bad reply.
|
|
94
95
|
*/
|
|
95
|
-
export
|
|
96
|
+
export { JEV_MODEL_MAX_OUTPUT_TOKENS } from "../../combos/jev-model-request";
|
|
96
97
|
|
|
97
98
|
/**
|
|
98
99
|
* The cheapest reasoning effort the decision model declares, so a reasoning model cannot spend
|
|
@@ -121,19 +122,17 @@ export function jevDecisionReasoningEffort(config: OcxConfig, model: string): st
|
|
|
121
122
|
* or conversation header. It therefore runs on credentials configured on provider rows only.
|
|
122
123
|
*/
|
|
123
124
|
export function createJevModelInvoker(context: JevModelInvokerContext): JevModelInvoke {
|
|
124
|
-
return async ({ model, instructions, input, signal }) => {
|
|
125
|
+
return async ({ model, instructions, input, signal, withoutQuota }) => {
|
|
125
126
|
const effort = jevDecisionReasoningEffort(context.config, model);
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
});
|
|
136
|
-
if (new TextEncoder().encode(body).byteLength > JEV_MAX_REQUEST_BYTES) {
|
|
127
|
+
let body = serializeJevModelRequest({ model, instructions, input }, effort);
|
|
128
|
+
/** True when the serialized decision request exceeds the request byte limit. */
|
|
129
|
+
const oversized = () => new TextEncoder().encode(body).byteLength > JEV_MAX_REQUEST_BYTES;
|
|
130
|
+
let quotaOmitted = false;
|
|
131
|
+
if (oversized() && withoutQuota) {
|
|
132
|
+
body = serializeJevModelRequest({ model, ...withoutQuota }, effort);
|
|
133
|
+
quotaOmitted = true;
|
|
134
|
+
}
|
|
135
|
+
if (oversized()) {
|
|
137
136
|
throw new JevModelInvokeError("malformed", "decision request too large");
|
|
138
137
|
}
|
|
139
138
|
const lease = tryAdmitTurn(undefined, signal);
|
|
@@ -178,7 +177,7 @@ export function createJevModelInvoker(context: JevModelInvokerContext): JevModel
|
|
|
178
177
|
return terminalResult(parsed, "");
|
|
179
178
|
})();
|
|
180
179
|
usage = result.usage;
|
|
181
|
-
return result;
|
|
180
|
+
return quotaOmitted ? { ...result, quotaOmitted: true } : result;
|
|
182
181
|
} finally {
|
|
183
182
|
// No final log row is written for this detached context, so settle its spend here.
|
|
184
183
|
childLog.spendTracker?.settle(childLog.usage ?? (usage
|
|
@@ -24,7 +24,7 @@ export function createNativeSteeringLogObserver(logCtx: RequestLogContext, onFir
|
|
|
24
24
|
if (usage && (usages.has(event.response.id) || usages.size < MAX_NATIVE_STEERING_RESPONSES)) {
|
|
25
25
|
// Retain numeric counters only, never arbitrary rawUsage metadata.
|
|
26
26
|
const counters: OcxUsage = { inputTokens: usage.inputTokens, outputTokens: usage.outputTokens };
|
|
27
|
-
for (const key of ["totalTokens", "cachedInputTokens", "cacheReadInputTokens", "cacheCreationInputTokens", "reasoningOutputTokens"] as const) {
|
|
27
|
+
for (const key of ["totalTokens", "cachedInputTokens", "cacheReadInputTokens", "cacheCreationInputTokens", "cacheCreation1hInputTokens", "reasoningOutputTokens"] as const) {
|
|
28
28
|
if (typeof usage[key] === "number") counters[key] = usage[key];
|
|
29
29
|
}
|
|
30
30
|
usages.set(event.response.id, counters);
|
|
@@ -33,7 +33,7 @@ export function createNativeSteeringLogObserver(logCtx: RequestLogContext, onFir
|
|
|
33
33
|
if (!usages.size) return;
|
|
34
34
|
const total: OcxUsage = { inputTokens: 0, outputTokens: 0 };
|
|
35
35
|
for (const usage of usages.values()) {
|
|
36
|
-
for (const key of ["inputTokens", "outputTokens", "totalTokens", "cachedInputTokens", "cacheReadInputTokens", "cacheCreationInputTokens", "reasoningOutputTokens"] as const) {
|
|
36
|
+
for (const key of ["inputTokens", "outputTokens", "totalTokens", "cachedInputTokens", "cacheReadInputTokens", "cacheCreationInputTokens", "cacheCreation1hInputTokens", "reasoningOutputTokens"] as const) {
|
|
37
37
|
const value = usage[key];
|
|
38
38
|
if (typeof value === "number" && Number.isFinite(value)) total[key] = (total[key] ?? 0) + value;
|
|
39
39
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { wasPermanentAccountRefusal } from "../../oauth/account-auto-pause";
|
|
1
2
|
import { isNativeControlResponse } from "./native-response-control";
|
|
2
3
|
import { createOutboundCredentialMask, createTerminalErrorRedactionBlockRewrite } from "./terminal-error-redaction";
|
|
3
4
|
import type { ResponsesRequestContext, ResponsesAdmissionState } from "./core-options";
|
|
@@ -527,7 +528,7 @@ export async function deliverPassthroughResponse(
|
|
|
527
528
|
} else if (!shouldDeferCodexResetDerivedCooldown(
|
|
528
529
|
upstreamResponse,
|
|
529
530
|
options.deferCodexResetDerivedCooldown,
|
|
530
|
-
) && !isReplayRefusalResponse(upstreamResponse)) {
|
|
531
|
+
) && !isReplayRefusalResponse(upstreamResponse) && !wasPermanentAccountRefusal(upstreamResponse)) {
|
|
531
532
|
// A refusal this proxy made is not evidence about the account. Recording it would
|
|
532
533
|
// classify the synthetic 429 as quota exhaustion and write a default cooldown against
|
|
533
534
|
// a credential the request may never have reached, and that false signal outlives the
|
|
@@ -572,7 +573,7 @@ export async function deliverPassthroughResponse(
|
|
|
572
573
|
// through sanitizePassthroughHeaders) so a redirect to a dead host can never
|
|
573
574
|
// masquerade as a pre-connection failure after the credential was seen.
|
|
574
575
|
// The numeric outcome above already classified it neutral — no streak.
|
|
575
|
-
if (isNonReplayableResponse(upstreamResponse) && !isReplayRefusalResponse(upstreamResponse)) {
|
|
576
|
+
if (isNonReplayableResponse(upstreamResponse) && !isReplayRefusalResponse(upstreamResponse) && !wasPermanentAccountRefusal(upstreamResponse)) {
|
|
576
577
|
return await sanitizeNonReplayableUpstreamError(upstreamResponse, upstream.signal);
|
|
577
578
|
}
|
|
578
579
|
if (upstreamResponse.status >= 300 && upstreamResponse.status < 400) {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { recoverAutoPausedAccount } from "./account-auto-pause-recovery";
|
|
2
|
+
import { adoptCodexWsStage, capturePassthroughAffinity } from "./passthrough-observations";
|
|
1
3
|
import { spendLedgerStorageErrorResponse } from "./spend-storage-error";
|
|
2
4
|
import { requestPacingOverloadResponse } from "./pacing-overload";
|
|
3
5
|
import { recordDistributedResponseFailure } from "../../providers/api-key-response-ownership";
|
|
@@ -99,7 +101,6 @@ import {
|
|
|
99
101
|
upstreamHostCircuitOpenResponse,
|
|
100
102
|
usesCodexForwardPoolAuth,
|
|
101
103
|
codexWsQuotaObserver,
|
|
102
|
-
isFixedCodexAccount,
|
|
103
104
|
codexPoolAccountModel400Denial,
|
|
104
105
|
shouldRetryCodexPoolAccountQuota,
|
|
105
106
|
shouldRetryCodexPoolAccountTransient,
|
|
@@ -111,7 +112,7 @@ import {
|
|
|
111
112
|
clearCodexModelDenialEvidence,
|
|
112
113
|
recordCodexModelDenialEvidence,
|
|
113
114
|
} from "../../codex/model-entitlements";
|
|
114
|
-
import { codexWsSocketDeathStage, isCodexWsUpstreamResponse
|
|
115
|
+
import { codexWsSocketDeathStage, isCodexWsUpstreamResponse } from "./codex-ws-wire";
|
|
115
116
|
import { linkAbortSignal } from "./core-lifetime";
|
|
116
117
|
import type { CodexAuthContext } from "../../codex/auth-context";
|
|
117
118
|
import { checkOutboundBodySize, describeOutboundBodyRefusal } from "./outbound-body-guard";
|
|
@@ -130,6 +131,7 @@ import {
|
|
|
130
131
|
} from "../../lib/upstream-retry";
|
|
131
132
|
import { mapCodexAuthContextErrorToResponse } from "./codex-auth-error";
|
|
132
133
|
import { classifyTransportFailureKind, transportErrorCode } from "../../lib/upstream-reachability";
|
|
134
|
+
import { captureAccountAutoPauseAttempt, wasAccountAutoPaused, wasPermanentAccountRefusal } from "../../oauth/account-auto-pause";
|
|
133
135
|
import { recordCodexUpstreamOutcome } from "../../codex/routing";
|
|
134
136
|
import { describeUpstreamConnectFailure } from "./upstream-error";
|
|
135
137
|
import type { OpaqueBlobRecoveryGuard } from "./core-opaque-recovery";
|
|
@@ -152,7 +154,6 @@ import {
|
|
|
152
154
|
rotateGenericOAuthAccountOn429,
|
|
153
155
|
failoverAccountSnapshot,
|
|
154
156
|
} from "../../oauth/generic-account-failover";
|
|
155
|
-
import { captureCodexAffinityDiagnostic } from "../../codex/affinity-debug";
|
|
156
157
|
import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "../../codex/catalog/native-models";
|
|
157
158
|
import {
|
|
158
159
|
attemptOpaqueBlobRecovery,
|
|
@@ -201,6 +202,7 @@ export async function preparePassthroughExchange(
|
|
|
201
202
|
| "genericFailoverAccountId"
|
|
202
203
|
| "passiveQuotaWriterGeneration"
|
|
203
204
|
| "oauthDispatch"
|
|
205
|
+
| "recoverAutoPausedSelection"
|
|
204
206
|
| "resolveSelectionAdapter"
|
|
205
207
|
| "resolveCopilotSelection"
|
|
206
208
|
| "copilotRefusalResponse"
|
|
@@ -733,22 +735,11 @@ export async function preparePassthroughExchange(
|
|
|
733
735
|
}
|
|
734
736
|
nativeHostState.lease = null;
|
|
735
737
|
};
|
|
736
|
-
/**
|
|
737
|
-
* #4191: a Codex WS exchange pins its content-free stage record on the
|
|
738
|
-
* Response it resolves (markCodexWsStage). Adopting the record here, at
|
|
739
|
-
* the single funnel every physical upstream response passes through,
|
|
740
|
-
* binds it to the attempt that actually served it — including the 502/504
|
|
741
|
-
* pre-response JSON settles that never reach the SSE relay.
|
|
742
|
-
*/
|
|
743
|
-
const adoptCodexWsStage = (response: Response): void => {
|
|
744
|
-
const stage = readCodexWsStage(response);
|
|
745
|
-
if (stage && logCtx.activeAttempt) logCtx.activeAttempt.codexWsStage = stage;
|
|
746
|
-
};
|
|
747
738
|
let upstreamResponse: Response, lastDistributedResponse: Response | undefined;
|
|
748
739
|
const adoptObservedResponse = <T extends Response>(response: T): T => {
|
|
749
740
|
if (!isNonReplayableResponse(response)) lastDistributedResponse = response.status === 401 || response.status === 429 ? response : undefined;
|
|
750
741
|
settleObservedHostResponse();
|
|
751
|
-
adoptCodexWsStage(response);
|
|
742
|
+
adoptCodexWsStage(response, logCtx);
|
|
752
743
|
return response;
|
|
753
744
|
};
|
|
754
745
|
let passthroughEstimate = typeof request.usageLog?.inputTokens === "number"
|
|
@@ -1050,6 +1041,7 @@ export async function preparePassthroughExchange(
|
|
|
1050
1041
|
providerName: route.providerName,
|
|
1051
1042
|
modelId: route.modelId,
|
|
1052
1043
|
onPhysicalDispatch: receiptMode ? sendBudgetState.noteInitialDispatch : undefined,
|
|
1044
|
+
accountAutoPause: (name, provider, headers) => captureAccountAutoPauseAttempt(name, provider, { config, codex: admissionState.authCtx }, headers),
|
|
1053
1045
|
onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
|
|
1054
1046
|
beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
|
|
1055
1047
|
? createCodexAuthDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
|
|
@@ -1187,6 +1179,7 @@ export async function preparePassthroughExchange(
|
|
|
1187
1179
|
dispatchOverride: oauthDispatch(request, parsed, "passthrough"),
|
|
1188
1180
|
providerName: route.providerName,
|
|
1189
1181
|
modelId: route.modelId,
|
|
1182
|
+
accountAutoPause: (name, provider, headers) => captureAccountAutoPauseAttempt(name, provider, { config, codex: admissionState.authCtx }, headers),
|
|
1190
1183
|
onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
|
|
1191
1184
|
beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
|
|
1192
1185
|
? createCodexAuthDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
|
|
@@ -1209,8 +1202,18 @@ export async function preparePassthroughExchange(
|
|
|
1209
1202
|
|
|
1210
1203
|
// Keep recovery kinds in sync with the generic `recovery:` loop below.
|
|
1211
1204
|
passthroughRecovery: for (;;) {
|
|
1205
|
+
if (wasAccountAutoPaused(upstreamResponse) && route.provider.authMode !== "forward") {
|
|
1206
|
+
const recovered = await recoverAutoPausedAccount({ requestKey: transportState, reserve: reserveCredentialHop,
|
|
1207
|
+
remaining: transientSendCapFor(transientRetryPolicyFor(route.provider)?.attempts, sendBudgetState.targetSendsUsed),
|
|
1208
|
+
refresh: () => transportState.recoverAutoPausedSelection(parsed), refetch: () => rebuildAndRefetch("oauth-account-429"), setPermit: permit => { sendBudgetState.pendingHopPermit = permit; } });
|
|
1209
|
+
if (!recovered) break;
|
|
1210
|
+
if ("failed" in recovered) return recovered.failed;
|
|
1211
|
+
upstreamResponse = recovered; continue passthroughRecovery;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
|
|
1212
1215
|
if (
|
|
1213
|
-
upstreamResponse.status === 401
|
|
1216
|
+
upstreamResponse.status === 401 && !wasPermanentAccountRefusal(upstreamResponse)
|
|
1214
1217
|
&& (admissionState.authCtx.kind === "main-pool" || admissionState.authCtx.kind === "pool")
|
|
1215
1218
|
&& usesCodexForwardPoolAuth(admissionState.authCtx, route.provider)
|
|
1216
1219
|
&& codex401ReplayKind === null
|
|
@@ -1293,6 +1296,7 @@ export async function preparePassthroughExchange(
|
|
|
1293
1296
|
dispatchOverride: oauthDispatch(request, parsed, "passthrough"),
|
|
1294
1297
|
providerName: route.providerName,
|
|
1295
1298
|
modelId: route.modelId,
|
|
1299
|
+
accountAutoPause: (name, provider, headers) => captureAccountAutoPauseAttempt(name, provider, { config, codex: admissionState.authCtx }, headers),
|
|
1296
1300
|
onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
|
|
1297
1301
|
beforeDispatch: headers => {
|
|
1298
1302
|
replayGuard?.(headers);
|
|
@@ -1336,14 +1340,14 @@ export async function preparePassthroughExchange(
|
|
|
1336
1340
|
continue passthroughRecovery;
|
|
1337
1341
|
}
|
|
1338
1342
|
|
|
1339
|
-
if (codex401ReplayKind !== null && upstreamResponse.status === 401) break;
|
|
1343
|
+
if (codex401ReplayKind !== null && upstreamResponse.status === 401 && !wasPermanentAccountRefusal(upstreamResponse)) break;
|
|
1340
1344
|
|
|
1341
1345
|
// Native Responses providers return before the generic adapter recovery loop below. Keep
|
|
1342
1346
|
// their OAuth contract identical: one pre-stream 401 forces a credential refresh and one
|
|
1343
1347
|
// rebuilt replay. xAI's current subscription models use this branch now that their official
|
|
1344
1348
|
// Grok CLI catalog declares the Responses backend.
|
|
1345
1349
|
if (
|
|
1346
|
-
upstreamResponse.status === 401
|
|
1350
|
+
upstreamResponse.status === 401 && !wasPermanentAccountRefusal(upstreamResponse)
|
|
1347
1351
|
&& isOAuth401ReplayProvider
|
|
1348
1352
|
&& transportState.sentOAuthSnapshot
|
|
1349
1353
|
&& !oauth401ReplayAttempted
|
|
@@ -1440,6 +1444,7 @@ export async function preparePassthroughExchange(
|
|
|
1440
1444
|
dispatchOverride: oauthDispatch(request, parsed, "passthrough"),
|
|
1441
1445
|
providerName: route.providerName,
|
|
1442
1446
|
modelId: route.modelId,
|
|
1447
|
+
accountAutoPause: (name, provider, headers) => captureAccountAutoPauseAttempt(name, provider, { config, codex: admissionState.authCtx }, headers),
|
|
1443
1448
|
onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
|
|
1444
1449
|
beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
|
|
1445
1450
|
? createCodexAuthDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
|
|
@@ -1586,6 +1591,7 @@ export async function preparePassthroughExchange(
|
|
|
1586
1591
|
dispatchOverride: oauthDispatch(request, parsed, "passthrough"),
|
|
1587
1592
|
providerName: route.providerName,
|
|
1588
1593
|
modelId: route.modelId,
|
|
1594
|
+
accountAutoPause: (name, provider, headers) => captureAccountAutoPauseAttempt(name, provider, { config, codex: admissionState.authCtx }, headers),
|
|
1589
1595
|
onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
|
|
1590
1596
|
beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
|
|
1591
1597
|
? createCodexAuthDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
|
|
@@ -1608,18 +1614,7 @@ export async function preparePassthroughExchange(
|
|
|
1608
1614
|
|| captureAuthCtx.kind === "pool"
|
|
1609
1615
|
|| captureAuthCtx.kind === "main-pool",
|
|
1610
1616
|
): void => {
|
|
1611
|
-
|
|
1612
|
-
captureCodexAffinityDiagnostic({
|
|
1613
|
-
inboundHeaders: req.headers,
|
|
1614
|
-
outboundHeaders: captureRequest.headers,
|
|
1615
|
-
authKind: captureAuthCtx.kind,
|
|
1616
|
-
accountMode: route.codexAccountMode,
|
|
1617
|
-
fixedAccount: isFixedCodexAccount(captureAuthCtx),
|
|
1618
|
-
credentialSubstituted,
|
|
1619
|
-
accountGatedModel: ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(route.modelId),
|
|
1620
|
-
wireModelNormalized: parsed.modelId !== route.modelId,
|
|
1621
|
-
status: response.status,
|
|
1622
|
-
});
|
|
1617
|
+
capturePassthroughAffinity(response, req.headers, captureRequest.headers, captureAuthCtx, route, parsed.modelId, credentialSubstituted);
|
|
1623
1618
|
};
|
|
1624
1619
|
recordDistributedResponseFailure(lastDistributedResponse ?? upstreamResponse, config, route.providerName, options.abortSignal);
|
|
1625
1620
|
captureAffinityResponse(upstreamResponse);
|
|
@@ -1646,7 +1641,9 @@ export async function preparePassthroughExchange(
|
|
|
1646
1641
|
options.abortSignal,
|
|
1647
1642
|
parsed.modelId,
|
|
1648
1643
|
);
|
|
1649
|
-
if (
|
|
1644
|
+
if (wasAccountAutoPaused(upstreamResponse)) {
|
|
1645
|
+
poolRetryOutcome = upstreamResponse.status;
|
|
1646
|
+
} else if (model400Denial !== undefined) {
|
|
1650
1647
|
// Spend this refusal on more than one retry. It is the account's own authenticated
|
|
1651
1648
|
// answer about this model, and the roster cache that selection otherwise reads expires
|
|
1652
1649
|
// five minutes after a catalog sync fills it -- so without remembering this, the next
|
|
@@ -1679,7 +1676,7 @@ export async function preparePassthroughExchange(
|
|
|
1679
1676
|
// ladder does have one — retrying the account the refreshed roster still grants — and
|
|
1680
1677
|
// keeps it. An earlier revision also broke here on a non-400 outcome, which no test could
|
|
1681
1678
|
// justify because this flag already produced the identical result.
|
|
1682
|
-
const storedReplaySpent = codex401ReplayKind === "stored";
|
|
1679
|
+
const storedReplaySpent = codex401ReplayKind === "stored" && !wasAccountAutoPaused(upstreamResponse);
|
|
1683
1680
|
const retry = await retryCodexPoolOnAlternateAccount({
|
|
1684
1681
|
callerAuthHeaders,
|
|
1685
1682
|
config,
|
|
@@ -1698,7 +1695,7 @@ export async function preparePassthroughExchange(
|
|
|
1698
1695
|
httpOnly: canonicalBufferedJson,
|
|
1699
1696
|
preOutputRefusalHold: preOutputRefusalHold(),
|
|
1700
1697
|
onResponse: (response, retryAuthCtx, retryRequest) => {
|
|
1701
|
-
adoptCodexWsStage(response);
|
|
1698
|
+
adoptCodexWsStage(response, logCtx);
|
|
1702
1699
|
captureAffinityResponse(
|
|
1703
1700
|
response,
|
|
1704
1701
|
retryAuthCtx,
|
|
@@ -14,6 +14,7 @@ import { guardDirectPassthroughBodyInactivity } from "../../lib/response-body-in
|
|
|
14
14
|
import { resolveStallTimeoutMs } from "../../stall-timeout";
|
|
15
15
|
import { releaseUpstreamHostAdmission } from "../../codex/upstream-host-health";
|
|
16
16
|
import { releaseCodexAuthContextProbeLease } from "../../codex/auth-context";
|
|
17
|
+
import { attachResponseToolOwnership } from "./response-tool-ownership";
|
|
17
18
|
|
|
18
19
|
/** Owns the native host lease across dispatch, recovery, and response construction. */
|
|
19
20
|
export async function executePassthroughResponse(
|
|
@@ -50,11 +51,11 @@ export async function executePassthroughResponse(
|
|
|
50
51
|
// Delivery has already classified SSE (including a missing upstream content type)
|
|
51
52
|
// and consumed bounded JSON/errors. Guard only its remaining direct body, outside
|
|
52
53
|
// the relay/lifetime wrappers so their prefetch cannot arm our inactivity clock.
|
|
53
|
-
return guardDirectPassthroughBodyInactivity(
|
|
54
|
+
return attachResponseToolOwnership(guardDirectPassthroughBodyInactivity(
|
|
54
55
|
response,
|
|
55
56
|
nativeExchange.upstream.signal,
|
|
56
57
|
resolveStallTimeoutMs(requestContext.config.stallTimeoutSec, { localUpstream: nativeExchange.localUpstream }),
|
|
57
|
-
);
|
|
58
|
+
), nativeExchange.providerExecutedCallTypes);
|
|
58
59
|
} finally {
|
|
59
60
|
if (nativeHostState.lease) {
|
|
60
61
|
releaseUpstreamHostAdmission(nativeHostState.lease);
|