@bitkyc08/opencodex 2.88.0 → 2.90.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-DgxkE6Md.js +58 -0
- package/gui/dist/assets/App-dnkJ85te.css +1 -0
- package/gui/dist/assets/{Tray-B5bC7AWL.js → Tray-DbTNc_88.js} +1 -1
- package/gui/dist/assets/{index-B5bsvxor.js → index-TmaHa54u.js} +15 -15
- package/gui/dist/assets/quota-summary-B16x5DZZ.js +1 -0
- package/gui/dist/index.html +1 -1
- package/package.json +2 -2
- package/src/adapters/anthropic/cache-retention.ts +117 -0
- package/src/adapters/anthropic/passthrough.ts +7 -4
- package/src/adapters/anthropic.ts +10 -3
- package/src/adapters/base.ts +4 -0
- package/src/adapters/claude-cli/adapter.ts +13 -7
- package/src/adapters/claude-cli/usage-admission.ts +14 -2
- package/src/adapters/coding-agent/protocol.ts +6 -0
- package/src/adapters/coding-agent/turn.ts +14 -4
- package/src/adapters/cursor/protobuf-request.ts +3 -0
- package/src/adapters/google-http.ts +3 -2
- package/src/adapters/google-progress-history.ts +19 -1
- package/src/adapters/google-vertex-endpoint.ts +67 -0
- package/src/adapters/google.ts +62 -37
- 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/compaction-search-history.ts +27 -0
- package/src/adapters/openai-responses/opengateway-input.ts +139 -0
- package/src/adapters/openai-responses/passthrough.ts +22 -13
- package/src/adapters/openai-responses/reasoning.ts +68 -1
- package/src/adapters/openai-responses/transport.ts +13 -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 +11 -1
- package/src/bridge/response-json.ts +2 -2
- package/src/bridge/sse.ts +10 -4
- 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/claude/inbound.ts +12 -26
- package/src/claude/outbound.ts +5 -0
- 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/ensure-desktop-cold-start.ts +2 -2
- package/src/cli/help.ts +1 -1
- 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-lifecycle.ts +19 -6
- package/src/codex/account-pause.ts +6 -1
- package/src/codex/account-runtime-state.ts +5 -0
- package/src/codex/account-store.ts +55 -9
- package/src/codex/account-usability.ts +4 -1
- package/src/codex/auth-api/account-list.ts +13 -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/main-account-probe.ts +3 -2
- 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 +57 -10
- 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-removal.ts +4 -4
- package/src/codex/cli-update-plan.ts +18 -3
- package/src/codex/collision-identity.ts +78 -0
- 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-hard-lock.ts +14 -0
- 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/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/combos/failover.ts +10 -2
- 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/persist-unlocked.ts +10 -1
- package/src/config/provider-validation.ts +7 -2
- 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 +476 -216
- package/src/generated/model-metadata.ts +1 -1
- package/src/images/loop.ts +34 -11
- 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 +82 -0
- package/src/lib/decision-quota-sink.ts +76 -0
- package/src/lib/provider-admission-wait.ts +14 -0
- package/src/lib/service-secrets.ts +37 -2
- package/src/lib/state-store-registrations.ts +25 -0
- package/src/lib/translator-budget.ts +1 -1
- package/src/lib/upstream-retry.ts +15 -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-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/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/chatgpt-shared.ts +10 -1
- package/src/oauth/dsh.ts +45 -2
- package/src/oauth/index.ts +5 -35
- package/src/oauth/kiro.ts +44 -1
- package/src/oauth/login-flow-state.ts +11 -2
- package/src/oauth/store.ts +35 -8
- package/src/oauth/types.ts +2 -0
- package/src/plugins/upstream-hooks.ts +14 -1
- 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 +25 -5
- package/src/providers/initial-model-selection.ts +2 -2
- package/src/providers/key-failover.ts +21 -13
- package/src/providers/key-store.ts +8 -3
- package/src/providers/mistral-transport.ts +207 -0
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/openai-sidecar.ts +5 -0
- package/src/providers/opencode-zen-rate-limit.ts +6 -72
- package/src/providers/quota/account-cache.ts +15 -1
- package/src/providers/quota/anthropic-account-quota.ts +20 -7
- 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 +45 -8
- package/src/providers/quota/failure-codes.ts +18 -0
- package/src/providers/quota/report-cache.ts +14 -0
- package/src/providers/quota/vendor-probes-oauth.ts +15 -6
- 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 +4 -1
- package/src/providers/quota-types.ts +6 -1
- package/src/providers/quota.ts +8 -3
- package/src/providers/registry/entries-extended.ts +14 -53
- package/src/providers/registry/model-seeds.ts +1 -1
- package/src/providers/registry.ts +1 -2
- package/src/providers/routed-config.ts +1 -1
- package/src/providers/stale-vision-classification-migration.ts +1 -1
- package/src/providers/xai-transport.ts +2 -1
- 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 +18 -10
- package/src/responses/reasoning-retention.ts +218 -11
- package/src/responses/schema.ts +3 -4
- package/src/responses/web-search-replay.ts +147 -0
- 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 +36 -20
- package/src/server/claude-messages.ts +2 -1
- package/src/server/index/optional-listeners.ts +6 -0
- package/src/server/index/serve-options.ts +8 -2
- package/src/server/index/websocket-handler.ts +10 -0
- package/src/server/lifecycle.ts +17 -1
- package/src/server/management/combo-routes.ts +17 -2
- package/src/server/management/oauth-account-routes.ts +12 -2
- 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 +29 -4
- package/src/server/messages-shared.ts +4 -1
- package/src/server/request-log.ts +23 -9
- package/src/server/responses/account-auto-pause-recovery.ts +26 -0
- package/src/server/responses/adapter-continuation.ts +2 -2
- package/src/server/responses/adapter-dispatch.ts +79 -30
- 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-session-recall.ts +25 -0
- package/src/server/responses/combo-stream-preflight.ts +10 -3
- package/src/server/responses/combo-tool-loop-pin.ts +70 -0
- package/src/server/responses/compact.ts +10 -5
- package/src/server/responses/context-overflow.ts +141 -2
- package/src/server/responses/core-auth.ts +20 -0
- package/src/server/responses/core-codex-account.ts +28 -15
- package/src/server/responses/core-combo.ts +34 -5
- package/src/server/responses/core-opaque-recovery.ts +20 -11
- package/src/server/responses/core.ts +12 -9
- package/src/server/responses/deepseek-image-files.ts +121 -0
- package/src/server/responses/empty-completion-guard.ts +10 -0
- package/src/server/responses/fetch-helpers.ts +65 -14
- package/src/server/responses/input-admission.ts +6 -2
- package/src/server/responses/jev-model-invoke.ts +13 -14
- package/src/server/responses/main-hard-lock-routing.ts +33 -8
- package/src/server/responses/native-steering-log.ts +2 -2
- 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 +5 -2
- package/src/server/responses/passthrough-dispatch.ts +42 -45
- package/src/server/responses/passthrough-key-rotation.ts +40 -0
- package/src/server/responses/passthrough-observations.ts +38 -0
- package/src/server/responses/reasoning-archive-error.ts +21 -0
- package/src/server/responses/request-compression.ts +98 -0
- package/src/server/responses/request-prepare.ts +11 -1
- package/src/server/responses/request-transport.ts +92 -5
- package/src/server/responses/run-turn-execution.ts +2 -0
- package/src/server/responses/sidecar-execution.ts +16 -3
- package/src/server/responses/terminal-guard.ts +2 -0
- 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/service/definition.ts +9 -1
- package/src/service/orchestration.ts +17 -0
- package/src/types/account-auto-pause.ts +15 -0
- package/src/types/config.ts +23 -1
- package/src/types/provider.ts +11 -5
- package/src/types/request.ts +36 -3
- 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 +7 -0
- package/src/web-search/loop.ts +39 -13
- package/src/web-search/progress-stream.ts +4 -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-CHC89EXQ.js +0 -58
- package/gui/dist/assets/App-wT3mXqFz.css +0 -1
- package/gui/dist/assets/quota-summary-D8y2wTsB.js +0 -1
|
@@ -110,6 +110,31 @@ export function recallComboForLane(
|
|
|
110
110
|
return entry.responseModel === model ? entry.comboId : undefined;
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
/**
|
|
114
|
+
* The concrete target that completed the lane's last turn for `comboId`, under the same
|
|
115
|
+
* freshness and ownership rules as `recallComboForLane`. A lane answered by another combo, or
|
|
116
|
+
* whose target left the combo or was disabled, recalls nothing.
|
|
117
|
+
*/
|
|
118
|
+
export function recallComboTargetForLane(
|
|
119
|
+
config: OcxConfig,
|
|
120
|
+
lane: string | undefined,
|
|
121
|
+
comboId: string,
|
|
122
|
+
): Pick<OcxComboTarget, "provider" | "model"> | undefined {
|
|
123
|
+
if (!lane || !comboId) return undefined;
|
|
124
|
+
const entry = recall.get(lane);
|
|
125
|
+
if (!entry || entry.comboId !== comboId) return undefined;
|
|
126
|
+
const combo = getCombo(config, entry.comboId);
|
|
127
|
+
const provider = config.providers[entry.target.provider];
|
|
128
|
+
if (Date.now() - entry.at >= RECALL_TTL_MS
|
|
129
|
+
|| !Object.hasOwn(config.providers, entry.target.provider)
|
|
130
|
+
|| !provider || provider.disabled === true
|
|
131
|
+
|| !combo?.targets.some(target => targetKey(target) === targetKey(entry.target))) {
|
|
132
|
+
deleteEntry(lane);
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
return { provider: entry.target.provider, model: entry.target.model };
|
|
136
|
+
}
|
|
137
|
+
|
|
113
138
|
/**
|
|
114
139
|
* Periodic expiry. Without it a lane that is never read again and never touched by a config
|
|
115
140
|
* reconciliation holds its entry for the life of the process: the existing TTL is only
|
|
@@ -115,12 +115,14 @@ function retryableZeroOutputTerminal(payload: unknown): boolean {
|
|
|
115
115
|
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
|
|
116
116
|
const event = payload as {
|
|
117
117
|
type?: unknown;
|
|
118
|
-
response?: { incomplete_details?: { reason?: unknown } };
|
|
118
|
+
response?: { incomplete_details?: { reason?: unknown }; output?: unknown };
|
|
119
119
|
};
|
|
120
120
|
if (bareErrorIsRetryable(event)) return true;
|
|
121
121
|
if (event.type === "response.failed") return true;
|
|
122
122
|
if (event.type !== "response.incomplete") return false;
|
|
123
123
|
const reason = event.response?.incomplete_details?.reason;
|
|
124
|
+
// A terminal-only upstream can carry output without preceding delta/item events.
|
|
125
|
+
if (reason === undefined) return !Array.isArray(event.response?.output) || event.response.output.length === 0;
|
|
124
126
|
return typeof reason === "string" && RETRYABLE_ZERO_OUTPUT_INCOMPLETE_REASONS.has(reason);
|
|
125
127
|
}
|
|
126
128
|
|
|
@@ -222,8 +224,13 @@ function failedTerminalResponse(
|
|
|
222
224
|
: topLevelError && typeof topLevelError === "object" && !Array.isArray(topLevelError)
|
|
223
225
|
? topLevelError as Record<string, unknown>
|
|
224
226
|
: {
|
|
225
|
-
type:
|
|
226
|
-
|
|
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",
|
|
227
234
|
message: typeof terminalPayload.message === "string"
|
|
228
235
|
? terminalPayload.message
|
|
229
236
|
: logCtx.upstreamError ?? "Provider stream failed before producing output",
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Combo target pinning for an in-flight tool loop (#7298).
|
|
3
|
+
*
|
|
4
|
+
* A combo picks a target per request. A tool loop is several requests that together form one
|
|
5
|
+
* assistant turn: the model emits a call, the client runs it, and the next request carries the
|
|
6
|
+
* result back. Re-picking between those requests hands the second half of a turn to a model that
|
|
7
|
+
* did not produce the first half. That is wasteful at best (a cold prompt cache, a different
|
|
8
|
+
* plan) and a hard failure at worst: DeepSeek's thinking mode refuses a tool continuation whose
|
|
9
|
+
* call carries no reasoning it can read, and a GPT turn never leaves one.
|
|
10
|
+
*
|
|
11
|
+
* So a continuation whose input ends in a tool output keeps the target that completed the lane's
|
|
12
|
+
* previous turn, as long as that target is still selectable right now: enabled, still in the
|
|
13
|
+
* combo, out of cooldown and passing every per-request eligibility filter the caller supplies.
|
|
14
|
+
* When it is not, normal selection (and normal failover) runs, and the strict serializers make
|
|
15
|
+
* the foreign history acceptable instead: see `backfillBareToolCallTurnReasoning` and the Chat
|
|
16
|
+
* adapter's reasoning placeholder.
|
|
17
|
+
*
|
|
18
|
+
* A new user message is never pinned; re-deciding per user turn is what a combo is for.
|
|
19
|
+
*/
|
|
20
|
+
import { pickComboTarget, targetKey, type ComboPick } from "../../combos";
|
|
21
|
+
import type { NormalizedComboTarget } from "../../combos/types";
|
|
22
|
+
import type { OcxConfig } from "../../types";
|
|
23
|
+
import { recallComboTargetForLane } from "./combo-session-recall";
|
|
24
|
+
|
|
25
|
+
export const COMBO_TOOL_LOOP_PIN_REASON = "combo-tool-loop-pin";
|
|
26
|
+
|
|
27
|
+
function isToolOutputItem(item: unknown): boolean {
|
|
28
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return false;
|
|
29
|
+
const type = (item as { type?: unknown }).type;
|
|
30
|
+
return typeof type === "string" && type.endsWith("_output");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Whether this request resumes an assistant turn by returning tool results: its Responses
|
|
35
|
+
* `input` (or Chat `messages`) ends with a tool output.
|
|
36
|
+
*/
|
|
37
|
+
export function isToolLoopContinuation(body: unknown): boolean {
|
|
38
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return false;
|
|
39
|
+
const { input, messages } = body as { input?: unknown; messages?: unknown };
|
|
40
|
+
if (Array.isArray(input)) return input.length > 0 && isToolOutputItem(input[input.length - 1]);
|
|
41
|
+
if (Array.isArray(messages) && messages.length > 0) {
|
|
42
|
+
const last = messages[messages.length - 1];
|
|
43
|
+
return !!last && typeof last === "object" && (last as { role?: unknown }).role === "tool";
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The pick that keeps a tool loop on its previous target, or undefined to select normally.
|
|
50
|
+
* Uses a non-mutating preview so a pinned request never disturbs round-robin rotation state.
|
|
51
|
+
*/
|
|
52
|
+
export function toolLoopPinnedPick(args: {
|
|
53
|
+
config: OcxConfig;
|
|
54
|
+
comboId: string;
|
|
55
|
+
lane: string | undefined;
|
|
56
|
+
body: unknown;
|
|
57
|
+
eligible: (target: NormalizedComboTarget) => boolean;
|
|
58
|
+
now?: number;
|
|
59
|
+
}): ComboPick | undefined {
|
|
60
|
+
if (!isToolLoopContinuation(args.body)) return undefined;
|
|
61
|
+
const previous = recallComboTargetForLane(args.config, args.lane, args.comboId);
|
|
62
|
+
if (!previous) return undefined;
|
|
63
|
+
const previousKey = targetKey(previous);
|
|
64
|
+
const pick = pickComboTarget(args.config, args.comboId, {
|
|
65
|
+
eligible: target => targetKey(target) === previousKey && args.eligible(target),
|
|
66
|
+
now: args.now,
|
|
67
|
+
preview: true,
|
|
68
|
+
});
|
|
69
|
+
return pick ? { ...pick, attempted: [previousKey] } : undefined;
|
|
70
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { captureAccountAutoPauseAttempt, wasAccountAutoPaused, wasPermanentAccountRefusal } from "../../oauth/account-auto-pause";
|
|
1
2
|
import { spendLedgerStorageErrorResponse } from "./spend-storage-error";
|
|
2
3
|
import { createPhysicalSendReporter, rebindPhysicalSend } from "../../lib/request-execution-budget";
|
|
3
4
|
import { createInferenceSendBudget } from "../inference/context";
|
|
@@ -87,6 +88,7 @@ import {
|
|
|
87
88
|
recordCodexUpstreamOutcome,
|
|
88
89
|
type CodexUpstreamOutcome,
|
|
89
90
|
} from "../../codex/routing";
|
|
91
|
+
import { codexFailureWindowOutcomeMeta } from "../../codex/routing/failure-window";
|
|
90
92
|
import {
|
|
91
93
|
applyAccountChangeConversationStateScrub,
|
|
92
94
|
conversationStateBindingFromAuth,
|
|
@@ -1058,8 +1060,10 @@ export async function handleResponsesCompact(
|
|
|
1058
1060
|
) => {
|
|
1059
1061
|
// A gateway response is not evidence about any Codex account.
|
|
1060
1062
|
if (gatewayTransport || !usesCodexForwardPoolAuth(ctx, route.provider)) return;
|
|
1063
|
+
if ((outcome === 401 || outcome === 403) && upstream && wasPermanentAccountRefusal(upstream)) return;
|
|
1061
1064
|
recordCodexUpstreamOutcome(config, ctx.accountId, outcome, {
|
|
1062
1065
|
...meta,
|
|
1066
|
+
...codexFailureWindowOutcomeMeta(ctx),
|
|
1063
1067
|
threadId: ctx.kind === "pool" || ctx.kind === "main-pool" ? ctx.affinityKey : undefined,
|
|
1064
1068
|
fixedAccount: ctx.fixedAccount,
|
|
1065
1069
|
modelId: selectedModelId,
|
|
@@ -1166,6 +1170,7 @@ export async function handleResponsesCompact(
|
|
|
1166
1170
|
providerFetch(sendProvider, undefined, {
|
|
1167
1171
|
providerName: route.providerName,
|
|
1168
1172
|
modelId: route.modelId,
|
|
1173
|
+
accountAutoPause: (name, provider, headers) => captureAccountAutoPauseAttempt(name, provider, { config, codex: sendAuthCtx }, headers),
|
|
1169
1174
|
beforeDispatch: isCanonicalOpenAiForwardProvider(sendProvider)
|
|
1170
1175
|
? createCodexAuthDispatchGuard(sendAuthCtx, config, selectedModelId, admission) : undefined,
|
|
1171
1176
|
dispatchOverride: sendProvider.authMode !== "forward" && sendProvider.apiKey
|
|
@@ -1287,7 +1292,7 @@ export async function handleResponsesCompact(
|
|
|
1287
1292
|
|
|
1288
1293
|
recordDistributedResponseFailure(upstream, config, route.providerName, req.signal);
|
|
1289
1294
|
if (
|
|
1290
|
-
upstream.status === 401
|
|
1295
|
+
upstream.status === 401 && !wasPermanentAccountRefusal(upstream)
|
|
1291
1296
|
&& (authCtx.kind === "main-pool" || authCtx.kind === "pool")
|
|
1292
1297
|
&& !gatewayTransport
|
|
1293
1298
|
&& usesCodexForwardPoolAuth(authCtx, compactProvider)
|
|
@@ -1411,12 +1416,12 @@ export async function handleResponsesCompact(
|
|
|
1411
1416
|
// surfaces to the client, which retries the compact task OUTSIDE the logical request
|
|
1412
1417
|
// — reporting exhausted retries while another pool account sat idle (#913).
|
|
1413
1418
|
if (
|
|
1414
|
-
(upstream.status === 429 || upstream.status === 402)
|
|
1419
|
+
(upstream.status === 429 || upstream.status === 402 || wasAccountAutoPaused(upstream))
|
|
1415
1420
|
// A replay refusal this proxy synthesized carries 429 for the client's benefit only.
|
|
1416
1421
|
// It is not pool quota evidence, and the alternate account below is another send of a
|
|
1417
1422
|
// compact turn that may already have been processed.
|
|
1418
1423
|
&& !isNonReplayableResponse(upstream)
|
|
1419
|
-
&& !storedPool401ReplayAttempted
|
|
1424
|
+
&& (!storedPool401ReplayAttempted || wasAccountAutoPaused(upstream))
|
|
1420
1425
|
&& !gatewayTransport
|
|
1421
1426
|
&& usesCodexForwardPoolAuth(authCtx, route.provider)
|
|
1422
1427
|
&& !authCtx.fixedAccount
|
|
@@ -1521,7 +1526,7 @@ export async function handleResponsesCompact(
|
|
|
1521
1526
|
// the same guards as the quota move, and a 5xx whose body confirms quota keeps today's path.
|
|
1522
1527
|
if (
|
|
1523
1528
|
shouldRetryCodexPoolAccountTransient(upstream)
|
|
1524
|
-
&& !storedPool401ReplayAttempted
|
|
1529
|
+
&& (!storedPool401ReplayAttempted || wasAccountAutoPaused(upstream))
|
|
1525
1530
|
&& !gatewayTransport
|
|
1526
1531
|
&& usesCodexForwardPoolAuth(authCtx, route.provider)
|
|
1527
1532
|
&& !authCtx.fixedAccount
|
|
@@ -1642,7 +1647,7 @@ export async function handleResponsesCompact(
|
|
|
1642
1647
|
inspectResponseLogJson(logCtx, compactText);
|
|
1643
1648
|
if (!options.compactionRoutingOverride) forgetCompactHandoffRoute(req, admission);
|
|
1644
1649
|
rememberServingConversationStateIssuer(outcomeCtx, codexPoolAffinityKey(req.headers));
|
|
1645
|
-
} else if (!options.compactionRoutingOverride && quotaFailure && !storedPool401ReplayAttempted) {
|
|
1650
|
+
} else if (!options.compactionRoutingOverride && quotaFailure && (!storedPool401ReplayAttempted || wasAccountAutoPaused(upstream))) {
|
|
1646
1651
|
const fallbackModel = compactHandoffRoute(req, admission, raw.model);
|
|
1647
1652
|
if (fallbackModel && !req.signal.aborted) {
|
|
1648
1653
|
const fallbackReq = new Request(req.url, {
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { bridgeToResponsesSSE, formatErrorResponse } from "../../bridge";
|
|
2
2
|
import { OLLAMA_LATE_ATTRIBUTION_LIMIT_MESSAGE } from "../../adapters/ollama-native";
|
|
3
|
+
import { isThroughputLimitText } from "../../claude/outbound";
|
|
4
|
+
import { isDefiniteContextOverflow } from "../../combos/failover";
|
|
5
|
+
import { classifyError } from "../../lib/errors";
|
|
3
6
|
import type { TranslatorBudget } from "../../lib/translator-budget";
|
|
4
7
|
import type { AdapterEvent } from "../../types";
|
|
5
8
|
|
|
@@ -32,8 +35,11 @@ export function jsonContextOverflowResponse(message = PROVIDER_INPUT_TOO_LARGE_M
|
|
|
32
35
|
}, { status: 413, headers: { "Cache-Control": "no-store" } });
|
|
33
36
|
}
|
|
34
37
|
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
/** The terminal error event every context overflow is reported as, streaming or in-stream. */
|
|
39
|
+
export function contextOverflowEvent(
|
|
40
|
+
message = PROVIDER_INPUT_TOO_LARGE_MESSAGE,
|
|
41
|
+
): Extract<AdapterEvent, { type: "error" }> {
|
|
42
|
+
return {
|
|
37
43
|
type: "error",
|
|
38
44
|
message,
|
|
39
45
|
status: 413,
|
|
@@ -43,6 +49,139 @@ async function* contextOverflowEvents(message: string): AsyncGenerator<AdapterEv
|
|
|
43
49
|
};
|
|
44
50
|
}
|
|
45
51
|
|
|
52
|
+
async function* contextOverflowEvents(message: string): AsyncGenerator<AdapterEvent> {
|
|
53
|
+
yield contextOverflowEvent(message);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Whether a web-search or image-bridge failure is the provider refusing an oversized input.
|
|
58
|
+
*
|
|
59
|
+
* An HTTP 413 is classified by status alone, exactly as the main path does. A 400/422 needs a
|
|
60
|
+
* definite overflow phrase in the provider message (the rule combo failover already trusts), so a
|
|
61
|
+
* plain bad request stays what it was, and rate-window wording ("too many tokens per minute")
|
|
62
|
+
* never reads as an overflow: compacting does not fix a throughput limit. An adapter that already
|
|
63
|
+
* classified its own failure as `context_length_exceeded` keeps that verdict whatever status it
|
|
64
|
+
* rode on. The local translator limit is excluded: compacting cannot shrink a buffer this proxy owns.
|
|
65
|
+
*/
|
|
66
|
+
export function isSidecarContextOverflow(
|
|
67
|
+
status: number | undefined,
|
|
68
|
+
message: string,
|
|
69
|
+
code?: string | null,
|
|
70
|
+
): boolean {
|
|
71
|
+
if (code === "translation_buffer_limit" || code === "rate_limit_exceeded") return false;
|
|
72
|
+
if (code === "context_length_exceeded") return true;
|
|
73
|
+
if (status === 413) return true;
|
|
74
|
+
return (status === 400 || status === 422) && !isThroughputLimitText(message)
|
|
75
|
+
&& isDefiniteContextOverflow(status, message);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The in-stream terminal for a sidecar overflow. A bare 413 carries no provider wording worth
|
|
80
|
+
* keeping and gets the proxy-owned message. A worded verdict keeps its own status and message:
|
|
81
|
+
* that is the evidence combo failover reads to try a larger-context target, and replacing it
|
|
82
|
+
* would end the chain at the first target that was merely too small.
|
|
83
|
+
*/
|
|
84
|
+
export function sidecarContextOverflowEvent(
|
|
85
|
+
status: number | undefined,
|
|
86
|
+
message: string,
|
|
87
|
+
): Extract<AdapterEvent, { type: "error" }> {
|
|
88
|
+
return status === undefined || !message || (status === 413 && !isDefiniteContextOverflow(413, message))
|
|
89
|
+
? contextOverflowEvent()
|
|
90
|
+
: { ...contextOverflowEvent(message), status };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** One sidecar loop failure, with what the loop knew when it raised it. */
|
|
94
|
+
export interface SidecarLoopFailure {
|
|
95
|
+
status: number;
|
|
96
|
+
message: string;
|
|
97
|
+
event?: Extract<AdapterEvent, { type: "error" }>;
|
|
98
|
+
/** The provider's own `error.code`, read from a bounded display-safe body. */
|
|
99
|
+
upstreamCode?: string;
|
|
100
|
+
/** The failed response was marked non-replayable: the provider may already have run the turn. */
|
|
101
|
+
nonReplayable?: boolean;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The overflow terminal a sidecar loop failure should end with, or `undefined` to keep the
|
|
106
|
+
* failure as it is. A non-replayable failure keeps its verdict: an overflow would invite a
|
|
107
|
+
* compacted resend of a turn the provider may already have run. An adapter event marked
|
|
108
|
+
* `retryable: false` under any other code is that adapter's own verdict and is not reread.
|
|
109
|
+
*/
|
|
110
|
+
export function sidecarLoopOverflowEvent(
|
|
111
|
+
failure: SidecarLoopFailure,
|
|
112
|
+
): Extract<AdapterEvent, { type: "error" }> | undefined {
|
|
113
|
+
if (failure.nonReplayable) return undefined;
|
|
114
|
+
const code = failure.event?.code ?? failure.upstreamCode;
|
|
115
|
+
if (failure.event?.retryable === false && code !== "context_length_exceeded") return undefined;
|
|
116
|
+
if (!isSidecarContextOverflow(failure.status, failure.message, code)) return undefined;
|
|
117
|
+
return sidecarContextOverflowEvent(failure.event?.status ?? failure.status, failure.message);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The in-stream terminal for a sidecar loop failure that carries no adapter event, or
|
|
122
|
+
* `undefined` to keep the loop's existing generic event. An overflow ends as the overflow
|
|
123
|
+
* terminal. A failure this classifier deliberately kept (non-replayable, or an upstream
|
|
124
|
+
* `rate_limit_exceeded`) gets an explicit code whenever the bridge's wording heuristics would
|
|
125
|
+
* otherwise rename it `context_length_exceeded` and undo that verdict on the wire.
|
|
126
|
+
*/
|
|
127
|
+
export function sidecarLoopTerminalEvent(
|
|
128
|
+
failure: SidecarLoopFailure,
|
|
129
|
+
): Extract<AdapterEvent, { type: "error" }> | undefined {
|
|
130
|
+
const overflow = sidecarLoopOverflowEvent(failure);
|
|
131
|
+
if (overflow || failure.event) return overflow;
|
|
132
|
+
if (classifyError(failure.status, "upstream_error", failure.message).code !== "context_length_exceeded") return undefined;
|
|
133
|
+
const rateLimited = failure.upstreamCode === "rate_limit_exceeded";
|
|
134
|
+
return {
|
|
135
|
+
type: "error",
|
|
136
|
+
status: failure.status,
|
|
137
|
+
message: failure.message,
|
|
138
|
+
errorType: rateLimited ? "rate_limit_error" : "upstream_error",
|
|
139
|
+
code: failure.upstreamCode ?? "upstream_error",
|
|
140
|
+
...(failure.nonReplayable ? { retryable: false } : {}),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Read a provider's `error.code` from an already bounded, display-safe error body. */
|
|
145
|
+
export function upstreamErrorCode(text: string): string | undefined {
|
|
146
|
+
try {
|
|
147
|
+
const code = (JSON.parse(text) as { error?: { code?: unknown } } | null)?.error?.code;
|
|
148
|
+
return typeof code === "string" && code.length <= 128 ? code : undefined;
|
|
149
|
+
} catch {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const sidecarOverflowResponses = new WeakSet<Response>();
|
|
155
|
+
|
|
156
|
+
/** Tag a loop's first-iteration JSON failure that the sidecar boundary should map to an overflow. */
|
|
157
|
+
export function markSidecarContextOverflow(response: Response, failure: SidecarLoopFailure): Response {
|
|
158
|
+
if (sidecarLoopOverflowEvent(failure)) sidecarOverflowResponses.add(response);
|
|
159
|
+
return response;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Map a sidecar loop's pre-stream HTTP failure onto the main path's overflow contract (#7316).
|
|
164
|
+
*
|
|
165
|
+
* Both loops answer a failed first iteration with their own small JSON error, so the overflow
|
|
166
|
+
* would otherwise reach Codex as a bare non-2xx that it resends unchanged through its retry
|
|
167
|
+
* budget. The loop classified the failure while it still had the provider's status, wording,
|
|
168
|
+
* code and replay verdict, and tagged the response; anything untagged is returned untouched.
|
|
169
|
+
* Combo attempts do not come through here: the combo layer reads the raw failure for its hop
|
|
170
|
+
* decision and maps a final 413 itself. A failure answered with `x-should-retry: false` keeps
|
|
171
|
+
* its status and header, so that contract never turns into an invitation to resend.
|
|
172
|
+
*/
|
|
173
|
+
export function sidecarContextOverflowResponse(
|
|
174
|
+
response: Response,
|
|
175
|
+
options: { stream: boolean; modelId: string; translatorBudget: TranslatorBudget },
|
|
176
|
+
): Response {
|
|
177
|
+
if (response.ok || !sidecarOverflowResponses.has(response)) return response;
|
|
178
|
+
if (response.headers.get("x-should-retry") === "false") return response;
|
|
179
|
+
void response.body?.cancel().catch(() => {});
|
|
180
|
+
return options.stream
|
|
181
|
+
? streamingContextOverflowResponse(options.modelId, options.translatorBudget)
|
|
182
|
+
: jsonContextOverflowResponse();
|
|
183
|
+
}
|
|
184
|
+
|
|
46
185
|
/**
|
|
47
186
|
* Convert a pre-stream provider 413 into the terminal Responses event Codex understands.
|
|
48
187
|
*
|
|
@@ -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";
|
|
@@ -590,3 +592,21 @@ export async function refreshNativeMainForwardAuth(args: {
|
|
|
590
592
|
}) ?? nativeMainRefreshFailureResponse(error) };
|
|
591
593
|
}
|
|
592
594
|
}
|
|
595
|
+
|
|
596
|
+
/** Inspect the original credential domain, before fallback changes the route or drops headers. */
|
|
597
|
+
export function mainHardLockFallbackCallerCheck(
|
|
598
|
+
req: Request, config: OcxConfig, options: HandleResponsesOptions,
|
|
599
|
+
): (sourceModel: string) => Promise<(() => boolean) | null> {
|
|
600
|
+
return async sourceModel => {
|
|
601
|
+
let sourceRoute: ReturnType<typeof routeConcreteModel>;
|
|
602
|
+
try { sourceRoute = routeConcreteModel(config, sourceModel); }
|
|
603
|
+
// This preview cannot replace the normal routing phase's existing error response.
|
|
604
|
+
catch { return null; }
|
|
605
|
+
const headers = codexRouteCredentialDomainHeaders(req, sourceRoute, options, false, config);
|
|
606
|
+
const { substituteMainCredential } = codexRouteCredentialOwnership(headers, config, sourceRoute, options);
|
|
607
|
+
return mainAccountHardLockCallerProof(headers, options.codexAuthPolicy ?? config, {
|
|
608
|
+
substituteMainCredentialForDirect: substituteMainCredential,
|
|
609
|
+
signal: options.abortSignal ?? req.signal,
|
|
610
|
+
});
|
|
611
|
+
};
|
|
612
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { captureAccountAutoPauseAttempt, wasPermanentAccountRefusal } from "../../oauth/account-auto-pause";
|
|
1
2
|
import type { OcxConfig, OcxProviderConfig, OcxParsedRequest } from "../../types";
|
|
2
3
|
import type { CodexAuthContext, CodexAuthPolicyConfig } from "../../codex/auth-context";
|
|
3
4
|
import type { CodexUpstreamOutcome } from "../../codex/routing";
|
|
@@ -10,6 +11,7 @@ import { claimMainQuotaDispatchForWs, isMainQuotaDispatchLive, renewMainQuotaDis
|
|
|
10
11
|
import type { CodexWsQuotaObserver } from "./codex-ws-metadata";
|
|
11
12
|
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
12
13
|
import { isCodexAccountGenerationLive } from "../../codex/account-store";
|
|
14
|
+
import { codexFailureWindowOutcomeMeta } from "../../codex/routing/failure-window";
|
|
13
15
|
import { applyAccountQuotaFromUpstreamHeaders as applyCapturedCodexQuota } from "../../codex/quota";
|
|
14
16
|
import type { RouteResult } from "../../router";
|
|
15
17
|
import {
|
|
@@ -92,20 +94,19 @@ export function sidecarOutcomeRecorder(
|
|
|
92
94
|
config: OcxConfig,
|
|
93
95
|
authCtx: CodexAuthContext,
|
|
94
96
|
): ((outcome: CodexUpstreamOutcome) => void) | undefined {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
: 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
|
+
});
|
|
109
110
|
}
|
|
110
111
|
|
|
111
112
|
|
|
@@ -144,9 +145,10 @@ export function recordCodexPreOutputRefusalOutcome(
|
|
|
144
145
|
modelId: string,
|
|
145
146
|
): void {
|
|
146
147
|
if (!isCodexWsPreOutputRefusal(response) || codexWsPreOutputRefusalOutcomeRecorded(response)) return;
|
|
147
|
-
if (!usesCodexForwardPoolAuth(authCtx, provider)) return;
|
|
148
|
+
if (!usesCodexForwardPoolAuth(authCtx, provider) || wasPermanentAccountRefusal(response)) return;
|
|
148
149
|
noteCodexWsPreOutputRefusalOutcome(response);
|
|
149
150
|
recordCodexUpstreamOutcome(config, authCtx.accountId, response.status, {
|
|
151
|
+
...codexFailureWindowOutcomeMeta(authCtx),
|
|
150
152
|
threadId: authCtx.affinityKey,
|
|
151
153
|
fixedAccount: authCtx.fixedAccount,
|
|
152
154
|
modelId,
|
|
@@ -675,6 +677,7 @@ export async function retryCodexPoolOnAlternateAccount(
|
|
|
675
677
|
if (codexWsPreOutputRefusalOutcomeRecorded(firstResponse)) return;
|
|
676
678
|
noteCodexWsPreOutputRefusalOutcome(firstResponse);
|
|
677
679
|
recordCodexUpstreamOutcome(config, firstAuthCtx.accountId, outcomeStatus, {
|
|
680
|
+
...codexFailureWindowOutcomeMeta(firstAuthCtx),
|
|
678
681
|
threadId: firstAuthCtx.affinityKey,
|
|
679
682
|
fixedAccount: firstAuthCtx.fixedAccount,
|
|
680
683
|
modelId: route.modelId,
|
|
@@ -692,6 +695,7 @@ export async function retryCodexPoolOnAlternateAccount(
|
|
|
692
695
|
if (outcomeStatus === firstResponse.status || (outcomeStatus !== 429 && outcomeStatus !== 402)) return;
|
|
693
696
|
recordCodexUpstreamOutcome(config, firstAuthCtx.accountId, outcomeStatus, {
|
|
694
697
|
...codexQuotaOutcomeMeta(firstResponse),
|
|
698
|
+
...codexFailureWindowOutcomeMeta(firstAuthCtx),
|
|
695
699
|
threadId: firstAuthCtx.affinityKey,
|
|
696
700
|
modelId: route.modelId,
|
|
697
701
|
probeLeaseId: codexProbeLeaseId(firstAuthCtx),
|
|
@@ -855,8 +859,13 @@ export async function retryCodexPoolOnAlternateAccount(
|
|
|
855
859
|
options.deferCodexResetDerivedCooldown,
|
|
856
860
|
);
|
|
857
861
|
const recordFirstOutcome = (): void => {
|
|
862
|
+
if (wasPermanentAccountRefusal(firstResponse)) {
|
|
863
|
+
noteCodexWsPreOutputRefusalOutcome(firstResponse);
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
858
866
|
recordCodexUpstreamOutcome(config, firstAuthCtx.accountId, outcomeStatus, {
|
|
859
867
|
...quotaMeta,
|
|
868
|
+
...codexFailureWindowOutcomeMeta(firstAuthCtx),
|
|
860
869
|
threadId: firstAuthCtx.affinityKey,
|
|
861
870
|
modelId: route.modelId,
|
|
862
871
|
probeLeaseId: codexProbeLeaseId(firstAuthCtx),
|
|
@@ -1032,6 +1041,7 @@ export async function retryCodexPoolOnAlternateAccount(
|
|
|
1032
1041
|
providerName: route.providerName,
|
|
1033
1042
|
modelId: route.modelId,
|
|
1034
1043
|
preOutputRefusalHold: args.preOutputRefusalHold,
|
|
1044
|
+
accountAutoPause: (name, provider, headers) => captureAccountAutoPauseAttempt(name, provider, { config, codex: retryAuthCtx }, headers),
|
|
1035
1045
|
onCodexWsQuota: codexWsQuotaObserver(retryAuthCtx, route.provider, route.modelId),
|
|
1036
1046
|
beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
|
|
1037
1047
|
? createCodexAuthDispatchGuard(retryAuthCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
|
|
@@ -1143,6 +1153,7 @@ export function codexForwardTerminalOutcomeRecorder(
|
|
|
1143
1153
|
logCtx?: RequestLogContext,
|
|
1144
1154
|
): ((status: ResponsesTerminalStatus, httpStatusOverride?: number) => void) | undefined {
|
|
1145
1155
|
if (!usesCodexForwardPoolAuth(authCtx, provider)) return undefined;
|
|
1156
|
+
const windowMeta = codexFailureWindowOutcomeMeta(authCtx);
|
|
1146
1157
|
return (status, httpStatusOverride) => {
|
|
1147
1158
|
const quotaStatus = [httpStatusOverride, logCtx?.terminalHttpStatus]
|
|
1148
1159
|
.find(value => value === 429 || value === 402);
|
|
@@ -1151,6 +1162,7 @@ export function codexForwardTerminalOutcomeRecorder(
|
|
|
1151
1162
|
// request. Don't penalize account health; record success to clear any
|
|
1152
1163
|
// prior soft-avoid so a healthy account isn't stuck avoided.
|
|
1153
1164
|
recordCodexUpstreamOutcome(config, authCtx.accountId, 200, {
|
|
1165
|
+
...windowMeta,
|
|
1154
1166
|
threadId: authCtx.affinityKey,
|
|
1155
1167
|
fixedAccount: authCtx.fixedAccount,
|
|
1156
1168
|
modelId,
|
|
@@ -1174,6 +1186,7 @@ export function codexForwardTerminalOutcomeRecorder(
|
|
|
1174
1186
|
? 200
|
|
1175
1187
|
: (quotaStatus ?? httpStatusOverride ?? logCtx?.terminalHttpStatus ?? 502);
|
|
1176
1188
|
recordCodexUpstreamOutcome(config, authCtx.accountId, outcome, {
|
|
1189
|
+
...windowMeta,
|
|
1177
1190
|
threadId: authCtx.affinityKey,
|
|
1178
1191
|
fixedAccount: authCtx.fixedAccount,
|
|
1179
1192
|
modelId,
|
|
@@ -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 {
|
|
@@ -589,7 +591,19 @@ export async function executeComboResponses(
|
|
|
589
591
|
waitForCooldownMs: combo.waitForCooldownMs,
|
|
590
592
|
abortSignal: options.abortSignal,
|
|
591
593
|
});
|
|
592
|
-
|
|
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({
|
|
593
607
|
eligible: targetEligible,
|
|
594
608
|
now: initialNow,
|
|
595
609
|
});
|
|
@@ -625,7 +639,7 @@ export async function executeComboResponses(
|
|
|
625
639
|
: comboUnavailable(comboId, undefined, comboExclusionReasons(config, comboId, targetEligible));
|
|
626
640
|
}
|
|
627
641
|
let jevDecision: JevDecision | undefined;
|
|
628
|
-
if (combo.strategy === "jev") {
|
|
642
|
+
if (combo.strategy === "jev" && !toolLoopPin) {
|
|
629
643
|
const choices = eligibleJevComboChoices(config, comboId, targetEligible, Date.now());
|
|
630
644
|
const first = choices[0];
|
|
631
645
|
if (!first) {
|
|
@@ -653,6 +667,7 @@ export async function executeComboResponses(
|
|
|
653
667
|
isDestinationAllowed: (providerName, modelId) => routeAllowedByScope(
|
|
654
668
|
resolveAdmissionModelScope(config, options.admission), { providerName, modelId },
|
|
655
669
|
),
|
|
670
|
+
...(combo.decisionQuotaSignals === true ? { decisionQuotaSignals: true, decisionQuotaTiers: combo.decisionQuotaTiers } : {}),
|
|
656
671
|
...(combo.decisionProvider ? { decisionProvider: combo.decisionProvider } : {}),
|
|
657
672
|
...(combo.decisionModel
|
|
658
673
|
? {
|
|
@@ -696,6 +711,7 @@ export async function executeComboResponses(
|
|
|
696
711
|
? { chosenProbability: decision.chosenProbability }
|
|
697
712
|
: {}),
|
|
698
713
|
...(decision.usage ? { usage: decision.usage } : {}),
|
|
714
|
+
...(decision.quota ? { quota: decision.quota } : {}),
|
|
699
715
|
});
|
|
700
716
|
console.debug("[combo] JEV decision", {
|
|
701
717
|
backend: decision.backend,
|
|
@@ -713,8 +729,11 @@ export async function executeComboResponses(
|
|
|
713
729
|
// One immutable combo selection trace, before any child dispatch; child
|
|
714
730
|
// adoption below must never replace it with a concrete child route trace.
|
|
715
731
|
const decision = comboRouteDecisionTrace(config, comboId, pick, requestedModel);
|
|
716
|
-
|
|
717
|
-
?
|
|
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 } }
|
|
718
737
|
: decision;
|
|
719
738
|
|
|
720
739
|
const originalReasoning = body && typeof body === "object" && !Array.isArray(body)
|
|
@@ -1146,6 +1165,9 @@ export async function executeComboResponses(
|
|
|
1146
1165
|
: comboFailureDecision(failure.response.status, failure.classificationText, {
|
|
1147
1166
|
code: failure.upstreamCode,
|
|
1148
1167
|
codexModelRefusal: failure.codexModelRefusal,
|
|
1168
|
+
// The adapter the failed leg actually resolved: a configured `kiro` provider can route a
|
|
1169
|
+
// model through another wire via `modelAdapters`, and an unresolved leg grants no hop.
|
|
1170
|
+
failedAdapter: childLog.providerAdapter,
|
|
1149
1171
|
});
|
|
1150
1172
|
const wantsStream = (rawBody as { stream?: unknown } | null)?.stream === true;
|
|
1151
1173
|
// Local byte admission has its own diagnostic; do not relabel it as an upstream refusal.
|
|
@@ -1193,8 +1215,15 @@ export async function executeComboResponses(
|
|
|
1193
1215
|
}
|
|
1194
1216
|
return lastFailure;
|
|
1195
1217
|
}
|
|
1218
|
+
// A bare status made a revoked credential read as a provider outage. Log only the parsed
|
|
1219
|
+
// code and message, never the raw envelope, as one redacted, bounded line.
|
|
1220
|
+
const failureReason = sanitizeLogMetadataString(
|
|
1221
|
+
redactSecretString([failure.upstreamCode, failure.upstreamMessage].filter(Boolean).join(": ")).replace(/\s+/g, " "),
|
|
1222
|
+
200,
|
|
1223
|
+
);
|
|
1196
1224
|
console.warn(
|
|
1197
|
-
`[combo] ${comboId}: ${targetKey(pick.target)} failed with ${failure.response.status}
|
|
1225
|
+
`[combo] ${comboId}: ${targetKey(pick.target)} failed with ${failure.response.status}`
|
|
1226
|
+
+ ` after ${Date.now() - started}ms${failureReason ? `: ${failureReason}` : ""}`,
|
|
1198
1227
|
);
|
|
1199
1228
|
const failureNow = Date.now();
|
|
1200
1229
|
const attemptedTargets = pick.attempted;
|