@pure01fx/dsh-openai-codex-auth 0.10.1 → 0.11.1
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 +12 -0
- package/README.md +3 -1
- package/client.js +115 -2
- package/lib/catalog.js +2 -7
- package/lib/cloud-context.d.ts +1 -1
- package/lib/cloud-context.js +3 -6
- package/lib/cloud-http.d.ts +0 -3
- package/lib/cloud-http.js +3 -12
- package/lib/cloud-images.d.ts +2 -3
- package/lib/cloud-images.js +9 -14
- package/lib/cloud-media.js +9 -12
- package/lib/cloud-search.d.ts +1 -1
- package/lib/cloud-search.js +11 -30
- package/lib/cloud-tools.js +2 -2
- package/lib/cloud-vision.js +2 -10
- package/lib/cloud-web-tool.js +11 -21
- package/lib/index.d.ts +1 -0
- package/lib/index.js +11 -0
- package/lib/native-http.d.ts +4 -5
- package/lib/native-http.js +43 -25
- package/lib/native-websocket-session.js +1 -3
- package/lib/native-websocket-socket.d.ts +2 -1
- package/lib/native-websocket-socket.js +22 -31
- package/lib/native-websocket.d.ts +1 -2
- package/lib/native-websocket.js +53 -46
- package/lib/network-telemetry.d.ts +61 -0
- package/lib/network-telemetry.js +104 -0
- package/lib/replay.d.ts +2 -3
- package/lib/replay.js +13 -43
- package/lib/responses.d.ts +0 -2
- package/lib/responses.js +1 -14
- package/lib/sse.d.ts +1 -3
- package/lib/sse.js +2 -17
- package/package.json +3 -1
package/lib/native-websocket.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** WebSocket v2 Responses transport with bounded session state and sticky HTTP fallback. */
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
|
+
import { networkTelemetry, trackNetwork, observeNetworkOutput } from './network-telemetry.js';
|
|
3
4
|
import { LlmError, ProviderRequestId, attributionHeaders, } from '@deepseek-ai/dsh-llm';
|
|
4
5
|
import { nativeCodexAuthorityHash } from './catalog.js';
|
|
5
6
|
import { NATIVE_CODEX_CONNECTION_FAILED_CODE, NATIVE_CODEX_STREAM_INTERRUPTED_CODE, isNativeCodexConnectionFailure, } from './native-adapter.js';
|
|
@@ -12,7 +13,6 @@ import { NodeNativeCodexWebSocketFactory, } from './native-websocket-socket.js';
|
|
|
12
13
|
import { NativeCodexWebSocketSessionState } from './native-websocket-session.js';
|
|
13
14
|
const WS_BETA = 'responses_websockets=2026-02-06';
|
|
14
15
|
const DEFAULT_IDLE_TIMEOUT_MS = 300_000;
|
|
15
|
-
const DEFAULT_MAX_FRAME_BYTES = 64 * 1024 * 1024;
|
|
16
16
|
const DEFAULT_MAX_SESSIONS = 32;
|
|
17
17
|
const DEFAULT_SESSION_IDLE_MS = 30 * 60_000;
|
|
18
18
|
const DEFAULT_MAX_RECONNECTS = 5;
|
|
@@ -21,14 +21,13 @@ const DEFAULT_MAX_RETRY_DELAY_MS = 10_000;
|
|
|
21
21
|
const INITIAL_CONNECTION_RETRY_DELAY_MS = 5_000;
|
|
22
22
|
const MAX_CONNECTION_RETRY_DELAY_MS = 60_000;
|
|
23
23
|
const MAX_TURN_STATE_BYTES = 4096;
|
|
24
|
-
const MAX_RETAINED_OUTPUT_BYTES = 64 * 1024 * 1024;
|
|
25
24
|
function failure(message, code, cause) {
|
|
26
25
|
return new LlmError(message, code, cause === undefined ? undefined : { cause });
|
|
27
26
|
}
|
|
28
27
|
function reconnectable(code) {
|
|
29
28
|
return [
|
|
30
29
|
'WS_RETRYABLE', 'WS_RETRYABLE_RESET', 'WS_PROTOCOL_ERROR',
|
|
31
|
-
'
|
|
30
|
+
'TIMEOUT',
|
|
32
31
|
NATIVE_CODEX_CONNECTION_FAILED_CODE,
|
|
33
32
|
].includes(code);
|
|
34
33
|
}
|
|
@@ -211,7 +210,6 @@ export class NativeCodexWebSocketTransport {
|
|
|
211
210
|
sessions = new Map();
|
|
212
211
|
connectTimeoutMs;
|
|
213
212
|
idleTimeoutMs;
|
|
214
|
-
maxFrameBytes;
|
|
215
213
|
maxSessions;
|
|
216
214
|
sessionIdleMs;
|
|
217
215
|
maxReconnects;
|
|
@@ -226,7 +224,6 @@ export class NativeCodexWebSocketTransport {
|
|
|
226
224
|
this.factory = options.webSocketFactory ?? new NodeNativeCodexWebSocketFactory();
|
|
227
225
|
this.connectTimeoutMs = boundedPositive(options.webSocketConnectTimeoutMs, 10_000, 120_000, 'WebSocket connect timeout');
|
|
228
226
|
this.idleTimeoutMs = boundedPositive(options.webSocketIdleTimeoutMs, DEFAULT_IDLE_TIMEOUT_MS, 60 * 60_000, 'WebSocket idle timeout');
|
|
229
|
-
this.maxFrameBytes = boundedPositive(options.maxWebSocketFrameBytes, DEFAULT_MAX_FRAME_BYTES, DEFAULT_MAX_FRAME_BYTES, 'WebSocket frame limit');
|
|
230
227
|
this.maxSessions = boundedPositive(options.maxWebSocketSessions, DEFAULT_MAX_SESSIONS, 256, 'WebSocket session limit');
|
|
231
228
|
this.sessionIdleMs = boundedPositive(options.webSocketSessionIdleMs, DEFAULT_SESSION_IDLE_MS, 24 * 60 * 60_000, 'WebSocket session idle limit');
|
|
232
229
|
this.maxReconnects = retryCount(options.maxWebSocketReconnects);
|
|
@@ -242,11 +239,13 @@ export class NativeCodexWebSocketTransport {
|
|
|
242
239
|
const jitter = 0.9 + Math.max(0, Math.min(1, random)) * 0.2;
|
|
243
240
|
return Math.max(1, Math.round(exponential * jitter));
|
|
244
241
|
}
|
|
245
|
-
async wait(retry, error, signal) {
|
|
242
|
+
async wait(retry, error, signal, telemetry) {
|
|
246
243
|
const delay = this.retryDelay(retry, error);
|
|
244
|
+
telemetry.retry(delay);
|
|
247
245
|
await (this.options.sleep ?? sleep)(delay, signal);
|
|
248
246
|
}
|
|
249
|
-
async waitForConnection(delayMs, signal) {
|
|
247
|
+
async waitForConnection(delayMs, signal, telemetry) {
|
|
248
|
+
telemetry.retry(delayMs, 'connection');
|
|
250
249
|
this.options.warn?.(`native Codex network is unavailable; reconnecting in ${delayMs}ms`);
|
|
251
250
|
await (this.options.sleep ?? sleep)(delayMs, signal);
|
|
252
251
|
}
|
|
@@ -328,7 +327,7 @@ export class NativeCodexWebSocketTransport {
|
|
|
328
327
|
...attributionHeaders(),
|
|
329
328
|
};
|
|
330
329
|
}
|
|
331
|
-
async ensureSocket(entry, prepared, credential, signal) {
|
|
330
|
+
async ensureSocket(entry, prepared, credential, signal, telemetry) {
|
|
332
331
|
const fingerprint = socketCredential(credential);
|
|
333
332
|
if (entry.socket !== undefined && entry.socketCredential === fingerprint)
|
|
334
333
|
return;
|
|
@@ -338,12 +337,12 @@ export class NativeCodexWebSocketTransport {
|
|
|
338
337
|
headers: this.headers(prepared, credential),
|
|
339
338
|
signal,
|
|
340
339
|
connectTimeoutMs: this.connectTimeoutMs,
|
|
341
|
-
maxFrameBytes: this.maxFrameBytes,
|
|
342
340
|
});
|
|
343
341
|
if (this.disposed) {
|
|
344
342
|
socket.close();
|
|
345
343
|
throw failure('native Codex WebSocket transport was disposed', 'DISPOSED');
|
|
346
344
|
}
|
|
345
|
+
telemetry.activity();
|
|
347
346
|
entry.socket = socket;
|
|
348
347
|
entry.socketCredential = fingerprint;
|
|
349
348
|
publishCodexRateLimits(credential.accountId, parseCodexRateLimitHeaders(socket.responseHeaders), this.options.onRateLimits, this.options.warn);
|
|
@@ -377,22 +376,21 @@ export class NativeCodexWebSocketTransport {
|
|
|
377
376
|
clearTimeout(timer);
|
|
378
377
|
}
|
|
379
378
|
}
|
|
380
|
-
async *exchange(entry, payload, generation, mode, accountId, prewarm, signal) {
|
|
379
|
+
async *exchange(entry, payload, generation, mode, accountId, prewarm, signal, telemetry) {
|
|
381
380
|
if (entry.socket === undefined)
|
|
382
381
|
throw failure('native Codex WebSocket is unavailable', 'WS_RETRYABLE');
|
|
383
382
|
const encoded = JSON.stringify(payload);
|
|
384
|
-
if (Buffer.byteLength(encoded) > 24 * 1024 * 1024) {
|
|
385
|
-
throw failure('native Codex WebSocket request exceeded the size limit', 'REQUEST_TOO_LARGE');
|
|
386
|
-
}
|
|
387
383
|
await entry.socket.send(encoded, signal);
|
|
384
|
+
telemetry.state('waiting');
|
|
388
385
|
const translator = new ResponsesStreamTranslator(prewarm ? undefined : {
|
|
389
386
|
provider: generation.provider,
|
|
390
387
|
model: mode.publicModel ?? generation.model,
|
|
391
388
|
});
|
|
392
389
|
const outputItems = [];
|
|
393
|
-
let outputBytes = 0;
|
|
394
390
|
while (true) {
|
|
395
391
|
const text = await this.receive(entry, signal);
|
|
392
|
+
if (entry.socket?.observeActivity === undefined)
|
|
393
|
+
telemetry.activity(Buffer.byteLength(text));
|
|
396
394
|
let event;
|
|
397
395
|
try {
|
|
398
396
|
event = JSON.parse(text);
|
|
@@ -420,14 +418,8 @@ export class NativeCodexWebSocketTransport {
|
|
|
420
418
|
entry.turnState = nextTurnState;
|
|
421
419
|
}
|
|
422
420
|
const output = normalizedOutputItem(event);
|
|
423
|
-
if (output !== undefined)
|
|
424
|
-
const nextOutputBytes = outputBytes + Buffer.byteLength(JSON.stringify(output));
|
|
425
|
-
if (nextOutputBytes > MAX_RETAINED_OUTPUT_BYTES) {
|
|
426
|
-
throw failure('native Codex WebSocket retained output exceeded the size limit', 'WS_RESPONSE_TOO_LARGE');
|
|
427
|
-
}
|
|
421
|
+
if (output !== undefined)
|
|
428
422
|
outputItems.push(output);
|
|
429
|
-
outputBytes = nextOutputBytes;
|
|
430
|
-
}
|
|
431
423
|
if (event.type === 'response.completed') {
|
|
432
424
|
const response = typeof event.response === 'object'
|
|
433
425
|
&& event.response !== null
|
|
@@ -447,28 +439,38 @@ export class NativeCodexWebSocketTransport {
|
|
|
447
439
|
}
|
|
448
440
|
}
|
|
449
441
|
}
|
|
450
|
-
async *attempt(entry, prepared, credential, signal) {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
if (this.options.onCompleted !== undefined) {
|
|
463
|
-
try {
|
|
464
|
-
this.options.onCompleted();
|
|
442
|
+
async *attempt(entry, prepared, credential, signal, telemetry) {
|
|
443
|
+
telemetry.attempt('websocket');
|
|
444
|
+
await this.ensureSocket(entry, prepared, credential, signal, telemetry);
|
|
445
|
+
const stopActivity = entry.socket?.observeActivity?.(bytes => telemetry.activity(bytes));
|
|
446
|
+
try {
|
|
447
|
+
let justPrewarmed = false;
|
|
448
|
+
if (!entry.prewarmAttempted) {
|
|
449
|
+
entry.prewarmAttempted = true;
|
|
450
|
+
const warm = entry.protocol.prewarm(withRequestMetadata(prepared.request, entry.turnState, prepared.mode.responsesLite !== undefined));
|
|
451
|
+
for await (const _chunk of this.exchange(entry, warm.payload, prepared.generation, prepared.mode, credential.accountId, true, signal, telemetry)) { /* prewarm is invisible */ }
|
|
452
|
+
entry.prewarmSucceeded = true;
|
|
453
|
+
justPrewarmed = true;
|
|
465
454
|
}
|
|
466
|
-
|
|
467
|
-
|
|
455
|
+
const plan = entry.protocol.plan(withRequestMetadata(prepared.request, entry.turnState, prepared.mode.responsesLite !== undefined), justPrewarmed);
|
|
456
|
+
yield* this.exchange(entry, plan.payload, prepared.generation, prepared.mode, credential.accountId, false, signal, telemetry);
|
|
457
|
+
if (this.options.onCompleted !== undefined) {
|
|
458
|
+
try {
|
|
459
|
+
this.options.onCompleted();
|
|
460
|
+
}
|
|
461
|
+
catch {
|
|
462
|
+
this.options.warn?.('native Codex usage refresh could not be scheduled');
|
|
463
|
+
}
|
|
468
464
|
}
|
|
469
465
|
}
|
|
466
|
+
finally {
|
|
467
|
+
stopActivity?.();
|
|
468
|
+
}
|
|
470
469
|
}
|
|
471
470
|
async *stream(generation, mode = {}) {
|
|
471
|
+
yield* trackNetwork(this.options.telemetry ?? networkTelemetry, 'websocket', generation.signal, handle => this.streamTracked(generation, mode, handle));
|
|
472
|
+
}
|
|
473
|
+
async *streamTracked(generation, mode, telemetry) {
|
|
472
474
|
if (this.disposed)
|
|
473
475
|
throw failure('native Codex WebSocket transport was disposed', 'DISPOSED');
|
|
474
476
|
const lifecycle = new AbortController();
|
|
@@ -515,7 +517,7 @@ export class NativeCodexWebSocketTransport {
|
|
|
515
517
|
let requestCompleted = false;
|
|
516
518
|
try {
|
|
517
519
|
if (entry.disabled) {
|
|
518
|
-
yield* this.http.stream(activeGeneration, fallbackMode());
|
|
520
|
+
yield* this.http.stream(activeGeneration, fallbackMode(), telemetry);
|
|
519
521
|
requestCompleted = true;
|
|
520
522
|
return;
|
|
521
523
|
}
|
|
@@ -524,6 +526,7 @@ export class NativeCodexWebSocketTransport {
|
|
|
524
526
|
let attemptedCredential;
|
|
525
527
|
const enteringPrewarm = !entry.prewarmAttempted;
|
|
526
528
|
try {
|
|
529
|
+
telemetry.state('preparing');
|
|
527
530
|
const credential = await this.options.resolveCredential(signal);
|
|
528
531
|
attemptedCredential = credential;
|
|
529
532
|
if (pinnedAccountId === undefined)
|
|
@@ -532,8 +535,9 @@ export class NativeCodexWebSocketTransport {
|
|
|
532
535
|
throw failure('native Codex account changed during request', 'AUTH');
|
|
533
536
|
}
|
|
534
537
|
this.assertFastAuthority(credential, mode);
|
|
535
|
-
for await (const chunk of this.attempt(entry, prepared, credential, signal)) {
|
|
538
|
+
for await (const chunk of this.attempt(entry, prepared, credential, signal, telemetry)) {
|
|
536
539
|
emitted = true;
|
|
540
|
+
observeNetworkOutput(telemetry, chunk);
|
|
537
541
|
yield chunk;
|
|
538
542
|
}
|
|
539
543
|
requestCompleted = true;
|
|
@@ -558,7 +562,7 @@ export class NativeCodexWebSocketTransport {
|
|
|
558
562
|
// the DSH turn in an invisible infinite reconnect loop.
|
|
559
563
|
if (attemptedCredential === undefined
|
|
560
564
|
&& isNativeCodexConnectionFailure(failureValue)) {
|
|
561
|
-
await this.waitForConnection(connectionRetryDelayMs, signal);
|
|
565
|
+
await this.waitForConnection(connectionRetryDelayMs, signal, telemetry);
|
|
562
566
|
connectionRetryDelayMs = Math.min(connectionRetryDelayMs * 2, MAX_CONNECTION_RETRY_DELAY_MS);
|
|
563
567
|
continue;
|
|
564
568
|
}
|
|
@@ -570,8 +574,10 @@ export class NativeCodexWebSocketTransport {
|
|
|
570
574
|
try {
|
|
571
575
|
const changed = await this.options.recoverCredential(attemptedCredential, signal);
|
|
572
576
|
recovered = true;
|
|
573
|
-
if (changed)
|
|
577
|
+
if (changed) {
|
|
578
|
+
telemetry.retry(0);
|
|
574
579
|
continue;
|
|
580
|
+
}
|
|
575
581
|
throw failure('native Codex rejected the configured credential', 'AUTH');
|
|
576
582
|
}
|
|
577
583
|
catch (recoveryError) {
|
|
@@ -581,7 +587,7 @@ export class NativeCodexWebSocketTransport {
|
|
|
581
587
|
if (generation.signal?.aborted)
|
|
582
588
|
throw recoveryError;
|
|
583
589
|
if (isNativeCodexConnectionFailure(recoveryError)) {
|
|
584
|
-
await this.waitForConnection(connectionRetryDelayMs, signal);
|
|
590
|
+
await this.waitForConnection(connectionRetryDelayMs, signal, telemetry);
|
|
585
591
|
connectionRetryDelayMs = Math.min(connectionRetryDelayMs * 2, MAX_CONNECTION_RETRY_DELAY_MS);
|
|
586
592
|
continue;
|
|
587
593
|
}
|
|
@@ -592,18 +598,19 @@ export class NativeCodexWebSocketTransport {
|
|
|
592
598
|
if (failureValue.code !== 'WS_UPGRADE_REQUIRED' && retryable
|
|
593
599
|
&& enteringPrewarm && !entry.prewarmSucceeded) {
|
|
594
600
|
entry.prewarmAttempted = true;
|
|
595
|
-
await this.wait(0, failureValue, signal);
|
|
601
|
+
await this.wait(0, failureValue, signal, telemetry);
|
|
596
602
|
continue;
|
|
597
603
|
}
|
|
598
604
|
if (failureValue.code !== 'WS_UPGRADE_REQUIRED' && retryable
|
|
599
605
|
&& reconnects < this.maxReconnects) {
|
|
600
|
-
await this.wait(reconnects, failureValue, signal);
|
|
606
|
+
await this.wait(reconnects, failureValue, signal, telemetry);
|
|
601
607
|
reconnects++;
|
|
602
608
|
continue;
|
|
603
609
|
}
|
|
604
610
|
if (failureValue.code === 'WS_UPGRADE_REQUIRED' || retryable) {
|
|
605
611
|
entry.disabled = true;
|
|
606
|
-
|
|
612
|
+
telemetry.retry(0);
|
|
613
|
+
yield* this.http.stream(activeGeneration, fallbackMode(), telemetry);
|
|
607
614
|
requestCompleted = true;
|
|
608
615
|
return;
|
|
609
616
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** Process-wide, content-free inference telemetry. All mutations are synchronous. */
|
|
2
|
+
import type { StreamChunk } from '@deepseek-ai/dsh-llm';
|
|
3
|
+
export type NetworkState = 'preparing' | 'connecting' | 'waiting' | 'streaming' | 'retrying' | 'completed' | 'failed' | 'cancelled';
|
|
4
|
+
export type NetworkTransport = 'http' | 'websocket';
|
|
5
|
+
export interface NetworkRequest {
|
|
6
|
+
id: string;
|
|
7
|
+
state: NetworkState;
|
|
8
|
+
transport: NetworkTransport;
|
|
9
|
+
startedAt: number;
|
|
10
|
+
lastActivityAt: number | null;
|
|
11
|
+
lastOutputAt: number | null;
|
|
12
|
+
retryAt: number | null;
|
|
13
|
+
retryKind: 'transient' | 'connection' | null;
|
|
14
|
+
retries: number;
|
|
15
|
+
receivedBytes: number;
|
|
16
|
+
attempts: number;
|
|
17
|
+
}
|
|
18
|
+
export interface NetworkSnapshot {
|
|
19
|
+
scope: 'process';
|
|
20
|
+
startedAt: number;
|
|
21
|
+
updatedAt: number;
|
|
22
|
+
activeCount: number;
|
|
23
|
+
totals: {
|
|
24
|
+
requests: number;
|
|
25
|
+
succeeded: number;
|
|
26
|
+
failed: number;
|
|
27
|
+
cancelled: number;
|
|
28
|
+
retries: number;
|
|
29
|
+
receivedBytes: number;
|
|
30
|
+
};
|
|
31
|
+
requests: NetworkRequest[];
|
|
32
|
+
truncated: boolean;
|
|
33
|
+
}
|
|
34
|
+
export interface NetworkRequestHandle {
|
|
35
|
+
state(state: 'preparing' | 'waiting'): void;
|
|
36
|
+
attempt(transport: NetworkTransport): void;
|
|
37
|
+
activity(bytes?: number): void;
|
|
38
|
+
output(): void;
|
|
39
|
+
retry(delayMs: number, kind?: 'transient' | 'connection'): void;
|
|
40
|
+
finish(state: 'completed' | 'failed' | 'cancelled'): void;
|
|
41
|
+
}
|
|
42
|
+
/** Keeps at most limit records, preferentially evicting terminal records. Handles
|
|
43
|
+
* remain owned by running generators; overflow never loses aggregate accounting.
|
|
44
|
+
* No timers, request content, external identifiers, or errors are retained. */
|
|
45
|
+
export declare class NetworkTelemetry {
|
|
46
|
+
private readonly limit;
|
|
47
|
+
private readonly now;
|
|
48
|
+
private readonly rows;
|
|
49
|
+
private readonly startedAt;
|
|
50
|
+
private activeCount;
|
|
51
|
+
private sequence;
|
|
52
|
+
private evicted;
|
|
53
|
+
private readonly totals;
|
|
54
|
+
constructor(limit?: number, now?: () => number);
|
|
55
|
+
begin(transport: NetworkTransport): NetworkRequestHandle;
|
|
56
|
+
snapshot(): NetworkSnapshot;
|
|
57
|
+
}
|
|
58
|
+
export declare const networkTelemetry: NetworkTelemetry;
|
|
59
|
+
export declare function observeNetworkOutput(handle: NetworkRequestHandle, chunk: StreamChunk): void;
|
|
60
|
+
/** Generator return/break is cancellation; fallback delegates with the same handle. */
|
|
61
|
+
export declare function trackNetwork<T>(telemetry: NetworkTelemetry, transport: NetworkTransport, signal: AbortSignal | undefined, run: (handle: NetworkRequestHandle) => AsyncIterable<T>): AsyncIterable<T>;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/** Keeps at most limit records, preferentially evicting terminal records. Handles
|
|
2
|
+
* remain owned by running generators; overflow never loses aggregate accounting.
|
|
3
|
+
* No timers, request content, external identifiers, or errors are retained. */
|
|
4
|
+
export class NetworkTelemetry {
|
|
5
|
+
limit;
|
|
6
|
+
now;
|
|
7
|
+
rows = new Map();
|
|
8
|
+
startedAt;
|
|
9
|
+
activeCount = 0;
|
|
10
|
+
sequence = 0;
|
|
11
|
+
evicted = false;
|
|
12
|
+
totals = { requests: 0, succeeded: 0, failed: 0, cancelled: 0, retries: 0, receivedBytes: 0 };
|
|
13
|
+
constructor(limit = 128, now = Date.now) {
|
|
14
|
+
this.limit = limit;
|
|
15
|
+
this.now = now;
|
|
16
|
+
if (!Number.isSafeInteger(limit) || limit < 1)
|
|
17
|
+
throw new RangeError('Invalid telemetry limit');
|
|
18
|
+
this.startedAt = now();
|
|
19
|
+
}
|
|
20
|
+
begin(transport) {
|
|
21
|
+
const row = {
|
|
22
|
+
id: String(++this.sequence), state: 'preparing', transport, startedAt: this.now(),
|
|
23
|
+
lastActivityAt: null, lastOutputAt: null, retryAt: null, retryKind: null, retries: 0, receivedBytes: 0, attempts: 0,
|
|
24
|
+
};
|
|
25
|
+
if (this.rows.size >= this.limit) {
|
|
26
|
+
const terminal = [...this.rows.values()].find(item => ['completed', 'failed', 'cancelled'].includes(item.state));
|
|
27
|
+
this.rows.delete(terminal?.id ?? this.rows.keys().next().value);
|
|
28
|
+
this.evicted = true;
|
|
29
|
+
}
|
|
30
|
+
this.rows.set(row.id, row);
|
|
31
|
+
this.activeCount++;
|
|
32
|
+
this.totals.requests++;
|
|
33
|
+
let finished = false;
|
|
34
|
+
const mutate = (fn) => {
|
|
35
|
+
if (finished)
|
|
36
|
+
return;
|
|
37
|
+
fn();
|
|
38
|
+
};
|
|
39
|
+
return {
|
|
40
|
+
state: state => mutate(() => { row.state = state; row.retryAt = null; if (state === 'waiting')
|
|
41
|
+
row.retryKind = null; }),
|
|
42
|
+
attempt: transport => mutate(() => {
|
|
43
|
+
row.transport = transport;
|
|
44
|
+
row.state = 'connecting';
|
|
45
|
+
row.retryAt = null;
|
|
46
|
+
row.attempts++;
|
|
47
|
+
}),
|
|
48
|
+
activity: (bytes = 0) => mutate(() => {
|
|
49
|
+
row.lastActivityAt = this.now();
|
|
50
|
+
if (Number.isSafeInteger(bytes) && bytes > 0) {
|
|
51
|
+
row.receivedBytes += bytes;
|
|
52
|
+
this.totals.receivedBytes += bytes;
|
|
53
|
+
}
|
|
54
|
+
}),
|
|
55
|
+
output: () => mutate(() => { row.state = 'streaming'; row.retryKind = null; row.lastOutputAt = this.now(); }),
|
|
56
|
+
retry: (delayMs, kind = 'transient') => mutate(() => {
|
|
57
|
+
row.retryKind = kind;
|
|
58
|
+
row.state = 'retrying';
|
|
59
|
+
row.retryAt = this.now() + Math.max(0, delayMs);
|
|
60
|
+
row.retries++;
|
|
61
|
+
this.totals.retries++;
|
|
62
|
+
}),
|
|
63
|
+
finish: state => mutate(() => {
|
|
64
|
+
row.state = state;
|
|
65
|
+
row.retryAt = null;
|
|
66
|
+
row.retryKind = null;
|
|
67
|
+
this.activeCount--;
|
|
68
|
+
this.totals[state === 'completed' ? 'succeeded' : state]++;
|
|
69
|
+
finished = true;
|
|
70
|
+
}),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
snapshot() {
|
|
74
|
+
return {
|
|
75
|
+
scope: 'process', startedAt: this.startedAt, updatedAt: this.now(),
|
|
76
|
+
activeCount: this.activeCount, totals: { ...this.totals },
|
|
77
|
+
requests: [...this.rows.values()].reverse().map(row => ({ ...row })), truncated: this.evicted,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
export const networkTelemetry = new NetworkTelemetry();
|
|
82
|
+
export function observeNetworkOutput(handle, chunk) {
|
|
83
|
+
if ((chunk.type === 'text-delta' || chunk.type === 'reasoning-delta') && chunk.text.length > 0
|
|
84
|
+
|| chunk.type === 'tool-call-delta')
|
|
85
|
+
handle.output();
|
|
86
|
+
if (chunk.type === 'finish' && chunk.reason.kind === 'error')
|
|
87
|
+
handle.finish('failed');
|
|
88
|
+
}
|
|
89
|
+
/** Generator return/break is cancellation; fallback delegates with the same handle. */
|
|
90
|
+
export async function* trackNetwork(telemetry, transport, signal, run) {
|
|
91
|
+
const handle = telemetry.begin(transport);
|
|
92
|
+
try {
|
|
93
|
+
yield* run(handle);
|
|
94
|
+
handle.finish('completed');
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
const code = typeof error === 'object' && error !== null && 'code' in error ? error.code : undefined;
|
|
98
|
+
handle.finish(signal?.aborted || code === 'ABORTED' || code === 'DISPOSED' ? 'cancelled' : 'failed');
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
handle.finish('cancelled');
|
|
103
|
+
}
|
|
104
|
+
}
|
package/lib/replay.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** Versioned Codex Responses continuation state. */
|
|
2
2
|
import { type ContentBlock } from '@deepseek-ai/dsh-llm';
|
|
3
3
|
export declare const NATIVE_CODEX_REPLAY_KIND = "openai-codex-native.responses-replay";
|
|
4
4
|
export declare const NATIVE_CODEX_REPLAY_VERSION = 1;
|
|
@@ -33,12 +33,11 @@ export interface NativeCodexReplaySource {
|
|
|
33
33
|
export declare function replayableItemId(value: string | undefined): string | undefined;
|
|
34
34
|
/** True only for legacy raw state or an rc.2 envelope emitted by this package. */
|
|
35
35
|
export declare function hasNativeCodexReplayKind(value: unknown): boolean;
|
|
36
|
-
/** Attempt-local
|
|
36
|
+
/** Attempt-local accumulator for completed replay descriptors. */
|
|
37
37
|
export declare class NativeCodexReplayCapture {
|
|
38
38
|
private readonly provider;
|
|
39
39
|
private readonly model;
|
|
40
40
|
private readonly descriptors;
|
|
41
|
-
private stateBytes;
|
|
42
41
|
constructor(provider: string, model: string);
|
|
43
42
|
add(item: NativeCodexReplayDescriptor): void;
|
|
44
43
|
finish(): NativeCodexReplayState | undefined;
|
package/lib/replay.js
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** Versioned Codex Responses continuation state. */
|
|
2
2
|
import { LlmError } from '@deepseek-ai/dsh-llm';
|
|
3
3
|
export const NATIVE_CODEX_REPLAY_KIND = 'openai-codex-native.responses-replay';
|
|
4
4
|
export const NATIVE_CODEX_REPLAY_VERSION = 1;
|
|
5
|
-
const MAX_REPLAY_ITEM_ID_BYTES = 256;
|
|
6
|
-
const MAX_REPLAY_CIPHERTEXT_BYTES = 64 * 1024 * 1024;
|
|
7
|
-
const MAX_REPLAY_STATE_BYTES = 64 * 1024 * 1024;
|
|
8
5
|
function failure(message, code = 'INVALID_REPLAY_STATE') {
|
|
9
6
|
return new LlmError(message, code);
|
|
10
7
|
}
|
|
@@ -17,30 +14,22 @@ function onlyKeys(row, keys) {
|
|
|
17
14
|
const allowed = new Set(keys);
|
|
18
15
|
return Object.keys(row).every(key => allowed.has(key));
|
|
19
16
|
}
|
|
20
|
-
function
|
|
21
|
-
return typeof value === 'string' && value.length > 0
|
|
22
|
-
&& Buffer.byteLength(value) <= maximum ? value : undefined;
|
|
17
|
+
function nonemptyString(value) {
|
|
18
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
23
19
|
}
|
|
24
20
|
/** Preserve only server item IDs that Codex itself would replay. */
|
|
25
21
|
export function replayableItemId(value) {
|
|
26
22
|
if (value === undefined)
|
|
27
23
|
return undefined;
|
|
28
|
-
if (Buffer.byteLength(value) > MAX_REPLAY_ITEM_ID_BYTES) {
|
|
29
|
-
throw failure('native Codex response item identity exceeded the replay limit', 'MALFORMED_RESPONSE');
|
|
30
|
-
}
|
|
31
24
|
const split = value.indexOf('_');
|
|
32
25
|
return split > 0 && split < value.length - 1 ? value : undefined;
|
|
33
26
|
}
|
|
34
|
-
function
|
|
35
|
-
let serialized;
|
|
27
|
+
function validateStateJson(value) {
|
|
36
28
|
try {
|
|
37
|
-
|
|
29
|
+
JSON.stringify(value);
|
|
38
30
|
}
|
|
39
31
|
catch {
|
|
40
|
-
throw failure('native Codex replay state is not lossless JSON'
|
|
41
|
-
}
|
|
42
|
-
if (Buffer.byteLength(serialized) > MAX_REPLAY_STATE_BYTES) {
|
|
43
|
-
throw failure('native Codex replay state exceeded the size limit', code);
|
|
32
|
+
throw failure('native Codex replay state is not lossless JSON');
|
|
44
33
|
}
|
|
45
34
|
}
|
|
46
35
|
function validateBlockArray(value) {
|
|
@@ -59,7 +48,7 @@ function parseDescriptor(value) {
|
|
|
59
48
|
if (row === undefined || typeof row.type !== 'string') {
|
|
60
49
|
throw failure('native Codex replay descriptor is invalid');
|
|
61
50
|
}
|
|
62
|
-
const id = row.id === undefined ? undefined :
|
|
51
|
+
const id = row.id === undefined ? undefined : nonemptyString(row.id);
|
|
63
52
|
const split = id?.indexOf('_') ?? -1;
|
|
64
53
|
if (row.id !== undefined && (id === undefined || split <= 0 || split >= id.length - 1)) {
|
|
65
54
|
throw failure('native Codex replay item identity is invalid');
|
|
@@ -74,7 +63,7 @@ function parseDescriptor(value) {
|
|
|
74
63
|
if (row.type === 'reasoning') {
|
|
75
64
|
const blocks = validateBlockArray(row.blocks);
|
|
76
65
|
const encryptedContent = row.encryptedContent === undefined
|
|
77
|
-
? undefined :
|
|
66
|
+
? undefined : nonemptyString(row.encryptedContent);
|
|
78
67
|
if (blocks === undefined
|
|
79
68
|
|| (row.encryptedContent !== undefined && encryptedContent === undefined)
|
|
80
69
|
|| !onlyKeys(row, ['type', 'id', 'blocks', 'encryptedContent'])) {
|
|
@@ -86,7 +75,7 @@ function parseDescriptor(value) {
|
|
|
86
75
|
};
|
|
87
76
|
}
|
|
88
77
|
if (row.type === 'function_call') {
|
|
89
|
-
const namespace = row.namespace === undefined ? undefined :
|
|
78
|
+
const namespace = row.namespace === undefined ? undefined : nonemptyString(row.namespace);
|
|
90
79
|
if (!Number.isSafeInteger(row.block) || Number(row.block) < 0
|
|
91
80
|
|| (row.namespace !== undefined && namespace === undefined)
|
|
92
81
|
|| !onlyKeys(row, ['type', 'id', 'namespace', 'block'])) {
|
|
@@ -109,15 +98,15 @@ export function hasNativeCodexReplayKind(value) {
|
|
|
109
98
|
}
|
|
110
99
|
function parseState(value) {
|
|
111
100
|
const payload = replayPayload(value);
|
|
112
|
-
|
|
101
|
+
validateStateJson(payload);
|
|
113
102
|
const row = object(payload);
|
|
114
103
|
if (row === undefined || row.kind !== NATIVE_CODEX_REPLAY_KIND
|
|
115
104
|
|| row.version !== NATIVE_CODEX_REPLAY_VERSION
|
|
116
105
|
|| !onlyKeys(row, ['kind', 'version', 'provider', 'model', 'items'])) {
|
|
117
106
|
throw failure('native Codex replay state kind or version is invalid');
|
|
118
107
|
}
|
|
119
|
-
const provider =
|
|
120
|
-
const model =
|
|
108
|
+
const provider = nonemptyString(row.provider);
|
|
109
|
+
const model = nonemptyString(row.model);
|
|
121
110
|
if (provider === undefined || model === undefined || !Array.isArray(row.items)
|
|
122
111
|
|| row.items.length === 0) {
|
|
123
112
|
throw failure('native Codex replay state metadata is invalid');
|
|
@@ -131,35 +120,17 @@ function parseState(value) {
|
|
|
131
120
|
items,
|
|
132
121
|
};
|
|
133
122
|
}
|
|
134
|
-
/** Attempt-local
|
|
123
|
+
/** Attempt-local accumulator for completed replay descriptors. */
|
|
135
124
|
export class NativeCodexReplayCapture {
|
|
136
125
|
provider;
|
|
137
126
|
model;
|
|
138
127
|
descriptors = [];
|
|
139
|
-
stateBytes;
|
|
140
128
|
constructor(provider, model) {
|
|
141
129
|
this.provider = provider;
|
|
142
130
|
this.model = model;
|
|
143
|
-
this.stateBytes = Buffer.byteLength(JSON.stringify({
|
|
144
|
-
kind: NATIVE_CODEX_REPLAY_KIND,
|
|
145
|
-
version: NATIVE_CODEX_REPLAY_VERSION,
|
|
146
|
-
provider,
|
|
147
|
-
model,
|
|
148
|
-
items: [],
|
|
149
|
-
}));
|
|
150
131
|
}
|
|
151
132
|
add(item) {
|
|
152
|
-
if (item.type === 'reasoning' && item.encryptedContent !== undefined
|
|
153
|
-
&& Buffer.byteLength(item.encryptedContent) > MAX_REPLAY_CIPHERTEXT_BYTES) {
|
|
154
|
-
throw failure('native Codex encrypted reasoning exceeded the replay limit', 'MALFORMED_RESPONSE');
|
|
155
|
-
}
|
|
156
|
-
const itemBytes = Buffer.byteLength(JSON.stringify(item));
|
|
157
|
-
const nextBytes = this.stateBytes + itemBytes + (this.descriptors.length === 0 ? 0 : 1);
|
|
158
|
-
if (nextBytes > MAX_REPLAY_STATE_BYTES) {
|
|
159
|
-
throw failure('native Codex replay state exceeded the size limit', 'REPLAY_STATE_TOO_LARGE');
|
|
160
|
-
}
|
|
161
133
|
this.descriptors.push(item);
|
|
162
|
-
this.stateBytes = nextBytes;
|
|
163
134
|
}
|
|
164
135
|
finish() {
|
|
165
136
|
return createNativeCodexReplayState(this.provider, this.model, this.descriptors);
|
|
@@ -176,7 +147,6 @@ export function createNativeCodexReplayState(provider, model, items) {
|
|
|
176
147
|
model,
|
|
177
148
|
items: items.map(item => ({ ...item })),
|
|
178
149
|
};
|
|
179
|
-
safeStateSize(state, 'REPLAY_STATE_TOO_LARGE');
|
|
180
150
|
try {
|
|
181
151
|
return parseState(state);
|
|
182
152
|
}
|
package/lib/responses.d.ts
CHANGED
|
@@ -109,11 +109,9 @@ export declare class ResponsesStreamTranslator {
|
|
|
109
109
|
private readonly order;
|
|
110
110
|
private readonly replayCapture;
|
|
111
111
|
private nextIndex;
|
|
112
|
-
private retainedBytes;
|
|
113
112
|
private sawToolCall;
|
|
114
113
|
terminated: boolean;
|
|
115
114
|
constructor(replayContext?: ResponsesReplayContext | undefined);
|
|
116
|
-
private reserve;
|
|
117
115
|
private append;
|
|
118
116
|
private fill;
|
|
119
117
|
private open;
|
package/lib/responses.js
CHANGED
|
@@ -6,7 +6,6 @@ import { NativeCodexReplayCapture, replayAssistantInput, replayableItemId, } fro
|
|
|
6
6
|
export const DEFAULT_CODEX_INSTRUCTIONS = 'You are Codex, an AI coding agent. Help the user with software engineering tasks.';
|
|
7
7
|
const CALL_ID_MAX_LENGTH = 64;
|
|
8
8
|
const CALL_ID_PREFIX = 'call_';
|
|
9
|
-
const MAX_RETAINED_RESPONSE_BYTES = 64 * 1024 * 1024;
|
|
10
9
|
const UUID_NAMESPACE_OID = Buffer.from('6ba7b8129dad11d180b400c04fd430c8', 'hex');
|
|
11
10
|
function uuidV5(namespace, name) {
|
|
12
11
|
const bytes = createHash('sha1').update(namespace).update(name).digest().subarray(0, 16);
|
|
@@ -321,7 +320,6 @@ export class ResponsesStreamTranslator {
|
|
|
321
320
|
order = [];
|
|
322
321
|
replayCapture;
|
|
323
322
|
nextIndex = 0;
|
|
324
|
-
retainedBytes = 0;
|
|
325
323
|
sawToolCall = false;
|
|
326
324
|
terminated = false;
|
|
327
325
|
constructor(replayContext) {
|
|
@@ -330,26 +328,15 @@ export class ResponsesStreamTranslator {
|
|
|
330
328
|
? undefined
|
|
331
329
|
: new NativeCodexReplayCapture(replayContext.provider, replayContext.model);
|
|
332
330
|
}
|
|
333
|
-
reserve(bytes) {
|
|
334
|
-
const nextBytes = this.retainedBytes + bytes;
|
|
335
|
-
if (!Number.isSafeInteger(nextBytes) || nextBytes > MAX_RETAINED_RESPONSE_BYTES) {
|
|
336
|
-
throw fixedError('native Codex response retained content exceeded the size limit', 'RESPONSE_TOO_LARGE');
|
|
337
|
-
}
|
|
338
|
-
this.retainedBytes = nextBytes;
|
|
339
|
-
}
|
|
340
331
|
append(block, delta) {
|
|
341
|
-
this.reserve(Buffer.byteLength(delta));
|
|
342
332
|
block.text += delta;
|
|
343
333
|
}
|
|
344
334
|
fill(block, text) {
|
|
345
335
|
if (block.text.length > 0)
|
|
346
336
|
return;
|
|
347
|
-
this.reserve(Buffer.byteLength(text));
|
|
348
337
|
block.text = text;
|
|
349
338
|
}
|
|
350
339
|
open(key, kind, chunks, callId = '', name) {
|
|
351
|
-
this.reserve(128 + Buffer.byteLength(key) + Buffer.byteLength(callId)
|
|
352
|
-
+ (name === undefined ? 0 : Buffer.byteLength(name)));
|
|
353
340
|
const block = {
|
|
354
341
|
index: this.nextIndex++, kind, text: '', callId,
|
|
355
342
|
...name === undefined ? {} : { name },
|
|
@@ -445,7 +432,7 @@ export class ResponsesStreamTranslator {
|
|
|
445
432
|
if (item.call_id === undefined || item.call_id.length === 0
|
|
446
433
|
|| item.name === undefined || item.name.length === 0
|
|
447
434
|
|| (item.namespace !== undefined && (typeof item.namespace !== 'string'
|
|
448
|
-
|| item.namespace.length === 0
|
|
435
|
+
|| item.namespace.length === 0))
|
|
449
436
|
|| typeof item.arguments !== 'string') {
|
|
450
437
|
throw fixedError('native Codex function call has invalid content', 'MALFORMED_RESPONSE');
|
|
451
438
|
}
|
package/lib/sse.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
export declare const DEFAULT_MAX_SSE_EVENT_BYTES: number;
|
|
2
1
|
export interface SseEvent {
|
|
3
2
|
data: string;
|
|
4
3
|
event?: string;
|
|
@@ -7,7 +6,6 @@ export interface ParseSseOptions {
|
|
|
7
6
|
signal?: AbortSignal;
|
|
8
7
|
onActivity?: () => void;
|
|
9
8
|
onBytes?: (bytes: number) => void;
|
|
10
|
-
maxEventBytes?: number;
|
|
11
9
|
}
|
|
12
|
-
/** Decode a byte stream into
|
|
10
|
+
/** Decode a byte stream into SSE frames. */
|
|
13
11
|
export declare function parseSse(stream: ReadableStream<Uint8Array>, options?: ParseSseOptions): AsyncGenerator<SseEvent>;
|