@bitkyc08/opencodex 2.72.0 → 2.73.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/gui/dist/assets/{App-Cf-WYstE.js → App-mHGa7dE1.js} +15 -15
- package/gui/dist/assets/{Tray-DkM0mULy.js → Tray-BxY_RYZg.js} +1 -1
- package/gui/dist/assets/{index-PWrwOfio.js → index-ur_BPgK1.js} +12 -12
- package/gui/dist/assets/{quota-summary-D-FKtrgr.js → quota-summary-D1Vq7N9H.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/cursor/native-exec.ts +28 -10
- package/src/adapters/devin/live-models.ts +5 -0
- package/src/adapters/kiro/reasoning.ts +2 -0
- package/src/adapters/openai-chat/messages.ts +4 -1
- package/src/adapters/openai-responses/passthrough.ts +12 -1
- package/src/adapters/openai-responses/reasoning.ts +15 -0
- package/src/bridge/internal.ts +12 -0
- package/src/cli/account-anthropic-threshold.ts +41 -0
- package/src/cli/account-api.ts +2 -0
- package/src/cli/account-extended.ts +32 -21
- package/src/cli/account.ts +1 -0
- package/src/cli/capabilities.ts +8 -4
- package/src/cli/index.ts +1 -1
- package/src/codex/catalog/build-entries.ts +6 -5
- package/src/codex/catalog/derive-entry.ts +3 -4
- package/src/codex/catalog/gather-capture.ts +2 -0
- package/src/codex/catalog/metadata.ts +6 -0
- package/src/codex/catalog/native-models.ts +17 -3
- package/src/codex/catalog/parsing.ts +4 -1
- package/src/codex/catalog/pinned-models.ts +3 -1
- package/src/codex/catalog/provider-models.ts +22 -2
- package/src/codex/data/roster-pinned-models.json +178 -1
- package/src/codex/model-entitlements.ts +1 -1
- package/src/codex/native-profile-store.ts +2 -1
- package/src/combos/resolve.ts +16 -1
- package/src/config/subagent-models.ts +30 -5
- package/src/generated/compatibility-version.json +95 -67
- package/src/generated/model-metadata.ts +4 -4
- package/src/lib/account-selection-events.ts +28 -0
- package/src/lib/errors.ts +7 -5
- package/src/lib/keyring-native.ts +132 -0
- package/src/lib/provider-outbound.ts +13 -0
- package/src/lib/retry-delay.ts +10 -0
- package/src/oauth/anthropic-account-threshold.ts +13 -0
- package/src/oauth/anthropic-routing.ts +223 -52
- package/src/oauth/index.ts +4 -2
- package/src/oauth/store.ts +45 -3
- package/src/oauth/types.ts +2 -0
- package/src/providers/api-key-resolve.ts +2 -4
- package/src/providers/codebuddy-models.ts +6 -0
- package/src/providers/kiro-models.ts +4 -0
- package/src/providers/label.ts +20 -1
- package/src/providers/quota/account-cache.ts +2 -1
- package/src/providers/quota/anthropic-cooldown-recovery.ts +118 -0
- package/src/providers/quota/vendor-probes-oauth.ts +84 -57
- package/src/providers/quota.ts +45 -46
- package/src/providers/registry/entries-core.ts +3 -1
- package/src/providers/registry/entries-extended.ts +14 -1
- package/src/providers/registry/model-seeds.ts +7 -1
- package/src/server/claude-messages.ts +84 -27
- package/src/server/management/anthropic-account-threshold.ts +27 -0
- package/src/server/management/oauth-account-routes.ts +17 -4
- package/src/server/management/route-registry.ts +1 -0
- package/src/server/messages-native-oauth.ts +20 -21
- package/src/server/messages-native.ts +39 -14
- package/src/server/relay-eager.ts +5 -2
- package/src/server/relay.ts +268 -82
- package/src/server/responses/adapter-continuation.ts +6 -0
- package/src/server/responses/adapter-delivery.ts +25 -7
- package/src/server/responses/adapter-dispatch.ts +31 -0
- package/src/server/responses/buffered-sse-json.ts +345 -0
- package/src/server/responses/core-codex-account.ts +4 -1
- package/src/server/responses/core-combo.ts +28 -0
- package/src/server/responses/core-lifetime.ts +4 -0
- package/src/server/responses/passthrough-delivery.ts +305 -10
- package/src/server/responses/passthrough-dispatch.ts +60 -25
- package/src/server/responses/passthrough-error.ts +3 -1
- package/src/server/responses/request-transport.ts +22 -5
- package/src/server/responses/terminal-error-redaction.ts +53 -0
- package/src/service/windows-taskxml.ts +20 -6
- package/src/service-manager-probe.ts +115 -6
- package/src/types/wire.ts +7 -0
- package/src/usage/expected-prices.ts +11 -0
|
@@ -23,6 +23,7 @@ import { resolveStallTimeoutMs } from "../../stall-timeout";
|
|
|
23
23
|
import { clientEncoderForDelivery, deliverClientEncodedResponse } from "../inference/client-encoder-delivery";
|
|
24
24
|
import { noteKiroServedSuccess } from "../../providers/kiro-usage";
|
|
25
25
|
import { persistKiroAccountState } from "../../providers/kiro-account-state-disk";
|
|
26
|
+
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
26
27
|
|
|
27
28
|
/** One responsibility of the Responses request pipeline; state owners are explicit. */
|
|
28
29
|
export async function deliverAdapterResponse(
|
|
@@ -30,6 +31,7 @@ export async function deliverAdapterResponse(
|
|
|
30
31
|
requestState: Pick<
|
|
31
32
|
PreparedResponsesRequest,
|
|
32
33
|
| "parsed"
|
|
34
|
+
| "route"
|
|
33
35
|
| "translatorBudget"
|
|
34
36
|
| "toolBridgeMaps"
|
|
35
37
|
| "rememberKiroDeliveredFinalAnswer"
|
|
@@ -51,6 +53,7 @@ export async function deliverAdapterResponse(
|
|
|
51
53
|
const { logCtx, options, config } = requestContext;
|
|
52
54
|
const {
|
|
53
55
|
parsed,
|
|
56
|
+
route,
|
|
54
57
|
translatorBudget,
|
|
55
58
|
toolBridgeMaps,
|
|
56
59
|
rememberKiroDeliveredFinalAnswer,
|
|
@@ -71,6 +74,7 @@ export async function deliverAdapterResponse(
|
|
|
71
74
|
} = responseEffects;
|
|
72
75
|
const { routedCompaction } = sidecarState;
|
|
73
76
|
const bodyInactivityMs = resolveStallTimeoutMs(config.stallTimeoutSec, { localUpstream });
|
|
77
|
+
const upstreamRequestsStream = parsed.stream || isCanonicalOpenAiForwardProvider(route.provider);
|
|
74
78
|
|
|
75
79
|
|
|
76
80
|
if (parsed.stream) {
|
|
@@ -199,15 +203,29 @@ export async function deliverAdapterResponse(
|
|
|
199
203
|
});
|
|
200
204
|
}
|
|
201
205
|
|
|
202
|
-
if (transportState.activeAdapter.parseResponse
|
|
206
|
+
if (transportState.activeAdapter.parseResponse
|
|
207
|
+
|| (upstreamRequestsStream && transportState.activeAdapter.parseStream)) {
|
|
203
208
|
let events: AdapterEvent[];
|
|
204
209
|
try {
|
|
205
|
-
const initialEvents =
|
|
206
|
-
|
|
207
|
-
upstream
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
210
|
+
const initialEvents: AdapterEvent[] = [];
|
|
211
|
+
if (upstreamRequestsStream && transportState.activeAdapter.parseStream) {
|
|
212
|
+
// The canonical ChatGPT adapter coerces its actual upstream request to SSE even for a
|
|
213
|
+
// JSON client. Routed compaction still owns the non-streaming client response, so fold
|
|
214
|
+
// that SSE through the adapter instead of handing it to parseResponse as JSON.
|
|
215
|
+
for await (const event of readResponseStreamWithInactivity(
|
|
216
|
+
upstreamResponse,
|
|
217
|
+
upstream.signal,
|
|
218
|
+
bodyInactivityMs,
|
|
219
|
+
response => transportState.activeAdapter.parseStream(response, translatorBudget, logCtx.activeTierMetadata),
|
|
220
|
+
)) initialEvents.push(event);
|
|
221
|
+
} else {
|
|
222
|
+
initialEvents.push(...await readResponseBodyWithInactivity(
|
|
223
|
+
upstreamResponse,
|
|
224
|
+
upstream.signal,
|
|
225
|
+
bodyInactivityMs,
|
|
226
|
+
response => transportState.activeAdapter.parseResponse!(response, translatorBudget, logCtx.activeTierMetadata),
|
|
227
|
+
));
|
|
228
|
+
}
|
|
211
229
|
for (const event of initialEvents) options.onCompactionRecoveryAdapterEvent?.(event);
|
|
212
230
|
let guardedEvents: AdapterEvent[];
|
|
213
231
|
if (terminalGuardEnabled) {
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
fetchWithResetRetry,
|
|
36
36
|
applyUpstreamRecoveryInit,
|
|
37
37
|
SendBudgetExhaustedError,
|
|
38
|
+
UpstreamRetryEvidenceError,
|
|
38
39
|
prepareSameTarget429Wait,
|
|
39
40
|
sleepWithAbort,
|
|
40
41
|
} from "../../lib/upstream-retry";
|
|
@@ -54,8 +55,10 @@ import { resolveCopilotApiBaseUrl } from "../../oauth/github-copilot";
|
|
|
54
55
|
import { resolveWireProtocolOverride } from "../adapter-resolve";
|
|
55
56
|
import { bindRouteReasoningReplayScope } from "./core-replay";
|
|
56
57
|
import {
|
|
58
|
+
AnthropicAccountCooldownError,
|
|
57
59
|
ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
|
|
58
60
|
rotateAnthropicAccountOn429,
|
|
61
|
+
recordAnthropicAccount429,
|
|
59
62
|
getAnthropicPoolAccessSnapshot,
|
|
60
63
|
formatAnthropicProviderForLog,
|
|
61
64
|
} from "../../oauth/anthropic-routing";
|
|
@@ -382,6 +385,18 @@ export async function prepareAdapterExchange(
|
|
|
382
385
|
cleanupUpstreamAbort();
|
|
383
386
|
upstream.abort();
|
|
384
387
|
if (options.abortSignal?.aborted) return clientCancelledResponse();
|
|
388
|
+
const refusal = err instanceof UpstreamRetryEvidenceError ? err.cause : err;
|
|
389
|
+
// A pause committed during pacing is local admission policy, not a failed upstream.
|
|
390
|
+
if (refusal instanceof OAuthAccountPausedError) {
|
|
391
|
+
return formatErrorResponse(403, "permission_error", publicOAuthAuthenticationErrorMessage(refusal));
|
|
392
|
+
}
|
|
393
|
+
if (refusal instanceof OAuthLoginRequiredError) {
|
|
394
|
+
return formatErrorResponse(401, "authentication_error", publicOAuthAuthenticationErrorMessage(refusal));
|
|
395
|
+
}
|
|
396
|
+
if (refusal instanceof AnthropicAccountCooldownError) {
|
|
397
|
+
return formatErrorResponse(429, "rate_limit_error", refusal.message,
|
|
398
|
+
refusal.retryAfterSeconds === null ? undefined : { retryAfter: String(refusal.retryAfterSeconds) });
|
|
399
|
+
}
|
|
385
400
|
// A budget refusal is a decision this process made, not an upstream fault. Reporting it as
|
|
386
401
|
// 502 does more than mislabel it: the Codex client retries 5xx and does not retry a 429, so
|
|
387
402
|
// blaming the provider makes the caller send the whole turn again -- the amplification this
|
|
@@ -588,6 +603,17 @@ export async function prepareAdapterExchange(
|
|
|
588
603
|
if (options.abortSignal?.aborted) {
|
|
589
604
|
return { failed: clientCancelledResponse() };
|
|
590
605
|
}
|
|
606
|
+
const refusal = err instanceof UpstreamRetryEvidenceError ? err.cause : err;
|
|
607
|
+
if (refusal instanceof OAuthAccountPausedError) {
|
|
608
|
+
return { failed: formatErrorResponse(403, "permission_error", publicOAuthAuthenticationErrorMessage(refusal)) };
|
|
609
|
+
}
|
|
610
|
+
if (refusal instanceof OAuthLoginRequiredError) {
|
|
611
|
+
return { failed: formatErrorResponse(401, "authentication_error", publicOAuthAuthenticationErrorMessage(refusal)) };
|
|
612
|
+
}
|
|
613
|
+
if (refusal instanceof AnthropicAccountCooldownError) {
|
|
614
|
+
return { failed: formatErrorResponse(429, "rate_limit_error", refusal.message,
|
|
615
|
+
refusal.retryAfterSeconds === null ? undefined : { retryAfter: String(refusal.retryAfterSeconds) }) };
|
|
616
|
+
}
|
|
591
617
|
// Same rule on the recovery leg: the ladder refused to send again, so the answer names
|
|
592
618
|
// this proxy rather than the provider it never reached.
|
|
593
619
|
if (err instanceof SendBudgetExhaustedError) {
|
|
@@ -1000,6 +1026,11 @@ export async function prepareAdapterExchange(
|
|
|
1000
1026
|
break;
|
|
1001
1027
|
}
|
|
1002
1028
|
}
|
|
1029
|
+
if (upstreamResponse.status === 429 && transportState.anthropicPoolAccountId
|
|
1030
|
+
&& transportState.anthropicPoolFailovers >= ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST) {
|
|
1031
|
+
recordAnthropicAccount429(config, transportState.anthropicPoolAccountId,
|
|
1032
|
+
upstreamResponse.headers.get("retry-after"), Date.now(), upstreamResponse.headers);
|
|
1033
|
+
}
|
|
1003
1034
|
// Generic OAuth account failover (#2568) rotates reactively after a refusal when
|
|
1004
1035
|
// two accounts are stored. Kiro additionally classifies bounded 400/403 refusals;
|
|
1005
1036
|
// all other providers retain the original 429 loop below.
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import type { ResponsesTerminalStatus } from "../../bridge";
|
|
2
|
+
import {
|
|
3
|
+
createSseInspector,
|
|
4
|
+
createSseTerminalOutputBoundary,
|
|
5
|
+
SseAggregateLimitError,
|
|
6
|
+
terminalStatusFromParsed,
|
|
7
|
+
type CodexSafetyBufferingFilterOptions,
|
|
8
|
+
type SseInspector,
|
|
9
|
+
} from "../relay";
|
|
10
|
+
import { SseFrameCountLimitError, SseFrameTooLargeError } from "../sse-frame-buffer";
|
|
11
|
+
import {
|
|
12
|
+
MAX_UPSTREAM_JSON_BODY_BYTES,
|
|
13
|
+
UPSTREAM_JSON_BODY_INACTIVITY_TIMEOUT_MS,
|
|
14
|
+
UPSTREAM_JSON_BODY_TOTAL_TIMEOUT_MS,
|
|
15
|
+
} from "./core-lifetime";
|
|
16
|
+
|
|
17
|
+
/** Larger than persistence inspection because this path must return the whole bounded response. */
|
|
18
|
+
export const MAX_BUFFERED_RESPONSES_OUTPUT_ITEMS = 10_000;
|
|
19
|
+
export const MAX_BUFFERED_RESPONSES_RECONSTRUCTION_BYTES = MAX_UPSTREAM_JSON_BODY_BYTES;
|
|
20
|
+
export const MAX_BUFFERED_RESPONSES_SSE_FRAMES = 100_000;
|
|
21
|
+
/**
|
|
22
|
+
* A buffered caller cannot consume partial output, so keep one independent lifetime ceiling even
|
|
23
|
+
* when the operator disables the ordinary silence clock. Fifteen minutes is intentionally much
|
|
24
|
+
* larger than the public default stall budget while still releasing an abandoned body eventually.
|
|
25
|
+
*/
|
|
26
|
+
export const BUFFERED_RESPONSES_TOTAL_TIMEOUT_MS = 15 * 60_000;
|
|
27
|
+
const BUFFERED_SSE_PROCESSING_SLICE_BYTES = 64 * 1024;
|
|
28
|
+
|
|
29
|
+
export type BufferedResponsesTerminal = {
|
|
30
|
+
status: ResponsesTerminalStatus;
|
|
31
|
+
response: Record<string, unknown>;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type BufferedResponsesSseFailure = {
|
|
35
|
+
ok: false;
|
|
36
|
+
kind: "aborted" | "malformed" | "missing_terminal" | "oversized" | "read_error" | "timeout";
|
|
37
|
+
error?: unknown;
|
|
38
|
+
/** Bounded diagnostic captured from a bare upstream `error` event. */
|
|
39
|
+
upstreamError?: string;
|
|
40
|
+
/** Fatal provider refusal verdict that must not be flattened into a retryable transport error. */
|
|
41
|
+
upstreamRefusalCode?: string;
|
|
42
|
+
/** Bounded structured class from the same bare error envelope as `upstreamError`. */
|
|
43
|
+
upstreamErrorType?: string;
|
|
44
|
+
/** Bounded structured code; unknown codes remain transport failures instead of message guesses. */
|
|
45
|
+
upstreamErrorCode?: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type BufferedResponsesSseResult = {
|
|
49
|
+
ok: true;
|
|
50
|
+
/** Present only when the caller needs to replay the bounded transcript through rewrites. */
|
|
51
|
+
bytes?: Uint8Array;
|
|
52
|
+
terminal: BufferedResponsesTerminal;
|
|
53
|
+
} | BufferedResponsesSseFailure;
|
|
54
|
+
|
|
55
|
+
type BufferedReadOptions = {
|
|
56
|
+
maxBytes?: number;
|
|
57
|
+
maxFrames?: number;
|
|
58
|
+
totalTimeoutMs?: number;
|
|
59
|
+
/** Absolute whole-turn deadline shared by every validation pass. */
|
|
60
|
+
deadlineAt?: number;
|
|
61
|
+
inactivityTimeoutMs?: number;
|
|
62
|
+
firstByteTimeoutMs?: number;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export type BufferedResponsesReadOptions = BufferedReadOptions & { deadlineAt: number };
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Resolve the read clocks for canonical SSE that must be folded into client JSON.
|
|
69
|
+
*
|
|
70
|
+
* A positive stall budget owns both time-to-first-byte and inter-chunk silence. A disabled budget
|
|
71
|
+
* must not become an immediate timeout, so both clocks fall back to the independent total ceiling.
|
|
72
|
+
* The ceiling remains separate because a JSON caller has no partial body to keep alive forever.
|
|
73
|
+
*/
|
|
74
|
+
export function bufferedResponsesReadOptions(
|
|
75
|
+
stallTimeoutMs: number,
|
|
76
|
+
totalTimeoutMs = BUFFERED_RESPONSES_TOTAL_TIMEOUT_MS,
|
|
77
|
+
startedAt = Date.now(),
|
|
78
|
+
): BufferedResponsesReadOptions {
|
|
79
|
+
if (!Number.isFinite(stallTimeoutMs)) throw new RangeError("stallTimeoutMs must be finite");
|
|
80
|
+
if (!Number.isSafeInteger(totalTimeoutMs) || totalTimeoutMs <= 0) {
|
|
81
|
+
throw new RangeError("totalTimeoutMs must be a positive safe integer");
|
|
82
|
+
}
|
|
83
|
+
if (!Number.isSafeInteger(startedAt) || startedAt < 0 || startedAt > Number.MAX_SAFE_INTEGER - totalTimeoutMs) {
|
|
84
|
+
throw new RangeError("startedAt must produce a safe deadline");
|
|
85
|
+
}
|
|
86
|
+
const silenceTimeoutMs = stallTimeoutMs > 0 ? Math.ceil(stallTimeoutMs) : totalTimeoutMs;
|
|
87
|
+
return {
|
|
88
|
+
totalTimeoutMs,
|
|
89
|
+
deadlineAt: startedAt + totalTimeoutMs,
|
|
90
|
+
firstByteTimeoutMs: silenceTimeoutMs,
|
|
91
|
+
inactivityTimeoutMs: silenceTimeoutMs,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
type Uint8ReadResult = Awaited<ReturnType<ReadableStreamDefaultReader<Uint8Array>["read"]>>;
|
|
96
|
+
|
|
97
|
+
class BufferedSseTimeoutError extends Error {
|
|
98
|
+
constructor(deadline: "total" | "inactivity") {
|
|
99
|
+
super(`buffered Responses SSE ${deadline} timeout`);
|
|
100
|
+
this.name = "BufferedSseTimeoutError";
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function createBufferedTerminalInspector(maxSourceBytes: number): {
|
|
105
|
+
inspector: SseInspector;
|
|
106
|
+
result: () => { ok: true; terminal: BufferedResponsesTerminal } | BufferedResponsesSseFailure;
|
|
107
|
+
} {
|
|
108
|
+
let malformed = false;
|
|
109
|
+
let terminalCount = 0;
|
|
110
|
+
let status: ResponsesTerminalStatus | undefined;
|
|
111
|
+
let terminalResponse: Record<string, unknown> | undefined;
|
|
112
|
+
const inspector = createSseInspector({
|
|
113
|
+
terminalReconstruction: {
|
|
114
|
+
maxItems: MAX_BUFFERED_RESPONSES_OUTPUT_ITEMS,
|
|
115
|
+
maxSourceBytes,
|
|
116
|
+
requireCompleteOutputIndices: true,
|
|
117
|
+
mergeSparseTerminalOutput: true,
|
|
118
|
+
},
|
|
119
|
+
onParsedPayload(payload) {
|
|
120
|
+
const candidateStatus = terminalStatusFromParsed(payload);
|
|
121
|
+
if (!candidateStatus) return;
|
|
122
|
+
terminalCount += 1;
|
|
123
|
+
status ??= candidateStatus;
|
|
124
|
+
},
|
|
125
|
+
onOpaquePayload(payload) {
|
|
126
|
+
if (payload !== "[DONE]") malformed = true;
|
|
127
|
+
},
|
|
128
|
+
onTerminalResponse(candidateStatus, response) {
|
|
129
|
+
if (candidateStatus === status && terminalResponse === undefined) {
|
|
130
|
+
if (response.status !== undefined && response.status !== candidateStatus) {
|
|
131
|
+
malformed = true;
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (response.output !== undefined && !Array.isArray(response.output)) {
|
|
135
|
+
malformed = true;
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
terminalResponse = {
|
|
139
|
+
...response,
|
|
140
|
+
status: candidateStatus,
|
|
141
|
+
output: response.output ?? [],
|
|
142
|
+
} as Record<string, unknown>;
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
return {
|
|
147
|
+
inspector,
|
|
148
|
+
result: () => {
|
|
149
|
+
if (malformed) return { ok: false, kind: "malformed" };
|
|
150
|
+
if (terminalCount !== 1 || status === undefined) return { ok: false, kind: "missing_terminal" };
|
|
151
|
+
if (!terminalResponse || terminalResponse.status !== status || !Array.isArray(terminalResponse.output)) {
|
|
152
|
+
return { ok: false, kind: "malformed" };
|
|
153
|
+
}
|
|
154
|
+
return { ok: true, terminal: { status, response: terminalResponse } };
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Strict synchronous seam for terminal/reconstruction tests; production feeds bytes incrementally. */
|
|
160
|
+
export function inspectBufferedResponsesTerminal(text: string):
|
|
161
|
+
| { ok: true; terminal: BufferedResponsesTerminal }
|
|
162
|
+
| BufferedResponsesSseFailure {
|
|
163
|
+
const state = createBufferedTerminalInspector(MAX_BUFFERED_RESPONSES_RECONSTRUCTION_BYTES);
|
|
164
|
+
try {
|
|
165
|
+
state.inspector.feed(new TextEncoder().encode(text));
|
|
166
|
+
state.inspector.finish();
|
|
167
|
+
return state.result();
|
|
168
|
+
} finally {
|
|
169
|
+
state.inspector.dispose();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function cancelReader(reader: ReadableStreamDefaultReader<Uint8Array>, reason?: unknown): void {
|
|
174
|
+
try { void reader.cancel(reason).catch(() => undefined); } catch { /* non-conforming stream */ }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function deadlineRead(
|
|
178
|
+
reader: ReadableStreamDefaultReader<Uint8Array>,
|
|
179
|
+
signal: AbortSignal | undefined,
|
|
180
|
+
deadlineAt: number,
|
|
181
|
+
deadline: "total" | "inactivity",
|
|
182
|
+
): Promise<Uint8ReadResult> {
|
|
183
|
+
if (signal?.aborted) throw signal.reason;
|
|
184
|
+
return await new Promise<Uint8ReadResult>((resolve, reject) => {
|
|
185
|
+
let settled = false;
|
|
186
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
187
|
+
const finish = (callback: () => void): void => {
|
|
188
|
+
if (settled) return;
|
|
189
|
+
settled = true;
|
|
190
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
191
|
+
signal?.removeEventListener("abort", onAbort);
|
|
192
|
+
callback();
|
|
193
|
+
};
|
|
194
|
+
const onAbort = () => finish(() => reject(signal?.reason));
|
|
195
|
+
timer = setTimeout(
|
|
196
|
+
() => finish(() => reject(new BufferedSseTimeoutError(deadline))),
|
|
197
|
+
Math.max(0, deadlineAt - Date.now()),
|
|
198
|
+
);
|
|
199
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
200
|
+
if (signal?.aborted) onAbort();
|
|
201
|
+
void reader.read().then(
|
|
202
|
+
value => finish(() => resolve(value)),
|
|
203
|
+
error => finish(() => reject(error)),
|
|
204
|
+
);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Consume through the first terminal, which must validate, with bounded framing and deadlines.
|
|
210
|
+
* `retainTranscript:false` feeds the strict assembler directly and never materializes the second,
|
|
211
|
+
* client-facing SSE transcript. Side effects remain the delivery owner's responsibility.
|
|
212
|
+
*/
|
|
213
|
+
export async function collectBufferedResponsesSse(
|
|
214
|
+
body: ReadableStream<Uint8Array>,
|
|
215
|
+
upstream: AbortController,
|
|
216
|
+
options: {
|
|
217
|
+
signal?: AbortSignal;
|
|
218
|
+
terminalBoundary?: CodexSafetyBufferingFilterOptions;
|
|
219
|
+
read?: BufferedReadOptions;
|
|
220
|
+
retainTranscript?: boolean;
|
|
221
|
+
} = {},
|
|
222
|
+
): Promise<BufferedResponsesSseResult> {
|
|
223
|
+
const maxBytes = options.read?.maxBytes ?? MAX_UPSTREAM_JSON_BODY_BYTES;
|
|
224
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) {
|
|
225
|
+
throw new RangeError("maxBytes must be a positive safe integer");
|
|
226
|
+
}
|
|
227
|
+
const maxFrames = options.read?.maxFrames ?? MAX_BUFFERED_RESPONSES_SSE_FRAMES;
|
|
228
|
+
const totalTimeoutMs = options.read?.totalTimeoutMs ?? UPSTREAM_JSON_BODY_TOTAL_TIMEOUT_MS;
|
|
229
|
+
const inactivityTimeoutMs = options.read?.inactivityTimeoutMs ?? UPSTREAM_JSON_BODY_INACTIVITY_TIMEOUT_MS;
|
|
230
|
+
const firstByteTimeoutMs = options.read?.firstByteTimeoutMs ?? totalTimeoutMs;
|
|
231
|
+
const absoluteDeadlineAt = options.read?.deadlineAt;
|
|
232
|
+
if (absoluteDeadlineAt !== undefined && (!Number.isSafeInteger(absoluteDeadlineAt) || absoluteDeadlineAt < 0)) {
|
|
233
|
+
throw new RangeError("deadlineAt must be a non-negative safe integer");
|
|
234
|
+
}
|
|
235
|
+
const retainTranscript = options.retainTranscript === true;
|
|
236
|
+
const reader = body.getReader();
|
|
237
|
+
const boundary = createSseTerminalOutputBoundary({
|
|
238
|
+
...options.terminalBoundary,
|
|
239
|
+
maxInputBytes: maxBytes,
|
|
240
|
+
maxOutputBytes: maxBytes,
|
|
241
|
+
maxFrames,
|
|
242
|
+
});
|
|
243
|
+
const state = createBufferedTerminalInspector(Math.min(
|
|
244
|
+
maxBytes,
|
|
245
|
+
MAX_BUFFERED_RESPONSES_RECONSTRUCTION_BYTES,
|
|
246
|
+
));
|
|
247
|
+
const utf8 = new TextDecoder("utf-8", { fatal: true });
|
|
248
|
+
let retained = retainTranscript ? new Uint8Array(Math.min(maxBytes, 64 * 1024)) : undefined;
|
|
249
|
+
let retainedBytes = 0;
|
|
250
|
+
const startedAt = Date.now();
|
|
251
|
+
const totalDeadlineAt = absoluteDeadlineAt ?? startedAt + totalTimeoutMs;
|
|
252
|
+
let inactivityDeadlineAt = startedAt + firstByteTimeoutMs;
|
|
253
|
+
|
|
254
|
+
const withBoundaryError = <T extends BufferedResponsesSseFailure>(failure: T): T => {
|
|
255
|
+
const upstreamError = boundary.upstreamError();
|
|
256
|
+
const upstreamRefusalCode = boundary.upstreamRefusalCode();
|
|
257
|
+
const upstreamErrorType = boundary.upstreamErrorType();
|
|
258
|
+
const upstreamErrorCode = boundary.upstreamErrorCode();
|
|
259
|
+
return {
|
|
260
|
+
...failure,
|
|
261
|
+
...(upstreamError === undefined ? {} : { upstreamError }),
|
|
262
|
+
...(upstreamRefusalCode === undefined ? {} : { upstreamRefusalCode }),
|
|
263
|
+
...(upstreamErrorType === undefined ? {} : { upstreamErrorType }),
|
|
264
|
+
...(upstreamErrorCode === undefined ? {} : { upstreamErrorCode }),
|
|
265
|
+
};
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const inspect = (chunk: Uint8Array): void => {
|
|
269
|
+
if (chunk.byteLength === 0) return;
|
|
270
|
+
// Validate incrementally without retaining a second decoded transcript. The shared inspector
|
|
271
|
+
// independently owns SSE framing and JSON reconstruction over the same bytes.
|
|
272
|
+
utf8.decode(chunk, { stream: true });
|
|
273
|
+
state.inspector.feed(chunk);
|
|
274
|
+
if (!retained) return;
|
|
275
|
+
if (chunk.byteLength > maxBytes - retainedBytes) throw new SseAggregateLimitError(maxBytes);
|
|
276
|
+
const required = retainedBytes + chunk.byteLength;
|
|
277
|
+
if (required > retained.byteLength) {
|
|
278
|
+
const grown = new Uint8Array(Math.min(maxBytes, Math.max(required, retained.byteLength * 2)));
|
|
279
|
+
grown.set(retained.subarray(0, retainedBytes));
|
|
280
|
+
retained = grown;
|
|
281
|
+
}
|
|
282
|
+
retained.set(chunk, retainedBytes);
|
|
283
|
+
retainedBytes = required;
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
for (;;) {
|
|
288
|
+
const deadlineAt = Math.min(totalDeadlineAt, inactivityDeadlineAt);
|
|
289
|
+
const deadline = totalDeadlineAt <= inactivityDeadlineAt ? "total" : "inactivity";
|
|
290
|
+
const { done, value } = await deadlineRead(reader, options.signal, deadlineAt, deadline);
|
|
291
|
+
if (options.signal?.aborted) throw options.signal.reason;
|
|
292
|
+
if (done) {
|
|
293
|
+
inspect(boundary.finish());
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
if (!value || value.byteLength === 0) continue;
|
|
297
|
+
inactivityDeadlineAt = Date.now() + inactivityTimeoutMs;
|
|
298
|
+
// Slice the reader's chunk before framing. This keeps one oversized network read from
|
|
299
|
+
// producing a transcript-sized array of frames/output buffers and gives the total deadline
|
|
300
|
+
// and aggregate frame cap a checkpoint between bounded pieces of synchronous work.
|
|
301
|
+
for (let offset = 0; offset < value.byteLength && !boundary.terminalSeen();) {
|
|
302
|
+
if (Date.now() >= totalDeadlineAt) throw new BufferedSseTimeoutError("total");
|
|
303
|
+
const end = Math.min(value.byteLength, offset + BUFFERED_SSE_PROCESSING_SLICE_BYTES);
|
|
304
|
+
inspect(boundary.feed(value.subarray(offset, end)));
|
|
305
|
+
offset = end;
|
|
306
|
+
}
|
|
307
|
+
if (boundary.terminalSeen()) {
|
|
308
|
+
cancelReader(reader, "Responses terminal event received");
|
|
309
|
+
break;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
utf8.decode();
|
|
313
|
+
state.inspector.finish();
|
|
314
|
+
if (options.signal?.aborted) throw options.signal.reason;
|
|
315
|
+
const terminal = state.result();
|
|
316
|
+
if (!terminal.ok) {
|
|
317
|
+
upstream.abort(new Error(`buffered Responses SSE ${terminal.kind}`));
|
|
318
|
+
return withBoundaryError(terminal);
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
...terminal,
|
|
322
|
+
...(retained ? { bytes: retained.subarray(0, retainedBytes) } : {}),
|
|
323
|
+
};
|
|
324
|
+
} catch (error) {
|
|
325
|
+
upstream.abort(error);
|
|
326
|
+
cancelReader(reader, error);
|
|
327
|
+
return withBoundaryError({
|
|
328
|
+
ok: false,
|
|
329
|
+
kind: options.signal?.aborted
|
|
330
|
+
? "aborted"
|
|
331
|
+
: error instanceof BufferedSseTimeoutError
|
|
332
|
+
? "timeout"
|
|
333
|
+
: error instanceof SseAggregateLimitError
|
|
334
|
+
|| error instanceof SseFrameTooLargeError
|
|
335
|
+
|| error instanceof SseFrameCountLimitError
|
|
336
|
+
? "oversized"
|
|
337
|
+
: "read_error",
|
|
338
|
+
error,
|
|
339
|
+
});
|
|
340
|
+
} finally {
|
|
341
|
+
boundary.dispose();
|
|
342
|
+
state.inspector.dispose();
|
|
343
|
+
try { reader.releaseLock(); } catch { /* a cancelled pending read may still own it briefly */ }
|
|
344
|
+
}
|
|
345
|
+
}
|
|
@@ -395,6 +395,8 @@ export interface CodexPoolAccountRetryArgs {
|
|
|
395
395
|
connectMs: number;
|
|
396
396
|
passthroughEstimate?: number;
|
|
397
397
|
stream: boolean;
|
|
398
|
+
/** Keep a buffered canonical client on HTTP/SSE after moving to another Pool account. */
|
|
399
|
+
httpOnly?: boolean;
|
|
398
400
|
onResponse?: (
|
|
399
401
|
response: Response,
|
|
400
402
|
authCtx: CodexAuthContext,
|
|
@@ -535,7 +537,7 @@ export async function retryCodexPoolOnAlternateAccount(
|
|
|
535
537
|
): Promise<CodexPoolAccountRetryResult> {
|
|
536
538
|
const {
|
|
537
539
|
callerAuthHeaders, config, route, parsed, logCtx, options, firstAuthCtx, firstResponse,
|
|
538
|
-
outcomeStatus, upstream, connectMs, passthroughEstimate, stream,
|
|
540
|
+
outcomeStatus, upstream, connectMs, passthroughEstimate, stream, httpOnly,
|
|
539
541
|
} = args;
|
|
540
542
|
const inboundWire = options.inboundWire ?? "responses";
|
|
541
543
|
const entitlementResolver = options.resolveCodexModelEntitlements ?? resolveCodexModelEntitlements;
|
|
@@ -877,6 +879,7 @@ export async function retryCodexPoolOnAlternateAccount(
|
|
|
877
879
|
connectMs,
|
|
878
880
|
stream,
|
|
879
881
|
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
882
|
+
httpOnly,
|
|
880
883
|
providerName: route.providerName,
|
|
881
884
|
modelId: route.modelId,
|
|
882
885
|
onCodexWsQuota: codexWsQuotaObserver(retryAuthCtx, route.provider, route.modelId),
|
|
@@ -46,6 +46,11 @@ import {
|
|
|
46
46
|
import { hasUnreadableEncryptedAgentTask } from "./encrypted-payload";
|
|
47
47
|
import { routeConcreteModel, comboRouteDecisionTrace } from "../../router";
|
|
48
48
|
import { memoryModelRouteReason } from "./memory-models";
|
|
49
|
+
import { poolAccountProviderLabel } from "../../providers/label";
|
|
50
|
+
import { getAccountSet } from "../../oauth/store";
|
|
51
|
+
import { formatAnthropicProviderForLog, getAnthropicAccountHealthSnapshot } from "../../oauth/anthropic-routing";
|
|
52
|
+
import { codexAccountLogLabel } from "../../codex/account-label";
|
|
53
|
+
import { codexQuotaScopeForModel, getCodexQuotaHealthSnapshot } from "../../codex/routing";
|
|
49
54
|
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
50
55
|
import type { AgentTaskRecoveryFailureReason } from "./agent-task-recovery";
|
|
51
56
|
import {
|
|
@@ -97,6 +102,22 @@ import { clientWireOf } from "../inference/client-wire";
|
|
|
97
102
|
*/
|
|
98
103
|
export const COMBO_TARGET_BASE_SENDS = CODEX_TEXT_GUARDED_BUDGET_POLICY.baseSendAllowance;
|
|
99
104
|
|
|
105
|
+
function cooledPoolAccountLabel(config: OcxConfig, providerName: string, modelId: string, label: string | undefined): string | undefined {
|
|
106
|
+
if (!label) return undefined;
|
|
107
|
+
if (providerName === "anthropic") {
|
|
108
|
+
const matches = getAccountSet("anthropic")?.accounts.filter(account =>
|
|
109
|
+
formatAnthropicProviderForLog("anthropic", account.id) === label) ?? [];
|
|
110
|
+
return matches.length === 1 && getAnthropicAccountHealthSnapshot(matches[0]!.id) ? label : undefined;
|
|
111
|
+
}
|
|
112
|
+
const provider = config.providers[providerName];
|
|
113
|
+
if (!provider || !isCanonicalOpenAiForwardProvider(provider)) return undefined;
|
|
114
|
+
const matches = (config.codexAccounts ?? []).filter(account =>
|
|
115
|
+
`${providerName}-${codexAccountLogLabel(account)}` === label);
|
|
116
|
+
return matches.length === 1
|
|
117
|
+
&& getCodexQuotaHealthSnapshot(matches[0]!.id, codexQuotaScopeForModel(modelId))
|
|
118
|
+
? label : undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
100
121
|
|
|
101
122
|
/**
|
|
102
123
|
* A combo's execution policy is DECLARED by the combo, not inherited from the single-target
|
|
@@ -977,6 +998,13 @@ export async function executeComboResponses(
|
|
|
977
998
|
status: failure.response.status,
|
|
978
999
|
code: failure.upstreamCode,
|
|
979
1000
|
message: failure.classificationText,
|
|
1001
|
+
// The dispatch rewrote this to name the pool account that actually served the turn.
|
|
1002
|
+
failedAccount: cooledPoolAccountLabel(
|
|
1003
|
+
config,
|
|
1004
|
+
pick.target.provider,
|
|
1005
|
+
pick.target.model,
|
|
1006
|
+
poolAccountProviderLabel(childLog.provider, pick.target.provider),
|
|
1007
|
+
),
|
|
980
1008
|
onCooldownRecorded: target => {
|
|
981
1009
|
failedTargetCooldownRecorded ||= targetKey(target) === failedTargetKey;
|
|
982
1010
|
},
|
|
@@ -4,6 +4,8 @@ import {
|
|
|
4
4
|
markNativePassthroughSseResponse,
|
|
5
5
|
isEagerRelaySseResponse,
|
|
6
6
|
markEagerRelaySseResponse,
|
|
7
|
+
isPreinspectedJsonResponse,
|
|
8
|
+
markPreinspectedJsonResponse,
|
|
7
9
|
} from "../relay";
|
|
8
10
|
|
|
9
11
|
// runTurn adapters own an event queue and perform their combo preflight before
|
|
@@ -77,6 +79,7 @@ export function finalizeOwnedTranslatorBudget(response: Response, budget: Transl
|
|
|
77
79
|
if (isEagerRelaySseResponse(response)) {
|
|
78
80
|
markEagerRelaySseResponse(finalizedResponse);
|
|
79
81
|
}
|
|
82
|
+
if (isPreinspectedJsonResponse(response)) markPreinspectedJsonResponse(finalizedResponse);
|
|
80
83
|
return finalizedResponse;
|
|
81
84
|
}
|
|
82
85
|
|
|
@@ -99,6 +102,7 @@ export function finalizeAccountLease(response: Response, release: () => void): R
|
|
|
99
102
|
const wrapped = new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers });
|
|
100
103
|
if (isNativePassthroughSseResponse(response)) markNativePassthroughSseResponse(wrapped);
|
|
101
104
|
if (isEagerRelaySseResponse(response)) markEagerRelaySseResponse(wrapped);
|
|
105
|
+
if (isPreinspectedJsonResponse(response)) markPreinspectedJsonResponse(wrapped);
|
|
102
106
|
return wrapped;
|
|
103
107
|
}
|
|
104
108
|
|