@pure01fx/dsh-openai-codex-auth 0.12.0 → 0.13.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 +24 -0
- package/CODEX-COMPATIBILITY.md +39 -0
- package/README.md +2 -2
- package/lib/catalog.js +2 -1
- package/lib/index.d.ts +11 -0
- package/lib/index.js +57 -2
- package/lib/native-adapter.d.ts +7 -0
- package/lib/native-adapter.js +25 -4
- package/lib/native-http.js +28 -50
- package/lib/native-websocket-socket.js +4 -5
- package/lib/native-websocket.js +68 -49
- package/lib/replay.d.ts +3 -0
- package/lib/replay.js +8 -2
- package/lib/responses.d.ts +8 -2
- package/lib/responses.js +43 -20
- package/lib/retry-after.d.ts +9 -0
- package/lib/retry-after.js +109 -0
- package/lib/upstream.d.ts +2 -2
- package/lib/upstream.js +2 -2
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +34 -30
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 { retryError, remainingRetryDelay, sleepRetry, jsonRetryAfterMs } from './retry-after.js';
|
|
3
4
|
import { networkTelemetry, trackNetwork, observeNetworkOutput } from './network-telemetry.js';
|
|
4
5
|
import { LlmError, ProviderRequestId, attributionHeaders, } from '@deepseek-ai/dsh-llm';
|
|
5
6
|
import { nativeCodexAuthorityHash } from './catalog.js';
|
|
@@ -22,7 +23,7 @@ const INITIAL_CONNECTION_RETRY_DELAY_MS = 5_000;
|
|
|
22
23
|
const MAX_CONNECTION_RETRY_DELAY_MS = 60_000;
|
|
23
24
|
const MAX_TURN_STATE_BYTES = 4096;
|
|
24
25
|
function failure(message, code, cause) {
|
|
25
|
-
return
|
|
26
|
+
return retryError(message, code, cause === undefined ? undefined : { cause });
|
|
26
27
|
}
|
|
27
28
|
function reconnectable(code) {
|
|
28
29
|
return [
|
|
@@ -36,9 +37,8 @@ function failedStepRetryable(code) {
|
|
|
36
37
|
}
|
|
37
38
|
function failedStepRetry(error) {
|
|
38
39
|
const facts = error.failure;
|
|
39
|
-
const providerRetryAfterMs = facts.providerRetryAfterMs
|
|
40
|
-
|
|
41
|
-
return new LlmError(`native Codex response stream was interrupted: ${error.message}`, NATIVE_CODEX_STREAM_INTERRUPTED_CODE, {
|
|
40
|
+
const providerRetryAfterMs = facts.providerRetryAfterMs;
|
|
41
|
+
return retryError(`native Codex response stream was interrupted: ${error.message}`, NATIVE_CODEX_STREAM_INTERRUPTED_CODE, {
|
|
42
42
|
cause: error,
|
|
43
43
|
...(facts.status === undefined ? {} : { status: facts.status }),
|
|
44
44
|
...(providerRetryAfterMs === undefined ? {} : { providerRetryAfterMs }),
|
|
@@ -65,26 +65,6 @@ function retryCount(value) {
|
|
|
65
65
|
}
|
|
66
66
|
return resolved;
|
|
67
67
|
}
|
|
68
|
-
function sleep(delayMs, signal) {
|
|
69
|
-
if (signal?.aborted) {
|
|
70
|
-
return Promise.reject(failure('native Codex WebSocket retry wait was aborted', 'ABORTED'));
|
|
71
|
-
}
|
|
72
|
-
return new Promise((resolve, reject) => {
|
|
73
|
-
const timer = setTimeout(done, delayMs);
|
|
74
|
-
const abort = () => {
|
|
75
|
-
clearTimeout(timer);
|
|
76
|
-
signal?.removeEventListener('abort', abort);
|
|
77
|
-
const reason = signal?.reason;
|
|
78
|
-
reject(reason instanceof LlmError
|
|
79
|
-
? reason : failure('native Codex WebSocket retry wait was aborted', 'ABORTED'));
|
|
80
|
-
};
|
|
81
|
-
function done() {
|
|
82
|
-
signal?.removeEventListener('abort', abort);
|
|
83
|
-
resolve();
|
|
84
|
-
}
|
|
85
|
-
signal?.addEventListener('abort', abort, { once: true });
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
68
|
function socketCredential(credential) {
|
|
89
69
|
return createHash('sha256')
|
|
90
70
|
.update(credential.accountId).update('\0').update(credential.accessToken)
|
|
@@ -123,15 +103,17 @@ function withRequestMetadata(request, turnState, responsesLite) {
|
|
|
123
103
|
},
|
|
124
104
|
};
|
|
125
105
|
}
|
|
126
|
-
function normalizedOutputItem(event) {
|
|
106
|
+
function normalizedOutputItem(event, translator) {
|
|
127
107
|
if (event.type !== 'response.output_item.done' || event.item === undefined)
|
|
128
108
|
return undefined;
|
|
129
109
|
const item = event.item;
|
|
130
110
|
const id = replayableItemId(item.id);
|
|
131
111
|
if (item.type === 'message') {
|
|
112
|
+
const phase = item.id === undefined ? undefined : translator.messagePhase(item.id);
|
|
132
113
|
return {
|
|
133
114
|
type: 'message', ...(id === undefined ? {} : { id }), role: 'assistant',
|
|
134
115
|
content: (item.content ?? []).map(part => ({ type: 'output_text', text: part.text })),
|
|
116
|
+
...(phase === undefined ? {} : { phase }),
|
|
135
117
|
};
|
|
136
118
|
}
|
|
137
119
|
if (item.type === 'reasoning') {
|
|
@@ -164,10 +146,9 @@ function eventHeader(event, name) {
|
|
|
164
146
|
}
|
|
165
147
|
function eventErrorFacts(event, status) {
|
|
166
148
|
const requestId = eventHeader(event, 'x-request-id');
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
const retryMs =
|
|
170
|
-
? Math.min(seconds * 1_000, 120_000) : undefined;
|
|
149
|
+
const nested = typeof event.error === 'object' && event.error !== null
|
|
150
|
+
? event.error : undefined;
|
|
151
|
+
const retryMs = jsonRetryAfterMs(nested?.headers) ?? jsonRetryAfterMs(event.headers);
|
|
171
152
|
return {
|
|
172
153
|
...(status === undefined ? {} : { status }),
|
|
173
154
|
...(retryMs === undefined ? {} : { providerRetryAfterMs: retryMs }),
|
|
@@ -187,28 +168,44 @@ function eventFailure(event) {
|
|
|
187
168
|
const facts = eventErrorFacts(event, status);
|
|
188
169
|
if (providerCode === 'websocket_connection_limit_reached'
|
|
189
170
|
|| providerCode === 'previous_response_not_found') {
|
|
190
|
-
return
|
|
171
|
+
return retryError('native Codex WebSocket requested a fresh connection', 'WS_RETRYABLE_RESET', facts);
|
|
191
172
|
}
|
|
192
173
|
if (providerCode === 'unauthorized' || providerCode === 'invalid_api_key' || status === 401) {
|
|
193
|
-
return
|
|
174
|
+
return retryError('native Codex WebSocket authentication failed', 'WS_AUTH', facts);
|
|
194
175
|
}
|
|
195
176
|
if (status === 403)
|
|
196
|
-
return
|
|
177
|
+
return retryError('native Codex WebSocket authorization failed', 'AUTH', facts);
|
|
197
178
|
const classified = responsesFailure(providerCode, typeof error.message === 'string' ? error.message : undefined);
|
|
198
179
|
if (classified.code !== 'SERVER') {
|
|
199
180
|
const delay = facts?.providerRetryAfterMs ?? classified.failure.providerRetryAfterMs;
|
|
200
|
-
return
|
|
181
|
+
return retryError(classified.message, classified.code === 'RATE_LIMIT' ? 'RATE_LIMITED' : classified.code, {
|
|
201
182
|
...facts,
|
|
202
|
-
...(delay === undefined ? {} : { providerRetryAfterMs:
|
|
183
|
+
...(delay === undefined ? {} : { providerRetryAfterMs: delay }),
|
|
203
184
|
});
|
|
204
185
|
}
|
|
205
186
|
if (providerCode === 'usage_limit_reached' || status === 429) {
|
|
206
|
-
return
|
|
187
|
+
return retryError('native Codex WebSocket rate limit was reached', 'RATE_LIMITED', facts);
|
|
207
188
|
}
|
|
208
189
|
if (status !== undefined && status >= 500) {
|
|
209
|
-
return
|
|
190
|
+
return retryError('native Codex WebSocket service failed', 'WS_RETRYABLE', facts);
|
|
210
191
|
}
|
|
211
|
-
return
|
|
192
|
+
return retryError('native Codex WebSocket request was rejected', 'INVALID_REQUEST', facts);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* One bounded log line naming the provider's own rejection, so an operator can
|
|
196
|
+
* see why the socket refused a request. The provider's free-form text stays in
|
|
197
|
+
* the log; it never becomes the provider-neutral error message.
|
|
198
|
+
*/
|
|
199
|
+
function rejectionDiagnostic(event) {
|
|
200
|
+
const error = typeof event.error === 'object' && event.error !== null
|
|
201
|
+
? event.error : event;
|
|
202
|
+
const code = typeof error.code === 'string' && error.code.length > 0 ? error.code : undefined;
|
|
203
|
+
const raw = typeof error.message === 'string' ? error.message : '';
|
|
204
|
+
const message = raw.replace(/\s+/g, ' ').trim().slice(0, 300);
|
|
205
|
+
const status = typeof event.status === 'number' ? ` status=${String(event.status)}` : '';
|
|
206
|
+
const detail = [code, message.length === 0 ? undefined : message]
|
|
207
|
+
.filter((part) => part !== undefined).join(': ');
|
|
208
|
+
return `native Codex WebSocket rejected the request${status}${detail.length === 0 ? '' : ` (${detail})`}`;
|
|
212
209
|
}
|
|
213
210
|
/** Prefer WebSocket v2; once safe retries exhaust, keep that DSH session on HTTP. */
|
|
214
211
|
export class NativeCodexWebSocketTransport {
|
|
@@ -239,9 +236,9 @@ export class NativeCodexWebSocketTransport {
|
|
|
239
236
|
this.maxRetryDelayMs = boundedPositive(options.maxRetryDelayMs, DEFAULT_MAX_RETRY_DELAY_MS, 120_000, 'WebSocket maximum retry delay');
|
|
240
237
|
}
|
|
241
238
|
retryDelay(retry, error) {
|
|
242
|
-
const providerDelay = error
|
|
239
|
+
const providerDelay = remainingRetryDelay(error);
|
|
243
240
|
if (providerDelay !== undefined)
|
|
244
|
-
return
|
|
241
|
+
return providerDelay;
|
|
245
242
|
const exponential = Math.min(this.initialRetryDelayMs * (2 ** retry), this.maxRetryDelayMs);
|
|
246
243
|
const random = this.options.random?.() ?? Math.random();
|
|
247
244
|
const jitter = 0.9 + Math.max(0, Math.min(1, random)) * 0.2;
|
|
@@ -250,12 +247,12 @@ export class NativeCodexWebSocketTransport {
|
|
|
250
247
|
async wait(retry, error, signal, telemetry) {
|
|
251
248
|
const delay = this.retryDelay(retry, error);
|
|
252
249
|
telemetry.retry(delay);
|
|
253
|
-
await (this.options.sleep ??
|
|
250
|
+
await (this.options.sleep ?? sleepRetry)(delay, signal);
|
|
254
251
|
}
|
|
255
252
|
async waitForConnection(delayMs, signal, telemetry) {
|
|
256
253
|
telemetry.retry(delayMs, 'connection');
|
|
257
254
|
this.options.warn?.(`native Codex network is unavailable; reconnecting in ${delayMs}ms`);
|
|
258
|
-
await (this.options.sleep ??
|
|
255
|
+
await (this.options.sleep ?? sleepRetry)(delayMs, signal);
|
|
259
256
|
}
|
|
260
257
|
closeEntry(entry) {
|
|
261
258
|
entry.socket?.close();
|
|
@@ -418,14 +415,17 @@ export class NativeCodexWebSocketTransport {
|
|
|
418
415
|
publishCodexRateLimits(accountId, parseCodexRateLimitHeaders(typeof rawEvent.headers === 'object' && rawEvent.headers !== null
|
|
419
416
|
? rawEvent.headers : undefined), this.options.onRateLimits, this.options.warn);
|
|
420
417
|
const wrapped = eventFailure(rawEvent);
|
|
421
|
-
if (wrapped !== undefined)
|
|
418
|
+
if (wrapped !== undefined) {
|
|
419
|
+
this.options.warn?.(rejectionDiagnostic(rawEvent));
|
|
422
420
|
throw wrapped;
|
|
421
|
+
}
|
|
423
422
|
if (entry.turnState === undefined) {
|
|
424
423
|
const nextTurnState = codexResponseTurnState(event);
|
|
425
424
|
if (nextTurnState !== undefined)
|
|
426
425
|
entry.turnState = nextTurnState;
|
|
427
426
|
}
|
|
428
|
-
const
|
|
427
|
+
const chunks = translator.push(event);
|
|
428
|
+
const output = normalizedOutputItem(event, translator);
|
|
429
429
|
if (output !== undefined)
|
|
430
430
|
outputItems.push(output);
|
|
431
431
|
if (event.type === 'response.completed') {
|
|
@@ -436,7 +436,6 @@ export class NativeCodexWebSocketTransport {
|
|
|
436
436
|
const responseId = typeof response?.id === 'string' ? response.id : '';
|
|
437
437
|
entry.protocol.complete(responseId, outputItems);
|
|
438
438
|
}
|
|
439
|
-
const chunks = translator.push(event);
|
|
440
439
|
if (!prewarm)
|
|
441
440
|
for (const chunk of chunks)
|
|
442
441
|
yield chunk;
|
|
@@ -497,10 +496,16 @@ export class NativeCodexWebSocketTransport {
|
|
|
497
496
|
throw failure('native Codex WebSocket transport was disposed', 'DISPOSED');
|
|
498
497
|
const key = sessionKey(generation, prepared.routingId);
|
|
499
498
|
const entry = this.entry(key);
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
499
|
+
// A second request for the same DSH session (the session-title call racing
|
|
500
|
+
// the first step, for example) cannot share one socket chain, and a
|
|
501
|
+
// saturated pool has no socket to lend. Both still have a correct transport
|
|
502
|
+
// over HTTP/SSE, so neither may fail the turn.
|
|
503
|
+
if (entry.busy || this.active.size >= this.maxSessions) {
|
|
504
|
+
this.options.warn?.(entry.busy
|
|
505
|
+
? 'native Codex WebSocket session already has an active request; serving this request over HTTP/SSE'
|
|
506
|
+
: 'native Codex WebSocket active session limit was reached; serving this request over HTTP/SSE');
|
|
507
|
+
yield* this.http.stream(activeGeneration, mode, telemetry);
|
|
508
|
+
return;
|
|
504
509
|
}
|
|
505
510
|
entry.busy = true;
|
|
506
511
|
this.active.set(entry, lifecycle);
|
|
@@ -564,6 +569,16 @@ export class NativeCodexWebSocketTransport {
|
|
|
564
569
|
throw failedStepRetry(failureValue);
|
|
565
570
|
throw failureValue;
|
|
566
571
|
}
|
|
572
|
+
// A rejected request is not a transient socket condition: retrying the
|
|
573
|
+
// socket repeats the rejection. The same payload has a correct
|
|
574
|
+
// transport over HTTP/SSE, so keep this DSH session on HTTP from now on
|
|
575
|
+
// instead of failing every later turn.
|
|
576
|
+
if (failureValue.code === 'INVALID_REQUEST') {
|
|
577
|
+
entry.disabled = true;
|
|
578
|
+
yield* this.http.stream(activeGeneration, fallbackMode(), telemetry);
|
|
579
|
+
requestCompleted = true;
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
567
582
|
// Credential resolution may need the network itself, so wait until it recovers.
|
|
568
583
|
// Once a credential is available, however, a WebSocket-only outage must consume
|
|
569
584
|
// the bounded reconnect budget and then fall back to HTTP/SSE instead of leaving
|
|
@@ -617,7 +632,11 @@ export class NativeCodexWebSocketTransport {
|
|
|
617
632
|
}
|
|
618
633
|
if (failureValue.code === 'WS_UPGRADE_REQUIRED' || retryable) {
|
|
619
634
|
entry.disabled = true;
|
|
620
|
-
|
|
635
|
+
if (remainingRetryDelay(failureValue) !== undefined) {
|
|
636
|
+
await this.wait(reconnects, failureValue, signal, telemetry);
|
|
637
|
+
}
|
|
638
|
+
else
|
|
639
|
+
telemetry.retry(0);
|
|
621
640
|
yield* this.http.stream(activeGeneration, fallbackMode(), telemetry);
|
|
622
641
|
requestCompleted = true;
|
|
623
642
|
return;
|
package/lib/replay.d.ts
CHANGED
|
@@ -2,10 +2,13 @@
|
|
|
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;
|
|
5
|
+
export type CodexMessagePhase = 'commentary' | 'partial_answer' | 'final_answer';
|
|
6
|
+
export declare function isCodexMessagePhase(value: unknown): value is CodexMessagePhase;
|
|
5
7
|
export type NativeCodexReplayDescriptor = {
|
|
6
8
|
type: 'message';
|
|
7
9
|
id?: string;
|
|
8
10
|
blocks: number[];
|
|
11
|
+
phase?: CodexMessagePhase;
|
|
9
12
|
} | {
|
|
10
13
|
type: 'reasoning';
|
|
11
14
|
id?: string;
|
package/lib/replay.js
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
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
|
+
export function isCodexMessagePhase(value) {
|
|
6
|
+
return value === 'commentary' || value === 'partial_answer' || value === 'final_answer';
|
|
7
|
+
}
|
|
5
8
|
function failure(message, code = 'INVALID_REPLAY_STATE') {
|
|
6
9
|
return new LlmError(message, code);
|
|
7
10
|
}
|
|
@@ -55,10 +58,12 @@ function parseDescriptor(value) {
|
|
|
55
58
|
}
|
|
56
59
|
if (row.type === 'message') {
|
|
57
60
|
const blocks = validateBlockArray(row.blocks);
|
|
58
|
-
if (blocks === undefined ||
|
|
61
|
+
if (blocks === undefined || (row.phase !== undefined && !isCodexMessagePhase(row.phase))
|
|
62
|
+
|| !onlyKeys(row, ['type', 'id', 'blocks', 'phase'])) {
|
|
59
63
|
throw failure('native Codex message replay descriptor is invalid');
|
|
60
64
|
}
|
|
61
|
-
return { type: 'message', ...(id === undefined ? {} : { id }), blocks
|
|
65
|
+
return { type: 'message', ...(id === undefined ? {} : { id }), blocks,
|
|
66
|
+
...(row.phase === undefined ? {} : { phase: row.phase }) };
|
|
62
67
|
}
|
|
63
68
|
if (row.type === 'reasoning') {
|
|
64
69
|
const blocks = validateBlockArray(row.blocks);
|
|
@@ -179,6 +184,7 @@ export function replayAssistantInput(content, source) {
|
|
|
179
184
|
input.push({
|
|
180
185
|
type: 'message', ...(item.id === undefined ? {} : { id: item.id }),
|
|
181
186
|
role: 'assistant', content: parts,
|
|
187
|
+
...(item.phase === undefined ? {} : { phase: item.phase }),
|
|
182
188
|
});
|
|
183
189
|
}
|
|
184
190
|
else if (item.type === 'reasoning') {
|
package/lib/responses.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ToolCallId, LlmError, type ContentBlock, type GenerateOptions, type StreamChunk, type TokenUsage, type ToolSchema } from '@deepseek-ai/dsh-llm';
|
|
2
2
|
import { type ParseSseOptions } from './sse.js';
|
|
3
|
-
import { type NativeCodexReplaySource } from './replay.js';
|
|
3
|
+
import { type CodexMessagePhase, type NativeCodexReplaySource } from './replay.js';
|
|
4
4
|
export declare const DEFAULT_CODEX_INSTRUCTIONS = "You are Codex, an AI coding agent. Help the user with software engineering tasks.";
|
|
5
5
|
export type CodexImageDetail = 'auto' | 'low' | 'high' | 'original';
|
|
6
6
|
export interface ResolvedImagePart {
|
|
@@ -53,7 +53,7 @@ export interface ResponsesUsage {
|
|
|
53
53
|
/** Map provider totals to DSH's strict disjoint token counts. */
|
|
54
54
|
export declare function mapResponsesUsage(usage: ResponsesUsage): TokenUsage;
|
|
55
55
|
/** Classify in-band failure data without reflecting provider text. */
|
|
56
|
-
export declare function responsesFailure(code?: string, message?: string): LlmError;
|
|
56
|
+
export declare function responsesFailure(code?: string, message?: string, headers?: unknown): LlmError;
|
|
57
57
|
interface ResponsesOutputItem {
|
|
58
58
|
type?: string;
|
|
59
59
|
id?: string;
|
|
@@ -64,6 +64,7 @@ interface ResponsesOutputItem {
|
|
|
64
64
|
encrypted_content?: string;
|
|
65
65
|
summary?: unknown[];
|
|
66
66
|
status?: string;
|
|
67
|
+
phase?: CodexMessagePhase | null;
|
|
67
68
|
content?: Array<{
|
|
68
69
|
type?: string;
|
|
69
70
|
text?: string;
|
|
@@ -86,6 +87,7 @@ export interface ResponsesStreamEvent {
|
|
|
86
87
|
error?: {
|
|
87
88
|
code?: string;
|
|
88
89
|
message?: string;
|
|
90
|
+
headers?: unknown;
|
|
89
91
|
};
|
|
90
92
|
incomplete_details?: {
|
|
91
93
|
reason?: string;
|
|
@@ -103,11 +105,15 @@ export declare class ResponsesStreamTranslator {
|
|
|
103
105
|
private readonly replayContext?;
|
|
104
106
|
private readonly blocks;
|
|
105
107
|
private readonly order;
|
|
108
|
+
private readonly messagePhases;
|
|
106
109
|
private readonly replayCapture;
|
|
107
110
|
private nextIndex;
|
|
108
111
|
private sawToolCall;
|
|
109
112
|
terminated: boolean;
|
|
110
113
|
constructor(replayContext?: ResponsesReplayContext | undefined);
|
|
114
|
+
/** The same effective phase feeds durable replay and WebSocket prefix comparison. */
|
|
115
|
+
messagePhase(id: string): CodexMessagePhase | undefined;
|
|
116
|
+
private captureMessagePhase;
|
|
111
117
|
private append;
|
|
112
118
|
private fill;
|
|
113
119
|
private open;
|
package/lib/responses.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
import { ToolCallId, CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, isContextWindowExceededError, isQuotaExceededError, LlmError, QUOTA_EXCEEDED_CODE, } from '@deepseek-ai/dsh-llm';
|
|
4
4
|
import { parseSse } from './sse.js';
|
|
5
|
-
import {
|
|
5
|
+
import { jsonRetryAfterMs, retryError } from './retry-after.js';
|
|
6
|
+
import { NativeCodexReplayCapture, isCodexMessagePhase, replayAssistantInput, replayableItemId, } from './replay.js';
|
|
6
7
|
export const DEFAULT_CODEX_INSTRUCTIONS = 'You are Codex, an AI coding agent. Help the user with software engineering tasks.';
|
|
7
8
|
const CALL_ID_MAX_LENGTH = 64;
|
|
8
9
|
const CALL_ID_PREFIX = 'call_';
|
|
@@ -211,11 +212,21 @@ export function codexRequestBody(options, messages, mode = {}) {
|
|
|
211
212
|
...mode.serviceTier === undefined ? {} : { service_tier: mode.serviceTier },
|
|
212
213
|
...options.sessionId === undefined ? {} : { prompt_cache_key: String(options.sessionId) },
|
|
213
214
|
};
|
|
215
|
+
const baseInstructions = mode.responsesLite?.instructionsTemplate ?? instructions;
|
|
216
|
+
const prefixNamespace = uuidBytes(uuidV5(UUID_NAMESPACE_OID, String(options.sessionId ?? options.model)));
|
|
217
|
+
const instructionPrefix = baseInstructions.length === 0 ? [] : [{
|
|
218
|
+
type: 'message',
|
|
219
|
+
id: `msg_${uuidV5(prefixNamespace, baseInstructions)}`,
|
|
220
|
+
role: 'developer',
|
|
221
|
+
content: [{ type: 'input_text', text: baseInstructions }],
|
|
222
|
+
internal_chat_message_metadata_passthrough: {
|
|
223
|
+
content_item_kinds: ['model.base_instructions'],
|
|
224
|
+
},
|
|
225
|
+
}];
|
|
214
226
|
if (mode.responsesLite === undefined) {
|
|
215
227
|
return {
|
|
216
228
|
...common,
|
|
217
|
-
|
|
218
|
-
input,
|
|
229
|
+
input: [...instructionPrefix, ...input],
|
|
219
230
|
...options.tools !== undefined && options.tools.length > 0
|
|
220
231
|
? { tools: toResponsesTools(options.tools) } : {},
|
|
221
232
|
parallel_tool_calls: true,
|
|
@@ -224,7 +235,6 @@ export function codexRequestBody(options, messages, mode = {}) {
|
|
|
224
235
|
};
|
|
225
236
|
}
|
|
226
237
|
const tools = toResponsesLiteTools(options.tools ?? []);
|
|
227
|
-
const baseInstructions = mode.responsesLite.instructionsTemplate ?? instructions;
|
|
228
238
|
const contextualInstructions = mode.responsesLite.instructionsTemplate !== undefined
|
|
229
239
|
&& resolved.instructions !== undefined && resolved.instructions !== baseInstructions
|
|
230
240
|
? [{
|
|
@@ -232,21 +242,12 @@ export function codexRequestBody(options, messages, mode = {}) {
|
|
|
232
242
|
content: [{ type: 'input_text', text: resolved.instructions }],
|
|
233
243
|
}]
|
|
234
244
|
: [];
|
|
235
|
-
const prefixNamespace = uuidBytes(uuidV5(UUID_NAMESPACE_OID, String(options.sessionId ?? options.model)));
|
|
236
245
|
const prefix = [{
|
|
237
246
|
type: 'additional_tools',
|
|
238
247
|
id: `at_${uuidV5(prefixNamespace, JSON.stringify(tools))}`,
|
|
239
248
|
role: 'developer',
|
|
240
249
|
tools,
|
|
241
|
-
},
|
|
242
|
-
type: 'message',
|
|
243
|
-
id: `msg_${uuidV5(prefixNamespace, baseInstructions)}`,
|
|
244
|
-
role: 'developer',
|
|
245
|
-
content: [{ type: 'input_text', text: baseInstructions }],
|
|
246
|
-
internal_chat_message_metadata_passthrough: {
|
|
247
|
-
content_item_kinds: ['model.base_instructions'],
|
|
248
|
-
},
|
|
249
|
-
}];
|
|
250
|
+
}, ...instructionPrefix];
|
|
250
251
|
return {
|
|
251
252
|
...common,
|
|
252
253
|
input: [...prefix, ...contextualInstructions, ...input],
|
|
@@ -293,10 +294,11 @@ function retryDelay(message) {
|
|
|
293
294
|
const value = Number(match[1]);
|
|
294
295
|
if (!Number.isFinite(value) || value <= 0)
|
|
295
296
|
return undefined;
|
|
296
|
-
|
|
297
|
+
const delay = match[2]?.toLowerCase() === 'ms' ? value : value * 1000;
|
|
298
|
+
return Number.isFinite(delay) && delay <= Number.MAX_SAFE_INTEGER ? delay : undefined;
|
|
297
299
|
}
|
|
298
300
|
/** Classify in-band failure data without reflecting provider text. */
|
|
299
|
-
export function responsesFailure(code, message) {
|
|
301
|
+
export function responsesFailure(code, message, headers) {
|
|
300
302
|
const detail = `${code ?? ''} ${message ?? ''}`;
|
|
301
303
|
if (code === 'context_length_exceeded' || code === 'context_window_exceeded'
|
|
302
304
|
|| isContextWindowExceededError(detail)) {
|
|
@@ -308,8 +310,8 @@ export function responsesFailure(code, message) {
|
|
|
308
310
|
return fixedError('native Codex account quota is exhausted', QUOTA_EXCEEDED_CODE);
|
|
309
311
|
}
|
|
310
312
|
if (code === 'rate_limit_exceeded' || code === 'slow_down') {
|
|
311
|
-
const delay = retryDelay(message);
|
|
312
|
-
return
|
|
313
|
+
const delay = jsonRetryAfterMs(headers) ?? retryDelay(message);
|
|
314
|
+
return retryError('native Codex request was rate limited', 'RATE_LIMIT', {
|
|
313
315
|
...delay === undefined ? {} : { providerRetryAfterMs: delay },
|
|
314
316
|
});
|
|
315
317
|
}
|
|
@@ -319,7 +321,10 @@ export function responsesFailure(code, message) {
|
|
|
319
321
|
if (code === 'bio_policy') {
|
|
320
322
|
return fixedError('native Codex rejected the request', 'INVALID_REQUEST');
|
|
321
323
|
}
|
|
322
|
-
|
|
324
|
+
const delay = jsonRetryAfterMs(headers);
|
|
325
|
+
return retryError('native Codex reported a failed response', 'SERVER', {
|
|
326
|
+
...delay === undefined ? {} : { providerRetryAfterMs: delay },
|
|
327
|
+
});
|
|
323
328
|
}
|
|
324
329
|
function eventItemId(event) {
|
|
325
330
|
if (typeof event.item_id === 'string' && event.item_id.length > 0)
|
|
@@ -343,6 +348,7 @@ export class ResponsesStreamTranslator {
|
|
|
343
348
|
replayContext;
|
|
344
349
|
blocks = new Map();
|
|
345
350
|
order = [];
|
|
351
|
+
messagePhases = new Map();
|
|
346
352
|
replayCapture;
|
|
347
353
|
nextIndex = 0;
|
|
348
354
|
sawToolCall = false;
|
|
@@ -353,6 +359,18 @@ export class ResponsesStreamTranslator {
|
|
|
353
359
|
? undefined
|
|
354
360
|
: new NativeCodexReplayCapture(replayContext.provider, replayContext.model);
|
|
355
361
|
}
|
|
362
|
+
/** The same effective phase feeds durable replay and WebSocket prefix comparison. */
|
|
363
|
+
messagePhase(id) {
|
|
364
|
+
return this.messagePhases.get(id);
|
|
365
|
+
}
|
|
366
|
+
captureMessagePhase(item) {
|
|
367
|
+
if (item.phase === undefined || item.phase === null)
|
|
368
|
+
return;
|
|
369
|
+
if (!isCodexMessagePhase(item.phase) || typeof item.id !== 'string' || item.id.length === 0) {
|
|
370
|
+
throw fixedError('native Codex message phase is invalid', 'MALFORMED_RESPONSE');
|
|
371
|
+
}
|
|
372
|
+
this.messagePhases.set(item.id, item.phase);
|
|
373
|
+
}
|
|
356
374
|
append(block, delta) {
|
|
357
375
|
block.text += delta;
|
|
358
376
|
}
|
|
@@ -400,6 +418,8 @@ export class ResponsesStreamTranslator {
|
|
|
400
418
|
switch (event.type) {
|
|
401
419
|
case 'response.output_item.added': {
|
|
402
420
|
const item = event.item;
|
|
421
|
+
if (item?.type === 'message')
|
|
422
|
+
this.captureMessagePhase(item);
|
|
403
423
|
if (item?.type !== 'function_call')
|
|
404
424
|
return chunks;
|
|
405
425
|
if (item.id === undefined || item.id.length === 0
|
|
@@ -483,6 +503,8 @@ export class ResponsesStreamTranslator {
|
|
|
483
503
|
});
|
|
484
504
|
}
|
|
485
505
|
else if (item.type === 'message') {
|
|
506
|
+
this.captureMessagePhase(item);
|
|
507
|
+
const phase = this.messagePhase(item.id);
|
|
486
508
|
if (!Array.isArray(item.content)) {
|
|
487
509
|
throw fixedError('native Codex message item has invalid content', 'MALFORMED_RESPONSE');
|
|
488
510
|
}
|
|
@@ -507,6 +529,7 @@ export class ResponsesStreamTranslator {
|
|
|
507
529
|
if (this.replayContext !== undefined)
|
|
508
530
|
this.replayCapture?.add({
|
|
509
531
|
type: 'message', ...(replayId === undefined ? {} : { id: replayId }), blocks: refs,
|
|
532
|
+
...(phase === undefined ? {} : { phase }),
|
|
510
533
|
});
|
|
511
534
|
}
|
|
512
535
|
else if (item.type === 'reasoning') {
|
|
@@ -595,7 +618,7 @@ export class ResponsesStreamTranslator {
|
|
|
595
618
|
return chunks;
|
|
596
619
|
}
|
|
597
620
|
case 'response.failed':
|
|
598
|
-
throw responsesFailure(event.response?.error?.code, event.response?.error?.message);
|
|
621
|
+
throw responsesFailure(event.response?.error?.code, event.response?.error?.message, event.response?.error?.headers);
|
|
599
622
|
case 'error':
|
|
600
623
|
throw responsesFailure(event.code, event.message);
|
|
601
624
|
default:
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Server advice is separate from the client's bounded exponential backoff. */
|
|
2
|
+
import { LlmError } from '@deepseek-ai/dsh-llm';
|
|
3
|
+
export declare function retryAfterMs(raw: unknown): number | undefined;
|
|
4
|
+
export declare function jsonRetryAfterMs(headers: unknown): number | undefined;
|
|
5
|
+
export declare function retryError(message: string, code: string, options?: ConstructorParameters<typeof LlmError>[2]): LlmError;
|
|
6
|
+
export declare function inheritRetryDeadline(error: LlmError, source: LlmError): LlmError;
|
|
7
|
+
export declare function remainingRetryDelay(error: LlmError): number | undefined;
|
|
8
|
+
/** Node timers overflow above 2^31-1; chunk long waits without shortening advice. */
|
|
9
|
+
export declare function sleepRetry(delay: number, signal?: AbortSignal): Promise<void>;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/** Server advice is separate from the client's bounded exponential backoff. */
|
|
2
|
+
import { LlmError } from '@deepseek-ai/dsh-llm';
|
|
3
|
+
const deadlines = new WeakMap();
|
|
4
|
+
function httpDate(value) {
|
|
5
|
+
const standard = /^([A-Za-z]{3}), ([0-9]{2}) ([A-Za-z]{3}) ([0-9]{4}) ([0-9]{2}:[0-9]{2}:[0-9]{2}) GMT$/.exec(value);
|
|
6
|
+
const obsolete = /^([A-Za-z]+), ([0-9]{2})-([A-Za-z]{3})-([0-9]{2}) ([0-9]{2}:[0-9]{2}:[0-9]{2}) GMT$/.exec(value);
|
|
7
|
+
const ascii = /^([A-Za-z]{3}) ([A-Za-z]{3}) ([ 0-9][0-9]) ([0-9]{2}:[0-9]{2}:[0-9]{2}) ([0-9]{4})$/.exec(value);
|
|
8
|
+
const parts = standard ?? obsolete;
|
|
9
|
+
const weekday = parts?.[1] ?? ascii?.[1];
|
|
10
|
+
const day = parts?.[2] ?? ascii?.[3];
|
|
11
|
+
const month = parts?.[3] ?? ascii?.[2];
|
|
12
|
+
const year = parts?.[4] ?? ascii?.[5];
|
|
13
|
+
const time = parts?.[5] ?? ascii?.[4];
|
|
14
|
+
if (!weekday || !day || !month || !year || !time)
|
|
15
|
+
return undefined;
|
|
16
|
+
const timestamp = Date.parse(value.endsWith('GMT') ? value : value + ' GMT');
|
|
17
|
+
if (!Number.isFinite(timestamp))
|
|
18
|
+
return undefined;
|
|
19
|
+
const date = new Date(timestamp);
|
|
20
|
+
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
21
|
+
const weekdays = obsolete
|
|
22
|
+
? ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
|
|
23
|
+
: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
24
|
+
// Date.parse normalizes impossible dates; HTTP header parsing must reject them.
|
|
25
|
+
if (date.getUTCDate() !== Number(day) || months[date.getUTCMonth()] !== month
|
|
26
|
+
|| weekdays[date.getUTCDay()] !== weekday
|
|
27
|
+
|| (obsolete ? date.getUTCFullYear() % 100 : date.getUTCFullYear()) !== Number(year)
|
|
28
|
+
|| date.toISOString().slice(11, 19) !== time)
|
|
29
|
+
return undefined;
|
|
30
|
+
return timestamp;
|
|
31
|
+
}
|
|
32
|
+
export function retryAfterMs(raw) {
|
|
33
|
+
if (typeof raw !== 'string' && typeof raw !== 'number')
|
|
34
|
+
return undefined;
|
|
35
|
+
const text = String(raw);
|
|
36
|
+
if (/[^\t\x20-\x7e]/.test(text))
|
|
37
|
+
return undefined;
|
|
38
|
+
const value = text.trim();
|
|
39
|
+
let delay;
|
|
40
|
+
if (/^\d+$/.test(value))
|
|
41
|
+
delay = Number(value) * 1000;
|
|
42
|
+
else {
|
|
43
|
+
const timestamp = httpDate(value);
|
|
44
|
+
if (timestamp === undefined)
|
|
45
|
+
return undefined;
|
|
46
|
+
delay = Math.max(0, timestamp - Date.now());
|
|
47
|
+
}
|
|
48
|
+
return Number.isSafeInteger(delay) && delay >= 0 ? delay : undefined;
|
|
49
|
+
}
|
|
50
|
+
export function jsonRetryAfterMs(headers) {
|
|
51
|
+
if (typeof headers !== 'object' || headers === null || Array.isArray(headers))
|
|
52
|
+
return undefined;
|
|
53
|
+
let value;
|
|
54
|
+
// serde_json's ordered map inserts later case variants over earlier ones.
|
|
55
|
+
for (const key of Object.keys(headers).sort()) {
|
|
56
|
+
const raw = headers[key];
|
|
57
|
+
if (key.toLowerCase() === 'retry-after'
|
|
58
|
+
&& (typeof raw === 'string' || typeof raw === 'number' || typeof raw === 'boolean')
|
|
59
|
+
&& !/[^\t\x20-\x7e]/.test(String(raw)))
|
|
60
|
+
value = raw;
|
|
61
|
+
}
|
|
62
|
+
return retryAfterMs(value);
|
|
63
|
+
}
|
|
64
|
+
export function retryError(message, code, options) {
|
|
65
|
+
const delay = options?.providerRetryAfterMs;
|
|
66
|
+
// The host permits only positive duration metadata; retain zero advice locally.
|
|
67
|
+
const normalized = { ...options };
|
|
68
|
+
if (delay === 0)
|
|
69
|
+
delete normalized.providerRetryAfterMs;
|
|
70
|
+
const error = new LlmError(message, code, normalized);
|
|
71
|
+
const inherited = options?.cause instanceof LlmError ? deadlines.get(options.cause) : undefined;
|
|
72
|
+
if (inherited !== undefined || delay !== undefined) {
|
|
73
|
+
deadlines.set(error, inherited ?? performance.now() + delay);
|
|
74
|
+
}
|
|
75
|
+
return error;
|
|
76
|
+
}
|
|
77
|
+
export function inheritRetryDeadline(error, source) {
|
|
78
|
+
const deadline = deadlines.get(source);
|
|
79
|
+
if (deadline !== undefined)
|
|
80
|
+
deadlines.set(error, deadline);
|
|
81
|
+
return error;
|
|
82
|
+
}
|
|
83
|
+
export function remainingRetryDelay(error) {
|
|
84
|
+
let deadline = deadlines.get(error);
|
|
85
|
+
if (deadline === undefined) {
|
|
86
|
+
const delay = error.failure.providerRetryAfterMs;
|
|
87
|
+
if (delay === undefined)
|
|
88
|
+
return undefined;
|
|
89
|
+
deadline = performance.now() + delay;
|
|
90
|
+
deadlines.set(error, deadline);
|
|
91
|
+
}
|
|
92
|
+
return Math.max(0, Math.ceil(deadline - performance.now()));
|
|
93
|
+
}
|
|
94
|
+
/** Node timers overflow above 2^31-1; chunk long waits without shortening advice. */
|
|
95
|
+
export async function sleepRetry(delay, signal) {
|
|
96
|
+
const deadline = performance.now() + delay;
|
|
97
|
+
do {
|
|
98
|
+
if (signal?.aborted)
|
|
99
|
+
throw signal.reason instanceof LlmError ? signal.reason : new LlmError('native Codex retry wait was aborted', 'ABORTED');
|
|
100
|
+
const remaining = Math.max(0, Math.ceil(deadline - performance.now()));
|
|
101
|
+
if (remaining === 0)
|
|
102
|
+
return;
|
|
103
|
+
await new Promise((resolve, reject) => {
|
|
104
|
+
const abort = () => { clearTimeout(timer); reject(signal?.reason instanceof LlmError ? signal.reason : new LlmError('native Codex retry wait was aborted', 'ABORTED')); };
|
|
105
|
+
const timer = setTimeout(() => { signal?.removeEventListener('abort', abort); resolve(); }, Math.min(remaining, 2_147_483_647));
|
|
106
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
107
|
+
});
|
|
108
|
+
} while (true);
|
|
109
|
+
}
|
package/lib/upstream.d.ts
CHANGED
|
@@ -4,5 +4,5 @@ export declare const TRACKED_CODEX_COMMIT = "ddf04ad26789d040f9ef6a96736f76602e3
|
|
|
4
4
|
export declare const TRACKED_CODEX_RELEASE = "main@ddf04ad";
|
|
5
5
|
/** Independently audited cloud search/image contracts; not local Codex executor parity. */
|
|
6
6
|
export declare const TRACKED_CODEX_CLOUD_COMMIT = "b348fc26674189f758d5941cdab3f78f258b2aa7";
|
|
7
|
-
/** Whole
|
|
8
|
-
export declare const CODEX_CLIENT_VERSION = "0.
|
|
7
|
+
/** Whole client version sent to the catalog; live gating can exceed bundled model minima. */
|
|
8
|
+
export declare const CODEX_CLIENT_VERSION = "0.162.0";
|
package/lib/upstream.js
CHANGED
|
@@ -4,5 +4,5 @@ export const TRACKED_CODEX_COMMIT = 'ddf04ad26789d040f9ef6a96736f76602e35a6cc';
|
|
|
4
4
|
export const TRACKED_CODEX_RELEASE = 'main@ddf04ad';
|
|
5
5
|
/** Independently audited cloud search/image contracts; not local Codex executor parity. */
|
|
6
6
|
export const TRACKED_CODEX_CLOUD_COMMIT = 'b348fc26674189f758d5941cdab3f78f258b2aa7';
|
|
7
|
-
/** Whole
|
|
8
|
-
export const CODEX_CLIENT_VERSION = '0.
|
|
7
|
+
/** Whole client version sent to the catalog; live gating can exceed bundled model minima. */
|
|
8
|
+
export const CODEX_CLIENT_VERSION = '0.162.0';
|
package/locale/en.json
ADDED