@oh-my-pi/pi-ai 18.3.0 → 18.3.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 +20 -0
- package/THIRD-PARTY-NOTICES.txt +2 -2
- package/dist/types/auth/policy.d.ts +8 -1
- package/dist/types/auth/pool.d.ts +8 -0
- package/dist/types/auth/types.d.ts +17 -7
- package/dist/types/auth/usage.d.ts +2 -0
- package/dist/types/auth-broker/discover.d.ts +22 -1
- package/dist/types/auth-storage.d.ts +35 -11
- package/dist/types/error/rate-limit.d.ts +3 -2
- package/dist/types/index.d.ts +1 -0
- package/dist/types/providers/anthropic-slow-mode.d.ts +109 -0
- package/dist/types/providers/anthropic-wire.d.ts +4 -0
- package/dist/types/providers/mock.d.ts +3 -1
- package/dist/types/providers/openai-codex/live-steering.d.ts +77 -0
- package/dist/types/providers/transform-messages.d.ts +9 -1
- package/dist/types/types.d.ts +56 -0
- package/dist/types/usage/xai-oauth.d.ts +6 -1
- package/dist/types/utils/http-inspector.d.ts +6 -0
- package/package.json +6 -6
- package/src/auth/policy.ts +27 -6
- package/src/auth/pool.ts +35 -2
- package/src/auth/refresh.ts +2 -2
- package/src/auth/types.ts +17 -7
- package/src/auth/usage.ts +5 -0
- package/src/auth-broker/discover.ts +57 -27
- package/src/auth-storage.ts +121 -35
- package/src/error/flags.ts +2 -1
- package/src/error/rate-limit.ts +8 -4
- package/src/index.ts +1 -0
- package/src/providers/anthropic-slow-mode.ts +232 -0
- package/src/providers/anthropic-wire.ts +4 -0
- package/src/providers/anthropic.ts +322 -22
- package/src/providers/cowork-fetch.ts +11 -4
- package/src/providers/google-shared.ts +30 -7
- package/src/providers/inference-headers.ts +7 -1
- package/src/providers/mock.ts +4 -0
- package/src/providers/openai-codex/live-steering.ts +237 -0
- package/src/providers/openai-codex-responses.ts +372 -73
- package/src/providers/transform-messages.ts +27 -7
- package/src/stream.ts +32 -2
- package/src/types.ts +59 -0
- package/src/usage/registry.ts +2 -1
- package/src/usage/xai-oauth.ts +31 -1
- package/src/utils/http-inspector.ts +21 -2
- package/src/utils/openrouter-headers.ts +3 -3
|
@@ -82,6 +82,13 @@ import {
|
|
|
82
82
|
transformRequestBody,
|
|
83
83
|
} from "./openai-codex/request-transformer";
|
|
84
84
|
import { applyCodexAccessPrograms, dropRejectedCodexAccessPrograms } from "./openai-codex/access-programs";
|
|
85
|
+
import {
|
|
86
|
+
type CodexAcceptedSteer,
|
|
87
|
+
type CodexSteerAck,
|
|
88
|
+
CodexSteerPump,
|
|
89
|
+
planSteeredRequest,
|
|
90
|
+
toSteerInputItems,
|
|
91
|
+
} from "./openai-codex/live-steering";
|
|
85
92
|
import { CodexApiError } from "./openai-codex/response-handler";
|
|
86
93
|
import { getCodexAttestationHeader } from "./openai-codex-attestation";
|
|
87
94
|
export { createOpenAICodexCompactionRequestContext } from "./openai-codex-compaction";
|
|
@@ -423,6 +430,12 @@ type CodexWebSocketSessionState = {
|
|
|
423
430
|
canAppend: boolean;
|
|
424
431
|
modelsEtag?: string;
|
|
425
432
|
connection?: CodexWebSocketConnection;
|
|
433
|
+
/**
|
|
434
|
+
* Steering the server accepted on `connection` for `lastResponseId`. The
|
|
435
|
+
* next request reads the server's automatic successor or sends only the
|
|
436
|
+
* pending tool output; see {@link planSteeredRequest}.
|
|
437
|
+
*/
|
|
438
|
+
acceptedSteering?: { connection: CodexWebSocketConnection; steers: CodexAcceptedSteer[] };
|
|
426
439
|
lastTransport?: CodexTransport;
|
|
427
440
|
fallbackCount: number;
|
|
428
441
|
lastFallbackAt?: number;
|
|
@@ -1636,6 +1649,8 @@ async function openInitialCodexEventStream(
|
|
|
1636
1649
|
break;
|
|
1637
1650
|
}
|
|
1638
1651
|
}
|
|
1652
|
+
} else if (websocketState) {
|
|
1653
|
+
releaseIdleCodexWebSocket(websocketState);
|
|
1639
1654
|
}
|
|
1640
1655
|
try {
|
|
1641
1656
|
return await openCodexSseTransport(model, requestContext, requestSetup, options, websocketState, transformedBody);
|
|
@@ -1795,8 +1810,78 @@ async function openCodexWebSocketTransport(
|
|
|
1795
1810
|
requestBodyForState: RequestBody;
|
|
1796
1811
|
transport: CodexTransport;
|
|
1797
1812
|
}> {
|
|
1813
|
+
const steered = websocketState.acceptedSteering;
|
|
1814
|
+
websocketState.acceptedSteering = undefined;
|
|
1798
1815
|
const canAppendBeforeRequest = websocketState.canAppend === true;
|
|
1799
|
-
|
|
1816
|
+
let chainedBody = buildCodexChainedRequestBody(requestContext.transformedBody, websocketState);
|
|
1817
|
+
const websocketHeaders = createCodexHeaders(
|
|
1818
|
+
requestContext.requestHeaders,
|
|
1819
|
+
requestContext.accountId,
|
|
1820
|
+
requestContext.apiKey,
|
|
1821
|
+
requestContext.codexClientVersion,
|
|
1822
|
+
requestContext.transportSessionId,
|
|
1823
|
+
"websocket",
|
|
1824
|
+
websocketState,
|
|
1825
|
+
requestContext.turnState,
|
|
1826
|
+
requestContext.responsesLite,
|
|
1827
|
+
requestContext.requestMetadata,
|
|
1828
|
+
await getCodexAttestationHeader(requestContext.accountId),
|
|
1829
|
+
requestContext.transformedBody,
|
|
1830
|
+
);
|
|
1831
|
+
let websocketConnection = await getOrCreateCodexWebSocketConnection(
|
|
1832
|
+
websocketState,
|
|
1833
|
+
requestContext.turnState,
|
|
1834
|
+
toWebSocketUrl(requestContext.url),
|
|
1835
|
+
websocketHeaders,
|
|
1836
|
+
model.provider,
|
|
1837
|
+
requestSetup.requestSignal,
|
|
1838
|
+
);
|
|
1839
|
+
const timeouts: CodexWebSocketRequestTimeouts = {
|
|
1840
|
+
idleTimeoutMs: requestSetup.websocketIdleTimeoutMs,
|
|
1841
|
+
firstEventTimeoutMs: requestSetup.websocketFirstEventTimeoutMs,
|
|
1842
|
+
};
|
|
1843
|
+
// Steering accepted for the previous response lives only on the socket that
|
|
1844
|
+
// accepted it; a replaced socket lost it, so the input is plain history.
|
|
1845
|
+
const steers = steered?.connection === websocketConnection ? steered.steers : [];
|
|
1846
|
+
const liveSteers = steers.filter(steer => !websocketConnection.hasFailedSteer([steer.id]));
|
|
1847
|
+
if (liveSteers.length > 0) {
|
|
1848
|
+
const plan = planSteeredRequest(
|
|
1849
|
+
chainedBody.previous_response_id ? chainedBody.input : undefined,
|
|
1850
|
+
liveSteers.flatMap(steer => steer.items),
|
|
1851
|
+
);
|
|
1852
|
+
if (plan.kind === "attach") {
|
|
1853
|
+
recordCodexTurnRequestDiagnostics(websocketState, chainedBody, "websocket", canAppendBeforeRequest);
|
|
1854
|
+
requestContext.rawRequestDump.body = chainedBody;
|
|
1855
|
+
return {
|
|
1856
|
+
eventStream: websocketConnection.streamRequest(
|
|
1857
|
+
{ attachSteerIds: liveSteers.map(steer => steer.id) },
|
|
1858
|
+
timeouts,
|
|
1859
|
+
requestSetup.requestSignal,
|
|
1860
|
+
onSseEvent,
|
|
1861
|
+
),
|
|
1862
|
+
requestBodyForState: structuredCloneJSON(requestContext.transformedBody),
|
|
1863
|
+
transport: "websocket",
|
|
1864
|
+
};
|
|
1865
|
+
}
|
|
1866
|
+
if (plan.kind === "create") {
|
|
1867
|
+
chainedBody = { ...chainedBody, input: plan.input };
|
|
1868
|
+
} else {
|
|
1869
|
+
// The server continues from input this request cannot line up with; a
|
|
1870
|
+
// fresh socket drops its queue and the full transcript carries the input.
|
|
1871
|
+
websocketConnection.close("steering-discard");
|
|
1872
|
+
websocketState.connection = undefined;
|
|
1873
|
+
resetCodexWebSocketAppendState(websocketState);
|
|
1874
|
+
chainedBody = requestContext.transformedBody;
|
|
1875
|
+
websocketConnection = await getOrCreateCodexWebSocketConnection(
|
|
1876
|
+
websocketState,
|
|
1877
|
+
requestContext.turnState,
|
|
1878
|
+
toWebSocketUrl(requestContext.url),
|
|
1879
|
+
websocketHeaders,
|
|
1880
|
+
model.provider,
|
|
1881
|
+
requestSetup.requestSignal,
|
|
1882
|
+
);
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1800
1885
|
// WebSocket frames cannot carry per-request HTTP headers. Canonical Codex
|
|
1801
1886
|
// request identity is already in `client_metadata`; connection-scoped
|
|
1802
1887
|
// compatibility values that can change after the upgrade ride alongside it
|
|
@@ -1818,20 +1903,6 @@ async function openCodexWebSocketTransport(
|
|
|
1818
1903
|
websocketRequest = replacementWebsocketRequest as typeof websocketRequest;
|
|
1819
1904
|
}
|
|
1820
1905
|
recordCodexTurnRequestDiagnostics(websocketState, websocketRequest, "websocket", canAppendBeforeRequest);
|
|
1821
|
-
const websocketHeaders = createCodexHeaders(
|
|
1822
|
-
requestContext.requestHeaders,
|
|
1823
|
-
requestContext.accountId,
|
|
1824
|
-
requestContext.apiKey,
|
|
1825
|
-
requestContext.codexClientVersion,
|
|
1826
|
-
requestContext.transportSessionId,
|
|
1827
|
-
"websocket",
|
|
1828
|
-
websocketState,
|
|
1829
|
-
requestContext.turnState,
|
|
1830
|
-
requestContext.responsesLite,
|
|
1831
|
-
requestContext.requestMetadata,
|
|
1832
|
-
await getCodexAttestationHeader(requestContext.accountId),
|
|
1833
|
-
requestContext.transformedBody,
|
|
1834
|
-
);
|
|
1835
1906
|
const requestBodyForState = structuredCloneJSON(requestContext.transformedBody);
|
|
1836
1907
|
// `onPayload` may rewrite the outgoing frame (e.g. drop `stream_options`);
|
|
1837
1908
|
// recorded state must reflect what was actually sent — the sequential-cutoff
|
|
@@ -1854,20 +1925,9 @@ async function openCodexWebSocketTransport(
|
|
|
1854
1925
|
retry,
|
|
1855
1926
|
retryBudget: CODEX_WEBSOCKET_RETRY_BUDGET,
|
|
1856
1927
|
});
|
|
1857
|
-
const websocketConnection = await getOrCreateCodexWebSocketConnection(
|
|
1858
|
-
websocketState,
|
|
1859
|
-
requestContext.turnState,
|
|
1860
|
-
toWebSocketUrl(requestContext.url),
|
|
1861
|
-
websocketHeaders,
|
|
1862
|
-
model.provider,
|
|
1863
|
-
requestSetup.requestSignal,
|
|
1864
|
-
);
|
|
1865
1928
|
const eventStream = websocketConnection.streamRequest(
|
|
1866
|
-
websocketRequest,
|
|
1867
|
-
|
|
1868
|
-
idleTimeoutMs: requestSetup.websocketIdleTimeoutMs,
|
|
1869
|
-
firstEventTimeoutMs: requestSetup.websocketFirstEventTimeoutMs,
|
|
1870
|
-
},
|
|
1929
|
+
{ send: websocketRequest },
|
|
1930
|
+
timeouts,
|
|
1871
1931
|
requestSetup.requestSignal,
|
|
1872
1932
|
onSseEvent,
|
|
1873
1933
|
);
|
|
@@ -2097,6 +2157,12 @@ class CodexStreamProcessor {
|
|
|
2097
2157
|
requestContext: CodexRequestContext;
|
|
2098
2158
|
startTime: number;
|
|
2099
2159
|
firstTokenTime?: number;
|
|
2160
|
+
/** Submits caller steering to the response currently streaming. */
|
|
2161
|
+
#steerPump?: CodexSteerPump;
|
|
2162
|
+
/** Socket {@link #steerPump} submits through. */
|
|
2163
|
+
#steerConnection?: CodexWebSocketConnection;
|
|
2164
|
+
/** A replaced attempt accepted steering its socket may still act on. */
|
|
2165
|
+
#steerTainted = false;
|
|
2100
2166
|
|
|
2101
2167
|
constructor(init: {
|
|
2102
2168
|
runtime: CodexStreamRuntime;
|
|
@@ -2150,10 +2216,15 @@ class CodexStreamProcessor {
|
|
|
2150
2216
|
});
|
|
2151
2217
|
throw new CodexProviderStreamError("Codex stream ended before terminal completion event", true);
|
|
2152
2218
|
}
|
|
2219
|
+
await this.#settleSteering(true);
|
|
2153
2220
|
return { firstTokenTime };
|
|
2154
2221
|
} catch (error) {
|
|
2222
|
+
// The failed attempt's response no longer takes input; a retry
|
|
2223
|
+
// steers its own response.
|
|
2224
|
+
await this.#retireSteering();
|
|
2155
2225
|
const recovered = await this.#recoverStreamError(error);
|
|
2156
2226
|
if (!recovered) {
|
|
2227
|
+
await this.#settleSteering(false);
|
|
2157
2228
|
throw error;
|
|
2158
2229
|
}
|
|
2159
2230
|
stream.push({ type: "start", partial: output });
|
|
@@ -2166,6 +2237,11 @@ class CodexStreamProcessor {
|
|
|
2166
2237
|
const eventType = typeof rawEvent.type === "string" ? rawEvent.type : "";
|
|
2167
2238
|
if (!eventType) return firstTokenTime;
|
|
2168
2239
|
|
|
2240
|
+
if (eventType === "response.created") {
|
|
2241
|
+
this.#startSteering(rawEvent);
|
|
2242
|
+
return firstTokenTime;
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2169
2245
|
if (eventType === "response.output_item.added") {
|
|
2170
2246
|
this.runtime.whitespaceToolCallArgumentsDelta = undefined;
|
|
2171
2247
|
if (!firstTokenTime) firstTokenTime = performance.now();
|
|
@@ -2376,6 +2452,67 @@ class CodexStreamProcessor {
|
|
|
2376
2452
|
return firstTokenTime;
|
|
2377
2453
|
}
|
|
2378
2454
|
|
|
2455
|
+
/** Start delivering caller steering into the response `response.created` announced. */
|
|
2456
|
+
#startSteering(rawEvent: Record<string, unknown>): void {
|
|
2457
|
+
const source = this.options?.liveSteering;
|
|
2458
|
+
const connection = this.runtime.websocketState?.connection;
|
|
2459
|
+
const responseId = asRecord(rawEvent.response)?.id;
|
|
2460
|
+
if (
|
|
2461
|
+
!source ||
|
|
2462
|
+
!connection ||
|
|
2463
|
+
this.#steerPump ||
|
|
2464
|
+
typeof responseId !== "string" ||
|
|
2465
|
+
this.runtime.transport !== "websocket" ||
|
|
2466
|
+
!this.model.compat.supportsSteering
|
|
2467
|
+
) {
|
|
2468
|
+
return;
|
|
2469
|
+
}
|
|
2470
|
+
this.#steerConnection = connection;
|
|
2471
|
+
this.#steerPump = new CodexSteerPump(source, connection, messages =>
|
|
2472
|
+
toSteerInputItems(convertMessages(this.model, { messages: [...messages] })),
|
|
2473
|
+
);
|
|
2474
|
+
this.#steerPump.start(responseId);
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
/** Stop steering an attempt that is being replaced; anything it delivered taints its socket. */
|
|
2478
|
+
async #retireSteering(): Promise<void> {
|
|
2479
|
+
const pump = this.#steerPump;
|
|
2480
|
+
if (!pump) return;
|
|
2481
|
+
this.#steerPump = undefined;
|
|
2482
|
+
const outcome = await pump.finish();
|
|
2483
|
+
if (outcome.accepted.length > 0 || outcome.uncertain) this.#steerTainted = true;
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
/**
|
|
2487
|
+
* Settle steering once the stream is over. Steering accepted for the final
|
|
2488
|
+
* response is handed to the next request; anything the session cannot line
|
|
2489
|
+
* up with the server's queue drops the socket so no stray successor streams
|
|
2490
|
+
* into a later request.
|
|
2491
|
+
*/
|
|
2492
|
+
async #settleSteering(completed: boolean): Promise<void> {
|
|
2493
|
+
const pump = this.#steerPump;
|
|
2494
|
+
this.#steerPump = undefined;
|
|
2495
|
+
const outcome = pump ? await pump.finish() : undefined;
|
|
2496
|
+
const accepted = outcome?.accepted ?? [];
|
|
2497
|
+
const tainted = this.#steerTainted || outcome?.uncertain === true;
|
|
2498
|
+
const state = this.runtime.websocketState;
|
|
2499
|
+
const connection = this.#steerConnection;
|
|
2500
|
+
if (!state || !connection || (accepted.length === 0 && !tainted)) return;
|
|
2501
|
+
if (
|
|
2502
|
+
completed &&
|
|
2503
|
+
!tainted &&
|
|
2504
|
+
state.canAppend &&
|
|
2505
|
+
state.connection === connection &&
|
|
2506
|
+
state.lastResponseId === outcome?.responseId
|
|
2507
|
+
) {
|
|
2508
|
+
state.acceptedSteering = { connection, steers: accepted };
|
|
2509
|
+
return;
|
|
2510
|
+
}
|
|
2511
|
+
connection.close("steering-discard");
|
|
2512
|
+
if (state.connection === connection) state.connection = undefined;
|
|
2513
|
+
resetCodexWebSocketAppendState(state);
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2379
2516
|
#flushSummaryDeltas(entry: CodexOpenItem | null): void {
|
|
2380
2517
|
if (entry?.block?.type !== "thinking") return;
|
|
2381
2518
|
for (const delta of this.runtime.takeSummaryDeltas(entry)) {
|
|
@@ -2529,6 +2666,21 @@ class CodexStreamProcessor {
|
|
|
2529
2666
|
output.responseId = responseId;
|
|
2530
2667
|
}
|
|
2531
2668
|
|
|
2669
|
+
const incompleteDetails =
|
|
2670
|
+
response &&
|
|
2671
|
+
"incomplete_details" in response &&
|
|
2672
|
+
response.incomplete_details &&
|
|
2673
|
+
typeof response.incomplete_details === "object"
|
|
2674
|
+
? response.incomplete_details
|
|
2675
|
+
: undefined;
|
|
2676
|
+
const incompleteReason =
|
|
2677
|
+
incompleteDetails && "reason" in incompleteDetails ? incompleteDetails.reason : undefined;
|
|
2678
|
+
// Steering stopped the response at an output boundary: every item is
|
|
2679
|
+
// complete and the server continues from it, so it chains and ends like a
|
|
2680
|
+
// completed response.
|
|
2681
|
+
const steered = status === "incomplete" && incompleteReason === "steered";
|
|
2682
|
+
if (steered) logger.debug("Codex response stopped for steering", { responseId });
|
|
2683
|
+
|
|
2532
2684
|
const state = runtime.websocketState;
|
|
2533
2685
|
if (state) {
|
|
2534
2686
|
if (runtime.transport !== "websocket") {
|
|
@@ -2545,7 +2697,7 @@ class CodexStreamProcessor {
|
|
|
2545
2697
|
if (responseId && replayableResponseItems && replayableResponseItems.length === nativeOutputItems.length) {
|
|
2546
2698
|
state.lastResponseId = responseId;
|
|
2547
2699
|
state.lastResponseItems = replayableResponseItems;
|
|
2548
|
-
state.canAppend = rawEvent.type === "response.done" || rawEvent.type === "response.completed";
|
|
2700
|
+
state.canAppend = rawEvent.type === "response.done" || rawEvent.type === "response.completed" || steered;
|
|
2549
2701
|
} else {
|
|
2550
2702
|
// No response id, or replay sanitization dropped an item the server
|
|
2551
2703
|
// still holds. Sanitization is 1:1-or-fewer, so either case makes the
|
|
@@ -2555,24 +2707,15 @@ class CodexStreamProcessor {
|
|
|
2555
2707
|
}
|
|
2556
2708
|
}
|
|
2557
2709
|
|
|
2558
|
-
const incompleteDetails =
|
|
2559
|
-
response &&
|
|
2560
|
-
"incomplete_details" in response &&
|
|
2561
|
-
response.incomplete_details &&
|
|
2562
|
-
typeof response.incomplete_details === "object"
|
|
2563
|
-
? response.incomplete_details
|
|
2564
|
-
: undefined;
|
|
2565
2710
|
const shouldPromoteIncompleteToolUse =
|
|
2566
2711
|
status === "incomplete" &&
|
|
2567
|
-
|
|
2568
|
-
"reason" in incompleteDetails &&
|
|
2569
|
-
incompleteDetails.reason === "max_output_tokens" &&
|
|
2712
|
+
incompleteReason === "max_output_tokens" &&
|
|
2570
2713
|
hasExecutableIncompleteResponsesToolCalls(output);
|
|
2571
2714
|
finalizePendingResponsesToolCalls(output);
|
|
2572
2715
|
|
|
2573
2716
|
calculateCost(model, output.usage, output.timestamp);
|
|
2574
2717
|
applyCodexServiceTierPricing(model, output.usage, serviceTier, runtime.requestBodyForState.service_tier);
|
|
2575
|
-
output.stopReason = mapOpenAIResponsesStopReason(status);
|
|
2718
|
+
output.stopReason = mapOpenAIResponsesStopReason(steered ? "completed" : status);
|
|
2576
2719
|
promoteResponsesToolUseStopReason(
|
|
2577
2720
|
output,
|
|
2578
2721
|
endTurn === true ? true : endTurn === false ? false : undefined,
|
|
@@ -2581,6 +2724,17 @@ class CodexStreamProcessor {
|
|
|
2581
2724
|
}
|
|
2582
2725
|
|
|
2583
2726
|
async #recoverStreamError(error: unknown): Promise<boolean> {
|
|
2727
|
+
if (
|
|
2728
|
+
error instanceof CodexSteerCommitError &&
|
|
2729
|
+
this.runtime.websocketState &&
|
|
2730
|
+
this.output.content.length === 0 &&
|
|
2731
|
+
!this.options?.signal?.aborted
|
|
2732
|
+
) {
|
|
2733
|
+
// No successor is coming: send this request explicitly, steering
|
|
2734
|
+
// input included as ordinary history.
|
|
2735
|
+
await this.#reopenWebSocketStream(this.runtime.websocketState);
|
|
2736
|
+
return true;
|
|
2737
|
+
}
|
|
2584
2738
|
if (await this.#tryDropRejectedAccessPrograms(error)) {
|
|
2585
2739
|
return true;
|
|
2586
2740
|
}
|
|
@@ -3241,6 +3395,19 @@ function recordCodexWebSocketFailure(state: CodexWebSocketSessionState, activate
|
|
|
3241
3395
|
}
|
|
3242
3396
|
}
|
|
3243
3397
|
|
|
3398
|
+
/**
|
|
3399
|
+
* WebSocket not selected for this request (e.g. the preference was switched off
|
|
3400
|
+
* mid-session): close an idle cached socket so it stops heartbeating and is
|
|
3401
|
+
* never reused against a transcript advanced over SSE.
|
|
3402
|
+
*/
|
|
3403
|
+
function releaseIdleCodexWebSocket(state: CodexWebSocketSessionState): void {
|
|
3404
|
+
const connection = state.connection;
|
|
3405
|
+
if (!connection || connection.isBusy()) return;
|
|
3406
|
+
connection.close("transport_not_selected");
|
|
3407
|
+
state.connection = undefined;
|
|
3408
|
+
resetCodexWebSocketAppendState(state);
|
|
3409
|
+
}
|
|
3410
|
+
|
|
3244
3411
|
function shouldUseCodexWebSocket(
|
|
3245
3412
|
model: Model<"openai-codex-responses">,
|
|
3246
3413
|
state: CodexWebSocketSessionState | undefined,
|
|
@@ -3635,6 +3802,18 @@ interface CodexWebSocketRequestTimeouts {
|
|
|
3635
3802
|
firstEventTimeoutMs?: number;
|
|
3636
3803
|
}
|
|
3637
3804
|
|
|
3805
|
+
/** What {@link CodexWebSocketConnection.streamRequest} puts on the wire before reading a response. */
|
|
3806
|
+
type CodexWebSocketExchange =
|
|
3807
|
+
| { send: Record<string, unknown> }
|
|
3808
|
+
/** Read the successor the server creates from accepted steering; nothing is sent. */
|
|
3809
|
+
| { attachSteerIds: readonly string[] };
|
|
3810
|
+
|
|
3811
|
+
interface CodexSteerWaiter {
|
|
3812
|
+
previousResponseId: string;
|
|
3813
|
+
resolve: (ack: CodexSteerAck) => void;
|
|
3814
|
+
reject: (error: Error) => void;
|
|
3815
|
+
}
|
|
3816
|
+
|
|
3638
3817
|
interface CodexWebSocketConnectionOptions {
|
|
3639
3818
|
onHandshakeHeaders?: (headers: Headers) => void;
|
|
3640
3819
|
proxy?: string;
|
|
@@ -3670,6 +3849,14 @@ class CodexWebSocketConnection {
|
|
|
3670
3849
|
* previous (cleanly-completed) response that outlived the queue drain.
|
|
3671
3850
|
*/
|
|
3672
3851
|
#lastSeenResponseId?: string;
|
|
3852
|
+
/** `response.steer` submissions awaiting acknowledgement, oldest first. */
|
|
3853
|
+
#steerWaiters: CodexSteerWaiter[] = [];
|
|
3854
|
+
/** Steering ids the server accepted on this socket. */
|
|
3855
|
+
#acceptedSteers = new Set<string>();
|
|
3856
|
+
/** Accepted steering the server later failed to commit to a successor. */
|
|
3857
|
+
#failedSteers = new Set<string>();
|
|
3858
|
+
/** Steering whose automatic successor the active attach request is reading. */
|
|
3859
|
+
#attachSteerIds?: ReadonlySet<string>;
|
|
3673
3860
|
|
|
3674
3861
|
constructor(url: string, headers: Record<string, string>, options: CodexWebSocketConnectionOptions) {
|
|
3675
3862
|
this.#url = url;
|
|
@@ -3687,6 +3874,11 @@ class CodexWebSocketConnection {
|
|
|
3687
3874
|
return this.#connectPromise !== undefined;
|
|
3688
3875
|
}
|
|
3689
3876
|
|
|
3877
|
+
/** True while a handshake or a request stream is in flight on this socket. */
|
|
3878
|
+
isBusy(): boolean {
|
|
3879
|
+
return this.#activeRequest || this.#connectPromise !== undefined;
|
|
3880
|
+
}
|
|
3881
|
+
|
|
3690
3882
|
/**
|
|
3691
3883
|
* Stricter variant of {@link isOpen} for the connection-pool reuse gate.
|
|
3692
3884
|
* Refuses sockets that have been silent past {@link CODEX_WEBSOCKET_MAX_IDLE_REUSE_MS}.
|
|
@@ -3718,6 +3910,7 @@ class CodexWebSocketConnection {
|
|
|
3718
3910
|
const socket = this.#socket;
|
|
3719
3911
|
this.#socket = null;
|
|
3720
3912
|
this.#stopHeartbeat();
|
|
3913
|
+
this.#rejectSteerWaiters(`websocket closed (${reason})`);
|
|
3721
3914
|
if (!socket || (socket.readyState !== WebSocket.OPEN && socket.readyState !== WebSocket.CONNECTING)) return;
|
|
3722
3915
|
try {
|
|
3723
3916
|
socket.close(1000, reason);
|
|
@@ -3808,6 +4001,7 @@ class CodexWebSocketConnection {
|
|
|
3808
4001
|
socket.onclose = event => {
|
|
3809
4002
|
this.#socket = null;
|
|
3810
4003
|
this.#stopHeartbeat();
|
|
4004
|
+
this.#rejectSteerWaiters(`websocket closed (${event.code})`);
|
|
3811
4005
|
if (!settled) {
|
|
3812
4006
|
settled = true;
|
|
3813
4007
|
clearPending();
|
|
@@ -3837,6 +4031,12 @@ class CodexWebSocketConnection {
|
|
|
3837
4031
|
}
|
|
3838
4032
|
}
|
|
3839
4033
|
notifyCodexWebSocketInbound(this.#streamObserver, parsed, text);
|
|
4034
|
+
// Steering acknowledgements belong to the submitter, not to the
|
|
4035
|
+
// response stream they interleave with.
|
|
4036
|
+
if (typeof parsed.type === "string" && parsed.type.startsWith("response.steer.")) {
|
|
4037
|
+
this.#handleSteerEvent(parsed);
|
|
4038
|
+
return;
|
|
4039
|
+
}
|
|
3840
4040
|
this.#push(parsed);
|
|
3841
4041
|
} catch (error) {
|
|
3842
4042
|
notifyCodexWebSocketMalformed(this.#streamObserver, event.data, error);
|
|
@@ -3853,7 +4053,7 @@ class CodexWebSocketConnection {
|
|
|
3853
4053
|
}
|
|
3854
4054
|
|
|
3855
4055
|
async *streamRequest(
|
|
3856
|
-
|
|
4056
|
+
exchange: CodexWebSocketExchange,
|
|
3857
4057
|
timeouts: CodexWebSocketRequestTimeouts,
|
|
3858
4058
|
signal?: AbortSignal,
|
|
3859
4059
|
onSseEvent?: (event: RawSseEvent) => void,
|
|
@@ -3870,6 +4070,8 @@ class CodexWebSocketConnection {
|
|
|
3870
4070
|
this.#activeRequest = true;
|
|
3871
4071
|
this.#streamObserver = onSseEvent;
|
|
3872
4072
|
// Drain any non-error frames left over from a prior request before sending.
|
|
4073
|
+
// An attach reads exactly those frames: the successor the server started
|
|
4074
|
+
// from accepted steering while no request was reading.
|
|
3873
4075
|
// `CodexStreamProcessor.process` breaks its `for-await` on the terminal event,
|
|
3874
4076
|
// which interrupts our generator at `yield next` (the post-yield `break`
|
|
3875
4077
|
// never runs). Any frame that landed between the consumer's break and the
|
|
@@ -3879,7 +4081,11 @@ class CodexWebSocketConnection {
|
|
|
3879
4081
|
// flip `sawFirstEvent` and silently downgrade the first-event timeout to
|
|
3880
4082
|
// the longer idle timeout. Transport errors are preserved so we surface
|
|
3881
4083
|
// the death signal instead of writing into a dead socket.
|
|
3882
|
-
|
|
4084
|
+
if ("send" in exchange) {
|
|
4085
|
+
this.#dropStaleFrames();
|
|
4086
|
+
} else {
|
|
4087
|
+
this.#attachSteerIds = new Set(exchange.attachSteerIds);
|
|
4088
|
+
}
|
|
3883
4089
|
const onAbort = () => {
|
|
3884
4090
|
this.#push(new CodexWebSocketTransportError(`request was aborted`, { cause: signal?.reason }));
|
|
3885
4091
|
// Closing can synchronously enqueue a generic onclose error.
|
|
@@ -3888,39 +4094,44 @@ class CodexWebSocketConnection {
|
|
|
3888
4094
|
if (signal) signal.addEventListener("abort", onAbort, { once: true });
|
|
3889
4095
|
|
|
3890
4096
|
try {
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
4097
|
+
if ("send" in exchange) {
|
|
4098
|
+
const request = exchange.send;
|
|
4099
|
+
const debugSession = isRequestDebugEnabled()
|
|
4100
|
+
? await createRequestDebugSession({
|
|
4101
|
+
protocol: "websocket",
|
|
4102
|
+
method: "POST",
|
|
4103
|
+
url: this.#url,
|
|
4104
|
+
headers: this.#headers,
|
|
4105
|
+
body: request,
|
|
4106
|
+
})
|
|
4107
|
+
: undefined;
|
|
4108
|
+
this.#debugResponseLog = debugSession
|
|
4109
|
+
? await debugSession.openResponseLog("WebSocket 101 Switching Protocols", this.#handshakeHeaders)
|
|
4110
|
+
: undefined;
|
|
3903
4111
|
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
4112
|
+
const requestPayload = JSON.stringify(request);
|
|
4113
|
+
notifyCodexWebSocketOutbound(onSseEvent, request, requestPayload);
|
|
4114
|
+
// Re-check liveness: the debug-session await above can outlive the socket.
|
|
4115
|
+
// Preserve the abort cause: onAbort already queued a caused error, but this
|
|
4116
|
+
// throw would otherwise mask it before #nextMessage() drains the queue.
|
|
4117
|
+
const socket = this.#socket;
|
|
4118
|
+
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
|
4119
|
+
if (signal?.aborted) {
|
|
4120
|
+
throw new CodexWebSocketTransportError(`websocket connection is unavailable`, {
|
|
4121
|
+
cause: signal.reason,
|
|
4122
|
+
});
|
|
4123
|
+
}
|
|
4124
|
+
throw new CodexWebSocketTransportError(`websocket connection is unavailable`);
|
|
3915
4125
|
}
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
4126
|
+
try {
|
|
4127
|
+
socket.send(requestPayload);
|
|
4128
|
+
} catch (error) {
|
|
4129
|
+
throw new CodexWebSocketTransportError(
|
|
4130
|
+
`websocket send failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
4131
|
+
);
|
|
4132
|
+
}
|
|
4133
|
+
} else if (this.hasFailedSteer(exchange.attachSteerIds)) {
|
|
4134
|
+
throw new CodexSteerCommitError("accepted steering was not committed to a successor response");
|
|
3924
4135
|
}
|
|
3925
4136
|
let sawFirstEvent = false;
|
|
3926
4137
|
const { idleTimeoutMs, firstEventTimeoutMs } = timeouts;
|
|
@@ -4039,6 +4250,7 @@ class CodexWebSocketConnection {
|
|
|
4039
4250
|
} finally {
|
|
4040
4251
|
this.#activeRequest = false;
|
|
4041
4252
|
this.#streamObserver = undefined;
|
|
4253
|
+
this.#attachSteerIds = undefined;
|
|
4042
4254
|
if (signal) {
|
|
4043
4255
|
signal.removeEventListener("abort", onAbort);
|
|
4044
4256
|
}
|
|
@@ -4048,6 +4260,83 @@ class CodexWebSocketConnection {
|
|
|
4048
4260
|
}
|
|
4049
4261
|
}
|
|
4050
4262
|
|
|
4263
|
+
/**
|
|
4264
|
+
* Submit `response.steer` for `previousResponseId`, which must be streaming
|
|
4265
|
+
* on this socket. Resolves on the server's acknowledgement.
|
|
4266
|
+
*/
|
|
4267
|
+
steer(previousResponseId: string, input: InputItem[]): Promise<CodexSteerAck> {
|
|
4268
|
+
const socket = this.#socket;
|
|
4269
|
+
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
|
4270
|
+
return Promise.reject(new CodexWebSocketTransportError(`websocket connection is unavailable`));
|
|
4271
|
+
}
|
|
4272
|
+
const event = { type: "response.steer", previous_response_id: previousResponseId, input };
|
|
4273
|
+
const payload = JSON.stringify(event);
|
|
4274
|
+
const { promise, resolve, reject } = Promise.withResolvers<CodexSteerAck>();
|
|
4275
|
+
const waiter: CodexSteerWaiter = { previousResponseId, resolve, reject };
|
|
4276
|
+
this.#steerWaiters.push(waiter);
|
|
4277
|
+
notifyCodexWebSocketOutbound(this.#streamObserver, event, payload);
|
|
4278
|
+
try {
|
|
4279
|
+
socket.send(payload);
|
|
4280
|
+
} catch (error) {
|
|
4281
|
+
this.#steerWaiters.splice(this.#steerWaiters.indexOf(waiter), 1);
|
|
4282
|
+
reject(
|
|
4283
|
+
new CodexWebSocketTransportError(
|
|
4284
|
+
`websocket send failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
4285
|
+
),
|
|
4286
|
+
);
|
|
4287
|
+
}
|
|
4288
|
+
return promise;
|
|
4289
|
+
}
|
|
4290
|
+
|
|
4291
|
+
/** True when the server failed to commit any of the accepted steering `ids`. */
|
|
4292
|
+
hasFailedSteer(ids: readonly string[]): boolean {
|
|
4293
|
+
return ids.some(id => this.#failedSteers.has(id));
|
|
4294
|
+
}
|
|
4295
|
+
|
|
4296
|
+
#handleSteerEvent(event: Record<string, unknown>): void {
|
|
4297
|
+
const steer = asRecord(event.steer);
|
|
4298
|
+
const id = typeof steer?.id === "string" ? steer.id : undefined;
|
|
4299
|
+
const previousResponseId = typeof steer?.previous_response_id === "string" ? steer.previous_response_id : "";
|
|
4300
|
+
if (event.type === "response.steer.accepted") {
|
|
4301
|
+
if (!id) return;
|
|
4302
|
+
this.#acceptedSteers.add(id);
|
|
4303
|
+
this.#takeSteerWaiter(previousResponseId)?.resolve({ accepted: true, id });
|
|
4304
|
+
return;
|
|
4305
|
+
}
|
|
4306
|
+
if (event.type === "response.steer.failed") {
|
|
4307
|
+
const error = asRecord(event.error);
|
|
4308
|
+
const code = typeof error?.code === "string" ? error.code : undefined;
|
|
4309
|
+
const message = typeof error?.message === "string" ? error.message : undefined;
|
|
4310
|
+
if (id && this.#acceptedSteers.has(id)) {
|
|
4311
|
+
// Accepted input that never reached a successor response.
|
|
4312
|
+
this.#failedSteers.add(id);
|
|
4313
|
+
if (this.#attachSteerIds?.has(id)) {
|
|
4314
|
+
this.#push(
|
|
4315
|
+
new CodexSteerCommitError(`steering ${id} was not committed: ${code ?? message ?? "unknown"}`),
|
|
4316
|
+
);
|
|
4317
|
+
}
|
|
4318
|
+
return;
|
|
4319
|
+
}
|
|
4320
|
+
this.#takeSteerWaiter(previousResponseId)?.resolve({ accepted: false, code, message });
|
|
4321
|
+
return;
|
|
4322
|
+
}
|
|
4323
|
+
// `response.steer.pending` restates that accepted input awaits tool output;
|
|
4324
|
+
// the request planner already derives that from the transcript.
|
|
4325
|
+
CODEX_DEBUG && logger.debug("[codex] codex steering event", { type: event.type, id, previousResponseId });
|
|
4326
|
+
}
|
|
4327
|
+
|
|
4328
|
+
#takeSteerWaiter(previousResponseId: string): CodexSteerWaiter | undefined {
|
|
4329
|
+
const index = this.#steerWaiters.findIndex(waiter => waiter.previousResponseId === previousResponseId);
|
|
4330
|
+
return index < 0 ? undefined : this.#steerWaiters.splice(index, 1)[0];
|
|
4331
|
+
}
|
|
4332
|
+
|
|
4333
|
+
#rejectSteerWaiters(reason: string): void {
|
|
4334
|
+
const waiters = this.#steerWaiters;
|
|
4335
|
+
if (waiters.length === 0) return;
|
|
4336
|
+
this.#steerWaiters = [];
|
|
4337
|
+
for (const waiter of waiters) waiter.reject(new CodexWebSocketTransportError(reason));
|
|
4338
|
+
}
|
|
4339
|
+
|
|
4051
4340
|
#captureHandshakeHeaders(socket: Bun.WebSocket, openEvent?: Event): void {
|
|
4052
4341
|
const headers = extractCodexWebSocketHandshakeHeaders(socket, openEvent);
|
|
4053
4342
|
if (!headers) return;
|
|
@@ -4806,6 +5095,16 @@ export class CodexWebSocketTransportError extends Error {
|
|
|
4806
5095
|
this.name = "CodexWebSocketTransportError";
|
|
4807
5096
|
}
|
|
4808
5097
|
}
|
|
5098
|
+
/**
|
|
5099
|
+
* The server failed to commit accepted steering to its automatic successor, so
|
|
5100
|
+
* the request that was reading that successor must be sent explicitly instead.
|
|
5101
|
+
*/
|
|
5102
|
+
class CodexSteerCommitError extends Error {
|
|
5103
|
+
constructor(message: string) {
|
|
5104
|
+
super(message);
|
|
5105
|
+
this.name = "CodexSteerCommitError";
|
|
5106
|
+
}
|
|
5107
|
+
}
|
|
4809
5108
|
class CodexWhitespaceToolCallLoopError extends Error {
|
|
4810
5109
|
constructor(message: string) {
|
|
4811
5110
|
super(message);
|