@bitkyc08/opencodex 2.88.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-B5bC7AWL.js → Tray-7z1COnmz.js} +1 -1
- package/gui/dist/assets/{index-B5bsvxor.js → index-uchD-ngf.js} +13 -13
- package/gui/dist/assets/{quota-summary-D8y2wTsB.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.ts +10 -3
- package/src/adapters/coding-agent/protocol.ts +6 -0
- package/src/adapters/google-http.ts +3 -2
- package/src/adapters/google-vertex-endpoint.ts +67 -0
- package/src/adapters/google.ts +35 -29
- 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 +11 -1
- package/src/bridge/response-json.ts +2 -2
- package/src/bridge/sse.ts +10 -4
- 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/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-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-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/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/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 +344 -144
- package/src/images/loop.ts +32 -9
- 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/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/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/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 +6 -72
- 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/web-search-replay.ts +138 -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 +28 -13
- package/src/server/claude-messages.ts +2 -1
- package/src/server/index/optional-listeners.ts +6 -0
- 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 +18 -4
- 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-dispatch.ts +77 -28
- package/src/server/responses/combo-session-recall.ts +25 -0
- package/src/server/responses/combo-stream-preflight.ts +3 -1
- package/src/server/responses/combo-tool-loop-pin.ts +70 -0
- package/src/server/responses/compact.ts +8 -5
- package/src/server/responses/context-overflow.ts +141 -2
- package/src/server/responses/core-codex-account.ts +7 -1
- package/src/server/responses/core-combo.ts +31 -5
- 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-observations.ts +38 -0
- package/src/server/responses/request-compression.ts +98 -0
- package/src/server/responses/request-transport.ts +32 -3
- 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/types/account-auto-pause.ts +15 -0
- package/src/types/config.ts +23 -1
- package/src/types/provider.ts +9 -5
- package/src/types/request.ts +20 -2
- package/src/types.ts +1 -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 +37 -11
- 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
|
@@ -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
|
*
|
|
@@ -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";
|
|
@@ -144,7 +145,7 @@ 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, {
|
|
150
151
|
threadId: authCtx.affinityKey,
|
|
@@ -855,6 +856,10 @@ export async function retryCodexPoolOnAlternateAccount(
|
|
|
855
856
|
options.deferCodexResetDerivedCooldown,
|
|
856
857
|
);
|
|
857
858
|
const recordFirstOutcome = (): void => {
|
|
859
|
+
if (wasPermanentAccountRefusal(firstResponse)) {
|
|
860
|
+
noteCodexWsPreOutputRefusalOutcome(firstResponse);
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
858
863
|
recordCodexUpstreamOutcome(config, firstAuthCtx.accountId, outcomeStatus, {
|
|
859
864
|
...quotaMeta,
|
|
860
865
|
threadId: firstAuthCtx.affinityKey,
|
|
@@ -1032,6 +1037,7 @@ export async function retryCodexPoolOnAlternateAccount(
|
|
|
1032
1037
|
providerName: route.providerName,
|
|
1033
1038
|
modelId: route.modelId,
|
|
1034
1039
|
preOutputRefusalHold: args.preOutputRefusalHold,
|
|
1040
|
+
accountAutoPause: (name, provider, headers) => captureAccountAutoPauseAttempt(name, provider, { config, codex: retryAuthCtx }, headers),
|
|
1035
1041
|
onCodexWsQuota: codexWsQuotaObserver(retryAuthCtx, route.provider, route.modelId),
|
|
1036
1042
|
beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
|
|
1037
1043
|
? createCodexAuthDispatchGuard(retryAuthCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
|
|
@@ -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)
|
|
@@ -1193,8 +1212,15 @@ export async function executeComboResponses(
|
|
|
1193
1212
|
}
|
|
1194
1213
|
return lastFailure;
|
|
1195
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
|
+
);
|
|
1196
1221
|
console.warn(
|
|
1197
|
-
`[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}` : ""}`,
|
|
1198
1224
|
);
|
|
1199
1225
|
const failureNow = Date.now();
|
|
1200
1226
|
const attemptedTargets = pick.attempted;
|
|
@@ -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) {
|