@oh-my-pi/pi-ai 16.2.1 → 16.2.2
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/CHANGELOG.md +27 -0
- package/dist/types/auth-gateway/server.d.ts +0 -19
- package/dist/types/auth-retry.d.ts +2 -10
- package/dist/types/auth-storage.d.ts +1 -2
- package/dist/types/dialect/demotion.d.ts +22 -0
- package/dist/types/dialect/index.d.ts +2 -0
- package/dist/types/error/abort.d.ts +14 -0
- package/dist/types/error/auth-classify.d.ts +16 -0
- package/dist/types/error/auth.d.ts +27 -0
- package/dist/types/error/aws.d.ts +23 -0
- package/dist/types/error/classes.d.ts +102 -0
- package/dist/types/error/finalize.d.ts +39 -0
- package/dist/types/error/flags.d.ts +70 -0
- package/dist/types/error/format.d.ts +20 -0
- package/dist/types/error/gateway.d.ts +20 -0
- package/dist/types/error/index.d.ts +13 -0
- package/dist/types/error/oauth.d.ts +43 -0
- package/dist/types/error/provider.d.ts +42 -0
- package/dist/types/{rate-limit-utils.d.ts → error/rate-limit.d.ts} +14 -1
- package/dist/types/error/retryable.d.ts +27 -0
- package/dist/types/error/validation.d.ts +32 -0
- package/dist/types/index.d.ts +1 -3
- package/dist/types/providers/amazon-bedrock.d.ts +0 -5
- package/dist/types/providers/anthropic-client.d.ts +2 -16
- package/dist/types/providers/anthropic.d.ts +6 -1
- package/dist/types/providers/aws-eventstream.d.ts +2 -1
- package/dist/types/providers/cursor.d.ts +8 -7
- package/dist/types/providers/google-gemini-cli.d.ts +0 -5
- package/dist/types/providers/google-shared.d.ts +0 -5
- package/dist/types/providers/ollama.d.ts +0 -5
- package/dist/types/providers/openai-codex/request-transformer.d.ts +2 -2
- package/dist/types/providers/openai-codex/response-handler.d.ts +1 -1
- package/dist/types/providers/openai-codex-responses.d.ts +2 -2
- package/dist/types/providers/openai-responses-server-schema.d.ts +6 -0
- package/dist/types/providers/openai-responses-wire.d.ts +1 -1
- package/dist/types/providers/openai-responses.d.ts +1 -0
- package/dist/types/providers/openai-shared.d.ts +8 -6
- package/dist/types/providers/pi-native-client.d.ts +0 -9
- package/dist/types/registry/oauth/xai-oauth.d.ts +0 -9
- package/dist/types/types.d.ts +6 -0
- package/dist/types/utils/block-symbols.d.ts +20 -0
- package/dist/types/utils/openai-http.d.ts +4 -8
- package/dist/types/utils/retry.d.ts +2 -14
- package/dist/types/utils/thinking-loop.d.ts +3 -1
- package/package.json +8 -4
- package/src/api-registry.ts +3 -1
- package/src/auth-broker/discover.ts +7 -2
- package/src/auth-broker/remote-store.ts +8 -7
- package/src/auth-gateway/server.ts +27 -115
- package/src/auth-retry.ts +9 -21
- package/src/auth-storage.ts +34 -49
- package/src/dialect/demotion.ts +31 -0
- package/src/dialect/factory.ts +0 -2
- package/src/dialect/index.ts +2 -0
- package/src/dialect/owned-stream.ts +0 -1
- package/src/dialect/thinking.ts +22 -10
- package/src/error/abort.ts +18 -0
- package/src/error/auth-classify.ts +30 -0
- package/src/error/auth.ts +48 -0
- package/src/error/aws.ts +31 -0
- package/src/error/classes.ts +186 -0
- package/src/error/finalize.ts +69 -0
- package/src/error/flags.ts +486 -0
- package/src/error/format.ts +36 -0
- package/src/error/gateway.ts +96 -0
- package/src/error/index.ts +13 -0
- package/src/error/oauth.ts +58 -0
- package/src/error/provider.ts +56 -0
- package/src/{rate-limit-utils.ts → error/rate-limit.ts} +9 -3
- package/src/error/retryable.ts +70 -0
- package/src/error/validation.ts +44 -0
- package/src/index.ts +1 -3
- package/src/providers/amazon-bedrock.ts +61 -57
- package/src/providers/anthropic-client.ts +13 -41
- package/src/providers/anthropic-messages-server.ts +9 -2
- package/src/providers/anthropic.ts +84 -147
- package/src/providers/aws-credentials.ts +41 -11
- package/src/providers/aws-eventstream.ts +12 -21
- package/src/providers/azure-openai-responses.ts +27 -17
- package/src/providers/cursor.ts +59 -53
- package/src/providers/devin.ts +28 -20
- package/src/providers/gitlab-duo-workflow.ts +30 -10
- package/src/providers/gitlab-duo.ts +22 -6
- package/src/providers/google-auth.ts +15 -5
- package/src/providers/google-gemini-cli.ts +46 -60
- package/src/providers/google-shared.ts +40 -38
- package/src/providers/google-vertex.ts +3 -2
- package/src/providers/google.ts +5 -1
- package/src/providers/mock.ts +10 -7
- package/src/providers/ollama.ts +34 -29
- package/src/providers/openai-chat-server.ts +2 -1
- package/src/providers/openai-codex/request-transformer.ts +13 -12
- package/src/providers/openai-codex/response-handler.ts +1 -1
- package/src/providers/openai-codex-responses.ts +931 -1012
- package/src/providers/openai-completions.ts +46 -36
- package/src/providers/openai-responses-server-schema.ts +3 -0
- package/src/providers/openai-responses-server.ts +82 -30
- package/src/providers/openai-responses-wire.ts +1 -1
- package/src/providers/openai-responses.ts +46 -22
- package/src/providers/openai-shared.ts +118 -51
- package/src/providers/pi-native-client.ts +12 -18
- package/src/providers/pi-native-server.ts +9 -6
- package/src/providers/register-builtins.ts +5 -2
- package/src/providers/transform-messages.ts +31 -63
- package/src/registry/alibaba-coding-plan.ts +6 -5
- package/src/registry/api-key-login.ts +4 -3
- package/src/registry/api-key-validation.ts +4 -3
- package/src/registry/cloudflare-ai-gateway.ts +4 -3
- package/src/registry/coreweave.ts +2 -1
- package/src/registry/deepseek.ts +2 -1
- package/src/registry/google-antigravity.ts +2 -1
- package/src/registry/google-gemini-cli.ts +2 -1
- package/src/registry/kagi.ts +4 -3
- package/src/registry/kilo.ts +32 -10
- package/src/registry/litellm.ts +4 -3
- package/src/registry/llama-cpp.ts +3 -2
- package/src/registry/lm-studio.ts +3 -2
- package/src/registry/nvidia.ts +7 -7
- package/src/registry/oauth/anthropic.ts +23 -6
- package/src/registry/oauth/callback-server.ts +5 -3
- package/src/registry/oauth/cursor.ts +18 -4
- package/src/registry/oauth/devin.ts +14 -3
- package/src/registry/oauth/github-copilot.ts +21 -10
- package/src/registry/oauth/gitlab-duo-workflow.ts +15 -3
- package/src/registry/oauth/gitlab-duo.ts +30 -6
- package/src/registry/oauth/google-antigravity.ts +14 -5
- package/src/registry/oauth/google-gemini-cli.ts +24 -7
- package/src/registry/oauth/google-oauth-shared.ts +6 -3
- package/src/registry/oauth/index.ts +19 -8
- package/src/registry/oauth/kimi.ts +40 -10
- package/src/registry/oauth/openai-codex.ts +26 -11
- package/src/registry/oauth/opencode.ts +4 -3
- package/src/registry/oauth/perplexity.ts +33 -11
- package/src/registry/oauth/xai-oauth.ts +62 -18
- package/src/registry/oauth/xiaomi.ts +21 -9
- package/src/registry/ollama-cloud.ts +5 -4
- package/src/registry/ollama.ts +3 -2
- package/src/registry/parallel.ts +4 -3
- package/src/registry/qwen-portal.ts +4 -3
- package/src/registry/tavily.ts +4 -3
- package/src/registry/vercel-ai-gateway.ts +4 -3
- package/src/registry/vllm.ts +3 -2
- package/src/stream.ts +79 -24
- package/src/types.ts +6 -0
- package/src/usage/claude.ts +2 -1
- package/src/usage/github-copilot.ts +4 -3
- package/src/usage/google-antigravity.ts +2 -1
- package/src/utils/abort.ts +4 -2
- package/src/utils/block-symbols.ts +32 -0
- package/src/utils/event-stream.ts +15 -3
- package/src/utils/http-inspector.ts +4 -4
- package/src/utils/idle-iterator.ts +6 -5
- package/src/utils/openai-http.ts +11 -46
- package/src/utils/parse-bind.ts +7 -5
- package/src/utils/proxy.ts +2 -1
- package/src/utils/retry.ts +7 -23
- package/src/utils/schema/normalize.ts +3 -2
- package/src/utils/thinking-loop.ts +15 -11
- package/src/utils/validation.ts +4 -3
- package/dist/types/dialect/pi.d.ts +0 -9
- package/dist/types/errors.d.ts +0 -24
- package/dist/types/utils/overflow.d.ts +0 -55
- package/src/dialect/pi.md +0 -55
- package/src/dialect/pi.ts +0 -600
- package/src/errors.ts +0 -32
- package/src/utils/overflow.ts +0 -140
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
$env,
|
|
12
12
|
$flag,
|
|
13
13
|
asRecord,
|
|
14
|
-
extractHttpStatusFromError,
|
|
15
14
|
fetchWithRetry,
|
|
16
15
|
logger,
|
|
17
16
|
parseStreamingJson,
|
|
@@ -20,6 +19,7 @@ import {
|
|
|
20
19
|
} from "@oh-my-pi/pi-utils";
|
|
21
20
|
import { type } from "arktype";
|
|
22
21
|
import packageJson from "../../package.json" with { type: "json" };
|
|
22
|
+
import * as AIError from "../error";
|
|
23
23
|
import { getEnvApiKey } from "../stream";
|
|
24
24
|
import type {
|
|
25
25
|
Api,
|
|
@@ -44,8 +44,9 @@ import {
|
|
|
44
44
|
getOpenAIResponsesHistoryPayload,
|
|
45
45
|
normalizeSystemPrompts,
|
|
46
46
|
} from "../utils";
|
|
47
|
+
import { clearStreamingPartialJson, kStreamingLastParseLen, kStreamingPartialJson } from "../utils/block-symbols";
|
|
47
48
|
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
48
|
-
import {
|
|
49
|
+
import type { RawHttpRequestDump } from "../utils/http-inspector";
|
|
49
50
|
import {
|
|
50
51
|
armPreResponseTimeout,
|
|
51
52
|
getOpenAIStreamFirstEventTimeoutMs,
|
|
@@ -103,7 +104,7 @@ import { transformMessages } from "./transform-messages";
|
|
|
103
104
|
export interface OpenAICodexResponsesOptions extends StreamOptions {
|
|
104
105
|
reasoning?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
105
106
|
reasoningSummary?: "auto" | "concise" | "detailed" | null;
|
|
106
|
-
/** `reasoning.context` replay scope
|
|
107
|
+
/** `reasoning.context` replay scope; defaults to `all_turns` for every Codex request when unset. */
|
|
107
108
|
reasoningContext?: CodexReasoningContext;
|
|
108
109
|
textVerbosity?: "low" | "medium" | "high";
|
|
109
110
|
include?: string[];
|
|
@@ -116,7 +117,7 @@ export interface OpenAICodexResponsesOptions extends StreamOptions {
|
|
|
116
117
|
* `x-openai-internal-codex-responses-lite: true` on HTTP requests and on the
|
|
117
118
|
* WebSocket upgrade (the marker is connection-scoped there, so lite and
|
|
118
119
|
* non-lite turns never share a pooled socket), strips image detail from
|
|
119
|
-
* input, and
|
|
120
|
+
* input, and disables parallel tool calling — mirroring codex-rs.
|
|
120
121
|
*/
|
|
121
122
|
responsesLite?: boolean;
|
|
122
123
|
/**
|
|
@@ -138,9 +139,9 @@ const CODEX_DEBUG = $flag("PI_CODEX_DEBUG");
|
|
|
138
139
|
const CODEX_MAX_RETRIES = 5;
|
|
139
140
|
const CODEX_RETRY_DELAY_MS = 500;
|
|
140
141
|
const CODEX_WEBSOCKET_CONNECT_TIMEOUT_MS = 10000;
|
|
141
|
-
const CODEX_WEBSOCKET_PING_INTERVAL_MS = 10_000;
|
|
142
|
-
const CODEX_WEBSOCKET_PONG_TIMEOUT_MS = 60_000;
|
|
143
|
-
const CODEX_WEBSOCKET_MESSAGE_QUEUE_CAPACITY = 4096;
|
|
142
|
+
const CODEX_WEBSOCKET_PING_INTERVAL_MS = Number($env.PI_CODEX_WEBSOCKET_PING_INTERVAL_MS || 10_000);
|
|
143
|
+
const CODEX_WEBSOCKET_PONG_TIMEOUT_MS = Number($env.PI_CODEX_WEBSOCKET_PONG_TIMEOUT_MS || 60_000);
|
|
144
|
+
const CODEX_WEBSOCKET_MESSAGE_QUEUE_CAPACITY = Number($env.PI_CODEX_WEBSOCKET_MESSAGE_QUEUE_CAPACITY || 4096);
|
|
144
145
|
/**
|
|
145
146
|
* Maximum quiet period (no inbound frames AND no observed pong) we'll trust a
|
|
146
147
|
* reused WebSocket for before forcing a fresh handshake. Codex backends and
|
|
@@ -154,7 +155,7 @@ const CODEX_WEBSOCKET_MESSAGE_QUEUE_CAPACITY = 4096;
|
|
|
154
155
|
* execution, user typing, etc.). Set `PI_CODEX_WEBSOCKET_MAX_IDLE_REUSE_MS=0`
|
|
155
156
|
* to disable.
|
|
156
157
|
*/
|
|
157
|
-
const CODEX_WEBSOCKET_MAX_IDLE_REUSE_MS = 30_000;
|
|
158
|
+
const CODEX_WEBSOCKET_MAX_IDLE_REUSE_MS = Number($env.PI_CODEX_WEBSOCKET_MAX_IDLE_REUSE_MS || 30_000);
|
|
158
159
|
/**
|
|
159
160
|
* Steady-state liveness ceiling for the Codex WebSocket transport. Distinct from
|
|
160
161
|
* the OMP-wide stream watchdog removed in #1392: a WebSocket can stay TCP-open
|
|
@@ -163,7 +164,7 @@ const CODEX_WEBSOCKET_MAX_IDLE_REUSE_MS = 30_000;
|
|
|
163
164
|
* states and trigger the WS→SSE fallback. Only applies AFTER the first event
|
|
164
165
|
* has arrived — slow first-token paths wait as long as the caller permits.
|
|
165
166
|
*/
|
|
166
|
-
const CODEX_WEBSOCKET_IDLE_TIMEOUT_MS = 300_000;
|
|
167
|
+
const CODEX_WEBSOCKET_IDLE_TIMEOUT_MS = Number($env.PI_CODEX_WEBSOCKET_IDLE_TIMEOUT_MS || 300_000);
|
|
167
168
|
/**
|
|
168
169
|
* Maximum wait for the first WebSocket event before falling back to SSE.
|
|
169
170
|
* Unlike a stream watchdog, this triggers a transport switch (not a request
|
|
@@ -171,8 +172,9 @@ const CODEX_WEBSOCKET_IDLE_TIMEOUT_MS = 300_000;
|
|
|
171
172
|
* SSE. Generous default so legitimately slow first-token providers still get
|
|
172
173
|
* a chance on the WS transport before falling through.
|
|
173
174
|
*/
|
|
174
|
-
const CODEX_WEBSOCKET_FIRST_EVENT_TIMEOUT_MS = 60_000;
|
|
175
|
-
const CODEX_WEBSOCKET_RETRY_BUDGET = CODEX_MAX_RETRIES;
|
|
175
|
+
const CODEX_WEBSOCKET_FIRST_EVENT_TIMEOUT_MS = Number($env.PI_CODEX_WEBSOCKET_FIRST_EVENT_TIMEOUT_MS || 60_000);
|
|
176
|
+
const CODEX_WEBSOCKET_RETRY_BUDGET = Number($env.PI_CODEX_WEBSOCKET_RETRY_BUDGET || CODEX_MAX_RETRIES);
|
|
177
|
+
const CODEX_WEBSOCKET_RETRY_DELAY_MS = Number($env.PI_CODEX_WEBSOCKET_RETRY_DELAY_MS || CODEX_RETRY_DELAY_MS);
|
|
176
178
|
const CODEX_WEBSOCKET_TRANSPORT_ERROR_PREFIX = "Codex websocket transport error";
|
|
177
179
|
const CODEX_RETRYABLE_EVENT_CODES = new Set(["model_error", "server_error", "internal_error"]);
|
|
178
180
|
const CODEX_RETRYABLE_EVENT_MESSAGE =
|
|
@@ -237,7 +239,10 @@ function createCodexWebSocketTimeoutMessage(reason: string, details: CodexWebSoc
|
|
|
237
239
|
|
|
238
240
|
type CodexTransport = "sse" | "websocket";
|
|
239
241
|
type CodexEventItem = ResponseReasoningItem | ResponseOutputMessage | ResponseFunctionToolCall | ResponseCustomToolCall;
|
|
240
|
-
type CodexOutputBlock =
|
|
242
|
+
type CodexOutputBlock =
|
|
243
|
+
| ThinkingContent
|
|
244
|
+
| TextContent
|
|
245
|
+
| (ToolCall & { [kStreamingPartialJson]: string; [kStreamingLastParseLen]?: number });
|
|
241
246
|
|
|
242
247
|
/**
|
|
243
248
|
* Per-session request-shape counters. Despite the name, these cover both
|
|
@@ -311,7 +316,7 @@ interface CodexOpenItem {
|
|
|
311
316
|
outputIndex?: number;
|
|
312
317
|
}
|
|
313
318
|
|
|
314
|
-
|
|
319
|
+
class CodexStreamRuntime {
|
|
315
320
|
eventStream: AsyncGenerator<Record<string, unknown>>;
|
|
316
321
|
requestBodyForState: RequestBody;
|
|
317
322
|
transport: CodexTransport;
|
|
@@ -321,29 +326,198 @@ interface CodexStreamRuntime {
|
|
|
321
326
|
* registers here; `output_item.done` removes. A keyed event whose `item_id`
|
|
322
327
|
* is not present is dropped rather than appended to a sibling.
|
|
323
328
|
*/
|
|
324
|
-
openItems
|
|
329
|
+
openItems = new Map<string, CodexOpenItem>();
|
|
325
330
|
/**
|
|
326
331
|
* Items open on the wire keyed by `output_index` for streams whose function
|
|
327
332
|
* call items omit `id`; these still carry `output_index` on deltas/done.
|
|
328
333
|
*/
|
|
329
|
-
openItemsByOutputIndex
|
|
334
|
+
openItemsByOutputIndex = new Map<number, CodexOpenItem>();
|
|
330
335
|
/**
|
|
331
336
|
* Most recently added open item for events that omit both `item_id` and
|
|
332
337
|
* `output_index`. Always tracks the latest `output_item.added`, including
|
|
333
338
|
* fully keyless items that never make it into the keyed maps; cleared when
|
|
334
339
|
* its item closes.
|
|
335
340
|
*/
|
|
336
|
-
currentEntry: CodexOpenItem | null;
|
|
341
|
+
currentEntry: CodexOpenItem | null = null;
|
|
337
342
|
/** Convenience mirrors of {@link currentEntry} for legacy singleton handlers. */
|
|
338
|
-
currentItem: CodexEventItem | null;
|
|
339
|
-
currentBlock: CodexOutputBlock | null;
|
|
340
|
-
nativeOutputItems: Array<Record<string, unknown
|
|
341
|
-
websocketStreamRetries
|
|
342
|
-
providerRetryAttempt
|
|
343
|
-
sawTerminalEvent
|
|
344
|
-
canSafelyReplayWebsocketOverSse
|
|
343
|
+
currentItem: CodexEventItem | null = null;
|
|
344
|
+
currentBlock: CodexOutputBlock | null = null;
|
|
345
|
+
nativeOutputItems: Array<Record<string, unknown>> = [];
|
|
346
|
+
websocketStreamRetries = 0;
|
|
347
|
+
providerRetryAttempt = 0;
|
|
348
|
+
sawTerminalEvent = false;
|
|
349
|
+
canSafelyReplayWebsocketOverSse = true;
|
|
345
350
|
whitespaceToolCallArgumentsDelta?: CodexWhitespaceToolCallArgumentsDeltaState;
|
|
346
|
-
whitespaceLoopRetries
|
|
351
|
+
whitespaceLoopRetries = 0;
|
|
352
|
+
|
|
353
|
+
constructor(initial: {
|
|
354
|
+
eventStream: AsyncGenerator<Record<string, unknown>>;
|
|
355
|
+
requestBodyForState: RequestBody;
|
|
356
|
+
transport: CodexTransport;
|
|
357
|
+
websocketState?: CodexWebSocketSessionState;
|
|
358
|
+
}) {
|
|
359
|
+
this.eventStream = initial.eventStream;
|
|
360
|
+
this.requestBodyForState = initial.requestBodyForState;
|
|
361
|
+
this.transport = initial.transport;
|
|
362
|
+
this.websocketState = initial.websocketState;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Wipe per-attempt accumulator state before a recovery path replays the turn.
|
|
367
|
+
* Keeps {@link openItems} and the legacy singleton-current pointers in lockstep
|
|
368
|
+
* with {@link nativeOutputItems} so a stale delta from the failed attempt can't
|
|
369
|
+
* bind to a sibling on the retry.
|
|
370
|
+
*/
|
|
371
|
+
resetAccumulators(): void {
|
|
372
|
+
this.openItems.clear();
|
|
373
|
+
this.openItemsByOutputIndex.clear();
|
|
374
|
+
this.currentEntry = null;
|
|
375
|
+
this.currentItem = null;
|
|
376
|
+
this.currentBlock = null;
|
|
377
|
+
this.nativeOutputItems.length = 0;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Look up the open item a Codex stream event targets. `item_id` wins because it
|
|
382
|
+
* uniquely identifies a response item; `output_index` covers idless function
|
|
383
|
+
* call items. A keyed event whose target is already closed is dropped instead
|
|
384
|
+
* of being routed to a sibling. Only streams that omit both keys fall back to
|
|
385
|
+
* {@link currentEntry} — the most recently added item, including fully keyless
|
|
386
|
+
* ones that never reached the keyed maps.
|
|
387
|
+
*/
|
|
388
|
+
openItemForEvent(rawEvent: Record<string, unknown>): CodexOpenItem | null {
|
|
389
|
+
const itemId = typeof rawEvent.item_id === "string" ? rawEvent.item_id : "";
|
|
390
|
+
if (itemId) return this.openItems.get(itemId) ?? null;
|
|
391
|
+
const outputIndex =
|
|
392
|
+
typeof rawEvent.output_index === "number" && Number.isFinite(rawEvent.output_index)
|
|
393
|
+
? Math.trunc(rawEvent.output_index)
|
|
394
|
+
: undefined;
|
|
395
|
+
if (outputIndex !== undefined) return this.openItemsByOutputIndex.get(outputIndex) ?? null;
|
|
396
|
+
return this.currentEntry;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
closeOpenItem(entry: CodexOpenItem | null | undefined): void {
|
|
400
|
+
if (!entry) return;
|
|
401
|
+
if (entry.itemId) this.openItems.delete(entry.itemId);
|
|
402
|
+
if (entry.outputIndex !== undefined) this.openItemsByOutputIndex.delete(entry.outputIndex);
|
|
403
|
+
if (this.currentEntry === entry) {
|
|
404
|
+
this.currentEntry = null;
|
|
405
|
+
this.currentItem = null;
|
|
406
|
+
this.currentBlock = null;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
observeWhitespaceToolCallArgumentsDelta(
|
|
411
|
+
rawEvent: Record<string, unknown>,
|
|
412
|
+
delta: string,
|
|
413
|
+
): CodexWhitespaceToolCallArgumentsDeltaInterruption | undefined {
|
|
414
|
+
if (!isJsonWhitespaceOnly(delta)) {
|
|
415
|
+
this.whitespaceToolCallArgumentsDelta = undefined;
|
|
416
|
+
return undefined;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const itemId =
|
|
420
|
+
typeof rawEvent.item_id === "string" && rawEvent.item_id.length > 0
|
|
421
|
+
? rawEvent.item_id
|
|
422
|
+
: (this.currentItem?.id ?? "");
|
|
423
|
+
const outputIndex =
|
|
424
|
+
typeof rawEvent.output_index === "number" && Number.isFinite(rawEvent.output_index)
|
|
425
|
+
? Math.trunc(rawEvent.output_index)
|
|
426
|
+
: undefined;
|
|
427
|
+
const sequenceNumber =
|
|
428
|
+
typeof rawEvent.sequence_number === "number" && Number.isFinite(rawEvent.sequence_number)
|
|
429
|
+
? Math.trunc(rawEvent.sequence_number)
|
|
430
|
+
: undefined;
|
|
431
|
+
let state = this.whitespaceToolCallArgumentsDelta;
|
|
432
|
+
if (!state || state.itemId !== itemId || state.outputIndex !== outputIndex) {
|
|
433
|
+
state = {
|
|
434
|
+
itemId,
|
|
435
|
+
outputIndex,
|
|
436
|
+
consecutiveEvents: 0,
|
|
437
|
+
consecutiveChars: 0,
|
|
438
|
+
firstSequenceNumber: sequenceNumber,
|
|
439
|
+
};
|
|
440
|
+
this.whitespaceToolCallArgumentsDelta = state;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
state.consecutiveEvents += 1;
|
|
444
|
+
state.consecutiveChars += delta.length;
|
|
445
|
+
state.lastSequenceNumber = sequenceNumber;
|
|
446
|
+
if (
|
|
447
|
+
state.consecutiveEvents < CODEX_WHITESPACE_TOOL_CALL_ARGUMENT_DELTA_EVENT_LIMIT &&
|
|
448
|
+
state.consecutiveChars < CODEX_WHITESPACE_TOOL_CALL_ARGUMENT_DELTA_CHAR_LIMIT
|
|
449
|
+
) {
|
|
450
|
+
return undefined;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const itemLabel = itemId ? ` for item ${itemId}` : "";
|
|
454
|
+
const sequenceLabel =
|
|
455
|
+
state.firstSequenceNumber === undefined || state.lastSequenceNumber === undefined
|
|
456
|
+
? ""
|
|
457
|
+
: `, sequence ${state.firstSequenceNumber}..${state.lastSequenceNumber}`;
|
|
458
|
+
return {
|
|
459
|
+
message: `Interrupted OpenAI Codex response after ${state.consecutiveEvents} consecutive whitespace-only tool-call argument delta events (${state.consecutiveChars} chars${sequenceLabel})${itemLabel}.`,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
handleToolCallArgumentsDelta(
|
|
464
|
+
rawEvent: Record<string, unknown>,
|
|
465
|
+
stream: AssistantMessageEventStream,
|
|
466
|
+
output: AssistantMessage,
|
|
467
|
+
): CodexWhitespaceToolCallArgumentsDeltaInterruption | undefined {
|
|
468
|
+
const delta = (rawEvent as { delta?: string }).delta || "";
|
|
469
|
+
// Observe BEFORE the item/block guard: degenerate whitespace frames can keep
|
|
470
|
+
// arriving after the item closed (entry detached) and still count as
|
|
471
|
+
// progress for the idle watchdogs — dropping them unobserved would reopen
|
|
472
|
+
// the infinite-loop hole the breaker exists for.
|
|
473
|
+
const interruption = this.observeWhitespaceToolCallArgumentsDelta(rawEvent, delta);
|
|
474
|
+
if (interruption) return interruption;
|
|
475
|
+
// Route to the entry the event keys to; a delta whose item already closed
|
|
476
|
+
// is dropped instead of leaking into a sibling tool call (#2619).
|
|
477
|
+
const entry = this.openItemForEvent(rawEvent);
|
|
478
|
+
if (!entry) return undefined;
|
|
479
|
+
if (entry.item.type !== "function_call" || entry.block?.type !== "toolCall") return undefined;
|
|
480
|
+
accumulateToolCallArgumentsDelta(entry.block, delta, stream, output, entry.contentIndex);
|
|
481
|
+
return undefined;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
handleToolCallArgumentsDone(rawEvent: Record<string, unknown>): void {
|
|
485
|
+
const entry = this.openItemForEvent(rawEvent);
|
|
486
|
+
if (entry?.item.type !== "function_call" || entry.block?.type !== "toolCall") return;
|
|
487
|
+
const args = (rawEvent as { arguments?: string }).arguments;
|
|
488
|
+
if (typeof args === "string") finalizeToolCallArgumentsDone(entry.block, args);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
handleCustomToolCallInputDelta(
|
|
492
|
+
rawEvent: Record<string, unknown>,
|
|
493
|
+
stream: AssistantMessageEventStream,
|
|
494
|
+
output: AssistantMessage,
|
|
495
|
+
): CodexWhitespaceToolCallArgumentsDeltaInterruption | undefined {
|
|
496
|
+
const delta = (rawEvent as { delta?: string }).delta || "";
|
|
497
|
+
// Observe BEFORE the item/block guard — see handleToolCallArgumentsDelta.
|
|
498
|
+
const interruption = this.observeWhitespaceToolCallArgumentsDelta(rawEvent, delta);
|
|
499
|
+
if (interruption) return interruption;
|
|
500
|
+
const entry = this.openItemForEvent(rawEvent);
|
|
501
|
+
if (!entry) return undefined;
|
|
502
|
+
if (entry.item.type !== "custom_tool_call" || entry.block?.type !== "toolCall") return undefined;
|
|
503
|
+
accumulateCustomToolCallInputDelta(entry.block, delta, stream, output, entry.contentIndex);
|
|
504
|
+
return undefined;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
handleCustomToolCallInputDone(rawEvent: Record<string, unknown>): void {
|
|
508
|
+
const entry = this.openItemForEvent(rawEvent);
|
|
509
|
+
if (entry?.item.type !== "custom_tool_call" || entry.block?.type !== "toolCall") return;
|
|
510
|
+
const input = (rawEvent as { input?: string }).input;
|
|
511
|
+
if (typeof input === "string") finalizeCustomToolCallInputDone(entry.block, input);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
handleResponseCreated(rawEvent: Record<string, unknown>): void {
|
|
515
|
+
const response = (rawEvent as { response?: { id?: string } }).response;
|
|
516
|
+
const state = this.websocketState;
|
|
517
|
+
if (state && this.transport === "websocket" && typeof response?.id === "string" && response.id.length > 0) {
|
|
518
|
+
state.lastResponseId = response.id;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
347
521
|
}
|
|
348
522
|
|
|
349
523
|
interface CodexWhitespaceToolCallArgumentsDeltaState {
|
|
@@ -359,12 +533,10 @@ interface CodexWhitespaceToolCallArgumentsDeltaInterruption {
|
|
|
359
533
|
message: string;
|
|
360
534
|
}
|
|
361
535
|
|
|
362
|
-
interface
|
|
536
|
+
interface CodexStreamFailureContext {
|
|
363
537
|
model: Model<"openai-codex-responses">;
|
|
364
538
|
output: AssistantMessage;
|
|
365
|
-
stream: AssistantMessageEventStream;
|
|
366
539
|
options: OpenAICodexResponsesOptions | undefined;
|
|
367
|
-
requestSetup: CodexRequestSetup;
|
|
368
540
|
requestContext: CodexRequestContext;
|
|
369
541
|
startTime: number;
|
|
370
542
|
firstTokenTime?: number;
|
|
@@ -374,20 +546,6 @@ interface CodexStreamCompletion {
|
|
|
374
546
|
firstTokenTime?: number;
|
|
375
547
|
}
|
|
376
548
|
|
|
377
|
-
function parseCodexNonNegativeInteger(value: string | undefined, fallback: number): number {
|
|
378
|
-
if (!value) return fallback;
|
|
379
|
-
const parsed = Number(value);
|
|
380
|
-
if (!Number.isFinite(parsed) || parsed < 0) return fallback;
|
|
381
|
-
return Math.trunc(parsed);
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
function parseCodexPositiveInteger(value: string | undefined, fallback: number): number {
|
|
385
|
-
if (!value) return fallback;
|
|
386
|
-
const parsed = Number(value);
|
|
387
|
-
if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
|
|
388
|
-
return Math.trunc(parsed);
|
|
389
|
-
}
|
|
390
|
-
|
|
391
549
|
function createCodexProviderSessionState(): CodexProviderSessionState {
|
|
392
550
|
const state: CodexProviderSessionState = {
|
|
393
551
|
webSocketSessions: new Map(),
|
|
@@ -639,13 +797,9 @@ function createRequestSetup(options: OpenAICodexResponsesOptions | undefined): C
|
|
|
639
797
|
? AbortSignal.any([options.signal, requestAbortController.signal])
|
|
640
798
|
: requestAbortController.signal;
|
|
641
799
|
const idleTimeoutMs = options?.streamIdleTimeoutMs ?? getOpenAIStreamIdleTimeoutMs();
|
|
642
|
-
const websocketIdleTimeoutMs =
|
|
643
|
-
options?.streamIdleTimeoutMs ??
|
|
644
|
-
parseCodexPositiveInteger($env.PI_CODEX_WEBSOCKET_IDLE_TIMEOUT_MS, CODEX_WEBSOCKET_IDLE_TIMEOUT_MS);
|
|
800
|
+
const websocketIdleTimeoutMs = options?.streamIdleTimeoutMs ?? CODEX_WEBSOCKET_IDLE_TIMEOUT_MS;
|
|
645
801
|
const firstEventTimeoutMs = options?.streamFirstEventTimeoutMs ?? getOpenAIStreamFirstEventTimeoutMs(idleTimeoutMs);
|
|
646
|
-
const websocketFirstEventTimeoutMs =
|
|
647
|
-
options?.streamFirstEventTimeoutMs ??
|
|
648
|
-
parseCodexPositiveInteger($env.PI_CODEX_WEBSOCKET_FIRST_EVENT_TIMEOUT_MS, CODEX_WEBSOCKET_FIRST_EVENT_TIMEOUT_MS);
|
|
802
|
+
const websocketFirstEventTimeoutMs = options?.streamFirstEventTimeoutMs ?? CODEX_WEBSOCKET_FIRST_EVENT_TIMEOUT_MS;
|
|
649
803
|
const wrapCodexSseStream = (
|
|
650
804
|
source: AsyncGenerator<Record<string, unknown>>,
|
|
651
805
|
): AsyncGenerator<Record<string, unknown>> =>
|
|
@@ -677,7 +831,7 @@ async function buildCodexRequestContext(
|
|
|
677
831
|
): Promise<CodexRequestContext> {
|
|
678
832
|
const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
|
|
679
833
|
if (!apiKey) {
|
|
680
|
-
throw new
|
|
834
|
+
throw new AIError.MissingApiKeyError(model.provider);
|
|
681
835
|
}
|
|
682
836
|
|
|
683
837
|
const accountId = getAccountId(apiKey);
|
|
@@ -791,10 +945,7 @@ async function openInitialCodexEventStream(
|
|
|
791
945
|
}> {
|
|
792
946
|
const { transformedBody, websocketState } = requestContext;
|
|
793
947
|
if (websocketState && shouldUseCodexWebSocket(model, websocketState, options?.preferWebsockets)) {
|
|
794
|
-
const websocketRetryBudget =
|
|
795
|
-
$env.PI_CODEX_WEBSOCKET_RETRY_BUDGET,
|
|
796
|
-
CODEX_WEBSOCKET_RETRY_BUDGET,
|
|
797
|
-
);
|
|
948
|
+
const websocketRetryBudget = CODEX_WEBSOCKET_RETRY_BUDGET;
|
|
798
949
|
let websocketRetries = 0;
|
|
799
950
|
while (true) {
|
|
800
951
|
try {
|
|
@@ -825,13 +976,9 @@ async function openInitialCodexEventStream(
|
|
|
825
976
|
});
|
|
826
977
|
if (!activateFallback) {
|
|
827
978
|
websocketRetries += 1;
|
|
828
|
-
await scheduler.wait(
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
{
|
|
832
|
-
signal: requestSetup.requestSignal,
|
|
833
|
-
},
|
|
834
|
-
);
|
|
979
|
+
await scheduler.wait(CODEX_WEBSOCKET_RETRY_DELAY_MS * Math.max(1, websocketRetries), {
|
|
980
|
+
signal: requestSetup.requestSignal,
|
|
981
|
+
});
|
|
835
982
|
continue;
|
|
836
983
|
}
|
|
837
984
|
break;
|
|
@@ -893,7 +1040,7 @@ async function openCodexWebSocketTransport(
|
|
|
893
1040
|
sentModelsEtagHeader: websocketHeaders.has(X_MODELS_ETAG_HEADER),
|
|
894
1041
|
requestType: websocketRequest.type,
|
|
895
1042
|
retry,
|
|
896
|
-
retryBudget:
|
|
1043
|
+
retryBudget: CODEX_WEBSOCKET_RETRY_BUDGET,
|
|
897
1044
|
});
|
|
898
1045
|
const websocketConnection = await getOrCreateCodexWebSocketConnection(
|
|
899
1046
|
websocketState,
|
|
@@ -973,131 +1120,6 @@ async function openCodexSseTransport(
|
|
|
973
1120
|
return { eventStream: await open(wireBody), requestBodyForState: structuredCloneJSON(wireBody), transport: "sse" };
|
|
974
1121
|
}
|
|
975
1122
|
|
|
976
|
-
async function reopenCodexWebSocketRuntimeStream(
|
|
977
|
-
context: CodexStreamProcessingContext,
|
|
978
|
-
runtime: CodexStreamRuntime,
|
|
979
|
-
state: CodexWebSocketSessionState,
|
|
980
|
-
): Promise<void> {
|
|
981
|
-
try {
|
|
982
|
-
const next = await openCodexWebSocketTransport(
|
|
983
|
-
context.model,
|
|
984
|
-
context.options,
|
|
985
|
-
context.requestContext,
|
|
986
|
-
context.requestSetup,
|
|
987
|
-
state,
|
|
988
|
-
runtime.websocketStreamRetries,
|
|
989
|
-
context.options ? event => context.options?.onSseEvent?.(event, context.model) : undefined,
|
|
990
|
-
);
|
|
991
|
-
runtime.eventStream = next.eventStream;
|
|
992
|
-
runtime.requestBodyForState = next.requestBodyForState;
|
|
993
|
-
runtime.transport = next.transport;
|
|
994
|
-
state.lastTransport = next.transport;
|
|
995
|
-
} catch (error) {
|
|
996
|
-
if (!(error instanceof CodexWebSocketTransportError)) throw error;
|
|
997
|
-
// Reopen failed at the websocket layer (handshake refused, connect timeout, etc.).
|
|
998
|
-
// Activate fallback so subsequent turns use SSE, and replay this turn over SSE
|
|
999
|
-
// instead of surfacing a raw transport error to the caller.
|
|
1000
|
-
recordCodexWebSocketFailure(state, true);
|
|
1001
|
-
CODEX_DEBUG &&
|
|
1002
|
-
logger.debug("[codex] codex websocket reopen failed, falling back to SSE", {
|
|
1003
|
-
error: error.message,
|
|
1004
|
-
retry: runtime.websocketStreamRetries,
|
|
1005
|
-
});
|
|
1006
|
-
await reopenCodexSseRuntimeStream(context, runtime, state);
|
|
1007
|
-
}
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
async function reopenCodexSseRuntimeStream(
|
|
1011
|
-
context: CodexStreamProcessingContext,
|
|
1012
|
-
runtime: CodexStreamRuntime,
|
|
1013
|
-
state: CodexWebSocketSessionState | undefined,
|
|
1014
|
-
): Promise<void> {
|
|
1015
|
-
const next = await openCodexSseTransport(
|
|
1016
|
-
context.model,
|
|
1017
|
-
context.requestContext,
|
|
1018
|
-
context.requestSetup,
|
|
1019
|
-
context.options,
|
|
1020
|
-
state,
|
|
1021
|
-
);
|
|
1022
|
-
runtime.eventStream = next.eventStream;
|
|
1023
|
-
runtime.requestBodyForState = next.requestBodyForState;
|
|
1024
|
-
runtime.transport = next.transport;
|
|
1025
|
-
if (state) {
|
|
1026
|
-
state.lastTransport = next.transport;
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
function createCodexStreamRuntime(initial: {
|
|
1031
|
-
eventStream: AsyncGenerator<Record<string, unknown>>;
|
|
1032
|
-
requestBodyForState: RequestBody;
|
|
1033
|
-
transport: CodexTransport;
|
|
1034
|
-
websocketState?: CodexWebSocketSessionState;
|
|
1035
|
-
}): CodexStreamRuntime {
|
|
1036
|
-
return {
|
|
1037
|
-
eventStream: initial.eventStream,
|
|
1038
|
-
requestBodyForState: initial.requestBodyForState,
|
|
1039
|
-
transport: initial.transport,
|
|
1040
|
-
websocketState: initial.websocketState,
|
|
1041
|
-
openItems: new Map(),
|
|
1042
|
-
openItemsByOutputIndex: new Map(),
|
|
1043
|
-
currentEntry: null,
|
|
1044
|
-
currentItem: null,
|
|
1045
|
-
currentBlock: null,
|
|
1046
|
-
nativeOutputItems: [],
|
|
1047
|
-
websocketStreamRetries: 0,
|
|
1048
|
-
providerRetryAttempt: 0,
|
|
1049
|
-
whitespaceLoopRetries: 0,
|
|
1050
|
-
sawTerminalEvent: false,
|
|
1051
|
-
canSafelyReplayWebsocketOverSse: true,
|
|
1052
|
-
whitespaceToolCallArgumentsDelta: undefined,
|
|
1053
|
-
};
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
/**
|
|
1057
|
-
* Wipe per-attempt accumulator state before a recovery path replays the turn.
|
|
1058
|
-
* Keeps {@link CodexStreamRuntime.openItems} and the legacy singleton-current
|
|
1059
|
-
* pointers in lockstep with {@link CodexStreamRuntime.nativeOutputItems} so a
|
|
1060
|
-
* stale delta from the failed attempt can't bind to a sibling on the retry.
|
|
1061
|
-
*/
|
|
1062
|
-
function resetCodexStreamAccumulators(runtime: CodexStreamRuntime): void {
|
|
1063
|
-
runtime.openItems.clear();
|
|
1064
|
-
runtime.openItemsByOutputIndex.clear();
|
|
1065
|
-
runtime.currentEntry = null;
|
|
1066
|
-
runtime.currentItem = null;
|
|
1067
|
-
runtime.currentBlock = null;
|
|
1068
|
-
runtime.nativeOutputItems.length = 0;
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
/**
|
|
1072
|
-
* Look up the open item a Codex stream event targets. `item_id` wins because it
|
|
1073
|
-
* uniquely identifies a response item; `output_index` covers idless function
|
|
1074
|
-
* call items. A keyed event whose target is already closed is dropped instead
|
|
1075
|
-
* of being routed to a sibling. Only streams that omit both keys fall back to
|
|
1076
|
-
* {@link CodexStreamRuntime.currentEntry} — the most recently added item,
|
|
1077
|
-
* including fully keyless ones that never reached the keyed maps.
|
|
1078
|
-
*/
|
|
1079
|
-
function openItemForEvent(runtime: CodexStreamRuntime, rawEvent: Record<string, unknown>): CodexOpenItem | null {
|
|
1080
|
-
const itemId = typeof rawEvent.item_id === "string" ? rawEvent.item_id : "";
|
|
1081
|
-
if (itemId) return runtime.openItems.get(itemId) ?? null;
|
|
1082
|
-
const outputIndex =
|
|
1083
|
-
typeof rawEvent.output_index === "number" && Number.isFinite(rawEvent.output_index)
|
|
1084
|
-
? Math.trunc(rawEvent.output_index)
|
|
1085
|
-
: undefined;
|
|
1086
|
-
if (outputIndex !== undefined) return runtime.openItemsByOutputIndex.get(outputIndex) ?? null;
|
|
1087
|
-
return runtime.currentEntry;
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
function closeCodexOpenItem(runtime: CodexStreamRuntime, entry: CodexOpenItem | null | undefined): void {
|
|
1091
|
-
if (!entry) return;
|
|
1092
|
-
if (entry.itemId) runtime.openItems.delete(entry.itemId);
|
|
1093
|
-
if (entry.outputIndex !== undefined) runtime.openItemsByOutputIndex.delete(entry.outputIndex);
|
|
1094
|
-
if (runtime.currentEntry === entry) {
|
|
1095
|
-
runtime.currentEntry = null;
|
|
1096
|
-
runtime.currentItem = null;
|
|
1097
|
-
runtime.currentBlock = null;
|
|
1098
|
-
}
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
1123
|
function isJsonWhitespaceOnly(value: string): boolean {
|
|
1102
1124
|
for (let index = 0; index < value.length; index += 1) {
|
|
1103
1125
|
const code = value.charCodeAt(index);
|
|
@@ -1108,261 +1130,13 @@ function isJsonWhitespaceOnly(value: string): boolean {
|
|
|
1108
1130
|
return true;
|
|
1109
1131
|
}
|
|
1110
1132
|
|
|
1111
|
-
function observeWhitespaceToolCallArgumentsDelta(
|
|
1112
|
-
runtime: CodexStreamRuntime,
|
|
1113
|
-
rawEvent: Record<string, unknown>,
|
|
1114
|
-
delta: string,
|
|
1115
|
-
): CodexWhitespaceToolCallArgumentsDeltaInterruption | undefined {
|
|
1116
|
-
if (!isJsonWhitespaceOnly(delta)) {
|
|
1117
|
-
runtime.whitespaceToolCallArgumentsDelta = undefined;
|
|
1118
|
-
return undefined;
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
const itemId =
|
|
1122
|
-
typeof rawEvent.item_id === "string" && rawEvent.item_id.length > 0
|
|
1123
|
-
? rawEvent.item_id
|
|
1124
|
-
: (runtime.currentItem?.id ?? "");
|
|
1125
|
-
const outputIndex =
|
|
1126
|
-
typeof rawEvent.output_index === "number" && Number.isFinite(rawEvent.output_index)
|
|
1127
|
-
? Math.trunc(rawEvent.output_index)
|
|
1128
|
-
: undefined;
|
|
1129
|
-
const sequenceNumber =
|
|
1130
|
-
typeof rawEvent.sequence_number === "number" && Number.isFinite(rawEvent.sequence_number)
|
|
1131
|
-
? Math.trunc(rawEvent.sequence_number)
|
|
1132
|
-
: undefined;
|
|
1133
|
-
let state = runtime.whitespaceToolCallArgumentsDelta;
|
|
1134
|
-
if (!state || state.itemId !== itemId || state.outputIndex !== outputIndex) {
|
|
1135
|
-
state = {
|
|
1136
|
-
itemId,
|
|
1137
|
-
outputIndex,
|
|
1138
|
-
consecutiveEvents: 0,
|
|
1139
|
-
consecutiveChars: 0,
|
|
1140
|
-
firstSequenceNumber: sequenceNumber,
|
|
1141
|
-
};
|
|
1142
|
-
runtime.whitespaceToolCallArgumentsDelta = state;
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
state.consecutiveEvents += 1;
|
|
1146
|
-
state.consecutiveChars += delta.length;
|
|
1147
|
-
state.lastSequenceNumber = sequenceNumber;
|
|
1148
|
-
if (
|
|
1149
|
-
state.consecutiveEvents < CODEX_WHITESPACE_TOOL_CALL_ARGUMENT_DELTA_EVENT_LIMIT &&
|
|
1150
|
-
state.consecutiveChars < CODEX_WHITESPACE_TOOL_CALL_ARGUMENT_DELTA_CHAR_LIMIT
|
|
1151
|
-
) {
|
|
1152
|
-
return undefined;
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
const itemLabel = itemId ? ` for item ${itemId}` : "";
|
|
1156
|
-
const sequenceLabel =
|
|
1157
|
-
state.firstSequenceNumber === undefined || state.lastSequenceNumber === undefined
|
|
1158
|
-
? ""
|
|
1159
|
-
: `, sequence ${state.firstSequenceNumber}..${state.lastSequenceNumber}`;
|
|
1160
|
-
return {
|
|
1161
|
-
message: `Interrupted OpenAI Codex response after ${state.consecutiveEvents} consecutive whitespace-only tool-call argument delta events (${state.consecutiveChars} chars${sequenceLabel})${itemLabel}.`,
|
|
1162
|
-
};
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
|
-
async function processCodexResponseStream(
|
|
1166
|
-
context: CodexStreamProcessingContext,
|
|
1167
|
-
runtime: CodexStreamRuntime,
|
|
1168
|
-
): Promise<CodexStreamCompletion> {
|
|
1169
|
-
const { output, stream } = context;
|
|
1170
|
-
stream.push({ type: "start", partial: output });
|
|
1171
|
-
|
|
1172
|
-
while (true) {
|
|
1173
|
-
try {
|
|
1174
|
-
let firstTokenTime = context.firstTokenTime;
|
|
1175
|
-
for await (const rawEvent of runtime.eventStream) {
|
|
1176
|
-
firstTokenTime = handleCodexStreamEvent(context, runtime, rawEvent, firstTokenTime);
|
|
1177
|
-
if (runtime.sawTerminalEvent) break;
|
|
1178
|
-
}
|
|
1179
|
-
return { firstTokenTime };
|
|
1180
|
-
} catch (error) {
|
|
1181
|
-
const recovered = await recoverCodexStreamError(context, runtime, error);
|
|
1182
|
-
if (!recovered) {
|
|
1183
|
-
throw error;
|
|
1184
|
-
}
|
|
1185
|
-
stream.push({ type: "start", partial: output });
|
|
1186
|
-
}
|
|
1187
|
-
}
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
function handleCodexStreamEvent(
|
|
1191
|
-
context: CodexStreamProcessingContext,
|
|
1192
|
-
runtime: CodexStreamRuntime,
|
|
1193
|
-
rawEvent: Record<string, unknown>,
|
|
1194
|
-
firstTokenTime: number | undefined,
|
|
1195
|
-
): number | undefined {
|
|
1196
|
-
const { model, output, stream } = context;
|
|
1197
|
-
const eventType = typeof rawEvent.type === "string" ? rawEvent.type : "";
|
|
1198
|
-
if (!eventType) return firstTokenTime;
|
|
1199
|
-
|
|
1200
|
-
if (eventType === "response.output_item.added") {
|
|
1201
|
-
runtime.whitespaceToolCallArgumentsDelta = undefined;
|
|
1202
|
-
if (!firstTokenTime) firstTokenTime = Date.now();
|
|
1203
|
-
const item = rawEvent.item as CodexEventItem;
|
|
1204
|
-
runtime.currentItem = item;
|
|
1205
|
-
runtime.currentBlock = createOutputBlockForItem(item);
|
|
1206
|
-
let contentIndex = -1;
|
|
1207
|
-
if (runtime.currentBlock) {
|
|
1208
|
-
output.content.push(runtime.currentBlock);
|
|
1209
|
-
contentIndex = output.content.length - 1;
|
|
1210
|
-
}
|
|
1211
|
-
// Track every open item by every stable key the wire gives us. `item.id`
|
|
1212
|
-
// is best; `output_index` preserves idless function/custom tool calls and
|
|
1213
|
-
// keeps their final args authoritative when only `output_item.done`
|
|
1214
|
-
// carries the full payload.
|
|
1215
|
-
const itemId = typeof (item as { id?: string }).id === "string" ? (item as { id: string }).id : undefined;
|
|
1216
|
-
const outputIndex =
|
|
1217
|
-
typeof rawEvent.output_index === "number" && Number.isFinite(rawEvent.output_index)
|
|
1218
|
-
? Math.trunc(rawEvent.output_index)
|
|
1219
|
-
: undefined;
|
|
1220
|
-
const entry: CodexOpenItem = { item, block: runtime.currentBlock, contentIndex, itemId, outputIndex };
|
|
1221
|
-
runtime.currentEntry = entry;
|
|
1222
|
-
if (itemId) runtime.openItems.set(itemId, entry);
|
|
1223
|
-
if (outputIndex !== undefined) runtime.openItemsByOutputIndex.set(outputIndex, entry);
|
|
1224
|
-
if (!runtime.currentBlock) return firstTokenTime;
|
|
1225
|
-
stream.push({
|
|
1226
|
-
type: getOutputBlockStartEventType(runtime.currentBlock),
|
|
1227
|
-
contentIndex,
|
|
1228
|
-
partial: output,
|
|
1229
|
-
});
|
|
1230
|
-
return firstTokenTime;
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
if (eventType === "response.reasoning_summary_part.added") {
|
|
1234
|
-
if (runtime.currentItem?.type === "reasoning") {
|
|
1235
|
-
appendReasoningSummaryPart(
|
|
1236
|
-
runtime.currentItem,
|
|
1237
|
-
(rawEvent as { part: ResponseReasoningItem["summary"][number] }).part,
|
|
1238
|
-
);
|
|
1239
|
-
}
|
|
1240
|
-
return firstTokenTime;
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
if (eventType === "response.reasoning_summary_text.delta") {
|
|
1244
|
-
if (runtime.currentItem?.type === "reasoning" && runtime.currentBlock?.type === "thinking") {
|
|
1245
|
-
appendReasoningSummaryTextDelta(
|
|
1246
|
-
runtime.currentItem,
|
|
1247
|
-
runtime.currentBlock,
|
|
1248
|
-
(rawEvent as { delta?: string }).delta || "",
|
|
1249
|
-
stream,
|
|
1250
|
-
output,
|
|
1251
|
-
output.content.length - 1,
|
|
1252
|
-
);
|
|
1253
|
-
}
|
|
1254
|
-
return firstTokenTime;
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
if (eventType === "response.reasoning_summary_part.done") {
|
|
1258
|
-
if (runtime.currentItem?.type === "reasoning" && runtime.currentBlock?.type === "thinking") {
|
|
1259
|
-
appendReasoningSummaryPartDone(
|
|
1260
|
-
runtime.currentItem,
|
|
1261
|
-
runtime.currentBlock,
|
|
1262
|
-
stream,
|
|
1263
|
-
output,
|
|
1264
|
-
output.content.length - 1,
|
|
1265
|
-
);
|
|
1266
|
-
}
|
|
1267
|
-
return firstTokenTime;
|
|
1268
|
-
}
|
|
1269
|
-
|
|
1270
|
-
if (eventType === "response.content_part.added") {
|
|
1271
|
-
if (runtime.currentItem?.type === "message") {
|
|
1272
|
-
appendMessageContentPart(
|
|
1273
|
-
runtime.currentItem,
|
|
1274
|
-
(rawEvent as { part?: ResponseOutputMessage["content"][number] }).part,
|
|
1275
|
-
);
|
|
1276
|
-
}
|
|
1277
|
-
return firstTokenTime;
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
if (eventType === "response.output_text.delta" || eventType === "response.refusal.delta") {
|
|
1281
|
-
if (runtime.currentItem?.type === "message" && runtime.currentBlock?.type === "text") {
|
|
1282
|
-
appendMessageTextDelta(
|
|
1283
|
-
runtime.currentItem,
|
|
1284
|
-
runtime.currentBlock,
|
|
1285
|
-
(rawEvent as { delta?: string }).delta || "",
|
|
1286
|
-
stream,
|
|
1287
|
-
output,
|
|
1288
|
-
output.content.length - 1,
|
|
1289
|
-
eventType === "response.refusal.delta" ? "refusal" : "output_text",
|
|
1290
|
-
);
|
|
1291
|
-
}
|
|
1292
|
-
return firstTokenTime;
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
|
-
if (eventType === "response.function_call_arguments.delta") {
|
|
1296
|
-
const interruption = handleToolCallArgumentsDelta(runtime, rawEvent, stream, output);
|
|
1297
|
-
if (interruption) {
|
|
1298
|
-
runtime.websocketState?.connection?.close("degenerate-tool-call");
|
|
1299
|
-
throw new CodexWhitespaceToolCallLoopError(interruption.message);
|
|
1300
|
-
}
|
|
1301
|
-
return firstTokenTime;
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
|
-
if (eventType === "response.function_call_arguments.done") {
|
|
1305
|
-
runtime.whitespaceToolCallArgumentsDelta = undefined;
|
|
1306
|
-
handleToolCallArgumentsDone(runtime, rawEvent);
|
|
1307
|
-
return firstTokenTime;
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
if (eventType === "response.custom_tool_call_input.delta") {
|
|
1311
|
-
const interruption = handleCustomToolCallInputDelta(runtime, rawEvent, stream, output);
|
|
1312
|
-
if (interruption) {
|
|
1313
|
-
runtime.websocketState?.connection?.close("degenerate-tool-call");
|
|
1314
|
-
throw new CodexWhitespaceToolCallLoopError(interruption.message);
|
|
1315
|
-
}
|
|
1316
|
-
return firstTokenTime;
|
|
1317
|
-
}
|
|
1318
|
-
|
|
1319
|
-
if (eventType === "response.custom_tool_call_input.done") {
|
|
1320
|
-
runtime.whitespaceToolCallArgumentsDelta = undefined;
|
|
1321
|
-
handleCustomToolCallInputDone(runtime, rawEvent);
|
|
1322
|
-
return firstTokenTime;
|
|
1323
|
-
}
|
|
1324
|
-
|
|
1325
|
-
if (eventType === "response.output_item.done") {
|
|
1326
|
-
runtime.whitespaceToolCallArgumentsDelta = undefined;
|
|
1327
|
-
handleOutputItemDone(model, output, stream, runtime, rawEvent);
|
|
1328
|
-
return firstTokenTime;
|
|
1329
|
-
}
|
|
1330
|
-
|
|
1331
|
-
if (eventType === "response.created") {
|
|
1332
|
-
handleResponseCreated(runtime, rawEvent);
|
|
1333
|
-
return firstTokenTime;
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
if (eventType === "response.completed" || eventType === "response.done" || eventType === "response.incomplete") {
|
|
1337
|
-
handleResponseCompleted(model, output, runtime, rawEvent);
|
|
1338
|
-
return firstTokenTime;
|
|
1339
|
-
}
|
|
1340
|
-
|
|
1341
|
-
if (eventType === "response.metadata") {
|
|
1342
|
-
const moderation = asRecord(rawEvent.metadata)?.[CODEX_MODERATION_METADATA_KEY];
|
|
1343
|
-
if (moderation !== undefined) {
|
|
1344
|
-
try {
|
|
1345
|
-
context.options?.onModerationMetadata?.(moderation);
|
|
1346
|
-
} catch {
|
|
1347
|
-
// Diagnostic observer: failures must not disturb the stream.
|
|
1348
|
-
}
|
|
1349
|
-
}
|
|
1350
|
-
return firstTokenTime;
|
|
1351
|
-
}
|
|
1352
|
-
|
|
1353
|
-
if (eventType === "error" || eventType === "response.failed") {
|
|
1354
|
-
throw createCodexProviderStreamError(rawEvent);
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
|
-
return firstTokenTime;
|
|
1358
|
-
}
|
|
1359
|
-
|
|
1360
1133
|
function createOutputBlockForItem(item: CodexEventItem): CodexOutputBlock | null {
|
|
1361
1134
|
if (item.type === "reasoning") {
|
|
1362
1135
|
return { type: "thinking", thinking: "" };
|
|
1363
1136
|
}
|
|
1364
1137
|
if (item.type === "message") {
|
|
1365
|
-
|
|
1138
|
+
const phase = item.phase === "commentary" || item.phase === "final_answer" ? item.phase : undefined;
|
|
1139
|
+
return { type: "text", text: "", textSignature: encodeTextSignatureV1(item.id, phase) };
|
|
1366
1140
|
}
|
|
1367
1141
|
if (item.type === "function_call") {
|
|
1368
1142
|
return {
|
|
@@ -1370,7 +1144,7 @@ function createOutputBlockForItem(item: CodexEventItem): CodexOutputBlock | null
|
|
|
1370
1144
|
id: encodeResponsesToolCallId(item.call_id, item.id),
|
|
1371
1145
|
name: item.name,
|
|
1372
1146
|
arguments: {},
|
|
1373
|
-
|
|
1147
|
+
[kStreamingPartialJson]: item.arguments || "",
|
|
1374
1148
|
};
|
|
1375
1149
|
}
|
|
1376
1150
|
if (item.type === "custom_tool_call") {
|
|
@@ -1383,7 +1157,7 @@ function createOutputBlockForItem(item: CodexEventItem): CodexOutputBlock | null
|
|
|
1383
1157
|
name: item.name,
|
|
1384
1158
|
arguments: { input: item.input ?? "" },
|
|
1385
1159
|
customWireName: item.name,
|
|
1386
|
-
|
|
1160
|
+
[kStreamingPartialJson]: item.input ?? "",
|
|
1387
1161
|
};
|
|
1388
1162
|
}
|
|
1389
1163
|
return null;
|
|
@@ -1395,619 +1169,778 @@ function getOutputBlockStartEventType(block: CodexOutputBlock): "thinking_start"
|
|
|
1395
1169
|
return "toolcall_start";
|
|
1396
1170
|
}
|
|
1397
1171
|
|
|
1398
|
-
function
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
//
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
if (interruption) return interruption;
|
|
1411
|
-
// Route to the entry the event keys to; a delta whose item already closed
|
|
1412
|
-
// is dropped instead of leaking into a sibling tool call (#2619).
|
|
1413
|
-
const entry = openItemForEvent(runtime, rawEvent);
|
|
1414
|
-
if (!entry) return undefined;
|
|
1415
|
-
if (entry.item.type !== "function_call" || entry.block?.type !== "toolCall") return undefined;
|
|
1416
|
-
accumulateToolCallArgumentsDelta(entry.block, delta, stream, output, entry.contentIndex);
|
|
1417
|
-
return undefined;
|
|
1172
|
+
function isCodexStalePreviousResponseError(error: unknown): boolean {
|
|
1173
|
+
if (error instanceof CodexProviderStreamError) return error.code === "previous_response_not_found";
|
|
1174
|
+
if (!(error instanceof Error)) return false;
|
|
1175
|
+
if ((error as { code?: string }).code === "previous_response_not_found") return true;
|
|
1176
|
+
// "unsupported": the backend intermittently rejects the parameter outright
|
|
1177
|
+
// with `{"detail":"Unsupported parameter: previous_response_id"}` (no
|
|
1178
|
+
// `error.code`); treat it like a stale chain so the turn replays with full
|
|
1179
|
+
// context instead of surfacing the 400.
|
|
1180
|
+
return (
|
|
1181
|
+
/previous[ _]?response/i.test(error.message) &&
|
|
1182
|
+
/not[ _]?found|invalid|expired|stale|unsupported/i.test(error.message)
|
|
1183
|
+
);
|
|
1418
1184
|
}
|
|
1419
1185
|
|
|
1420
|
-
function
|
|
1421
|
-
const
|
|
1422
|
-
if (
|
|
1423
|
-
|
|
1424
|
-
|
|
1186
|
+
async function handleCodexStreamFailure(context: CodexStreamFailureContext, error: unknown): Promise<AssistantMessage> {
|
|
1187
|
+
const { output } = context;
|
|
1188
|
+
if (context.requestContext.websocketState) {
|
|
1189
|
+
resetCodexWebSocketAppendState(context.requestContext.websocketState);
|
|
1190
|
+
context.requestContext.websocketState.turnState = undefined;
|
|
1191
|
+
context.requestContext.websocketState.modelsEtag = undefined;
|
|
1192
|
+
}
|
|
1193
|
+
const result = await AIError.finalize(error, {
|
|
1194
|
+
api: context.model.api,
|
|
1195
|
+
signal: context.options?.signal,
|
|
1196
|
+
rawRequestDump: context.requestContext.rawRequestDump,
|
|
1197
|
+
});
|
|
1198
|
+
output.stopReason = result.stopReason;
|
|
1199
|
+
output.errorStatus = result.status;
|
|
1200
|
+
output.errorId = result.id;
|
|
1201
|
+
output.errorMessage = result.message;
|
|
1202
|
+
output.duration = performance.now() - context.startTime;
|
|
1203
|
+
if (context.firstTokenTime) {
|
|
1204
|
+
output.ttft = context.firstTokenTime - context.startTime;
|
|
1205
|
+
}
|
|
1206
|
+
return output;
|
|
1425
1207
|
}
|
|
1426
1208
|
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1209
|
+
/**
|
|
1210
|
+
* Owns one `streamOpenAICodexResponses` call: the request scaffolding
|
|
1211
|
+
* (model/output/stream/options/request context) plus the per-attempt
|
|
1212
|
+
* {@link CodexStreamRuntime}. Drives the event loop in {@link process}, applies
|
|
1213
|
+
* the transport-fallback / retry recovery ladder, and emits the final message
|
|
1214
|
+
* in {@link finalize}. The runtime object is mutated in place across retries
|
|
1215
|
+
* (event stream and accumulators are swapped/reset), never reassigned.
|
|
1216
|
+
*/
|
|
1217
|
+
class CodexStreamProcessor {
|
|
1218
|
+
runtime: CodexStreamRuntime;
|
|
1219
|
+
model: Model<"openai-codex-responses">;
|
|
1220
|
+
output: AssistantMessage;
|
|
1221
|
+
stream: AssistantMessageEventStream;
|
|
1222
|
+
options: OpenAICodexResponsesOptions | undefined;
|
|
1223
|
+
requestSetup: CodexRequestSetup;
|
|
1224
|
+
requestContext: CodexRequestContext;
|
|
1225
|
+
startTime: number;
|
|
1226
|
+
firstTokenTime?: number;
|
|
1443
1227
|
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1228
|
+
constructor(init: {
|
|
1229
|
+
runtime: CodexStreamRuntime;
|
|
1230
|
+
model: Model<"openai-codex-responses">;
|
|
1231
|
+
output: AssistantMessage;
|
|
1232
|
+
stream: AssistantMessageEventStream;
|
|
1233
|
+
options: OpenAICodexResponsesOptions | undefined;
|
|
1234
|
+
requestSetup: CodexRequestSetup;
|
|
1235
|
+
requestContext: CodexRequestContext;
|
|
1236
|
+
startTime: number;
|
|
1237
|
+
}) {
|
|
1238
|
+
this.runtime = init.runtime;
|
|
1239
|
+
this.model = init.model;
|
|
1240
|
+
this.output = init.output;
|
|
1241
|
+
this.stream = init.stream;
|
|
1242
|
+
this.options = init.options;
|
|
1243
|
+
this.requestSetup = init.requestSetup;
|
|
1244
|
+
this.requestContext = init.requestContext;
|
|
1245
|
+
this.startTime = init.startTime;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
async process(): Promise<CodexStreamCompletion> {
|
|
1249
|
+
const { output, stream } = this;
|
|
1250
|
+
stream.push({ type: "start", partial: output });
|
|
1450
1251
|
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
// routes `output_item.done` to the block that received `output_item.added`.
|
|
1468
|
-
const itemId = typeof (item as { id?: string }).id === "string" ? (item as { id: string }).id : "";
|
|
1469
|
-
const entry = (itemId ? runtime.openItems.get(itemId) : null) ?? openItemForEvent(runtime, rawEvent);
|
|
1470
|
-
const block = entry?.block ?? null;
|
|
1471
|
-
const contentIndex = entry?.contentIndex ?? output.content.length - 1;
|
|
1472
|
-
|
|
1473
|
-
if (item.type === "reasoning" && block?.type === "thinking") {
|
|
1474
|
-
block.thinking = item.summary?.map(summary => summary.text).join("\n\n") || "";
|
|
1475
|
-
block.thinkingSignature = JSON.stringify(item);
|
|
1476
|
-
stream.push({
|
|
1477
|
-
type: "thinking_end",
|
|
1478
|
-
contentIndex,
|
|
1479
|
-
content: block.thinking,
|
|
1480
|
-
partial: output,
|
|
1481
|
-
});
|
|
1482
|
-
closeCodexOpenItem(runtime, entry);
|
|
1483
|
-
return;
|
|
1252
|
+
while (true) {
|
|
1253
|
+
try {
|
|
1254
|
+
let firstTokenTime = this.firstTokenTime;
|
|
1255
|
+
for await (const rawEvent of this.runtime.eventStream) {
|
|
1256
|
+
firstTokenTime = this.#handleStreamEvent(rawEvent, firstTokenTime);
|
|
1257
|
+
if (this.runtime.sawTerminalEvent) break;
|
|
1258
|
+
}
|
|
1259
|
+
return { firstTokenTime };
|
|
1260
|
+
} catch (error) {
|
|
1261
|
+
const recovered = await this.#recoverStreamError(error);
|
|
1262
|
+
if (!recovered) {
|
|
1263
|
+
throw error;
|
|
1264
|
+
}
|
|
1265
|
+
stream.push({ type: "start", partial: output });
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1484
1268
|
}
|
|
1485
1269
|
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1270
|
+
#handleStreamEvent(rawEvent: Record<string, unknown>, firstTokenTime: number | undefined): number | undefined {
|
|
1271
|
+
const { output, stream } = this;
|
|
1272
|
+
const eventType = typeof rawEvent.type === "string" ? rawEvent.type : "";
|
|
1273
|
+
if (!eventType) return firstTokenTime;
|
|
1274
|
+
|
|
1275
|
+
if (eventType === "response.output_item.added") {
|
|
1276
|
+
this.runtime.whitespaceToolCallArgumentsDelta = undefined;
|
|
1277
|
+
if (!firstTokenTime) firstTokenTime = performance.now();
|
|
1278
|
+
const item = rawEvent.item as CodexEventItem;
|
|
1279
|
+
this.runtime.currentItem = item;
|
|
1280
|
+
this.runtime.currentBlock = createOutputBlockForItem(item);
|
|
1281
|
+
let contentIndex = -1;
|
|
1282
|
+
if (this.runtime.currentBlock) {
|
|
1283
|
+
output.content.push(this.runtime.currentBlock);
|
|
1284
|
+
contentIndex = output.content.length - 1;
|
|
1285
|
+
}
|
|
1286
|
+
// Track every open item by every stable key the wire gives us. `item.id`
|
|
1287
|
+
// is best; `output_index` preserves idless function/custom tool calls and
|
|
1288
|
+
// keeps their final args authoritative when only `output_item.done`
|
|
1289
|
+
// carries the full payload.
|
|
1290
|
+
const itemId = typeof (item as { id?: string }).id === "string" ? (item as { id: string }).id : undefined;
|
|
1291
|
+
const outputIndex =
|
|
1292
|
+
typeof rawEvent.output_index === "number" && Number.isFinite(rawEvent.output_index)
|
|
1293
|
+
? Math.trunc(rawEvent.output_index)
|
|
1294
|
+
: undefined;
|
|
1295
|
+
const entry: CodexOpenItem = { item, block: this.runtime.currentBlock, contentIndex, itemId, outputIndex };
|
|
1296
|
+
this.runtime.currentEntry = entry;
|
|
1297
|
+
if (itemId) this.runtime.openItems.set(itemId, entry);
|
|
1298
|
+
if (outputIndex !== undefined) this.runtime.openItemsByOutputIndex.set(outputIndex, entry);
|
|
1299
|
+
if (!this.runtime.currentBlock) return firstTokenTime;
|
|
1300
|
+
stream.push({
|
|
1301
|
+
type: getOutputBlockStartEventType(this.runtime.currentBlock),
|
|
1302
|
+
contentIndex,
|
|
1303
|
+
partial: output,
|
|
1304
|
+
});
|
|
1305
|
+
return firstTokenTime;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
if (eventType === "response.reasoning_summary_part.added") {
|
|
1309
|
+
if (this.runtime.currentItem?.type === "reasoning") {
|
|
1310
|
+
appendReasoningSummaryPart(
|
|
1311
|
+
this.runtime.currentItem,
|
|
1312
|
+
(rawEvent as { part: ResponseReasoningItem["summary"][number] }).part,
|
|
1313
|
+
);
|
|
1314
|
+
}
|
|
1315
|
+
return firstTokenTime;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
if (eventType === "response.reasoning_summary_text.delta") {
|
|
1319
|
+
if (this.runtime.currentItem?.type === "reasoning" && this.runtime.currentBlock?.type === "thinking") {
|
|
1320
|
+
appendReasoningSummaryTextDelta(
|
|
1321
|
+
this.runtime.currentItem,
|
|
1322
|
+
this.runtime.currentBlock,
|
|
1323
|
+
(rawEvent as { delta?: string }).delta || "",
|
|
1324
|
+
stream,
|
|
1325
|
+
output,
|
|
1326
|
+
output.content.length - 1,
|
|
1327
|
+
);
|
|
1328
|
+
}
|
|
1329
|
+
return firstTokenTime;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
if (eventType === "response.reasoning_summary_part.done") {
|
|
1333
|
+
if (this.runtime.currentItem?.type === "reasoning" && this.runtime.currentBlock?.type === "thinking") {
|
|
1334
|
+
appendReasoningSummaryPartDone(
|
|
1335
|
+
this.runtime.currentItem,
|
|
1336
|
+
this.runtime.currentBlock,
|
|
1337
|
+
stream,
|
|
1338
|
+
output,
|
|
1339
|
+
output.content.length - 1,
|
|
1340
|
+
);
|
|
1341
|
+
}
|
|
1342
|
+
return firstTokenTime;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
if (eventType === "response.content_part.added") {
|
|
1346
|
+
if (this.runtime.currentItem?.type === "message") {
|
|
1347
|
+
appendMessageContentPart(
|
|
1348
|
+
this.runtime.currentItem,
|
|
1349
|
+
(rawEvent as { part?: ResponseOutputMessage["content"][number] }).part,
|
|
1350
|
+
);
|
|
1351
|
+
}
|
|
1352
|
+
return firstTokenTime;
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
if (eventType === "response.output_text.delta" || eventType === "response.refusal.delta") {
|
|
1356
|
+
if (this.runtime.currentItem?.type === "message" && this.runtime.currentBlock?.type === "text") {
|
|
1357
|
+
appendMessageTextDelta(
|
|
1358
|
+
this.runtime.currentItem,
|
|
1359
|
+
this.runtime.currentBlock,
|
|
1360
|
+
(rawEvent as { delta?: string }).delta || "",
|
|
1361
|
+
stream,
|
|
1362
|
+
output,
|
|
1363
|
+
output.content.length - 1,
|
|
1364
|
+
eventType === "response.refusal.delta" ? "refusal" : "output_text",
|
|
1365
|
+
);
|
|
1366
|
+
}
|
|
1367
|
+
return firstTokenTime;
|
|
1368
|
+
}
|
|
1501
1369
|
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
// Persist the authoritative final args on the stored block; the throttled
|
|
1511
|
-
// delta parser may have left block.arguments stale (often `{}`).
|
|
1512
|
-
block.arguments = toolCall.arguments;
|
|
1513
|
-
delete (block as { partialJson?: string }).partialJson;
|
|
1514
|
-
delete (block as { lastParseLen?: number }).lastParseLen;
|
|
1515
|
-
}
|
|
1516
|
-
// Detach so a late/duplicate arguments.delta cannot append to the
|
|
1517
|
-
// finished block or trip the whitespace-loop guard against it.
|
|
1518
|
-
closeCodexOpenItem(runtime, entry);
|
|
1519
|
-
runtime.canSafelyReplayWebsocketOverSse = false;
|
|
1520
|
-
stream.push({ type: "toolcall_end", contentIndex, toolCall, partial: output });
|
|
1521
|
-
return;
|
|
1522
|
-
}
|
|
1370
|
+
if (eventType === "response.function_call_arguments.delta") {
|
|
1371
|
+
const interruption = this.runtime.handleToolCallArgumentsDelta(rawEvent, stream, output);
|
|
1372
|
+
if (interruption) {
|
|
1373
|
+
this.runtime.websocketState?.connection?.close("degenerate-tool-call");
|
|
1374
|
+
throw new CodexWhitespaceToolCallLoopError(interruption.message);
|
|
1375
|
+
}
|
|
1376
|
+
return firstTokenTime;
|
|
1377
|
+
}
|
|
1523
1378
|
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
const toolCall: ToolCall = {
|
|
1529
|
-
type: "toolCall",
|
|
1530
|
-
id: encodeResponsesToolCallId(item.call_id, item.id),
|
|
1531
|
-
name: item.name,
|
|
1532
|
-
arguments: { input: rawInput },
|
|
1533
|
-
customWireName: item.name,
|
|
1534
|
-
};
|
|
1535
|
-
if (block?.type === "toolCall") {
|
|
1536
|
-
block.arguments = { input: rawInput };
|
|
1537
|
-
delete (block as { partialJson?: string }).partialJson;
|
|
1379
|
+
if (eventType === "response.function_call_arguments.done") {
|
|
1380
|
+
this.runtime.whitespaceToolCallArgumentsDelta = undefined;
|
|
1381
|
+
this.runtime.handleToolCallArgumentsDone(rawEvent);
|
|
1382
|
+
return firstTokenTime;
|
|
1538
1383
|
}
|
|
1539
|
-
closeCodexOpenItem(runtime, entry);
|
|
1540
|
-
runtime.canSafelyReplayWebsocketOverSse = false;
|
|
1541
|
-
stream.push({ type: "toolcall_end", contentIndex, toolCall, partial: output });
|
|
1542
|
-
return;
|
|
1543
|
-
}
|
|
1544
1384
|
|
|
1545
|
-
|
|
1546
|
-
|
|
1385
|
+
if (eventType === "response.custom_tool_call_input.delta") {
|
|
1386
|
+
const interruption = this.runtime.handleCustomToolCallInputDelta(rawEvent, stream, output);
|
|
1387
|
+
if (interruption) {
|
|
1388
|
+
this.runtime.websocketState?.connection?.close("degenerate-tool-call");
|
|
1389
|
+
throw new CodexWhitespaceToolCallLoopError(interruption.message);
|
|
1390
|
+
}
|
|
1391
|
+
return firstTokenTime;
|
|
1392
|
+
}
|
|
1547
1393
|
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
}
|
|
1554
|
-
}
|
|
1394
|
+
if (eventType === "response.custom_tool_call_input.done") {
|
|
1395
|
+
this.runtime.whitespaceToolCallArgumentsDelta = undefined;
|
|
1396
|
+
this.runtime.handleCustomToolCallInputDone(rawEvent);
|
|
1397
|
+
return firstTokenTime;
|
|
1398
|
+
}
|
|
1555
1399
|
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
rawEvent: Record<string, unknown>,
|
|
1561
|
-
): void {
|
|
1562
|
-
runtime.sawTerminalEvent = true;
|
|
1563
|
-
const response = (
|
|
1564
|
-
rawEvent as {
|
|
1565
|
-
response?: {
|
|
1566
|
-
id?: string;
|
|
1567
|
-
usage?: {
|
|
1568
|
-
input_tokens?: number;
|
|
1569
|
-
output_tokens?: number;
|
|
1570
|
-
total_tokens?: number;
|
|
1571
|
-
input_tokens_details?: { cached_tokens?: number };
|
|
1572
|
-
output_tokens_details?: { reasoning_tokens?: number };
|
|
1573
|
-
};
|
|
1574
|
-
status?: string;
|
|
1575
|
-
service_tier?: ServiceTier | "default";
|
|
1576
|
-
end_turn?: boolean;
|
|
1577
|
-
};
|
|
1400
|
+
if (eventType === "response.output_item.done") {
|
|
1401
|
+
this.runtime.whitespaceToolCallArgumentsDelta = undefined;
|
|
1402
|
+
this.#handleOutputItemDone(rawEvent);
|
|
1403
|
+
return firstTokenTime;
|
|
1578
1404
|
}
|
|
1579
|
-
).response;
|
|
1580
1405
|
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1406
|
+
if (eventType === "response.created") {
|
|
1407
|
+
this.runtime.handleResponseCreated(rawEvent);
|
|
1408
|
+
return firstTokenTime;
|
|
1409
|
+
}
|
|
1585
1410
|
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
// SSE turns never chain (previous_response_id is websocket-only on this
|
|
1590
|
-
// endpoint); a completed SSE turn also invalidates any websocket append
|
|
1591
|
-
// baseline, which no longer matches the transcript.
|
|
1592
|
-
resetCodexWebSocketAppendState(state);
|
|
1593
|
-
} else {
|
|
1594
|
-
state.lastRequest = structuredCloneJSON(runtime.requestBodyForState);
|
|
1595
|
-
if (typeof response?.id === "string" && response.id.length > 0) {
|
|
1596
|
-
state.lastResponseId = response.id;
|
|
1597
|
-
state.lastResponseItems = stripInputItemIds(structuredCloneJSON(runtime.nativeOutputItems));
|
|
1598
|
-
state.canAppend = rawEvent.type === "response.done" || rawEvent.type === "response.completed";
|
|
1599
|
-
} else {
|
|
1600
|
-
// Without a response id the append baseline cannot be trusted.
|
|
1601
|
-
state.canAppend = false;
|
|
1602
|
-
}
|
|
1411
|
+
if (eventType === "response.completed" || eventType === "response.done" || eventType === "response.incomplete") {
|
|
1412
|
+
this.#handleResponseCompleted(rawEvent);
|
|
1413
|
+
return firstTokenTime;
|
|
1603
1414
|
}
|
|
1604
|
-
}
|
|
1605
1415
|
|
|
1606
|
-
|
|
1416
|
+
if (eventType === "response.metadata") {
|
|
1417
|
+
const moderation = asRecord(rawEvent.metadata)?.[CODEX_MODERATION_METADATA_KEY];
|
|
1418
|
+
if (moderation !== undefined) {
|
|
1419
|
+
try {
|
|
1420
|
+
this.options?.onModerationMetadata?.(moderation);
|
|
1421
|
+
} catch {
|
|
1422
|
+
// Diagnostic observer: failures must not disturb the stream.
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
return firstTokenTime;
|
|
1426
|
+
}
|
|
1607
1427
|
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
promoteResponsesToolUseStopReason(output, response?.end_turn);
|
|
1612
|
-
}
|
|
1428
|
+
if (eventType === "error" || eventType === "response.failed") {
|
|
1429
|
+
throw createCodexProviderStreamError(rawEvent);
|
|
1430
|
+
}
|
|
1613
1431
|
|
|
1614
|
-
|
|
1615
|
-
context: CodexStreamProcessingContext,
|
|
1616
|
-
runtime: CodexStreamRuntime,
|
|
1617
|
-
error: unknown,
|
|
1618
|
-
): Promise<boolean> {
|
|
1619
|
-
if (await tryRecoverCodexWhitespaceToolCallLoop(context, runtime, error)) {
|
|
1620
|
-
return true;
|
|
1621
|
-
}
|
|
1622
|
-
if (await tryReconnectCodexWebSocketOnConnectionLimit(context, runtime, error)) {
|
|
1623
|
-
return true;
|
|
1624
|
-
}
|
|
1625
|
-
if (await tryRecoverCodexPreviousResponseNotFound(context, runtime, error)) {
|
|
1626
|
-
return true;
|
|
1627
|
-
}
|
|
1628
|
-
if (await tryReplayWebsocketFailureOverSse(context, runtime, error)) {
|
|
1629
|
-
return true;
|
|
1630
|
-
}
|
|
1631
|
-
if (await tryRetryCodexProviderError(context, runtime, error)) {
|
|
1632
|
-
return true;
|
|
1432
|
+
return firstTokenTime;
|
|
1633
1433
|
}
|
|
1634
|
-
return false;
|
|
1635
|
-
}
|
|
1636
1434
|
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1435
|
+
#handleOutputItemDone(rawEvent: Record<string, unknown>): void {
|
|
1436
|
+
const { runtime, output, stream } = this;
|
|
1437
|
+
const rawItem = rawEvent.item;
|
|
1438
|
+
if (!rawItem || typeof rawItem !== "object") return;
|
|
1439
|
+
const item = structuredCloneJSON(rawItem) as CodexEventItem;
|
|
1440
|
+
runtime.nativeOutputItems.push(item as unknown as Record<string, unknown>);
|
|
1441
|
+
|
|
1442
|
+
// Match the finalization to the OPEN ITEM that started this block, not the
|
|
1443
|
+
// singleton current — interleaved items can finish out of order, so the
|
|
1444
|
+
// most-recently-added block may belong to a sibling (#2619). Some Codex
|
|
1445
|
+
// function/custom tool items omit `id`; in that case `output_index` still
|
|
1446
|
+
// routes `output_item.done` to the block that received `output_item.added`.
|
|
1447
|
+
const itemId = typeof (item as { id?: string }).id === "string" ? (item as { id: string }).id : "";
|
|
1448
|
+
const entry = (itemId ? runtime.openItems.get(itemId) : null) ?? runtime.openItemForEvent(rawEvent);
|
|
1449
|
+
const block = entry?.block ?? null;
|
|
1450
|
+
const contentIndex = entry?.contentIndex ?? output.content.length - 1;
|
|
1451
|
+
|
|
1452
|
+
if (item.type === "reasoning" && block?.type === "thinking") {
|
|
1453
|
+
block.thinking = item.summary?.map(summary => summary.text).join("\n\n") || "";
|
|
1454
|
+
block.thinkingSignature = JSON.stringify(item);
|
|
1455
|
+
stream.push({
|
|
1456
|
+
type: "thinking_end",
|
|
1457
|
+
contentIndex,
|
|
1458
|
+
content: block.thinking,
|
|
1459
|
+
partial: output,
|
|
1460
|
+
});
|
|
1461
|
+
runtime.closeOpenItem(entry);
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1649
1464
|
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
error: unknown,
|
|
1666
|
-
): Promise<boolean> {
|
|
1667
|
-
if (!(error instanceof CodexWhitespaceToolCallLoopError)) {
|
|
1668
|
-
return false;
|
|
1669
|
-
}
|
|
1670
|
-
// Drop the half-built degenerate tool call whether or not we retry, so it
|
|
1671
|
-
// never reaches the caller's message.
|
|
1672
|
-
dropTrailingDegenerateToolCall(context.output, runtime);
|
|
1673
|
-
if (
|
|
1674
|
-
runtime.whitespaceLoopRetries >= CODEX_WHITESPACE_LOOP_RETRY_LIMIT ||
|
|
1675
|
-
!runtime.canSafelyReplayWebsocketOverSse ||
|
|
1676
|
-
context.output.content.some(block => block.type !== "thinking") ||
|
|
1677
|
-
context.options?.signal?.aborted
|
|
1678
|
-
) {
|
|
1679
|
-
return false;
|
|
1680
|
-
}
|
|
1465
|
+
if (item.type === "message" && block?.type === "text") {
|
|
1466
|
+
block.text = item.content
|
|
1467
|
+
.map(content => (content.type === "output_text" ? content.text : content.refusal))
|
|
1468
|
+
.join("");
|
|
1469
|
+
const phase = item.phase === "commentary" || item.phase === "final_answer" ? item.phase : undefined;
|
|
1470
|
+
block.textSignature = encodeTextSignatureV1(item.id, phase);
|
|
1471
|
+
stream.push({
|
|
1472
|
+
type: "text_end",
|
|
1473
|
+
contentIndex,
|
|
1474
|
+
content: block.text,
|
|
1475
|
+
partial: output,
|
|
1476
|
+
});
|
|
1477
|
+
runtime.closeOpenItem(entry);
|
|
1478
|
+
return;
|
|
1479
|
+
}
|
|
1681
1480
|
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1481
|
+
if (item.type === "function_call") {
|
|
1482
|
+
const toolCall: ToolCall = {
|
|
1483
|
+
type: "toolCall",
|
|
1484
|
+
id: encodeResponsesToolCallId(item.call_id, item.id),
|
|
1485
|
+
name: item.name,
|
|
1486
|
+
arguments: parseStreamingJson(item.arguments || "{}"),
|
|
1487
|
+
};
|
|
1488
|
+
if (block?.type === "toolCall") {
|
|
1489
|
+
// Persist the authoritative final args on the stored block; the throttled
|
|
1490
|
+
// delta parser may have left block.arguments stale (often `{}`).
|
|
1491
|
+
block.arguments = toolCall.arguments;
|
|
1492
|
+
clearStreamingPartialJson(block);
|
|
1493
|
+
}
|
|
1494
|
+
// Detach so a late/duplicate arguments.delta cannot append to the
|
|
1495
|
+
// finished block or trip the whitespace-loop guard against it.
|
|
1496
|
+
runtime.closeOpenItem(entry);
|
|
1497
|
+
runtime.canSafelyReplayWebsocketOverSse = false;
|
|
1498
|
+
stream.push({ type: "toolcall_end", contentIndex, toolCall, partial: output });
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
if (item.type === "custom_tool_call") {
|
|
1503
|
+
const partial = block?.type === "toolCall" ? block[kStreamingPartialJson] : undefined;
|
|
1504
|
+
const rawInput = partial && partial.length > 0 ? partial : (item.input ?? "");
|
|
1505
|
+
const toolCall: ToolCall = {
|
|
1506
|
+
type: "toolCall",
|
|
1507
|
+
id: encodeResponsesToolCallId(item.call_id, item.id),
|
|
1508
|
+
name: item.name,
|
|
1509
|
+
arguments: { input: rawInput },
|
|
1510
|
+
customWireName: item.name,
|
|
1511
|
+
};
|
|
1512
|
+
if (block?.type === "toolCall") {
|
|
1513
|
+
block.arguments = { input: rawInput };
|
|
1514
|
+
clearStreamingPartialJson(block);
|
|
1515
|
+
}
|
|
1516
|
+
runtime.closeOpenItem(entry);
|
|
1517
|
+
runtime.canSafelyReplayWebsocketOverSse = false;
|
|
1518
|
+
stream.push({ type: "toolcall_end", contentIndex, toolCall, partial: output });
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1688
1521
|
}
|
|
1689
1522
|
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1523
|
+
#handleResponseCompleted(rawEvent: Record<string, unknown>): void {
|
|
1524
|
+
const { runtime, model, output } = this;
|
|
1525
|
+
runtime.sawTerminalEvent = true;
|
|
1526
|
+
const response = (
|
|
1527
|
+
rawEvent as {
|
|
1528
|
+
response?: {
|
|
1529
|
+
id?: string;
|
|
1530
|
+
usage?: {
|
|
1531
|
+
input_tokens?: number;
|
|
1532
|
+
output_tokens?: number;
|
|
1533
|
+
total_tokens?: number;
|
|
1534
|
+
input_tokens_details?: { cached_tokens?: number };
|
|
1535
|
+
output_tokens_details?: { reasoning_tokens?: number };
|
|
1536
|
+
};
|
|
1537
|
+
status?: string;
|
|
1538
|
+
service_tier?: ServiceTier | "default";
|
|
1539
|
+
end_turn?: boolean;
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
).response;
|
|
1696
1543
|
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
context.firstTokenTime = undefined;
|
|
1702
|
-
await scheduler.wait(CODEX_WHITESPACE_LOOP_RETRY_DELAY_MS * runtime.whitespaceLoopRetries, {
|
|
1703
|
-
signal: context.requestSetup.requestSignal,
|
|
1704
|
-
});
|
|
1544
|
+
populateResponsesUsageFromResponse(output, response?.usage);
|
|
1545
|
+
if (typeof response?.id === "string" && response.id.length > 0) {
|
|
1546
|
+
output.responseId = response.id;
|
|
1547
|
+
}
|
|
1705
1548
|
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1549
|
+
const state = runtime.websocketState;
|
|
1550
|
+
if (state) {
|
|
1551
|
+
if (runtime.transport !== "websocket") {
|
|
1552
|
+
// SSE turns never chain (previous_response_id is websocket-only on this
|
|
1553
|
+
// endpoint); a completed SSE turn also invalidates any websocket append
|
|
1554
|
+
// baseline, which no longer matches the transcript.
|
|
1555
|
+
resetCodexWebSocketAppendState(state);
|
|
1556
|
+
} else {
|
|
1557
|
+
state.lastRequest = structuredCloneJSON(runtime.requestBodyForState);
|
|
1558
|
+
if (typeof response?.id === "string" && response.id.length > 0) {
|
|
1559
|
+
state.lastResponseId = response.id;
|
|
1560
|
+
state.lastResponseItems = stripInputItemIds(structuredCloneJSON(runtime.nativeOutputItems));
|
|
1561
|
+
state.canAppend = rawEvent.type === "response.done" || rawEvent.type === "response.completed";
|
|
1562
|
+
} else {
|
|
1563
|
+
// Without a response id the append baseline cannot be trusted.
|
|
1564
|
+
state.canAppend = false;
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1710
1568
|
|
|
1711
|
-
|
|
1712
|
-
return true;
|
|
1713
|
-
}
|
|
1569
|
+
finalizePendingResponsesToolCalls(output);
|
|
1714
1570
|
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
context: CodexStreamProcessingContext,
|
|
1725
|
-
runtime: CodexStreamRuntime,
|
|
1726
|
-
error: unknown,
|
|
1727
|
-
): Promise<boolean> {
|
|
1728
|
-
if (!(error instanceof CodexProviderStreamError) || error.code !== "websocket_connection_limit_reached") {
|
|
1729
|
-
return false;
|
|
1571
|
+
calculateCost(model, output.usage);
|
|
1572
|
+
applyCodexServiceTierPricing(
|
|
1573
|
+
model,
|
|
1574
|
+
output.usage,
|
|
1575
|
+
response?.service_tier,
|
|
1576
|
+
runtime.requestBodyForState.service_tier,
|
|
1577
|
+
);
|
|
1578
|
+
output.stopReason = mapOpenAIResponsesStopReason(response?.status as ResponseStatus | undefined);
|
|
1579
|
+
promoteResponsesToolUseStopReason(output, response?.end_turn);
|
|
1730
1580
|
}
|
|
1731
|
-
|
|
1732
|
-
|
|
1581
|
+
|
|
1582
|
+
async #recoverStreamError(error: unknown): Promise<boolean> {
|
|
1583
|
+
if (await this.#tryRecoverWhitespaceToolCallLoop(error)) {
|
|
1584
|
+
return true;
|
|
1585
|
+
}
|
|
1586
|
+
if (await this.#tryReconnectWebSocketOnConnectionLimit(error)) {
|
|
1587
|
+
return true;
|
|
1588
|
+
}
|
|
1589
|
+
if (await this.#tryRecoverPreviousResponseNotFound(error)) {
|
|
1590
|
+
return true;
|
|
1591
|
+
}
|
|
1592
|
+
if (await this.#tryReplayWebsocketFailureOverSse(error)) {
|
|
1593
|
+
return true;
|
|
1594
|
+
}
|
|
1595
|
+
if (await this.#tryRetryProviderError(error)) {
|
|
1596
|
+
return true;
|
|
1597
|
+
}
|
|
1733
1598
|
return false;
|
|
1734
1599
|
}
|
|
1735
1600
|
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1601
|
+
/**
|
|
1602
|
+
* Recover from the degenerate whitespace-only tool-call argument loop
|
|
1603
|
+
* ({@link CodexWhitespaceToolCallLoopError}). The interrupted function call has
|
|
1604
|
+
* no usable arguments, so drop the partial turn and replay the request from
|
|
1605
|
+
* scratch — bounded by {@link CODEX_WHITESPACE_LOOP_RETRY_LIMIT}. Sampling
|
|
1606
|
+
* nondeterminism usually breaks the loop on a fresh attempt; once the budget is
|
|
1607
|
+
* exhausted the original error is surfaced (now without the junk tool call
|
|
1608
|
+
* polluting the message). Replay is refused once any visible content was already
|
|
1609
|
+
* delivered to the consumer — a finished tool call (`canSafelyReplayWebsocketOverSse`),
|
|
1610
|
+
* or any streamed text/commentary block still in `output.content` after the degenerate
|
|
1611
|
+
* tool call is dropped — because replaying re-emits already-streamed deltas.
|
|
1612
|
+
*/
|
|
1613
|
+
async #tryRecoverWhitespaceToolCallLoop(error: unknown): Promise<boolean> {
|
|
1614
|
+
if (!(error instanceof CodexWhitespaceToolCallLoopError)) {
|
|
1615
|
+
return false;
|
|
1616
|
+
}
|
|
1617
|
+
// Drop the half-built degenerate tool call whether or not we retry, so it
|
|
1618
|
+
// never reaches the caller's message.
|
|
1619
|
+
this.#dropTrailingDegenerateToolCall();
|
|
1620
|
+
if (
|
|
1621
|
+
this.runtime.whitespaceLoopRetries >= CODEX_WHITESPACE_LOOP_RETRY_LIMIT ||
|
|
1622
|
+
!this.runtime.canSafelyReplayWebsocketOverSse ||
|
|
1623
|
+
this.output.content.some(block => block.type !== "thinking") ||
|
|
1624
|
+
this.options?.signal?.aborted
|
|
1625
|
+
) {
|
|
1626
|
+
return false;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
this.runtime.whitespaceLoopRetries += 1;
|
|
1630
|
+
const websocketState = this.requestContext.websocketState;
|
|
1631
|
+
if (websocketState) {
|
|
1632
|
+
resetCodexWebSocketAppendState(websocketState);
|
|
1633
|
+
websocketState.turnState = undefined;
|
|
1634
|
+
websocketState.modelsEtag = undefined;
|
|
1635
|
+
}
|
|
1740
1636
|
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1637
|
+
CODEX_DEBUG &&
|
|
1638
|
+
logger.debug("[codex] retrying codex turn after whitespace-only tool-call argument loop", {
|
|
1639
|
+
retry: this.runtime.whitespaceLoopRetries,
|
|
1640
|
+
retryBudget: CODEX_WHITESPACE_LOOP_RETRY_LIMIT,
|
|
1641
|
+
transport: this.runtime.transport,
|
|
1642
|
+
});
|
|
1746
1643
|
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1644
|
+
this.runtime.resetAccumulators();
|
|
1645
|
+
this.runtime.sawTerminalEvent = false;
|
|
1646
|
+
this.runtime.whitespaceToolCallArgumentsDelta = undefined;
|
|
1647
|
+
resetOutputState(this.output);
|
|
1648
|
+
this.firstTokenTime = undefined;
|
|
1649
|
+
await scheduler.wait(CODEX_WHITESPACE_LOOP_RETRY_DELAY_MS * this.runtime.whitespaceLoopRetries, {
|
|
1650
|
+
signal: this.requestSetup.requestSignal,
|
|
1751
1651
|
});
|
|
1752
1652
|
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
recordCodexWebSocketFailure(websocketState, true);
|
|
1760
|
-
await reopenCodexSseRuntimeStream(context, runtime, websocketState);
|
|
1653
|
+
if (this.runtime.transport === "websocket" && websocketState) {
|
|
1654
|
+
await this.#reopenWebSocketStream(websocketState);
|
|
1655
|
+
return true;
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
await this.#reopenSseStream(websocketState);
|
|
1761
1659
|
return true;
|
|
1762
1660
|
}
|
|
1763
1661
|
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
await reopenCodexSseRuntimeStream(context, runtime, websocketState);
|
|
1777
|
-
return true;
|
|
1662
|
+
/**
|
|
1663
|
+
* Pop the half-built degenerate tool-call block (the one whose arguments were
|
|
1664
|
+
* nothing but whitespace) off the output accumulator so it never surfaces in the
|
|
1665
|
+
* caller's message. Any legitimate content produced before it is preserved.
|
|
1666
|
+
*/
|
|
1667
|
+
#dropTrailingDegenerateToolCall(): void {
|
|
1668
|
+
const { runtime, output } = this;
|
|
1669
|
+
const block = runtime.currentBlock;
|
|
1670
|
+
if (block && block.type === "toolCall" && output.content[output.content.length - 1] === block) {
|
|
1671
|
+
output.content.pop();
|
|
1672
|
+
}
|
|
1673
|
+
runtime.closeOpenItem(runtime.currentEntry);
|
|
1778
1674
|
}
|
|
1779
|
-
runtime.websocketStreamRetries += 1;
|
|
1780
|
-
await scheduler.wait(
|
|
1781
|
-
parseCodexPositiveInteger($env.PI_CODEX_WEBSOCKET_RETRY_DELAY_MS, CODEX_RETRY_DELAY_MS) *
|
|
1782
|
-
Math.max(1, runtime.websocketStreamRetries),
|
|
1783
|
-
{
|
|
1784
|
-
signal: context.requestSetup.requestSignal,
|
|
1785
|
-
},
|
|
1786
|
-
);
|
|
1787
|
-
await reopenCodexWebSocketRuntimeStream(context, runtime, websocketState);
|
|
1788
|
-
return true;
|
|
1789
|
-
}
|
|
1790
1675
|
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1676
|
+
/**
|
|
1677
|
+
* Handles `websocket_connection_limit_reached` errors by closing the stale connection
|
|
1678
|
+
* and opening a fresh websocket. If content has already been emitted to the caller,
|
|
1679
|
+
* falls back to SSE replay (same as other WS failures) since we cannot safely
|
|
1680
|
+
* continue a partial response on a new connection. If a tool call was already
|
|
1681
|
+
* delivered (`canSafelyReplayWebsocketOverSse` is false), the error surfaces
|
|
1682
|
+
* instead — replaying would re-emit the same tool calls.
|
|
1683
|
+
*/
|
|
1684
|
+
async #tryReconnectWebSocketOnConnectionLimit(error: unknown): Promise<boolean> {
|
|
1685
|
+
if (!(error instanceof CodexProviderStreamError) || error.code !== "websocket_connection_limit_reached") {
|
|
1686
|
+
return false;
|
|
1687
|
+
}
|
|
1688
|
+
const websocketState = this.requestContext.websocketState;
|
|
1689
|
+
if (!websocketState || this.runtime.transport !== "websocket" || this.options?.signal?.aborted) {
|
|
1690
|
+
return false;
|
|
1691
|
+
}
|
|
1804
1692
|
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
): Promise<boolean> {
|
|
1810
|
-
const websocketState = context.requestContext.websocketState;
|
|
1811
|
-
if (
|
|
1812
|
-
!isCodexStalePreviousResponseError(error) ||
|
|
1813
|
-
!websocketState ||
|
|
1814
|
-
context.output.content.length > 0 ||
|
|
1815
|
-
context.options?.signal?.aborted ||
|
|
1816
|
-
runtime.providerRetryAttempt >= CODEX_MAX_RETRIES
|
|
1817
|
-
) {
|
|
1818
|
-
return false;
|
|
1819
|
-
}
|
|
1820
|
-
if (runtime.transport !== "websocket") {
|
|
1821
|
-
// SSE never sends previous_response_id; let other recovery handle it.
|
|
1822
|
-
return false;
|
|
1823
|
-
}
|
|
1693
|
+
// Close the stale connection so getOrCreateCodexWebSocketConnection creates a fresh one.
|
|
1694
|
+
websocketState.connection?.close("connection_limit");
|
|
1695
|
+
websocketState.connection = undefined;
|
|
1696
|
+
resetCodexWebSocketAppendState(websocketState);
|
|
1824
1697
|
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
runtime.sawTerminalEvent = false;
|
|
1831
|
-
resetOutputState(context.output);
|
|
1832
|
-
context.firstTokenTime = undefined;
|
|
1698
|
+
if (this.output.content.length > 0 && !this.runtime.canSafelyReplayWebsocketOverSse) {
|
|
1699
|
+
// A toolcall_end already reached the consumer; a full replay would emit
|
|
1700
|
+
// the same tool calls a second time. Let the error surface instead.
|
|
1701
|
+
return false;
|
|
1702
|
+
}
|
|
1833
1703
|
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
return true;
|
|
1840
|
-
}
|
|
1704
|
+
CODEX_DEBUG &&
|
|
1705
|
+
logger.debug("[codex] codex websocket connection limit reached, reconnecting", {
|
|
1706
|
+
hadContent: this.output.content.length > 0,
|
|
1707
|
+
retry: this.runtime.websocketStreamRetries,
|
|
1708
|
+
});
|
|
1841
1709
|
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
)
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
runtime.canSafelyReplayWebsocketOverSse &&
|
|
1853
|
-
!runtime.sawTerminalEvent &&
|
|
1854
|
-
!context.options?.signal?.aborted;
|
|
1855
|
-
if (!canReplay) return false;
|
|
1856
|
-
|
|
1857
|
-
const state = websocketState;
|
|
1858
|
-
const streamError = error instanceof Error ? error : new Error(String(error));
|
|
1859
|
-
const replayingBufferedOutputOverSse = context.output.content.length > 0;
|
|
1860
|
-
const fatalWebSocketMessage = streamError.message.toLowerCase();
|
|
1861
|
-
const isFatal = CODEX_WEBSOCKET_FATAL_PATTERNS.some(pattern =>
|
|
1862
|
-
fatalWebSocketMessage.includes(pattern.toLowerCase()),
|
|
1863
|
-
);
|
|
1864
|
-
const activateFallback =
|
|
1865
|
-
replayingBufferedOutputOverSse ||
|
|
1866
|
-
isFatal ||
|
|
1867
|
-
runtime.websocketStreamRetries >=
|
|
1868
|
-
parseCodexNonNegativeInteger($env.PI_CODEX_WEBSOCKET_RETRY_BUDGET, CODEX_WEBSOCKET_RETRY_BUDGET);
|
|
1869
|
-
recordCodexWebSocketFailure(state, activateFallback);
|
|
1870
|
-
CODEX_DEBUG &&
|
|
1871
|
-
logger.debug("[codex] codex websocket stream fallback", {
|
|
1872
|
-
error: streamError.message,
|
|
1873
|
-
retry: runtime.websocketStreamRetries,
|
|
1874
|
-
retryBudget: parseCodexNonNegativeInteger($env.PI_CODEX_WEBSOCKET_RETRY_BUDGET, CODEX_WEBSOCKET_RETRY_BUDGET),
|
|
1875
|
-
activated: activateFallback,
|
|
1876
|
-
fatal: isFatal,
|
|
1877
|
-
replayedBufferedOutput: replayingBufferedOutputOverSse,
|
|
1878
|
-
});
|
|
1710
|
+
if (this.output.content.length > 0) {
|
|
1711
|
+
// Content already emitted to the caller — cannot safely continue on a new WS.
|
|
1712
|
+
// Reset and replay the full request over SSE.
|
|
1713
|
+
this.runtime.resetAccumulators();
|
|
1714
|
+
resetOutputState(this.output);
|
|
1715
|
+
this.firstTokenTime = undefined;
|
|
1716
|
+
recordCodexWebSocketFailure(websocketState, true);
|
|
1717
|
+
await this.#reopenSseStream(websocketState);
|
|
1718
|
+
return true;
|
|
1719
|
+
}
|
|
1879
1720
|
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
//
|
|
1883
|
-
//
|
|
1884
|
-
//
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
)
|
|
1894
|
-
|
|
1721
|
+
// No content emitted yet — clear accumulator state from the failed attempt
|
|
1722
|
+
// (blockless native items can exist even with empty content) and reconnect
|
|
1723
|
+
// over websocket, bounded by the shared retry budget: an account-scoped
|
|
1724
|
+
// limit can reject every fresh connection, and an unbounded loop would
|
|
1725
|
+
// hammer the endpoint with zero backoff.
|
|
1726
|
+
this.runtime.resetAccumulators();
|
|
1727
|
+
this.firstTokenTime = undefined;
|
|
1728
|
+
if (this.runtime.websocketStreamRetries >= CODEX_WEBSOCKET_RETRY_BUDGET) {
|
|
1729
|
+
recordCodexWebSocketFailure(websocketState, true);
|
|
1730
|
+
await this.#reopenSseStream(websocketState);
|
|
1731
|
+
return true;
|
|
1732
|
+
}
|
|
1733
|
+
this.runtime.websocketStreamRetries += 1;
|
|
1734
|
+
await scheduler.wait(CODEX_WEBSOCKET_RETRY_DELAY_MS * Math.max(1, this.runtime.websocketStreamRetries), {
|
|
1735
|
+
signal: this.requestSetup.requestSignal,
|
|
1736
|
+
});
|
|
1737
|
+
await this.#reopenWebSocketStream(websocketState);
|
|
1895
1738
|
return true;
|
|
1896
1739
|
}
|
|
1897
1740
|
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
context.output.content.length > 0 ||
|
|
1914
|
-
runtime.providerRetryAttempt >= CODEX_MAX_RETRIES ||
|
|
1915
|
-
context.options?.signal?.aborted
|
|
1916
|
-
) {
|
|
1917
|
-
return false;
|
|
1918
|
-
}
|
|
1741
|
+
async #tryRecoverPreviousResponseNotFound(error: unknown): Promise<boolean> {
|
|
1742
|
+
const websocketState = this.requestContext.websocketState;
|
|
1743
|
+
if (
|
|
1744
|
+
!isCodexStalePreviousResponseError(error) ||
|
|
1745
|
+
!websocketState ||
|
|
1746
|
+
this.output.content.length > 0 ||
|
|
1747
|
+
this.options?.signal?.aborted ||
|
|
1748
|
+
this.runtime.providerRetryAttempt >= CODEX_MAX_RETRIES
|
|
1749
|
+
) {
|
|
1750
|
+
return false;
|
|
1751
|
+
}
|
|
1752
|
+
if (this.runtime.transport !== "websocket") {
|
|
1753
|
+
// SSE never sends previous_response_id; let other recovery handle it.
|
|
1754
|
+
return false;
|
|
1755
|
+
}
|
|
1919
1756
|
|
|
1920
|
-
|
|
1921
|
-
const websocketState = context.requestContext.websocketState;
|
|
1922
|
-
if (websocketState) {
|
|
1757
|
+
this.runtime.providerRetryAttempt += 1;
|
|
1923
1758
|
resetCodexWebSocketAppendState(websocketState);
|
|
1924
1759
|
websocketState.turnState = undefined;
|
|
1925
1760
|
websocketState.modelsEtag = undefined;
|
|
1761
|
+
this.runtime.resetAccumulators();
|
|
1762
|
+
this.runtime.sawTerminalEvent = false;
|
|
1763
|
+
resetOutputState(this.output);
|
|
1764
|
+
this.firstTokenTime = undefined;
|
|
1765
|
+
|
|
1766
|
+
CODEX_DEBUG &&
|
|
1767
|
+
logger.debug("[codex] codex previous_response_id expired; retrying with full context", {
|
|
1768
|
+
retry: this.runtime.providerRetryAttempt,
|
|
1769
|
+
});
|
|
1770
|
+
await this.#reopenWebSocketStream(websocketState);
|
|
1771
|
+
return true;
|
|
1926
1772
|
}
|
|
1927
1773
|
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1774
|
+
async #tryReplayWebsocketFailureOverSse(error: unknown): Promise<boolean> {
|
|
1775
|
+
const websocketState = this.requestContext.websocketState;
|
|
1776
|
+
const canReplay =
|
|
1777
|
+
this.runtime.transport === "websocket" &&
|
|
1778
|
+
websocketState &&
|
|
1779
|
+
isCodexWebSocketRetryableStreamError(error) &&
|
|
1780
|
+
this.runtime.canSafelyReplayWebsocketOverSse &&
|
|
1781
|
+
!this.runtime.sawTerminalEvent &&
|
|
1782
|
+
!this.options?.signal?.aborted;
|
|
1783
|
+
if (!canReplay) return false;
|
|
1784
|
+
|
|
1785
|
+
const state = websocketState;
|
|
1786
|
+
const streamError = error instanceof Error ? error : new Error(String(error));
|
|
1787
|
+
const replayingBufferedOutputOverSse = this.output.content.length > 0;
|
|
1788
|
+
const fatalWebSocketMessage = streamError.message.toLowerCase();
|
|
1789
|
+
const isFatal = CODEX_WEBSOCKET_FATAL_PATTERNS.some(pattern =>
|
|
1790
|
+
fatalWebSocketMessage.includes(pattern.toLowerCase()),
|
|
1791
|
+
);
|
|
1792
|
+
const activateFallback =
|
|
1793
|
+
replayingBufferedOutputOverSse ||
|
|
1794
|
+
isFatal ||
|
|
1795
|
+
this.runtime.websocketStreamRetries >= CODEX_WEBSOCKET_RETRY_BUDGET;
|
|
1796
|
+
recordCodexWebSocketFailure(state, activateFallback);
|
|
1797
|
+
CODEX_DEBUG &&
|
|
1798
|
+
logger.debug("[codex] codex websocket stream fallback", {
|
|
1799
|
+
error: streamError.message,
|
|
1800
|
+
retry: this.runtime.websocketStreamRetries,
|
|
1801
|
+
retryBudget: CODEX_WEBSOCKET_RETRY_BUDGET,
|
|
1802
|
+
activated: activateFallback,
|
|
1803
|
+
fatal: isFatal,
|
|
1804
|
+
replayedBufferedOutput: replayingBufferedOutputOverSse,
|
|
1805
|
+
});
|
|
1935
1806
|
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1807
|
+
if (!activateFallback) {
|
|
1808
|
+
this.runtime.websocketStreamRetries += 1;
|
|
1809
|
+
// Full re-send on a fresh socket: clear accumulator state from the failed
|
|
1810
|
+
// attempt. Content is empty here, but blockless native items (e.g.
|
|
1811
|
+
// web_search_call) may already have accumulated.
|
|
1812
|
+
this.runtime.resetAccumulators();
|
|
1813
|
+
this.firstTokenTime = undefined;
|
|
1814
|
+
await scheduler.wait(CODEX_WEBSOCKET_RETRY_DELAY_MS * Math.max(1, this.runtime.websocketStreamRetries), {
|
|
1815
|
+
signal: this.requestSetup.requestSignal,
|
|
1816
|
+
});
|
|
1817
|
+
await this.#reopenWebSocketStream(state);
|
|
1818
|
+
return true;
|
|
1819
|
+
}
|
|
1943
1820
|
|
|
1944
|
-
|
|
1945
|
-
|
|
1821
|
+
this.runtime.resetAccumulators();
|
|
1822
|
+
resetOutputState(this.output);
|
|
1823
|
+
this.firstTokenTime = undefined;
|
|
1824
|
+
|
|
1825
|
+
await this.#reopenSseStream(state);
|
|
1946
1826
|
return true;
|
|
1947
1827
|
}
|
|
1948
1828
|
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1829
|
+
async #tryRetryProviderError(error: unknown): Promise<boolean> {
|
|
1830
|
+
if (
|
|
1831
|
+
!(error instanceof CodexProviderStreamError && error.retryable) ||
|
|
1832
|
+
this.output.content.length > 0 ||
|
|
1833
|
+
this.runtime.providerRetryAttempt >= CODEX_MAX_RETRIES ||
|
|
1834
|
+
this.options?.signal?.aborted
|
|
1835
|
+
) {
|
|
1836
|
+
return false;
|
|
1837
|
+
}
|
|
1952
1838
|
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
if (context.options?.signal?.aborted) {
|
|
1960
|
-
throw new Error("Request was aborted");
|
|
1961
|
-
}
|
|
1962
|
-
if (!runtime.sawTerminalEvent) {
|
|
1963
|
-
if (context.requestContext.websocketState) {
|
|
1964
|
-
resetCodexWebSocketAppendState(context.requestContext.websocketState);
|
|
1965
|
-
context.requestContext.websocketState.turnState = undefined;
|
|
1966
|
-
context.requestContext.websocketState.modelsEtag = undefined;
|
|
1839
|
+
this.runtime.providerRetryAttempt += 1;
|
|
1840
|
+
const websocketState = this.requestContext.websocketState;
|
|
1841
|
+
if (websocketState) {
|
|
1842
|
+
resetCodexWebSocketAppendState(websocketState);
|
|
1843
|
+
websocketState.turnState = undefined;
|
|
1844
|
+
websocketState.modelsEtag = undefined;
|
|
1967
1845
|
}
|
|
1846
|
+
|
|
1968
1847
|
CODEX_DEBUG &&
|
|
1969
|
-
logger.debug("[codex] codex stream
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
sentModelsEtagHeader: Boolean(context.requestContext.websocketState?.modelsEtag),
|
|
1848
|
+
logger.debug("[codex] retrying codex provider stream error", {
|
|
1849
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1850
|
+
retry: this.runtime.providerRetryAttempt,
|
|
1851
|
+
retryBudget: CODEX_MAX_RETRIES,
|
|
1852
|
+
transport: this.runtime.transport,
|
|
1975
1853
|
});
|
|
1976
|
-
throw new Error("Codex stream ended before terminal completion event");
|
|
1977
|
-
}
|
|
1978
|
-
if (output.stopReason === "aborted" || output.stopReason === "error") {
|
|
1979
|
-
throw new Error("Codex response failed");
|
|
1980
|
-
}
|
|
1981
1854
|
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1855
|
+
this.runtime.resetAccumulators();
|
|
1856
|
+
this.runtime.sawTerminalEvent = false;
|
|
1857
|
+
resetOutputState(this.output);
|
|
1858
|
+
this.firstTokenTime = undefined;
|
|
1859
|
+
await scheduler.wait(CODEX_RETRY_DELAY_MS * this.runtime.providerRetryAttempt, {
|
|
1860
|
+
signal: this.requestSetup.requestSignal,
|
|
1861
|
+
});
|
|
1862
|
+
|
|
1863
|
+
if (this.runtime.transport === "websocket" && websocketState) {
|
|
1864
|
+
await this.#reopenWebSocketStream(websocketState);
|
|
1865
|
+
return true;
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
await this.#reopenSseStream(websocketState);
|
|
1869
|
+
return true;
|
|
1986
1870
|
}
|
|
1987
|
-
return output;
|
|
1988
|
-
}
|
|
1989
1871
|
|
|
1990
|
-
async
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1872
|
+
async #reopenWebSocketStream(state: CodexWebSocketSessionState): Promise<void> {
|
|
1873
|
+
try {
|
|
1874
|
+
const next = await openCodexWebSocketTransport(
|
|
1875
|
+
this.model,
|
|
1876
|
+
this.options,
|
|
1877
|
+
this.requestContext,
|
|
1878
|
+
this.requestSetup,
|
|
1879
|
+
state,
|
|
1880
|
+
this.runtime.websocketStreamRetries,
|
|
1881
|
+
this.options ? event => this.options?.onSseEvent?.(event, this.model) : undefined,
|
|
1882
|
+
);
|
|
1883
|
+
this.runtime.eventStream = next.eventStream;
|
|
1884
|
+
this.runtime.requestBodyForState = next.requestBodyForState;
|
|
1885
|
+
this.runtime.transport = next.transport;
|
|
1886
|
+
state.lastTransport = next.transport;
|
|
1887
|
+
} catch (error) {
|
|
1888
|
+
if (!(error instanceof CodexWebSocketTransportError)) throw error;
|
|
1889
|
+
// Reopen failed at the websocket layer (handshake refused, connect timeout, etc.).
|
|
1890
|
+
// Activate fallback so subsequent turns use SSE, and replay this turn over SSE
|
|
1891
|
+
// instead of surfacing a raw transport error to the caller.
|
|
1892
|
+
recordCodexWebSocketFailure(state, true);
|
|
1893
|
+
CODEX_DEBUG &&
|
|
1894
|
+
logger.debug("[codex] codex websocket reopen failed, falling back to SSE", {
|
|
1895
|
+
error: error.message,
|
|
1896
|
+
retry: this.runtime.websocketStreamRetries,
|
|
1897
|
+
});
|
|
1898
|
+
await this.#reopenSseStream(state);
|
|
1899
|
+
}
|
|
1997
1900
|
}
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
1901
|
+
|
|
1902
|
+
async #reopenSseStream(state: CodexWebSocketSessionState | undefined): Promise<void> {
|
|
1903
|
+
const next = await openCodexSseTransport(this.model, this.requestContext, this.requestSetup, this.options, state);
|
|
1904
|
+
this.runtime.eventStream = next.eventStream;
|
|
1905
|
+
this.runtime.requestBodyForState = next.requestBodyForState;
|
|
1906
|
+
this.runtime.transport = next.transport;
|
|
1907
|
+
if (state) {
|
|
1908
|
+
state.lastTransport = next.transport;
|
|
1909
|
+
}
|
|
2002
1910
|
}
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
1911
|
+
|
|
1912
|
+
finalize(completion: CodexStreamCompletion): AssistantMessage {
|
|
1913
|
+
const { output } = this;
|
|
1914
|
+
if (this.options?.signal?.aborted) {
|
|
1915
|
+
throw new AIError.AbortError();
|
|
1916
|
+
}
|
|
1917
|
+
if (!this.runtime.sawTerminalEvent) {
|
|
1918
|
+
if (this.requestContext.websocketState) {
|
|
1919
|
+
resetCodexWebSocketAppendState(this.requestContext.websocketState);
|
|
1920
|
+
this.requestContext.websocketState.turnState = undefined;
|
|
1921
|
+
this.requestContext.websocketState.modelsEtag = undefined;
|
|
1922
|
+
}
|
|
1923
|
+
CODEX_DEBUG &&
|
|
1924
|
+
logger.debug("[codex] codex stream ended unexpectedly", {
|
|
1925
|
+
transport: this.runtime.transport,
|
|
1926
|
+
terminalEventSeen: this.runtime.sawTerminalEvent,
|
|
1927
|
+
unexpectedStreamEnd: true,
|
|
1928
|
+
sentTurnStateHeader: Boolean(this.requestContext.websocketState?.turnState),
|
|
1929
|
+
sentModelsEtagHeader: Boolean(this.requestContext.websocketState?.modelsEtag),
|
|
1930
|
+
});
|
|
1931
|
+
throw new CodexProviderStreamError("Codex stream ended before terminal completion event", false);
|
|
1932
|
+
}
|
|
1933
|
+
if (output.stopReason === "aborted" || output.stopReason === "error") {
|
|
1934
|
+
throw new CodexProviderStreamError("Codex response failed", false);
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
output.providerPayload = createOpenAIResponsesHistoryPayload(this.model.provider, this.runtime.nativeOutputItems);
|
|
1938
|
+
output.duration = performance.now() - this.startTime;
|
|
1939
|
+
if (completion.firstTokenTime) {
|
|
1940
|
+
output.ttft = completion.firstTokenTime - this.startTime;
|
|
1941
|
+
}
|
|
1942
|
+
return output;
|
|
2009
1943
|
}
|
|
2010
|
-
return output;
|
|
2011
1944
|
}
|
|
2012
1945
|
|
|
2013
1946
|
export const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses"> = (
|
|
@@ -2018,7 +1951,7 @@ export const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses"
|
|
|
2018
1951
|
const stream = new AssistantMessageEventStream();
|
|
2019
1952
|
|
|
2020
1953
|
(async () => {
|
|
2021
|
-
const startTime =
|
|
1954
|
+
const startTime = performance.now();
|
|
2022
1955
|
const output: AssistantMessage = {
|
|
2023
1956
|
role: "assistant",
|
|
2024
1957
|
content: [],
|
|
@@ -2037,13 +1970,13 @@ export const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses"
|
|
|
2037
1970
|
timestamp: Date.now(),
|
|
2038
1971
|
};
|
|
2039
1972
|
const requestSetup = createRequestSetup(options);
|
|
2040
|
-
let processingContext:
|
|
1973
|
+
let processingContext: CodexStreamProcessor | undefined;
|
|
2041
1974
|
let requestContext: CodexRequestContext | undefined;
|
|
2042
1975
|
|
|
2043
1976
|
try {
|
|
2044
1977
|
requestContext = await buildCodexRequestContext(model, context, options, output);
|
|
2045
1978
|
const initialTransport = await openInitialCodexEventStream(model, options, requestSetup, requestContext);
|
|
2046
|
-
const runtime =
|
|
1979
|
+
const runtime = new CodexStreamRuntime({
|
|
2047
1980
|
...initialTransport,
|
|
2048
1981
|
websocketState: requestContext.websocketState,
|
|
2049
1982
|
});
|
|
@@ -2051,7 +1984,8 @@ export const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses"
|
|
|
2051
1984
|
requestContext.websocketState.lastTransport = initialTransport.transport;
|
|
2052
1985
|
}
|
|
2053
1986
|
|
|
2054
|
-
processingContext = {
|
|
1987
|
+
processingContext = new CodexStreamProcessor({
|
|
1988
|
+
runtime,
|
|
2055
1989
|
model,
|
|
2056
1990
|
output,
|
|
2057
1991
|
stream,
|
|
@@ -2059,11 +1993,11 @@ export const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses"
|
|
|
2059
1993
|
requestSetup,
|
|
2060
1994
|
requestContext,
|
|
2061
1995
|
startTime,
|
|
2062
|
-
};
|
|
1996
|
+
});
|
|
2063
1997
|
|
|
2064
|
-
const completion = await
|
|
1998
|
+
const completion = await processingContext.process();
|
|
2065
1999
|
processingContext.firstTokenTime = completion.firstTokenTime;
|
|
2066
|
-
const message =
|
|
2000
|
+
const message = processingContext.finalize(completion);
|
|
2067
2001
|
stream.push({ type: "done", reason: message.stopReason as "stop" | "length" | "toolUse", message });
|
|
2068
2002
|
stream.end();
|
|
2069
2003
|
} catch (error) {
|
|
@@ -2072,9 +2006,7 @@ export const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses"
|
|
|
2072
2006
|
({
|
|
2073
2007
|
model,
|
|
2074
2008
|
output,
|
|
2075
|
-
stream,
|
|
2076
2009
|
options,
|
|
2077
|
-
requestSetup,
|
|
2078
2010
|
requestContext: requestContext ?? {
|
|
2079
2011
|
apiKey: "",
|
|
2080
2012
|
accountId: "",
|
|
@@ -2093,7 +2025,7 @@ export const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses"
|
|
|
2093
2025
|
},
|
|
2094
2026
|
},
|
|
2095
2027
|
startTime,
|
|
2096
|
-
} satisfies
|
|
2028
|
+
} satisfies CodexStreamFailureContext);
|
|
2097
2029
|
try {
|
|
2098
2030
|
const failure = await handleCodexStreamFailure(failureContext, error);
|
|
2099
2031
|
stream.push({ type: "error", reason: failure.stopReason as "error" | "aborted", error: failure });
|
|
@@ -2342,7 +2274,7 @@ function buildCodexChainedRequestBody(
|
|
|
2342
2274
|
): RequestBody {
|
|
2343
2275
|
const chainable = state?.canAppend === true;
|
|
2344
2276
|
const appendInput = chainable
|
|
2345
|
-
? buildResponsesDeltaInput
|
|
2277
|
+
? buildResponsesDeltaInput(state.lastRequest, state.lastResponseItems, requestBody)
|
|
2346
2278
|
: null;
|
|
2347
2279
|
if (appendInput && appendInput.length > 0 && state?.lastResponseId) {
|
|
2348
2280
|
const body: RequestBody = { ...requestBody, previous_response_id: state.lastResponseId, input: appendInput };
|
|
@@ -2451,10 +2383,7 @@ class CodexWebSocketConnection {
|
|
|
2451
2383
|
*/
|
|
2452
2384
|
isHealthyForReuse(): boolean {
|
|
2453
2385
|
if (!this.isOpen()) return false;
|
|
2454
|
-
const maxIdleMs =
|
|
2455
|
-
$env.PI_CODEX_WEBSOCKET_MAX_IDLE_REUSE_MS,
|
|
2456
|
-
CODEX_WEBSOCKET_MAX_IDLE_REUSE_MS,
|
|
2457
|
-
);
|
|
2386
|
+
const maxIdleMs = CODEX_WEBSOCKET_MAX_IDLE_REUSE_MS;
|
|
2458
2387
|
if (maxIdleMs <= 0) return true;
|
|
2459
2388
|
// Initial connect sets #lastInboundAt; any later message or pong refreshes
|
|
2460
2389
|
// it. A zero value means the field was never initialized, which itself is
|
|
@@ -2618,7 +2547,7 @@ class CodexWebSocketConnection {
|
|
|
2618
2547
|
this.#activeRequest = true;
|
|
2619
2548
|
this.#streamObserver = onSseEvent;
|
|
2620
2549
|
// Drain any non-error frames left over from a prior request before sending.
|
|
2621
|
-
// `
|
|
2550
|
+
// `CodexStreamProcessor.process` breaks its `for-await` on the terminal event,
|
|
2622
2551
|
// which interrupts our generator at `yield next` (the post-yield `break`
|
|
2623
2552
|
// never runs). Any frame that landed between the consumer's break and the
|
|
2624
2553
|
// generator's `finally` lingers in `#queue` and would otherwise become the
|
|
@@ -2815,10 +2744,7 @@ class CodexWebSocketConnection {
|
|
|
2815
2744
|
|
|
2816
2745
|
#startHeartbeat(socket: Bun.WebSocket): void {
|
|
2817
2746
|
this.#stopHeartbeat();
|
|
2818
|
-
const intervalMs =
|
|
2819
|
-
$env.PI_CODEX_WEBSOCKET_PING_INTERVAL_MS,
|
|
2820
|
-
CODEX_WEBSOCKET_PING_INTERVAL_MS,
|
|
2821
|
-
);
|
|
2747
|
+
const intervalMs = CODEX_WEBSOCKET_PING_INTERVAL_MS;
|
|
2822
2748
|
if (intervalMs <= 0) return;
|
|
2823
2749
|
|
|
2824
2750
|
this.#lastPingAt = 0;
|
|
@@ -2849,10 +2775,7 @@ class CodexWebSocketConnection {
|
|
|
2849
2775
|
// finally fired. Instead, trigger on inbound silence: if we sent a
|
|
2850
2776
|
// ping at least `pongTimeoutMs` ago and have received no traffic of
|
|
2851
2777
|
// any kind (data frame or pong) since, the socket is unhealthy.
|
|
2852
|
-
const pongTimeoutMs =
|
|
2853
|
-
$env.PI_CODEX_WEBSOCKET_PONG_TIMEOUT_MS,
|
|
2854
|
-
CODEX_WEBSOCKET_PONG_TIMEOUT_MS,
|
|
2855
|
-
);
|
|
2778
|
+
const pongTimeoutMs = CODEX_WEBSOCKET_PONG_TIMEOUT_MS;
|
|
2856
2779
|
if (
|
|
2857
2780
|
pongTimeoutMs > 0 &&
|
|
2858
2781
|
this.#lastPingAt > 0 &&
|
|
@@ -2939,17 +2862,10 @@ class CodexWebSocketConnection {
|
|
|
2939
2862
|
this.#wakeWaiters();
|
|
2940
2863
|
return;
|
|
2941
2864
|
}
|
|
2942
|
-
if (
|
|
2943
|
-
item !== null &&
|
|
2944
|
-
this.#queue.length >=
|
|
2945
|
-
parseCodexPositiveInteger(
|
|
2946
|
-
$env.PI_CODEX_WEBSOCKET_MESSAGE_QUEUE_CAPACITY,
|
|
2947
|
-
CODEX_WEBSOCKET_MESSAGE_QUEUE_CAPACITY,
|
|
2948
|
-
)
|
|
2949
|
-
) {
|
|
2865
|
+
if (item !== null && this.#queue.length >= CODEX_WEBSOCKET_MESSAGE_QUEUE_CAPACITY) {
|
|
2950
2866
|
this.#failQueue(
|
|
2951
2867
|
new CodexWebSocketTransportError(
|
|
2952
|
-
`websocket message queue exceeded ${
|
|
2868
|
+
`websocket message queue exceeded ${CODEX_WEBSOCKET_MESSAGE_QUEUE_CAPACITY} items`,
|
|
2953
2869
|
),
|
|
2954
2870
|
"queue-overflow",
|
|
2955
2871
|
);
|
|
@@ -3101,7 +3017,7 @@ async function openCodexSseEventStream(
|
|
|
3101
3017
|
}
|
|
3102
3018
|
updateCodexSessionMetadataFromHeaders(state, response.headers);
|
|
3103
3019
|
if (!response.body) {
|
|
3104
|
-
throw new
|
|
3020
|
+
throw new CodexProviderStreamError("No response body", false);
|
|
3105
3021
|
}
|
|
3106
3022
|
return readSseJson<Record<string, unknown>>(response.body, signal, event =>
|
|
3107
3023
|
onSseEvent?.({ event: event.event, data: event.data, raw: [...event.raw] }, undefined),
|
|
@@ -3198,7 +3114,10 @@ function resolveCodexResponsesUrl(baseUrl: string | undefined): string {
|
|
|
3198
3114
|
function getAccountId(accessToken: string): string {
|
|
3199
3115
|
const accountId = getCodexAccountId(accessToken);
|
|
3200
3116
|
if (!accountId) {
|
|
3201
|
-
throw new
|
|
3117
|
+
throw new AIError.OAuthError("Failed to extract accountId from token", {
|
|
3118
|
+
kind: "validation",
|
|
3119
|
+
provider: "openai",
|
|
3120
|
+
});
|
|
3202
3121
|
}
|
|
3203
3122
|
return accountId;
|
|
3204
3123
|
}
|