@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/cloud-vision.js
CHANGED
|
@@ -10,8 +10,8 @@ export function parseCloudVisionArgs(value) {
|
|
|
10
10
|
const row = value;
|
|
11
11
|
if (Object.keys(row).some(key => !['prompt', 'images', 'model', 'detail', 'reasoning_effort'].includes(key)))
|
|
12
12
|
throw new Error('Unknown vision argument');
|
|
13
|
-
if (typeof row.prompt !== 'string' || !row.prompt.trim()
|
|
14
|
-
throw new Error('Vision prompt must be a nonempty
|
|
13
|
+
if (typeof row.prompt !== 'string' || !row.prompt.trim())
|
|
14
|
+
throw new Error('Vision prompt must be a nonempty string');
|
|
15
15
|
const selection = validateImageSelection(row);
|
|
16
16
|
if (!selection.images)
|
|
17
17
|
throw new Error('Vision requires explicit images');
|
|
@@ -73,21 +73,13 @@ export async function inspectCloudImages(args, deps) {
|
|
|
73
73
|
...(model.instructionsTemplate === undefined ? {} : { instructionsTemplate: model.instructionsTemplate }),
|
|
74
74
|
} } : {}) });
|
|
75
75
|
const texts = new Map();
|
|
76
|
-
let visibleBytes = 0;
|
|
77
76
|
let usage;
|
|
78
77
|
let finished = false;
|
|
79
78
|
for await (const chunk of chunks) {
|
|
80
79
|
if (chunk.type === 'tool-call-delta' || (chunk.type === 'block-start' && chunk.blockType === 'tool-call'))
|
|
81
80
|
throw new LlmError('Image inspection returned an unexpected tool call', 'UNSUPPORTED');
|
|
82
|
-
if (chunk.type === 'text-delta') {
|
|
83
|
-
visibleBytes += Buffer.byteLength(chunk.text);
|
|
84
|
-
if (visibleBytes > 128 * 1024)
|
|
85
|
-
throw new LlmError('Image inspection output exceeded limit', 'RESPONSE_TOO_LARGE');
|
|
86
|
-
}
|
|
87
81
|
if (chunk.type === 'block-end' && chunk.block.type === 'text') {
|
|
88
82
|
texts.set(chunk.index, chunk.block.text);
|
|
89
|
-
if ([...texts.values()].reduce((bytes, text) => bytes + Buffer.byteLength(text), 0) > 128 * 1024)
|
|
90
|
-
throw new LlmError('Image inspection output exceeded limit', 'RESPONSE_TOO_LARGE');
|
|
91
83
|
}
|
|
92
84
|
if (chunk.type === 'usage')
|
|
93
85
|
usage = chunk.usage;
|
package/lib/cloud-web-tool.js
CHANGED
|
@@ -22,7 +22,7 @@ const operations = {
|
|
|
22
22
|
response_length: enumeration('short', 'medium', 'long'),
|
|
23
23
|
};
|
|
24
24
|
const parameters = object({
|
|
25
|
-
commands: { ...object(operations), description: 'One or more nonempty command arrays
|
|
25
|
+
commands: { ...object(operations), description: 'One or more nonempty command arrays. Dependent actions require separate calls. screenshot only supports zero-indexed PDF pages.' },
|
|
26
26
|
model: { ...str, description: 'Explicit Codex catalog model; otherwise cloudTools.searchModel, then current openai-codex model. No guessed fallback.' },
|
|
27
27
|
context: { ...str, description: 'Optional explicit text. Default includes last two visible human texts and at most 1000 UTF-8 bytes of visible assistant text.' },
|
|
28
28
|
mode: enumeration('cached', 'indexed', 'live'), search_context_size: enumeration('low', 'medium', 'high'),
|
|
@@ -51,12 +51,8 @@ function inputImage(value) {
|
|
|
51
51
|
async function admitSearchImages(body, attachments, signal) {
|
|
52
52
|
const images = [];
|
|
53
53
|
const notes = new Set();
|
|
54
|
-
|
|
55
|
-
let bytes = 0;
|
|
56
|
-
const visit = async (item, depth) => {
|
|
54
|
+
const visit = async (item) => {
|
|
57
55
|
signal.throwIfAborted();
|
|
58
|
-
if (depth > 8)
|
|
59
|
-
return item;
|
|
60
56
|
if (inputImage(item)) {
|
|
61
57
|
if (publicUrl(item.image_url)) {
|
|
62
58
|
notes.add('Remote image unavailable: the public DSH web boundary has no binary image retrieval. Image and source links are preserved.');
|
|
@@ -69,12 +65,9 @@ async function admitSearchImages(body, attachments, signal) {
|
|
|
69
65
|
try {
|
|
70
66
|
if (!attachments)
|
|
71
67
|
throw new Error('Attachment service unavailable');
|
|
72
|
-
if (++seen > 4 || match[2].length > 12 * 1024 * 1024)
|
|
73
|
-
throw new Error('Image count or byte limit');
|
|
74
68
|
const data = Buffer.from(match[2], 'base64');
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
throw new Error('Invalid or oversized inline image');
|
|
69
|
+
if (!data.length || data.toString('base64') !== match[2])
|
|
70
|
+
throw new Error('Invalid inline image');
|
|
78
71
|
signal.throwIfAborted();
|
|
79
72
|
ref = await attachments.saveImage({ data, mediaType: match[1] });
|
|
80
73
|
signal.throwIfAborted();
|
|
@@ -87,26 +80,23 @@ async function admitSearchImages(body, attachments, signal) {
|
|
|
87
80
|
// Binary transfer data must not become historical text; unknown fields remain opaque.
|
|
88
81
|
return { ...item, image_url: ref ? '[DSH attachment ' + ref.attachmentId + ']' : '[inline image unavailable]' };
|
|
89
82
|
}
|
|
90
|
-
//
|
|
83
|
+
// Visit known content blocks while keeping unknown object fields opaque.
|
|
91
84
|
if (Array.isArray(item)) {
|
|
92
|
-
if (item.length > 100)
|
|
93
|
-
return item; // Unknown large arrays stay opaque for bounded parsing.
|
|
94
85
|
const result = [];
|
|
95
86
|
for (const value of item)
|
|
96
|
-
result.push(await visit(value
|
|
87
|
+
result.push(await visit(value));
|
|
97
88
|
return result;
|
|
98
89
|
}
|
|
99
|
-
if (record(item) && Array.isArray(item.content)
|
|
100
|
-
return { ...item, content: await visit(item.content
|
|
90
|
+
if (record(item) && Array.isArray(item.content)) {
|
|
91
|
+
return { ...item, content: await visit(item.content) };
|
|
101
92
|
}
|
|
102
93
|
return item;
|
|
103
94
|
};
|
|
104
95
|
if (!record(body) || !Array.isArray(body.results))
|
|
105
96
|
return { body, images, notes };
|
|
106
|
-
// Leave excess entries for the response parser to mark explicitly as truncated.
|
|
107
97
|
const results = [];
|
|
108
|
-
for (
|
|
109
|
-
results.push(
|
|
98
|
+
for (const item of body.results)
|
|
99
|
+
results.push(await visit(item));
|
|
110
100
|
return { body: { ...body, results }, images, notes };
|
|
111
101
|
}
|
|
112
102
|
const markerPrefix = '[codex_web context: ';
|
|
@@ -208,7 +198,7 @@ export function registerCodexWeb(ctx, deps) {
|
|
|
208
198
|
checkReferences(args, messages, id, recent);
|
|
209
199
|
const response = await deps.client.post('alpha/search', body, { credential, signal: exec.signal });
|
|
210
200
|
// Validate the envelope before any attachment write; image bytes are replaced
|
|
211
|
-
// before the final
|
|
201
|
+
// before the final result projection so inline images become attachments.
|
|
212
202
|
if (!record(response.body) || typeof response.body.output !== 'string' || (response.body.results != null && !Array.isArray(response.body.results)))
|
|
213
203
|
throw new LlmError('Invalid Codex search response', 'CODEX_CLOUD_FAILED');
|
|
214
204
|
const media = await admitSearchImages(response.body, agent.ctx.attachments, exec.signal);
|
package/lib/index.d.ts
CHANGED
|
@@ -200,6 +200,7 @@ export declare class OpenAICodexAuth extends Service {
|
|
|
200
200
|
private sendText;
|
|
201
201
|
private trustedManagementRequest;
|
|
202
202
|
private requireCsrf;
|
|
203
|
+
private handleNetwork;
|
|
203
204
|
private handleStatus;
|
|
204
205
|
private handleAccountUsage;
|
|
205
206
|
private handleDeviceStart;
|
package/lib/index.js
CHANGED
|
@@ -18,6 +18,7 @@ import { NativeCodexCloudClient } from './cloud-http.js';
|
|
|
18
18
|
import { registerCodexImageTools } from './cloud-tools.js';
|
|
19
19
|
import { registerCodexWeb } from './cloud-web-tool.js';
|
|
20
20
|
import { mergeDirectUsage, normalizeUsage } from './usage.js';
|
|
21
|
+
import { networkTelemetry } from './network-telemetry.js';
|
|
21
22
|
export { normalizeUsage } from './usage.js';
|
|
22
23
|
export { CODEX_CLIENT_VERSION, TRACKED_CODEX_COMMIT, TRACKED_CODEX_RELEASE, TRACKED_CODEX_REPOSITORY, } from './upstream.js';
|
|
23
24
|
const CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
|
|
@@ -829,6 +830,7 @@ export class OpenAICodexAuth extends Service {
|
|
|
829
830
|
disposers.push(ctx.webServer.register({ kind: 'exact', path, handler }));
|
|
830
831
|
};
|
|
831
832
|
register('/openai-codex/status', (req, res) => this.handleStatus(req, res));
|
|
833
|
+
register('/openai-codex/network', (req, res) => this.handleNetwork(req, res));
|
|
832
834
|
register('/openai-codex/device/start', (req, res) => this.handleDeviceStart(req, res));
|
|
833
835
|
register('/openai-codex/browser/start', (req, res) => this.handleBrowserStart(req, res));
|
|
834
836
|
register('/openai-codex/browser/prepare', (req, res) => this.handleBrowserPrepare(req, res));
|
|
@@ -2013,6 +2015,15 @@ export class OpenAICodexAuth extends Service {
|
|
|
2013
2015
|
this.sendJson(res, 403, { error: 'Invalid CSRF token.' });
|
|
2014
2016
|
return false;
|
|
2015
2017
|
}
|
|
2018
|
+
handleNetwork(req, res) {
|
|
2019
|
+
if (!this.trustedManagementRequest(req, res))
|
|
2020
|
+
return;
|
|
2021
|
+
if (req.method !== 'GET') {
|
|
2022
|
+
this.sendJson(res, 405, { error: 'GET only' }, { allow: 'GET' });
|
|
2023
|
+
return;
|
|
2024
|
+
}
|
|
2025
|
+
this.sendJson(res, 200, networkTelemetry.snapshot());
|
|
2026
|
+
}
|
|
2016
2027
|
async handleStatus(req, res) {
|
|
2017
2028
|
if (!this.trustedManagementRequest(req, res))
|
|
2018
2029
|
return;
|
package/lib/native-http.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type NetworkTelemetry, type NetworkRequestHandle } from './network-telemetry.js';
|
|
1
2
|
import { type ContentBlock, type GenerateOptions, type StreamChunk } from '@deepseek-ai/dsh-llm';
|
|
2
3
|
import { type NativeCodexCredential } from './catalog.js';
|
|
3
4
|
import { type NativeCodexTransportMode } from './native-adapter.js';
|
|
@@ -16,15 +17,14 @@ export interface NativeCodexHttpOptions {
|
|
|
16
17
|
resolveCredential(signal?: AbortSignal): Promise<NativeCodexCredential>;
|
|
17
18
|
recoverCredential?(previous: NativeCodexCredential, signal?: AbortSignal): Promise<boolean>;
|
|
18
19
|
readImage?(attachment: ImageBlock['attachment'], signal?: AbortSignal): Promise<NativeCodexImageRead>;
|
|
20
|
+
telemetry?: NetworkTelemetry;
|
|
19
21
|
fetch?: typeof fetch;
|
|
20
22
|
endpoint?: string;
|
|
21
23
|
requestTimeoutMs?: number;
|
|
22
24
|
streamIdleTimeoutMs?: number;
|
|
23
|
-
maxSseEventBytes?: number;
|
|
24
25
|
maxTransientRetries?: number;
|
|
25
26
|
initialRetryDelayMs?: number;
|
|
26
27
|
maxRetryDelayMs?: number;
|
|
27
|
-
maxRequestBodyBytes?: number;
|
|
28
28
|
random?: () => number;
|
|
29
29
|
sleep?: (delayMs: number, signal?: AbortSignal) => Promise<void>;
|
|
30
30
|
createRequestId?: () => string;
|
|
@@ -37,7 +37,6 @@ export interface NativeCodexPreparedRequest {
|
|
|
37
37
|
generation: GenerateOptions;
|
|
38
38
|
mode: NativeCodexTransportMode;
|
|
39
39
|
request: Record<string, unknown>;
|
|
40
|
-
body: string;
|
|
41
40
|
routingId: string;
|
|
42
41
|
routingHint: string;
|
|
43
42
|
}
|
|
@@ -51,13 +50,13 @@ export declare class NativeCodexHttpTransport {
|
|
|
51
50
|
private readonly maxRetries;
|
|
52
51
|
private readonly initialDelayMs;
|
|
53
52
|
private readonly maxDelayMs;
|
|
54
|
-
private readonly maxBodyBytes;
|
|
55
53
|
constructor(options: NativeCodexHttpOptions);
|
|
56
54
|
private retryDelay;
|
|
57
55
|
private wait;
|
|
58
56
|
private waitForConnection;
|
|
59
57
|
endpointUrl(): string;
|
|
60
58
|
prepare(generation: GenerateOptions, mode?: NativeCodexTransportMode): Promise<NativeCodexPreparedRequest>;
|
|
61
|
-
stream(generation: GenerateOptions, mode?: NativeCodexTransportMode): AsyncIterable<StreamChunk>;
|
|
59
|
+
stream(generation: GenerateOptions, mode?: NativeCodexTransportMode, telemetry?: NetworkRequestHandle): AsyncIterable<StreamChunk>;
|
|
60
|
+
private streamTracked;
|
|
62
61
|
}
|
|
63
62
|
export {};
|
package/lib/native-http.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Native ChatGPT Codex HTTP/SSE transport with safe pre-output retries. */
|
|
2
2
|
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import { networkTelemetry, trackNetwork, observeNetworkOutput } from './network-telemetry.js';
|
|
3
4
|
import { CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, isContextWindowExceededError, isQuotaExceededError, LlmError, ProviderRequestId, QUOTA_EXCEEDED_CODE, attributionHeaders, } from '@deepseek-ai/dsh-llm';
|
|
4
5
|
import { nativeCodexAuthorityHash } from './catalog.js';
|
|
5
6
|
import { nativeCodexEndpoint } from './endpoint.js';
|
|
@@ -16,7 +17,6 @@ const DEFAULT_INITIAL_RETRY_DELAY_MS = 200;
|
|
|
16
17
|
const DEFAULT_MAX_RETRY_DELAY_MS = 10_000;
|
|
17
18
|
const INITIAL_CONNECTION_RETRY_DELAY_MS = 5_000;
|
|
18
19
|
const MAX_CONNECTION_RETRY_DELAY_MS = 60_000;
|
|
19
|
-
const DEFAULT_MAX_REQUEST_BODY_BYTES = 24 * 1024 * 1024;
|
|
20
20
|
const MAX_ERROR_BODY_BYTES = 64 * 1024;
|
|
21
21
|
function aborted(message = 'native Codex request was aborted') {
|
|
22
22
|
return new LlmError(message, 'ABORTED');
|
|
@@ -199,7 +199,7 @@ async function resolveMessages(generation, options) {
|
|
|
199
199
|
}
|
|
200
200
|
return messages;
|
|
201
201
|
}
|
|
202
|
-
async function boundedError(response, signal) {
|
|
202
|
+
async function boundedError(response, signal, telemetry) {
|
|
203
203
|
if (response.body === null)
|
|
204
204
|
return {};
|
|
205
205
|
const reader = response.body.getReader();
|
|
@@ -223,6 +223,7 @@ async function boundedError(response, signal) {
|
|
|
223
223
|
return {};
|
|
224
224
|
if (done)
|
|
225
225
|
break;
|
|
226
|
+
telemetry.activity(value.byteLength);
|
|
226
227
|
total += value.byteLength;
|
|
227
228
|
if (total > MAX_ERROR_BODY_BYTES) {
|
|
228
229
|
await reader.cancel().catch(() => { });
|
|
@@ -276,8 +277,8 @@ function errorFacts(response, maxDelayMs) {
|
|
|
276
277
|
: { requestId: ProviderRequestId(requestId) },
|
|
277
278
|
};
|
|
278
279
|
}
|
|
279
|
-
async function httpFailure(response, maxDelayMs, signal) {
|
|
280
|
-
const detail = await boundedError(response, signal);
|
|
280
|
+
async function httpFailure(response, maxDelayMs, signal, telemetry) {
|
|
281
|
+
const detail = await boundedError(response, signal, telemetry);
|
|
281
282
|
const facts = errorFacts(response, maxDelayMs);
|
|
282
283
|
const classification = `${detail.code ?? ''} ${detail.message ?? ''}`;
|
|
283
284
|
if (response.status === 401 || response.status === 403) {
|
|
@@ -357,7 +358,6 @@ export class NativeCodexHttpTransport {
|
|
|
357
358
|
maxRetries;
|
|
358
359
|
initialDelayMs;
|
|
359
360
|
maxDelayMs;
|
|
360
|
-
maxBodyBytes;
|
|
361
361
|
constructor(options) {
|
|
362
362
|
this.options = options;
|
|
363
363
|
this.fetchImpl = options.fetch ?? fetch;
|
|
@@ -367,7 +367,6 @@ export class NativeCodexHttpTransport {
|
|
|
367
367
|
this.maxRetries = safeRetryCount(options.maxTransientRetries);
|
|
368
368
|
this.initialDelayMs = safePositiveInteger(options.initialRetryDelayMs, DEFAULT_INITIAL_RETRY_DELAY_MS, 'initial retry delay');
|
|
369
369
|
this.maxDelayMs = safePositiveInteger(options.maxRetryDelayMs, DEFAULT_MAX_RETRY_DELAY_MS, 'maximum retry delay');
|
|
370
|
-
this.maxBodyBytes = safePositiveInteger(options.maxRequestBodyBytes, DEFAULT_MAX_REQUEST_BODY_BYTES, 'request body limit');
|
|
371
370
|
}
|
|
372
371
|
retryDelay(retry, providerDelay) {
|
|
373
372
|
if (providerDelay !== undefined)
|
|
@@ -377,11 +376,13 @@ export class NativeCodexHttpTransport {
|
|
|
377
376
|
const jitter = 0.9 + Math.max(0, Math.min(1, random)) * 0.2;
|
|
378
377
|
return Math.max(1, Math.round(exponential * jitter));
|
|
379
378
|
}
|
|
380
|
-
async wait(retry, error, signal) {
|
|
379
|
+
async wait(retry, error, signal, telemetry) {
|
|
381
380
|
const delay = this.retryDelay(retry, error.failure.providerRetryAfterMs);
|
|
381
|
+
telemetry.retry(delay);
|
|
382
382
|
await (this.options.sleep ?? sleep)(delay, signal);
|
|
383
383
|
}
|
|
384
|
-
async waitForConnection(delayMs, signal) {
|
|
384
|
+
async waitForConnection(delayMs, signal, telemetry) {
|
|
385
|
+
telemetry.retry(delayMs, 'connection');
|
|
385
386
|
this.options.warn?.(`native Codex network is unavailable; reconnecting in ${delayMs}ms`);
|
|
386
387
|
await (this.options.sleep ?? sleep)(delayMs, signal);
|
|
387
388
|
}
|
|
@@ -410,24 +411,36 @@ export class NativeCodexHttpTransport {
|
|
|
410
411
|
sessionId: stablePromptCacheKey(sessionId),
|
|
411
412
|
};
|
|
412
413
|
let request;
|
|
413
|
-
let body;
|
|
414
414
|
try {
|
|
415
415
|
request = codexRequestBody(wireOptions, messages, mode);
|
|
416
|
-
body = JSON.stringify(request);
|
|
417
416
|
}
|
|
418
417
|
catch (error) {
|
|
419
418
|
if (error instanceof LlmError)
|
|
420
419
|
throw error;
|
|
421
420
|
throw fixedFailure('native Codex request could not be encoded', 'INVALID_ARGS', { cause: error });
|
|
422
421
|
}
|
|
423
|
-
|
|
424
|
-
|
|
422
|
+
// WebSocket chooses a full request or an incremental suffix after preparation.
|
|
423
|
+
// Serialize only in the selected transport.
|
|
424
|
+
return { generation, mode, request, routingId, routingHint };
|
|
425
|
+
}
|
|
426
|
+
async *stream(generation, mode = {}, telemetry) {
|
|
427
|
+
if (telemetry !== undefined) {
|
|
428
|
+
yield* this.streamTracked(generation, mode, telemetry);
|
|
429
|
+
return;
|
|
425
430
|
}
|
|
426
|
-
|
|
431
|
+
yield* trackNetwork(this.options.telemetry ?? networkTelemetry, 'http', generation.signal, handle => this.streamTracked(generation, mode, handle));
|
|
427
432
|
}
|
|
428
|
-
async *
|
|
433
|
+
async *streamTracked(generation, mode, telemetry) {
|
|
434
|
+
telemetry.state('preparing');
|
|
429
435
|
const prepared = await this.prepare(generation, mode);
|
|
430
|
-
const {
|
|
436
|
+
const { request, routingId, routingHint } = prepared;
|
|
437
|
+
let body;
|
|
438
|
+
try {
|
|
439
|
+
body = JSON.stringify(request);
|
|
440
|
+
}
|
|
441
|
+
catch (error) {
|
|
442
|
+
throw fixedFailure('native Codex request could not be encoded', 'INVALID_ARGS', { cause: error });
|
|
443
|
+
}
|
|
431
444
|
let activeTurnState = mode.turnState;
|
|
432
445
|
if (activeTurnState !== undefined
|
|
433
446
|
&& (activeTurnState.length === 0 || Buffer.byteLength(activeTurnState) > 4096
|
|
@@ -440,6 +453,7 @@ export class NativeCodexHttpTransport {
|
|
|
440
453
|
let pinnedAccountId = mode.pinnedAccountId;
|
|
441
454
|
while (true) {
|
|
442
455
|
throwIfAborted(generation.signal);
|
|
456
|
+
telemetry.state('preparing');
|
|
443
457
|
const watchdog = attemptWatchdog(generation.signal, this.requestTimeoutMs, this.idleTimeoutMs);
|
|
444
458
|
let response;
|
|
445
459
|
let credential;
|
|
@@ -458,6 +472,7 @@ export class NativeCodexHttpTransport {
|
|
|
458
472
|
throw fixedFailure('native Codex Fast capability authority changed before request', 'FAST_CAPABILITY_UNAVAILABLE');
|
|
459
473
|
}
|
|
460
474
|
fetching = true;
|
|
475
|
+
telemetry.attempt('http');
|
|
461
476
|
response = await this.fetchImpl(this.endpoint, {
|
|
462
477
|
method: 'POST',
|
|
463
478
|
redirect: 'error',
|
|
@@ -490,16 +505,18 @@ export class NativeCodexHttpTransport {
|
|
|
490
505
|
: connection ?? mappedFailure(error, watchdog, generation.signal);
|
|
491
506
|
watchdog.stop();
|
|
492
507
|
if (connection !== undefined) {
|
|
493
|
-
await this.waitForConnection(connectionRetryDelayMs, generation.signal);
|
|
508
|
+
await this.waitForConnection(connectionRetryDelayMs, generation.signal, telemetry);
|
|
494
509
|
connectionRetryDelayMs = Math.min(connectionRetryDelayMs * 2, MAX_CONNECTION_RETRY_DELAY_MS);
|
|
495
510
|
continue;
|
|
496
511
|
}
|
|
497
512
|
if (retryable(failure) && transientRetries < this.maxRetries) {
|
|
498
|
-
await this.wait(transientRetries++, failure, generation.signal);
|
|
513
|
+
await this.wait(transientRetries++, failure, generation.signal, telemetry);
|
|
499
514
|
continue;
|
|
500
515
|
}
|
|
501
516
|
throw failure;
|
|
502
517
|
}
|
|
518
|
+
telemetry.activity();
|
|
519
|
+
telemetry.state('waiting');
|
|
503
520
|
publishCodexRateLimits(credential.accountId, parseCodexRateLimitHeaders(response.headers), this.options.onRateLimits, this.options.warn);
|
|
504
521
|
if (activeTurnState === undefined) {
|
|
505
522
|
const headerTurnState = boundedCodexTurnState(response.headers.get('x-codex-turn-state'));
|
|
@@ -514,8 +531,10 @@ export class NativeCodexHttpTransport {
|
|
|
514
531
|
const changed = await this.options.recoverCredential(credential, watchdog.signal);
|
|
515
532
|
recovered = true;
|
|
516
533
|
watchdog.stop();
|
|
517
|
-
if (changed)
|
|
534
|
+
if (changed) {
|
|
535
|
+
telemetry.retry(0);
|
|
518
536
|
continue;
|
|
537
|
+
}
|
|
519
538
|
throw fixedFailure('native Codex rejected the configured credential', 'AUTH', errorFacts(response, this.maxDelayMs));
|
|
520
539
|
}
|
|
521
540
|
catch (error) {
|
|
@@ -523,7 +542,7 @@ export class NativeCodexHttpTransport {
|
|
|
523
542
|
const failure = connection ?? mappedFailure(error, watchdog, generation.signal);
|
|
524
543
|
watchdog.stop();
|
|
525
544
|
if (connection !== undefined) {
|
|
526
|
-
await this.waitForConnection(connectionRetryDelayMs, generation.signal);
|
|
545
|
+
await this.waitForConnection(connectionRetryDelayMs, generation.signal, telemetry);
|
|
527
546
|
connectionRetryDelayMs = Math.min(connectionRetryDelayMs * 2, MAX_CONNECTION_RETRY_DELAY_MS);
|
|
528
547
|
continue;
|
|
529
548
|
}
|
|
@@ -531,13 +550,13 @@ export class NativeCodexHttpTransport {
|
|
|
531
550
|
}
|
|
532
551
|
}
|
|
533
552
|
if (!response.ok) {
|
|
534
|
-
let failure = await httpFailure(response, this.maxDelayMs, watchdog.signal);
|
|
553
|
+
let failure = await httpFailure(response, this.maxDelayMs, watchdog.signal, telemetry);
|
|
535
554
|
if (generation.signal?.aborted || watchdog.timedOut()) {
|
|
536
555
|
failure = mappedFailure(failure, watchdog, generation.signal);
|
|
537
556
|
}
|
|
538
557
|
watchdog.stop();
|
|
539
558
|
if (retryable(failure) && transientRetries < this.maxRetries) {
|
|
540
|
-
await this.wait(transientRetries++, failure, generation.signal);
|
|
559
|
+
await this.wait(transientRetries++, failure, generation.signal, telemetry);
|
|
541
560
|
continue;
|
|
542
561
|
}
|
|
543
562
|
throw failure;
|
|
@@ -553,9 +572,7 @@ export class NativeCodexHttpTransport {
|
|
|
553
572
|
for await (const chunk of streamResponses(response.body, {
|
|
554
573
|
signal: watchdog.signal,
|
|
555
574
|
onActivity: watchdog.pulse,
|
|
556
|
-
|
|
557
|
-
? {}
|
|
558
|
-
: { maxEventBytes: this.options.maxSseEventBytes },
|
|
575
|
+
onBytes: bytes => telemetry.activity(bytes),
|
|
559
576
|
onMalformedEvent: () => {
|
|
560
577
|
this.options.warn?.('native Codex ignored a malformed SSE event');
|
|
561
578
|
},
|
|
@@ -578,6 +595,7 @@ export class NativeCodexHttpTransport {
|
|
|
578
595
|
},
|
|
579
596
|
})) {
|
|
580
597
|
emitted = true;
|
|
598
|
+
observeNetworkOutput(telemetry, chunk);
|
|
581
599
|
if (chunk.type === 'finish'
|
|
582
600
|
&& ['stop', 'tool-calls', 'max-tokens'].includes(chunk.reason.kind))
|
|
583
601
|
completed = true;
|
|
@@ -596,7 +614,7 @@ export class NativeCodexHttpTransport {
|
|
|
596
614
|
catch (error) {
|
|
597
615
|
const failure = mappedFailure(error, watchdog, generation.signal);
|
|
598
616
|
if (!emitted && retryable(failure) && transientRetries < this.maxRetries) {
|
|
599
|
-
await this.wait(transientRetries++, failure, generation.signal);
|
|
617
|
+
await this.wait(transientRetries++, failure, generation.signal, telemetry);
|
|
600
618
|
continue;
|
|
601
619
|
}
|
|
602
620
|
if (emitted && retryable(failure))
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/** Pure WebSocket v2 previous-response and incremental suffix state. */
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
import { LlmError } from '@deepseek-ai/dsh-llm';
|
|
4
|
-
const MAX_RESPONSE_ID_BYTES = 256;
|
|
5
4
|
const IGNORED_REUSE_FIELDS = new Set([
|
|
6
5
|
'input', 'previous_response_id', 'generate', 'client_metadata',
|
|
7
6
|
'stream_options', 'access_programs',
|
|
@@ -84,8 +83,7 @@ export class NativeCodexWebSocketSessionState {
|
|
|
84
83
|
return { ...plan, payload: { ...plan.payload, generate: false } };
|
|
85
84
|
}
|
|
86
85
|
complete(responseId, outputItems) {
|
|
87
|
-
if (this.pending === undefined || responseId.length === 0
|
|
88
|
-
|| Buffer.byteLength(responseId) > MAX_RESPONSE_ID_BYTES) {
|
|
86
|
+
if (this.pending === undefined || responseId.length === 0) {
|
|
89
87
|
this.reset();
|
|
90
88
|
throw failure('native Codex WebSocket completion identity is invalid');
|
|
91
89
|
}
|
|
@@ -10,6 +10,8 @@ export interface NativeCodexWebSocket {
|
|
|
10
10
|
readonly responseHeaders: Readonly<Record<string, string>>;
|
|
11
11
|
send(text: string, signal?: AbortSignal): Promise<void>;
|
|
12
12
|
receive(signal?: AbortSignal): Promise<NativeCodexWebSocketFrame>;
|
|
13
|
+
/** Observe incoming payload bytes while a logical inference owns the socket. */
|
|
14
|
+
observeActivity?(observer: (bytes: number) => void): () => void;
|
|
13
15
|
close(): void;
|
|
14
16
|
}
|
|
15
17
|
export interface NativeCodexWebSocketConnectOptions {
|
|
@@ -17,7 +19,6 @@ export interface NativeCodexWebSocketConnectOptions {
|
|
|
17
19
|
headers: Record<string, string>;
|
|
18
20
|
signal?: AbortSignal;
|
|
19
21
|
connectTimeoutMs?: number;
|
|
20
|
-
maxFrameBytes?: number;
|
|
21
22
|
}
|
|
22
23
|
export interface NativeCodexWebSocketFactory {
|
|
23
24
|
connect(options: NativeCodexWebSocketConnectOptions): Promise<NativeCodexWebSocket>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** Node WebSocket client seam with injectable deterministic factories. */
|
|
2
2
|
import { LlmError, ProviderRequestId } from '@deepseek-ai/dsh-llm';
|
|
3
3
|
import { HttpsProxyAgent } from 'https-proxy-agent';
|
|
4
4
|
import { getProxyForUrl } from 'proxy-from-env';
|
|
@@ -6,8 +6,6 @@ import { nativeCodexEndpoint } from './endpoint.js';
|
|
|
6
6
|
import { NATIVE_CODEX_CONNECTION_FAILED_CODE, isNativeCodexConnectionFailure, } from './native-adapter.js';
|
|
7
7
|
import WebSocket from 'ws';
|
|
8
8
|
const DEFAULT_CONNECT_TIMEOUT_MS = 10_000;
|
|
9
|
-
const DEFAULT_MAX_FRAME_BYTES = 64 * 1024 * 1024;
|
|
10
|
-
const MAX_QUEUED_BYTES = 64 * 1024 * 1024;
|
|
11
9
|
function failure(message, code, cause) {
|
|
12
10
|
return new LlmError(message, code, cause === undefined ? undefined : { cause });
|
|
13
11
|
}
|
|
@@ -54,29 +52,29 @@ export function nativeCodexWebSocketUrl(endpoint) {
|
|
|
54
52
|
class NodeNativeCodexWebSocket {
|
|
55
53
|
socket;
|
|
56
54
|
responseHeaders;
|
|
57
|
-
maxFrameBytes;
|
|
58
55
|
queue = [];
|
|
59
|
-
queuedBytes = 0;
|
|
60
56
|
waiters = [];
|
|
61
57
|
ended = false;
|
|
62
|
-
|
|
58
|
+
activityObserver;
|
|
59
|
+
observeActivity(observer) {
|
|
60
|
+
this.activityObserver = observer;
|
|
61
|
+
return () => { if (this.activityObserver === observer)
|
|
62
|
+
this.activityObserver = undefined; };
|
|
63
|
+
}
|
|
64
|
+
constructor(socket, responseHeaders) {
|
|
63
65
|
this.socket = socket;
|
|
64
66
|
this.responseHeaders = responseHeaders;
|
|
65
|
-
|
|
67
|
+
socket.on('ping', data => { this.activityObserver?.(data.byteLength); });
|
|
68
|
+
socket.on('pong', data => { this.activityObserver?.(data.byteLength); });
|
|
66
69
|
socket.on('message', (data, isBinary) => {
|
|
70
|
+
this.activityObserver?.(Array.isArray(data) ? data.reduce((sum, part) => sum + part.byteLength, 0) : data.byteLength);
|
|
67
71
|
if (isBinary) {
|
|
68
72
|
this.fail(failure('native Codex WebSocket returned a binary frame', 'WS_PROTOCOL_ERROR'));
|
|
69
73
|
return;
|
|
70
74
|
}
|
|
71
|
-
const
|
|
72
|
-
? data.reduce((total, chunk) => total + chunk.byteLength, 0) : data.byteLength;
|
|
73
|
-
if (bytes > maxFrameBytes) {
|
|
74
|
-
this.fail(failure('native Codex WebSocket frame exceeded the size limit', 'WS_FRAME_TOO_LARGE'));
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
const buffer = Array.isArray(data) ? Buffer.concat(data, bytes)
|
|
75
|
+
const buffer = Array.isArray(data) ? Buffer.concat(data)
|
|
78
76
|
: Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
79
|
-
this.push({ type: 'text', text: buffer.toString('utf8') }
|
|
77
|
+
this.push({ type: 'text', text: buffer.toString('utf8') });
|
|
80
78
|
});
|
|
81
79
|
socket.on('close', (code, reason) => {
|
|
82
80
|
this.ended = true;
|
|
@@ -86,7 +84,7 @@ class NodeNativeCodexWebSocket {
|
|
|
86
84
|
this.fail(failure('native Codex WebSocket transport failed', 'WS_RETRYABLE', error));
|
|
87
85
|
});
|
|
88
86
|
}
|
|
89
|
-
push(value
|
|
87
|
+
push(value) {
|
|
90
88
|
const waiter = this.waiters.shift();
|
|
91
89
|
if (waiter !== undefined) {
|
|
92
90
|
if (value instanceof LlmError)
|
|
@@ -95,12 +93,7 @@ class NodeNativeCodexWebSocket {
|
|
|
95
93
|
waiter.resolve(value);
|
|
96
94
|
return;
|
|
97
95
|
}
|
|
98
|
-
|
|
99
|
-
this.fail(failure('native Codex WebSocket queued too much response data', 'WS_RESPONSE_TOO_LARGE'));
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
this.queue.push({ value, bytes });
|
|
103
|
-
this.queuedBytes += bytes;
|
|
96
|
+
this.queue.push(value);
|
|
104
97
|
}
|
|
105
98
|
fail(error) {
|
|
106
99
|
if (!this.ended)
|
|
@@ -110,9 +103,8 @@ class NodeNativeCodexWebSocket {
|
|
|
110
103
|
for (const waiter of waiters)
|
|
111
104
|
waiter.reject(error);
|
|
112
105
|
this.queue.length = 0;
|
|
113
|
-
this.queuedBytes = 0;
|
|
114
106
|
if (waiters.length === 0)
|
|
115
|
-
this.queue.push(
|
|
107
|
+
this.queue.push(error);
|
|
116
108
|
}
|
|
117
109
|
async send(text, signal) {
|
|
118
110
|
if (signal?.aborted)
|
|
@@ -140,10 +132,9 @@ class NodeNativeCodexWebSocket {
|
|
|
140
132
|
throw abortFailure(signal);
|
|
141
133
|
const queued = this.queue.shift();
|
|
142
134
|
if (queued !== undefined) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
return queued.value;
|
|
135
|
+
if (queued instanceof LlmError)
|
|
136
|
+
throw queued;
|
|
137
|
+
return queued;
|
|
147
138
|
}
|
|
148
139
|
return new Promise((resolve, reject) => {
|
|
149
140
|
let waiter;
|
|
@@ -173,7 +164,6 @@ class NodeNativeCodexWebSocket {
|
|
|
173
164
|
export class NodeNativeCodexWebSocketFactory {
|
|
174
165
|
async connect(options) {
|
|
175
166
|
const timeout = positive(options.connectTimeoutMs, DEFAULT_CONNECT_TIMEOUT_MS, 'connect timeout');
|
|
176
|
-
const maximum = positive(options.maxFrameBytes, DEFAULT_MAX_FRAME_BYTES, 'frame limit');
|
|
177
167
|
if (options.signal?.aborted)
|
|
178
168
|
throw abortFailure(options.signal);
|
|
179
169
|
return new Promise((resolve, reject) => {
|
|
@@ -184,7 +174,8 @@ export class NodeNativeCodexWebSocketFactory {
|
|
|
184
174
|
headers: options.headers,
|
|
185
175
|
...(agent === undefined ? {} : { agent }),
|
|
186
176
|
handshakeTimeout: timeout,
|
|
187
|
-
|
|
177
|
+
// ws uses zero to disable its default message-size ceiling.
|
|
178
|
+
maxPayload: 0,
|
|
188
179
|
perMessageDeflate: true,
|
|
189
180
|
});
|
|
190
181
|
const abort = () => {
|
|
@@ -215,7 +206,7 @@ export class NodeNativeCodexWebSocketFactory {
|
|
|
215
206
|
});
|
|
216
207
|
socket.on('open', () => {
|
|
217
208
|
options.signal?.removeEventListener('abort', abort);
|
|
218
|
-
resolve(new NodeNativeCodexWebSocket(socket, responseHeaders
|
|
209
|
+
resolve(new NodeNativeCodexWebSocket(socket, responseHeaders));
|
|
219
210
|
});
|
|
220
211
|
socket.on('error', (error) => {
|
|
221
212
|
options.signal?.removeEventListener('abort', abort);
|
|
@@ -6,7 +6,6 @@ export interface NativeCodexWebSocketTransportOptions extends NativeCodexHttpOpt
|
|
|
6
6
|
webSocketFactory?: NativeCodexWebSocketFactory;
|
|
7
7
|
webSocketConnectTimeoutMs?: number;
|
|
8
8
|
webSocketIdleTimeoutMs?: number;
|
|
9
|
-
maxWebSocketFrameBytes?: number;
|
|
10
9
|
maxWebSocketSessions?: number;
|
|
11
10
|
webSocketSessionIdleMs?: number;
|
|
12
11
|
maxWebSocketReconnects?: number;
|
|
@@ -19,7 +18,6 @@ export declare class NativeCodexWebSocketTransport implements NativeCodexTranspo
|
|
|
19
18
|
private readonly sessions;
|
|
20
19
|
private readonly connectTimeoutMs;
|
|
21
20
|
private readonly idleTimeoutMs;
|
|
22
|
-
private readonly maxFrameBytes;
|
|
23
21
|
private readonly maxSessions;
|
|
24
22
|
private readonly sessionIdleMs;
|
|
25
23
|
private readonly maxReconnects;
|
|
@@ -42,5 +40,6 @@ export declare class NativeCodexWebSocketTransport implements NativeCodexTranspo
|
|
|
42
40
|
private exchange;
|
|
43
41
|
private attempt;
|
|
44
42
|
stream(generation: GenerateOptions, mode?: NativeCodexTransportMode): AsyncIterable<StreamChunk>;
|
|
43
|
+
private streamTracked;
|
|
45
44
|
dispose(): void;
|
|
46
45
|
}
|