@bitkyc08/opencodex 2.56.0 → 2.58.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/bin/ocx.mjs +10 -0
- package/gui/dist/assets/{index-D4zuyIxQ.js → index-BbrHOIY0.js} +21 -21
- package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/adapters/codebuddy/adapter.ts +2 -1
- package/src/adapters/codebuddy/scaffold-guard.ts +249 -0
- package/src/adapters/command-code.ts +12 -3
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/discovery.ts +65 -1
- package/src/adapters/cursor/envelope-echo.ts +8 -2
- package/src/adapters/cursor/live-transport.ts +5 -1
- package/src/adapters/cursor/protobuf-events.ts +110 -11
- package/src/adapters/cursor/protobuf-request.ts +19 -1
- package/src/adapters/cursor/text-toolcall.ts +230 -0
- package/src/adapters/cursor/thread-continuity.ts +67 -0
- package/src/adapters/cursor/types.ts +5 -0
- package/src/adapters/cursor.ts +55 -5
- package/src/adapters/google-http.ts +38 -13
- package/src/adapters/google.ts +7 -7
- package/src/adapters/kiro/payload.ts +17 -3
- package/src/adapters/kiro/reasoning.ts +70 -7
- package/src/adapters/kiro/stream.ts +8 -2
- package/src/adapters/kiro/wire.ts +2 -1
- package/src/adapters/kiro-events.ts +21 -13
- package/src/adapters/mimo-free.ts +32 -17
- package/src/adapters/ollama-native.ts +42 -8
- package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
- package/src/adapters/openai-chat/tool-schema.ts +25 -7
- package/src/adapters/openai-chat.ts +8 -8
- package/src/adapters/openai-responses/passthrough.ts +62 -5
- package/src/adapters/openai-responses/request-strips.ts +43 -0
- package/src/adapters/physical-send.ts +50 -0
- package/src/bridge/errors.ts +26 -2
- package/src/bridge/response-json.ts +8 -2
- package/src/bridge/sse.ts +20 -2
- package/src/claude/desktop-profile.ts +66 -9
- package/src/claude/outbound.ts +32 -4
- package/src/cli/account-main.ts +1 -1
- package/src/cli/capabilities.ts +2 -2
- package/src/cli/combo.ts +10 -1
- package/src/cli/config-command.ts +35 -18
- package/src/cli/dispatch.ts +17 -4
- package/src/cli/index.ts +92 -7
- package/src/cli/registry.ts +2 -1
- package/src/cli/system-command.ts +74 -5
- package/src/cli/uninstall-client-state.ts +12 -0
- package/src/clients/config-export.ts +7 -3
- package/src/codex/account-label.ts +14 -3
- package/src/codex/account-store.ts +113 -26
- package/src/codex/account-usability.ts +21 -0
- package/src/codex/auth-api/login-flow.ts +14 -2
- package/src/codex/auth-api/reset-credit-service.ts +11 -2
- package/src/codex/auth-context.ts +199 -15
- package/src/codex/catalog/aggregation.ts +80 -1
- package/src/codex/catalog/model-visibility.ts +1 -0
- package/src/codex/catalog/remote.ts +30 -0
- package/src/codex/catalog/retained-sync.ts +9 -1
- package/src/codex/catalog/routed-gather.ts +38 -1
- package/src/codex/cli-install-provenance.ts +7 -1
- package/src/codex/convergence.ts +7 -2
- package/src/codex/desktop-app/types.ts +11 -2
- package/src/codex/desktop-app/windows.ts +5 -5
- package/src/codex/desktop-switches.ts +145 -0
- package/src/codex/history-job.ts +5 -1
- package/src/codex/history-provider.ts +33 -4
- package/src/codex/history-worker.ts +14 -1
- package/src/codex/inject/remove.ts +145 -7
- package/src/codex/inject/restore.ts +231 -32
- package/src/codex/inject.ts +12 -16
- package/src/codex/loopback-target.ts +9 -0
- package/src/codex/model-entitlements.ts +152 -15
- package/src/codex/native-profile-startup.ts +64 -20
- package/src/codex/pool-refresh-backoff.ts +12 -3
- package/src/codex/quota-rejection.ts +104 -15
- package/src/codex/routing/cache-affinity.ts +70 -0
- package/src/codex/routing/cooldown-math.ts +10 -0
- package/src/codex/routing/selection.ts +79 -2
- package/src/codex/routing/thread-affinity.ts +50 -2
- package/src/codex/routing/transient-hold-dispatch.ts +141 -0
- package/src/codex/routing.ts +29 -49
- package/src/codex/warmup.ts +1 -1
- package/src/combos/failover.ts +85 -0
- package/src/combos/request.ts +17 -10
- package/src/combos/types.ts +23 -2
- package/src/config/atomic-write.ts +83 -8
- package/src/config/pending-teardown.ts +31 -0
- package/src/config/schema/config-schema.ts +2 -0
- package/src/config/schema/leaf-validators.ts +1 -0
- package/src/generated/compatibility-version.json +272 -180
- package/src/images/loop.ts +1 -1
- package/src/lib/bounded-subprocess.ts +62 -10
- package/src/lib/errors.ts +17 -0
- package/src/lib/request-execution-budget.ts +147 -21
- package/src/lib/spend-reservation-ledger.ts +18 -0
- package/src/lib/state-store-registrations.ts +6 -2
- package/src/lib/test-home-guard.ts +85 -1
- package/src/lib/upstream-retry.ts +77 -10
- package/src/lib/windows-elevation.ts +76 -14
- package/src/lib/windows-secret-acl.ts +151 -15
- package/src/lib/windows-user-principal.ts +5 -1
- package/src/oauth/index.ts +2 -2
- package/src/oauth/key-providers.ts +2 -2
- package/src/providers/derive.ts +6 -0
- package/src/providers/kiro-models.ts +4 -3
- package/src/providers/label.ts +19 -1
- package/src/providers/model-discovery.ts +35 -7
- package/src/providers/registry/entries-core.ts +18 -0
- package/src/providers/registry/entries-extended.ts +59 -28
- package/src/providers/registry/model-seeds.ts +71 -17
- package/src/providers/registry/types.ts +9 -0
- package/src/responses/reasoning-envelope.ts +6 -3
- package/src/responses/spill-store.ts +17 -0
- package/src/responses/state/body-policy.ts +25 -0
- package/src/responses/state/spill-queue.ts +8 -6
- package/src/responses/state.ts +3 -22
- package/src/router.ts +4 -0
- package/src/routing/identity-domains.ts +21 -14
- package/src/routing/probe-lease.ts +103 -1
- package/src/server/auth-cors.ts +1 -0
- package/src/server/chat-completions.ts +3 -1
- package/src/server/chat-native.ts +37 -9
- package/src/server/index/live-sideband.ts +37 -1
- package/src/server/index/websocket-handler.ts +54 -3
- package/src/server/index.ts +5 -5
- package/src/server/inspection-tee.ts +107 -0
- package/src/server/live.ts +46 -1
- package/src/server/management/combo-routes.ts +10 -1
- package/src/server/management/config-routes.ts +27 -5
- package/src/server/models-capabilities.ts +24 -3
- package/src/server/relay-eager.ts +2 -0
- package/src/server/relay.ts +14 -19
- package/src/server/request-log.ts +127 -3
- package/src/server/response-log-body.ts +153 -0
- package/src/server/responses/account-change-state.ts +74 -0
- package/src/server/responses/adapter-continuation.ts +33 -7
- package/src/server/responses/adapter-delivery.ts +5 -11
- package/src/server/responses/adapter-dispatch.ts +84 -13
- package/src/server/responses/codex-ws-exchange.ts +65 -4
- package/src/server/responses/codex-ws-wire.ts +5 -0
- package/src/server/responses/collaboration.ts +74 -4
- package/src/server/responses/combo-session-recall.ts +68 -8
- package/src/server/responses/combo-stream-preflight.ts +68 -5
- package/src/server/responses/compact.ts +54 -13
- package/src/server/responses/core-auth.ts +2 -0
- package/src/server/responses/core-codex-account.ts +51 -3
- package/src/server/responses/core-combo.ts +129 -23
- package/src/server/responses/core-errors.ts +18 -0
- package/src/server/responses/core-options.ts +3 -0
- package/src/server/responses/core-replay.ts +105 -32
- package/src/server/responses/core.ts +3 -3
- package/src/server/responses/encrypted-payload.ts +0 -1
- package/src/server/responses/fetch-helpers.ts +4 -1
- package/src/server/responses/input-admission.ts +126 -6
- package/src/server/responses/native-injection-protocol.ts +42 -0
- package/src/server/responses/native-injection-replay.ts +105 -0
- package/src/server/responses/native-injection.ts +242 -0
- package/src/server/responses/native-response-control.ts +56 -0
- package/src/server/responses/native-response-json.ts +14 -0
- package/src/server/responses/native-response-output.ts +37 -0
- package/src/server/responses/native-steering-log.ts +44 -0
- package/src/server/responses/native-steering-policy.ts +49 -0
- package/src/server/responses/native-steering-replay.ts +126 -0
- package/src/server/responses/native-steering-settings.ts +76 -0
- package/src/server/responses/native-steering.ts +400 -0
- package/src/server/responses/native-tool-results.ts +130 -0
- package/src/server/responses/passthrough-delivery.ts +30 -6
- package/src/server/responses/passthrough-dispatch.ts +61 -11
- package/src/server/responses/passthrough-error.ts +38 -2
- package/src/server/responses/request-prepare.ts +173 -22
- package/src/server/responses/request-send-budget.ts +97 -2
- package/src/server/responses/request-spend.ts +147 -0
- package/src/server/responses/request-transport.ts +62 -3
- package/src/server/responses/run-turn-execution.ts +59 -31
- package/src/server/responses/sidecar-execution.ts +7 -13
- package/src/server/responses/terminal-guard.ts +65 -4
- package/src/server/responses/ws-upstream.ts +21 -1
- package/src/server/responses-undeclared-tool-guard.ts +9 -5
- package/src/server/stop-teardown.ts +8 -1
- package/src/server/ws-bridge.ts +16 -1
- package/src/service/cli.ts +13 -1
- package/src/service/windows-ops.ts +210 -16
- package/src/service/windows-scheduler.ts +28 -21
- package/src/service.ts +1 -1
- package/src/types/config.ts +8 -1
- package/src/types/provider.ts +13 -0
- package/src/types/request.ts +8 -5
- package/src/types/tools.ts +24 -0
- package/src/types.ts +2 -0
- package/src/update/index.ts +10 -0
- package/src/update/stop-contract.d.mts +1 -0
- package/src/update/stop-contract.mjs +19 -0
- package/src/update/stop-decision.d.mts +1 -1
- package/src/update/stop-decision.mjs +12 -3
- package/src/usage/log.ts +1 -1
- package/src/vision/anthropic-describe.ts +1 -1
- package/src/vision/describe.ts +5 -5
- package/src/web-search/anthropic-executor.ts +1 -1
- package/src/web-search/exa-executor.ts +1 -1
- package/src/web-search/executor.ts +1 -1
- package/src/web-search/gemini-executor.ts +1 -1
- package/src/web-search/loop.ts +1 -1
- package/src/web-search/ollama-executor.ts +1 -1
- package/src/web-search/parse.ts +67 -14
- package/src/web-search/passthrough-bridge.ts +64 -31
- package/src/web-search/xai-executor.ts +1 -1
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { ResponsesTerminalStatus } from "../../bridge";
|
|
2
|
+
import { comboFailureDecision } from "../../combos";
|
|
3
|
+
import { httpStatusFromTerminalError } from "../../lib/errors";
|
|
2
4
|
import type { RequestLogContext } from "../request-log";
|
|
3
5
|
import { createSseInspector } from "../relay";
|
|
4
6
|
import { MAX_CLIENT_SSE_FRAME_BYTES } from "../sse-frame-buffer";
|
|
@@ -30,12 +32,67 @@ const RETRYABLE_ZERO_OUTPUT_INCOMPLETE_REASONS = new Set([
|
|
|
30
32
|
"upstream_stall_timeout",
|
|
31
33
|
]);
|
|
32
34
|
|
|
35
|
+
function bareErrorStatus(payload: unknown): number | undefined {
|
|
36
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return undefined;
|
|
37
|
+
const event = payload as Record<string, unknown>;
|
|
38
|
+
if (event.type !== "error") return undefined;
|
|
39
|
+
const nested = event.error;
|
|
40
|
+
const error = nested && typeof nested === "object" && !Array.isArray(nested)
|
|
41
|
+
? nested as Record<string, unknown>
|
|
42
|
+
: event;
|
|
43
|
+
const explicitStatus = [
|
|
44
|
+
event.status,
|
|
45
|
+
event.status_code,
|
|
46
|
+
event.http_status,
|
|
47
|
+
error.status,
|
|
48
|
+
error.status_code,
|
|
49
|
+
error.http_status,
|
|
50
|
+
]
|
|
51
|
+
.map(value => typeof value === "number" && Number.isInteger(value)
|
|
52
|
+
? value
|
|
53
|
+
: typeof value === "string" && /^\d{3}$/.test(value.trim())
|
|
54
|
+
? Number(value)
|
|
55
|
+
: undefined)
|
|
56
|
+
.find(value => value !== undefined && value >= 400 && value <= 599);
|
|
57
|
+
const code = typeof error.code === "string"
|
|
58
|
+
? error.code
|
|
59
|
+
: typeof event.code === "string" ? event.code : null;
|
|
60
|
+
if (explicitStatus === undefined && code === "invalid_request_error") return 400;
|
|
61
|
+
return explicitStatus ?? httpStatusFromTerminalError({
|
|
62
|
+
type: typeof error.type === "string" && error.type !== "error" ? error.type : undefined,
|
|
63
|
+
code,
|
|
64
|
+
message: typeof error.message === "string"
|
|
65
|
+
? error.message
|
|
66
|
+
: typeof event.message === "string" ? event.message : undefined,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function bareErrorIsRetryable(payload: unknown): boolean {
|
|
71
|
+
const status = bareErrorStatus(payload);
|
|
72
|
+
if (status === undefined || !payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
const event = payload as Record<string, unknown>;
|
|
76
|
+
const nested = event.error;
|
|
77
|
+
const error = nested && typeof nested === "object" && !Array.isArray(nested)
|
|
78
|
+
? nested as Record<string, unknown>
|
|
79
|
+
: event;
|
|
80
|
+
const code = typeof error.code === "string"
|
|
81
|
+
? error.code
|
|
82
|
+
: typeof event.code === "string" ? event.code : null;
|
|
83
|
+
const message = typeof error.message === "string"
|
|
84
|
+
? error.message
|
|
85
|
+
: typeof event.message === "string" ? event.message : "";
|
|
86
|
+
return comboFailureDecision(status, message, { code }) === "hop";
|
|
87
|
+
}
|
|
88
|
+
|
|
33
89
|
function retryableZeroOutputTerminal(payload: unknown): boolean {
|
|
34
90
|
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
|
|
35
91
|
const event = payload as {
|
|
36
92
|
type?: unknown;
|
|
37
93
|
response?: { incomplete_details?: { reason?: unknown } };
|
|
38
94
|
};
|
|
95
|
+
if (bareErrorIsRetryable(event)) return true;
|
|
39
96
|
if (event.type === "response.failed") return true;
|
|
40
97
|
if (event.type !== "response.incomplete") return false;
|
|
41
98
|
const reason = event.response?.incomplete_details?.reason;
|
|
@@ -95,12 +152,17 @@ function failedTerminalResponse(
|
|
|
95
152
|
? nested as Record<string, unknown>
|
|
96
153
|
: {};
|
|
97
154
|
const nestedError = terminalResponse.error;
|
|
155
|
+
const topLevelError = terminalPayload.error;
|
|
98
156
|
const error = nestedError && typeof nestedError === "object" && !Array.isArray(nestedError)
|
|
99
157
|
? nestedError as Record<string, unknown>
|
|
158
|
+
: topLevelError && typeof topLevelError === "object" && !Array.isArray(topLevelError)
|
|
159
|
+
? topLevelError as Record<string, unknown>
|
|
100
160
|
: {
|
|
101
161
|
type: "upstream_error",
|
|
102
162
|
code: "upstream_server_error",
|
|
103
|
-
message:
|
|
163
|
+
message: typeof terminalPayload.message === "string"
|
|
164
|
+
? terminalPayload.message
|
|
165
|
+
: logCtx.upstreamError ?? "Provider stream failed before producing output",
|
|
104
166
|
};
|
|
105
167
|
const headers = new Headers(response.headers);
|
|
106
168
|
headers.set("content-type", "application/json");
|
|
@@ -117,7 +179,7 @@ function failedTerminalResponse(
|
|
|
117
179
|
...(usage && typeof usage === "object" && !Array.isArray(usage) ? { usage } : {}),
|
|
118
180
|
},
|
|
119
181
|
}), {
|
|
120
|
-
status: logCtx.terminalHttpStatus ?? 502,
|
|
182
|
+
status: logCtx.terminalHttpStatus ?? bareErrorStatus(terminalPayload) ?? 502,
|
|
121
183
|
headers,
|
|
122
184
|
});
|
|
123
185
|
}
|
|
@@ -154,11 +216,12 @@ export async function preflightComboStreamResponse(
|
|
|
154
216
|
const inspector = createSseInspector({
|
|
155
217
|
logCtx,
|
|
156
218
|
onParsedPayload: payload => {
|
|
219
|
+
if (terminalStatus !== undefined || outputCommitted || retryableTerminalPayload) return;
|
|
157
220
|
const retryable = retryableTerminal(payload);
|
|
158
221
|
const matchedBareError = retryable && payload !== null && typeof payload === "object"
|
|
159
222
|
&& !Array.isArray(payload) && (payload as { type?: unknown }).type === "error";
|
|
160
|
-
//
|
|
161
|
-
//
|
|
223
|
+
// A zero-output bare error is terminal evidence. Explicit client errors stay
|
|
224
|
+
// committed; unknown and retryable upstream failures may advance the combo.
|
|
162
225
|
if (comboStreamPayloadCommitsOutput(payload) && !matchedBareError) outputCommitted = true;
|
|
163
226
|
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return;
|
|
164
227
|
if (retryable) retryableTerminalPayload = payload as Record<string, unknown>;
|
|
@@ -197,7 +260,7 @@ export async function preflightComboStreamResponse(
|
|
|
197
260
|
}
|
|
198
261
|
|
|
199
262
|
// A bare error event is not a protocol terminal (terminalStatus stays undefined),
|
|
200
|
-
// so its
|
|
263
|
+
// so its retryable classification doubles as the terminal evidence.
|
|
201
264
|
if ((terminalStatus === "failed" || terminalStatus === "incomplete"
|
|
202
265
|
|| retryableTerminalPayload?.type === "error")
|
|
203
266
|
&& !outputCommitted && retryableTerminalPayload) {
|
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
resolveCodexAuthContext,
|
|
54
54
|
codexPoolAffinityKey,
|
|
55
55
|
codexProbeLeaseId,
|
|
56
|
+
codexTransientProbeGrant,
|
|
56
57
|
codexProbeQuotaScope,
|
|
57
58
|
releaseCodexAuthContextProbeLease,
|
|
58
59
|
stripCodexRuntimeProviderFields,
|
|
@@ -71,7 +72,9 @@ import {
|
|
|
71
72
|
import {
|
|
72
73
|
applyAccountChangeConversationStateScrub,
|
|
73
74
|
conversationStateBindingFromAuth,
|
|
75
|
+
accountChangeFileReferenceRefusal,
|
|
74
76
|
rememberServingConversationStateIssuer,
|
|
77
|
+
conversationCarriesUploadedFiles,
|
|
75
78
|
} from "./account-change-state";
|
|
76
79
|
import {
|
|
77
80
|
TokenRefreshError,
|
|
@@ -82,6 +85,7 @@ import {
|
|
|
82
85
|
fetchWithResetRetry,
|
|
83
86
|
fetchWithTransientRetry,
|
|
84
87
|
applyUpstreamRecoveryInit,
|
|
88
|
+
isNonReplayableResponse,
|
|
85
89
|
SendBudgetExhaustedError,
|
|
86
90
|
TRANSIENT_RETRY_MAX_ATTEMPTS,
|
|
87
91
|
type UpstreamSendRecovery,
|
|
@@ -140,7 +144,6 @@ import {
|
|
|
140
144
|
catalogModelSupportsServiceTier,
|
|
141
145
|
finishRequestAttempt,
|
|
142
146
|
inspectResponseLogJson,
|
|
143
|
-
noteAttemptSend,
|
|
144
147
|
readConfiguredCodexServiceTier,
|
|
145
148
|
requestLogSpeedLabel,
|
|
146
149
|
sealRequestAttemptIdentity,
|
|
@@ -735,6 +738,14 @@ export async function handleResponsesCompact(
|
|
|
735
738
|
beginCodexAccountSelection: codexAccountSelectionForTurn(turnAdmissionLease),
|
|
736
739
|
signal: req.signal,
|
|
737
740
|
nativeMainRefreshDependencies: options.nativeMainRefreshDependencies,
|
|
741
|
+
// #4778: the same retention the regular Responses path passes at its final auth. The
|
|
742
|
+
// post-429 guard below only declines to move AFTER this resolution has already bound
|
|
743
|
+
// an account, so without the bit here a quota-driven rebind could have carried the
|
|
744
|
+
// conversation off its issuing account before that guard is ever consulted -- and an
|
|
745
|
+
// uploaded file is readable only by the account that received it. Answered from `raw`,
|
|
746
|
+
// the same object and the same predicate the guard below uses, so the two can never
|
|
747
|
+
// disagree about which conversations are in scope.
|
|
748
|
+
retainAccountForUploadedFiles: conversationCarriesUploadedFiles(raw),
|
|
738
749
|
});
|
|
739
750
|
logCtx.accountLogLabel = codexAuthContextLogLabel(authCtx, config);
|
|
740
751
|
const selected = await materializeCodexUpstreamAuthAsync(req.headers, authCtx, {
|
|
@@ -789,6 +800,14 @@ export async function handleResponsesCompact(
|
|
|
789
800
|
{
|
|
790
801
|
const binding = conversationStateBindingFromAuth(authCtx, codexPoolAffinityKey(req.headers));
|
|
791
802
|
if (binding) {
|
|
803
|
+
// Refused rather than scrubbed: an uploaded file is content the caller attached, not
|
|
804
|
+
// continuation state the turn can do without.
|
|
805
|
+
const refusal = accountChangeFileReferenceRefusal({
|
|
806
|
+
body: raw,
|
|
807
|
+
bindingKey: binding.bindingKey,
|
|
808
|
+
servingAccountId: binding.accountId,
|
|
809
|
+
});
|
|
810
|
+
if (refusal) return refusal;
|
|
792
811
|
applyAccountChangeConversationStateScrub({
|
|
793
812
|
body: raw,
|
|
794
813
|
bindingKey: binding.bindingKey,
|
|
@@ -869,6 +888,7 @@ export async function handleResponsesCompact(
|
|
|
869
888
|
// replacement (#2887). Also covers the replay's own second 401.
|
|
870
889
|
...(ctx.kind === "pool" ? { credentialGeneration: ctx.generation } : {}),
|
|
871
890
|
probeQuotaScope: codexProbeQuotaScope(ctx),
|
|
891
|
+
transientProbe: codexTransientProbeGrant(ctx),
|
|
872
892
|
writerGeneration: ctx.kind === "pool" || ctx.kind === "main-pool"
|
|
873
893
|
? ctx.writerGeneration
|
|
874
894
|
: undefined,
|
|
@@ -1068,6 +1088,10 @@ export async function handleResponsesCompact(
|
|
|
1068
1088
|
// — reporting exhausted retries while another pool account sat idle (#913).
|
|
1069
1089
|
if (
|
|
1070
1090
|
(upstream.status === 429 || upstream.status === 402)
|
|
1091
|
+
// A replay refusal this proxy synthesized carries 429 for the client's benefit only.
|
|
1092
|
+
// It is not pool quota evidence, and the alternate account below is another send of a
|
|
1093
|
+
// compact turn that may already have been processed.
|
|
1094
|
+
&& !isNonReplayableResponse(upstream)
|
|
1071
1095
|
&& !storedPool401ReplayAttempted
|
|
1072
1096
|
&& usesCodexForwardPoolAuth(authCtx, route.provider)
|
|
1073
1097
|
&& !authCtx.fixedAccount
|
|
@@ -1082,15 +1106,22 @@ export async function handleResponsesCompact(
|
|
|
1082
1106
|
].filter(Boolean);
|
|
1083
1107
|
// Build the alternate COMPLETELY before cancelling the first body: if construction
|
|
1084
1108
|
// throws, the first rejection is still intact and can be returned to the client.
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1109
|
+
// The same reasoning refuses an uploaded-file move here: no alternate can read a file the
|
|
1110
|
+
// issuing account received, so which one is chosen is irrelevant and asking before the
|
|
1111
|
+
// resolution costs nothing. It also reuses the guarantee the comment above depends on --
|
|
1112
|
+
// the first body is still uncancelled -- so the client gets the original rejection rather
|
|
1113
|
+
// than an inaccessible-file error from account B (#4710).
|
|
1114
|
+
const alternate = conversationCarriesUploadedFiles(raw)
|
|
1115
|
+
? undefined
|
|
1116
|
+
: await resolveAlternateCompactContext({
|
|
1117
|
+
req,
|
|
1118
|
+
admission,
|
|
1119
|
+
config,
|
|
1120
|
+
route,
|
|
1121
|
+
selectedModelId,
|
|
1122
|
+
excludeAccountId: authCtx.accountId,
|
|
1123
|
+
turnAdmissionLease,
|
|
1124
|
+
});
|
|
1094
1125
|
// Resolution can await a credential refresh, so the client may have gone away
|
|
1095
1126
|
// while we were choosing B. Re-check before spending anything: recording A,
|
|
1096
1127
|
// cancelling its body, and sending B are all observable side effects, and B's
|
|
@@ -1193,8 +1224,14 @@ export async function handleResponsesCompact(
|
|
|
1193
1224
|
const bufferedErrorText = buffered.ok
|
|
1194
1225
|
? ""
|
|
1195
1226
|
: await buffered.clone().text().catch(() => "");
|
|
1196
|
-
|
|
1197
|
-
|
|
1227
|
+
// The client-facing 429 of a synthesized replay refusal says nothing about this
|
|
1228
|
+
// account's quota. Pool accounting keeps reading it as the transport failure it is,
|
|
1229
|
+
// which is also what it recorded before the status was corrected for the client.
|
|
1230
|
+
const replayRefused = isNonReplayableResponse(upstream);
|
|
1231
|
+
const explicitQuotaStatus = !replayRefused
|
|
1232
|
+
&& (buffered.status === 429 || buffered.status === 402);
|
|
1233
|
+
const bodyInferredQuota = !replayRefused
|
|
1234
|
+
&& !buffered.ok
|
|
1198
1235
|
&& !explicitQuotaStatus
|
|
1199
1236
|
&& isRateLimitOrQuotaFailureMessage(bufferedErrorText);
|
|
1200
1237
|
const quotaFailure = explicitQuotaStatus || bodyInferredQuota;
|
|
@@ -1207,7 +1244,11 @@ export async function handleResponsesCompact(
|
|
|
1207
1244
|
// A body-confirmed quota failure can arrive behind a generic 5xx. Record it as
|
|
1208
1245
|
// quota evidence; otherwise preserve the real upstream status so a local buffering
|
|
1209
1246
|
// failure after a 200 cannot soft-avoid a healthy account or rotate a thread.
|
|
1210
|
-
recordCompactPoolOutcome(
|
|
1247
|
+
recordCompactPoolOutcome(
|
|
1248
|
+
outcomeCtx,
|
|
1249
|
+
bodyInferredQuota ? 429 : replayRefused ? 502 : upstream.status,
|
|
1250
|
+
{ retryAfter, resetAt },
|
|
1251
|
+
);
|
|
1211
1252
|
// Lift usage and response metadata from the buffered upstream JSON into the
|
|
1212
1253
|
// request log; the routed branch gets the same through handleResponses. The
|
|
1213
1254
|
// synthetic buffer errors are not upstream bodies and stay uninspected.
|
|
@@ -138,6 +138,7 @@ export async function resolveResponsesCodexAuth(
|
|
|
138
138
|
route: RouteResult,
|
|
139
139
|
options: HandleResponsesOptions,
|
|
140
140
|
credentialDomainWasRewritten = false,
|
|
141
|
+
retainAccountForUploadedFiles = false,
|
|
141
142
|
): Promise<ResponsesAuthResolution> {
|
|
142
143
|
try {
|
|
143
144
|
let authInputHeaders = codexRouteCredentialDomainHeaders(
|
|
@@ -232,6 +233,7 @@ export async function resolveResponsesCodexAuth(
|
|
|
232
233
|
resolveCodexModelEntitlements: options.resolveCodexModelEntitlements,
|
|
233
234
|
signal: options.abortSignal,
|
|
234
235
|
nativeMainRefreshDependencies: options.nativeMainRefreshDependencies,
|
|
236
|
+
retainAccountForUploadedFiles,
|
|
235
237
|
});
|
|
236
238
|
options.onCodexAuthContextResolved?.(authCtx);
|
|
237
239
|
} else {
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
resetUpstreamHostHealth,
|
|
18
18
|
} from "../../codex/upstream-host-health";
|
|
19
19
|
import { safeOriginLabel, fetchWithHeaderTimeout, providerFetch } from "./fetch-helpers";
|
|
20
|
+
import { classifyPoolRecoveryDispatch } from "../../routing/probe-lease";
|
|
20
21
|
import { formatErrorResponse } from "../../bridge";
|
|
21
22
|
import { readBoundedResponseBody } from "../../lib/bounded-body";
|
|
22
23
|
import { upstreamErrorMessageFromPayload, isRateLimitOrQuotaFailureMessage } from "../../lib/errors";
|
|
@@ -40,6 +41,7 @@ import { MAIN_CODEX_ACCOUNT_ID } from "../../codex/main-account";
|
|
|
40
41
|
import { slugsEquivalent } from "../../providers/slug-codec";
|
|
41
42
|
import {
|
|
42
43
|
codexProbeLeaseId,
|
|
44
|
+
codexTransientProbeGrant,
|
|
43
45
|
codexProbeQuotaScope,
|
|
44
46
|
releaseCodexAuthContextProbeLease,
|
|
45
47
|
resolveCodexAuthContext,
|
|
@@ -60,13 +62,14 @@ import { bindRouteReasoningReplayScope } from "./core-replay";
|
|
|
60
62
|
import {
|
|
61
63
|
conversationStateBindingFromAuth,
|
|
62
64
|
applyAccountChangeConversationStateScrub,
|
|
65
|
+
conversationCarriesUploadedFiles,
|
|
63
66
|
} from "./account-change-state";
|
|
64
67
|
import {
|
|
65
68
|
recordAdapterReasoning,
|
|
66
69
|
recordAdapterTier,
|
|
67
70
|
sealRequestAttemptIdentity,
|
|
68
71
|
recordAttemptCredentialSource,
|
|
69
|
-
|
|
72
|
+
noteProviderAttemptSend,
|
|
70
73
|
} from "../request-log";
|
|
71
74
|
import { codexAuthContextLogLabel } from "../../codex/account-label";
|
|
72
75
|
import { chargeWorkflowSends } from "../../lib/workflow-budget";
|
|
@@ -221,7 +224,17 @@ export async function shouldRetryCodexPoolAccountQuota(
|
|
|
221
224
|
// A post-send WebSocket gateway status must not become a second account's send; the
|
|
222
225
|
// body carries no quota evidence either, but the marker is the contract, not the prose.
|
|
223
226
|
if (isNonReplayableResponse(response)) return false;
|
|
224
|
-
if (response.status === 402 || response.status === 429)
|
|
227
|
+
if (response.status === 402 || response.status === 429) {
|
|
228
|
+
// Status alone used to authorize the move, which is right for a limit the ACCOUNT owns and
|
|
229
|
+
// wrong for one it merely belongs to. An organization- or project-scoped exhaustion refuses
|
|
230
|
+
// every credential inside that organization, so the second account meets the same counter
|
|
231
|
+
// and the only thing the rotation buys is a second cold prompt prefix (#4546). Positive
|
|
232
|
+
// evidence is required to withhold it: the helper fails closed, so an unreadable or
|
|
233
|
+
// ambiguous body keeps the broad #584 behaviour unchanged, and `rate_limit_exceeded`,
|
|
234
|
+
// `slow_down` and plan-level exhaustion still rotate exactly as before.
|
|
235
|
+
const { codexScopedExhaustionCode } = await import("../../codex/quota-rejection");
|
|
236
|
+
return await codexScopedExhaustionCode(response, { signal }) === undefined;
|
|
237
|
+
}
|
|
225
238
|
if (response.status < 500 || response.status >= 600) return false;
|
|
226
239
|
try {
|
|
227
240
|
// Reject malformed UTF-8 instead of matching quota words around replacement characters.
|
|
@@ -459,6 +472,7 @@ export async function retryCodexPoolOnAlternateAccount(
|
|
|
459
472
|
modelId: route.modelId,
|
|
460
473
|
probeLeaseId: codexProbeLeaseId(firstAuthCtx),
|
|
461
474
|
probeQuotaScope: codexProbeQuotaScope(firstAuthCtx),
|
|
475
|
+
transientProbe: codexTransientProbeGrant(firstAuthCtx),
|
|
462
476
|
writerGeneration: firstAuthCtx.writerGeneration,
|
|
463
477
|
});
|
|
464
478
|
};
|
|
@@ -489,6 +503,17 @@ export async function retryCodexPoolOnAlternateAccount(
|
|
|
489
503
|
recordUnmovedTransientOutcome();
|
|
490
504
|
return { kind: "no-alternate" };
|
|
491
505
|
}
|
|
506
|
+
// An uploaded file is readable only by the account it was sent to, so NO alternate can serve
|
|
507
|
+
// this body. Which account would be chosen does not change that, which is why this asks before
|
|
508
|
+
// the resolution rather than after it: refusing here reserves no send, cancels no response, and
|
|
509
|
+
// leaves the caller holding the first account's rejection to return unchanged (#4710). The
|
|
510
|
+
// initial-dispatch sites answer with a 400 instead, because there is no earlier response there
|
|
511
|
+
// to fall back to. A same-account replay -- the gated-model 400 ladder above -- is unaffected,
|
|
512
|
+
// since it never leaves the issuing account.
|
|
513
|
+
if (!retryAuthCtx && conversationCarriesUploadedFiles(parsed._rawBody)) {
|
|
514
|
+
recordUnmovedTransientOutcome();
|
|
515
|
+
return { kind: "no-alternate" };
|
|
516
|
+
}
|
|
492
517
|
// An account move is the guarded profile's fourth send and draws the single shared
|
|
493
518
|
// final-recovery reserve. Nothing bounded it per request before: `excludeAccountId` excludes
|
|
494
519
|
// only the account that just failed, and the caller's recovery loop can return here after the
|
|
@@ -557,6 +582,7 @@ export async function retryCodexPoolOnAlternateAccount(
|
|
|
557
582
|
modelId: route.modelId,
|
|
558
583
|
probeLeaseId: codexProbeLeaseId(firstAuthCtx),
|
|
559
584
|
probeQuotaScope: codexProbeQuotaScope(firstAuthCtx),
|
|
585
|
+
transientProbe: codexTransientProbeGrant(firstAuthCtx),
|
|
560
586
|
writerGeneration: firstAuthCtx.writerGeneration,
|
|
561
587
|
});
|
|
562
588
|
}
|
|
@@ -588,6 +614,7 @@ export async function retryCodexPoolOnAlternateAccount(
|
|
|
588
614
|
modelId: route.modelId,
|
|
589
615
|
probeLeaseId: codexProbeLeaseId(firstAuthCtx),
|
|
590
616
|
probeQuotaScope: codexProbeQuotaScope(firstAuthCtx),
|
|
617
|
+
transientProbe: codexTransientProbeGrant(firstAuthCtx),
|
|
591
618
|
writerGeneration: firstAuthCtx.writerGeneration,
|
|
592
619
|
// Retry already advanced the RR ring via excludeAccountId — reuse for promotion.
|
|
593
620
|
...(retryAuthCtx.accountId ? { promoteAccountId: retryAuthCtx.accountId } : {}),
|
|
@@ -693,16 +720,35 @@ export async function retryCodexPoolOnAlternateAccount(
|
|
|
693
720
|
// The same-account gated-model 400 ladder below keeps its own `maxRetrySends` bound and
|
|
694
721
|
// does not take the reserve again; only the move itself does.
|
|
695
722
|
if (accountMovePermit) {
|
|
723
|
+
// The pool-wide recovery window is consulted BEFORE the request-local permit is used.
|
|
724
|
+
// `reserveDispatch` charges at reservation time and `release()` is the only way back, so
|
|
725
|
+
// using the permit first and refusing afterwards would spend a send the request never
|
|
726
|
+
// made. An account move is recovery traffic like any other: one request's own budget
|
|
727
|
+
// cannot see that a thousand other requests are moving at the same moment, which is
|
|
728
|
+
// precisely the amplification this window exists to bound (#4701).
|
|
729
|
+
//
|
|
730
|
+
// A refusal here is not a new failure mode: "no alternate was available" is already the
|
|
731
|
+
// outcome when the pool has nowhere to move this request to, and it is handled.
|
|
732
|
+
if (!classifyPoolRecoveryDispatch("retry").admitted) {
|
|
733
|
+
accountMovePermit.release();
|
|
734
|
+
accountMovePermit = undefined;
|
|
735
|
+
// The alternate context was resolved and will not send. Hand back whatever recovery
|
|
736
|
+
// lease it is holding rather than leaving that account unprobeable.
|
|
737
|
+
releaseCodexAuthContextProbeLease(retryAuthCtx);
|
|
738
|
+
recordUnmovedTransientOutcome();
|
|
739
|
+
return { kind: "no-alternate" };
|
|
740
|
+
}
|
|
696
741
|
const charged = accountMovePermit.use();
|
|
697
742
|
accountMovePermit = undefined;
|
|
698
743
|
if (!charged) {
|
|
744
|
+
releaseCodexAuthContextProbeLease(retryAuthCtx);
|
|
699
745
|
recordUnmovedTransientOutcome();
|
|
700
746
|
return { kind: "no-alternate" };
|
|
701
747
|
}
|
|
702
748
|
// The move is a physical send like any other, so the root workflow is charged too.
|
|
703
749
|
chargeWorkflowSends(args.options.workflowRootId, 1);
|
|
704
750
|
}
|
|
705
|
-
|
|
751
|
+
noteProviderAttemptSend(logCtx, route.providerName, route.provider, passthroughEstimate);
|
|
706
752
|
try {
|
|
707
753
|
upstreamResponse = await fetchWithHeaderTimeout(
|
|
708
754
|
request.url,
|
|
@@ -827,6 +873,7 @@ export function codexForwardTerminalOutcomeRecorder(
|
|
|
827
873
|
modelId,
|
|
828
874
|
probeLeaseId: codexProbeLeaseId(authCtx),
|
|
829
875
|
probeQuotaScope: codexProbeQuotaScope(authCtx),
|
|
876
|
+
transientProbe: codexTransientProbeGrant(authCtx),
|
|
830
877
|
writerGeneration: authCtx.writerGeneration,
|
|
831
878
|
});
|
|
832
879
|
return;
|
|
@@ -849,6 +896,7 @@ export function codexForwardTerminalOutcomeRecorder(
|
|
|
849
896
|
modelId,
|
|
850
897
|
probeLeaseId: codexProbeLeaseId(authCtx),
|
|
851
898
|
probeQuotaScope: codexProbeQuotaScope(authCtx),
|
|
899
|
+
transientProbe: codexTransientProbeGrant(authCtx),
|
|
852
900
|
writerGeneration: authCtx.writerGeneration,
|
|
853
901
|
// A mid-stream terminal can carry a semantic 401 long after the credential was
|
|
854
902
|
// replaced. It is never replayed — the client already saw output — but it must
|