@pure01fx/dsh-openai-codex-auth 0.11.2 → 0.13.0
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 +36 -1
- package/CODEX-COMPATIBILITY.md +39 -0
- package/README.md +3 -3
- package/lib/cloud-context.js +9 -13
- package/lib/cloud-media.d.ts +1 -1
- package/lib/cloud-media.js +2 -1
- package/lib/cloud-web-tool.js +4 -5
- package/lib/native-adapter.js +2 -1
- package/lib/native-http.js +49 -75
- package/lib/native-websocket-socket.js +4 -5
- package/lib/native-websocket.js +31 -45
- package/lib/replay.d.ts +3 -0
- package/lib/replay.js +8 -2
- package/lib/responses.d.ts +13 -11
- package/lib/responses.js +73 -30
- 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/package.json +43 -37
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)
|
|
@@ -98,7 +78,8 @@ function sessionKey(generation, routingId) {
|
|
|
98
78
|
function turnKey(generation) {
|
|
99
79
|
for (let index = generation.messages.length - 1; index >= 0; index--) {
|
|
100
80
|
const message = generation.messages[index];
|
|
101
|
-
|
|
81
|
+
// RequestUserInput doesn't have source or id
|
|
82
|
+
if (message && 'source' in message && message.source?.kind === 'user' && 'id' in message) {
|
|
102
83
|
return createHash('sha256').update(String(message.id)).digest('base64url');
|
|
103
84
|
}
|
|
104
85
|
}
|
|
@@ -122,15 +103,17 @@ function withRequestMetadata(request, turnState, responsesLite) {
|
|
|
122
103
|
},
|
|
123
104
|
};
|
|
124
105
|
}
|
|
125
|
-
function normalizedOutputItem(event) {
|
|
106
|
+
function normalizedOutputItem(event, translator) {
|
|
126
107
|
if (event.type !== 'response.output_item.done' || event.item === undefined)
|
|
127
108
|
return undefined;
|
|
128
109
|
const item = event.item;
|
|
129
110
|
const id = replayableItemId(item.id);
|
|
130
111
|
if (item.type === 'message') {
|
|
112
|
+
const phase = item.id === undefined ? undefined : translator.messagePhase(item.id);
|
|
131
113
|
return {
|
|
132
114
|
type: 'message', ...(id === undefined ? {} : { id }), role: 'assistant',
|
|
133
115
|
content: (item.content ?? []).map(part => ({ type: 'output_text', text: part.text })),
|
|
116
|
+
...(phase === undefined ? {} : { phase }),
|
|
134
117
|
};
|
|
135
118
|
}
|
|
136
119
|
if (item.type === 'reasoning') {
|
|
@@ -163,10 +146,9 @@ function eventHeader(event, name) {
|
|
|
163
146
|
}
|
|
164
147
|
function eventErrorFacts(event, status) {
|
|
165
148
|
const requestId = eventHeader(event, 'x-request-id');
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
const retryMs =
|
|
169
|
-
? 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);
|
|
170
152
|
return {
|
|
171
153
|
...(status === undefined ? {} : { status }),
|
|
172
154
|
...(retryMs === undefined ? {} : { providerRetryAfterMs: retryMs }),
|
|
@@ -186,28 +168,28 @@ function eventFailure(event) {
|
|
|
186
168
|
const facts = eventErrorFacts(event, status);
|
|
187
169
|
if (providerCode === 'websocket_connection_limit_reached'
|
|
188
170
|
|| providerCode === 'previous_response_not_found') {
|
|
189
|
-
return
|
|
171
|
+
return retryError('native Codex WebSocket requested a fresh connection', 'WS_RETRYABLE_RESET', facts);
|
|
190
172
|
}
|
|
191
173
|
if (providerCode === 'unauthorized' || providerCode === 'invalid_api_key' || status === 401) {
|
|
192
|
-
return
|
|
174
|
+
return retryError('native Codex WebSocket authentication failed', 'WS_AUTH', facts);
|
|
193
175
|
}
|
|
194
176
|
if (status === 403)
|
|
195
|
-
return
|
|
177
|
+
return retryError('native Codex WebSocket authorization failed', 'AUTH', facts);
|
|
196
178
|
const classified = responsesFailure(providerCode, typeof error.message === 'string' ? error.message : undefined);
|
|
197
179
|
if (classified.code !== 'SERVER') {
|
|
198
180
|
const delay = facts?.providerRetryAfterMs ?? classified.failure.providerRetryAfterMs;
|
|
199
|
-
return
|
|
181
|
+
return retryError(classified.message, classified.code === 'RATE_LIMIT' ? 'RATE_LIMITED' : classified.code, {
|
|
200
182
|
...facts,
|
|
201
|
-
...(delay === undefined ? {} : { providerRetryAfterMs:
|
|
183
|
+
...(delay === undefined ? {} : { providerRetryAfterMs: delay }),
|
|
202
184
|
});
|
|
203
185
|
}
|
|
204
186
|
if (providerCode === 'usage_limit_reached' || status === 429) {
|
|
205
|
-
return
|
|
187
|
+
return retryError('native Codex WebSocket rate limit was reached', 'RATE_LIMITED', facts);
|
|
206
188
|
}
|
|
207
189
|
if (status !== undefined && status >= 500) {
|
|
208
|
-
return
|
|
190
|
+
return retryError('native Codex WebSocket service failed', 'WS_RETRYABLE', facts);
|
|
209
191
|
}
|
|
210
|
-
return
|
|
192
|
+
return retryError('native Codex WebSocket request was rejected', 'INVALID_REQUEST', facts);
|
|
211
193
|
}
|
|
212
194
|
/** Prefer WebSocket v2; once safe retries exhaust, keep that DSH session on HTTP. */
|
|
213
195
|
export class NativeCodexWebSocketTransport {
|
|
@@ -238,9 +220,9 @@ export class NativeCodexWebSocketTransport {
|
|
|
238
220
|
this.maxRetryDelayMs = boundedPositive(options.maxRetryDelayMs, DEFAULT_MAX_RETRY_DELAY_MS, 120_000, 'WebSocket maximum retry delay');
|
|
239
221
|
}
|
|
240
222
|
retryDelay(retry, error) {
|
|
241
|
-
const providerDelay = error
|
|
223
|
+
const providerDelay = remainingRetryDelay(error);
|
|
242
224
|
if (providerDelay !== undefined)
|
|
243
|
-
return
|
|
225
|
+
return providerDelay;
|
|
244
226
|
const exponential = Math.min(this.initialRetryDelayMs * (2 ** retry), this.maxRetryDelayMs);
|
|
245
227
|
const random = this.options.random?.() ?? Math.random();
|
|
246
228
|
const jitter = 0.9 + Math.max(0, Math.min(1, random)) * 0.2;
|
|
@@ -249,12 +231,12 @@ export class NativeCodexWebSocketTransport {
|
|
|
249
231
|
async wait(retry, error, signal, telemetry) {
|
|
250
232
|
const delay = this.retryDelay(retry, error);
|
|
251
233
|
telemetry.retry(delay);
|
|
252
|
-
await (this.options.sleep ??
|
|
234
|
+
await (this.options.sleep ?? sleepRetry)(delay, signal);
|
|
253
235
|
}
|
|
254
236
|
async waitForConnection(delayMs, signal, telemetry) {
|
|
255
237
|
telemetry.retry(delayMs, 'connection');
|
|
256
238
|
this.options.warn?.(`native Codex network is unavailable; reconnecting in ${delayMs}ms`);
|
|
257
|
-
await (this.options.sleep ??
|
|
239
|
+
await (this.options.sleep ?? sleepRetry)(delayMs, signal);
|
|
258
240
|
}
|
|
259
241
|
closeEntry(entry) {
|
|
260
242
|
entry.socket?.close();
|
|
@@ -424,7 +406,8 @@ export class NativeCodexWebSocketTransport {
|
|
|
424
406
|
if (nextTurnState !== undefined)
|
|
425
407
|
entry.turnState = nextTurnState;
|
|
426
408
|
}
|
|
427
|
-
const
|
|
409
|
+
const chunks = translator.push(event);
|
|
410
|
+
const output = normalizedOutputItem(event, translator);
|
|
428
411
|
if (output !== undefined)
|
|
429
412
|
outputItems.push(output);
|
|
430
413
|
if (event.type === 'response.completed') {
|
|
@@ -435,7 +418,6 @@ export class NativeCodexWebSocketTransport {
|
|
|
435
418
|
const responseId = typeof response?.id === 'string' ? response.id : '';
|
|
436
419
|
entry.protocol.complete(responseId, outputItems);
|
|
437
420
|
}
|
|
438
|
-
const chunks = translator.push(event);
|
|
439
421
|
if (!prewarm)
|
|
440
422
|
for (const chunk of chunks)
|
|
441
423
|
yield chunk;
|
|
@@ -616,7 +598,11 @@ export class NativeCodexWebSocketTransport {
|
|
|
616
598
|
}
|
|
617
599
|
if (failureValue.code === 'WS_UPGRADE_REQUIRED' || retryable) {
|
|
618
600
|
entry.disabled = true;
|
|
619
|
-
|
|
601
|
+
if (remainingRetryDelay(failureValue) !== undefined) {
|
|
602
|
+
await this.wait(reconnects, failureValue, signal, telemetry);
|
|
603
|
+
}
|
|
604
|
+
else
|
|
605
|
+
telemetry.retry(0);
|
|
620
606
|
yield* this.http.stream(activeGeneration, fallbackMode(), telemetry);
|
|
621
607
|
requestCompleted = true;
|
|
622
608
|
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 {
|
|
@@ -9,18 +9,14 @@ export interface ResolvedImagePart {
|
|
|
9
9
|
dataBase64: string;
|
|
10
10
|
detail?: CodexImageDetail;
|
|
11
11
|
}
|
|
12
|
-
export interface ResolvedToolResultPart {
|
|
13
|
-
type: 'tool-result';
|
|
14
|
-
toolCallId: ToolCallId;
|
|
15
|
-
content: readonly ResolvedContentPart[];
|
|
16
|
-
isError?: boolean;
|
|
17
|
-
}
|
|
18
12
|
export type ResolvedContentPart = Exclude<ContentBlock, {
|
|
19
|
-
type: 'image'
|
|
20
|
-
}> | ResolvedImagePart
|
|
13
|
+
type: 'image';
|
|
14
|
+
}> | ResolvedImagePart;
|
|
21
15
|
export interface ResolvedMessage {
|
|
22
|
-
role: 'system' | 'user' | 'assistant';
|
|
16
|
+
role: 'system' | 'user' | 'assistant' | 'tool' | 'developer';
|
|
23
17
|
content: readonly ResolvedContentPart[];
|
|
18
|
+
toolCallId?: ToolCallId;
|
|
19
|
+
isError?: boolean;
|
|
24
20
|
replaySource?: NativeCodexReplaySource;
|
|
25
21
|
}
|
|
26
22
|
export interface ResponsesRequestInput {
|
|
@@ -57,7 +53,7 @@ export interface ResponsesUsage {
|
|
|
57
53
|
/** Map provider totals to DSH's strict disjoint token counts. */
|
|
58
54
|
export declare function mapResponsesUsage(usage: ResponsesUsage): TokenUsage;
|
|
59
55
|
/** Classify in-band failure data without reflecting provider text. */
|
|
60
|
-
export declare function responsesFailure(code?: string, message?: string): LlmError;
|
|
56
|
+
export declare function responsesFailure(code?: string, message?: string, headers?: unknown): LlmError;
|
|
61
57
|
interface ResponsesOutputItem {
|
|
62
58
|
type?: string;
|
|
63
59
|
id?: string;
|
|
@@ -68,6 +64,7 @@ interface ResponsesOutputItem {
|
|
|
68
64
|
encrypted_content?: string;
|
|
69
65
|
summary?: unknown[];
|
|
70
66
|
status?: string;
|
|
67
|
+
phase?: CodexMessagePhase | null;
|
|
71
68
|
content?: Array<{
|
|
72
69
|
type?: string;
|
|
73
70
|
text?: string;
|
|
@@ -90,6 +87,7 @@ export interface ResponsesStreamEvent {
|
|
|
90
87
|
error?: {
|
|
91
88
|
code?: string;
|
|
92
89
|
message?: string;
|
|
90
|
+
headers?: unknown;
|
|
93
91
|
};
|
|
94
92
|
incomplete_details?: {
|
|
95
93
|
reason?: string;
|
|
@@ -107,11 +105,15 @@ export declare class ResponsesStreamTranslator {
|
|
|
107
105
|
private readonly replayContext?;
|
|
108
106
|
private readonly blocks;
|
|
109
107
|
private readonly order;
|
|
108
|
+
private readonly messagePhases;
|
|
110
109
|
private readonly replayCapture;
|
|
111
110
|
private nextIndex;
|
|
112
111
|
private sawToolCall;
|
|
113
112
|
terminated: boolean;
|
|
114
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;
|
|
115
117
|
private append;
|
|
116
118
|
private fill;
|
|
117
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_';
|
|
@@ -26,12 +27,12 @@ function imageItem(image) {
|
|
|
26
27
|
return { type: 'input_image', image_url: `data:${image.mediaType};base64,${image.dataBase64}`,
|
|
27
28
|
...image.detail === undefined ? {} : { detail: image.detail } };
|
|
28
29
|
}
|
|
29
|
-
function toolOutput(
|
|
30
|
-
const images =
|
|
30
|
+
function toolOutput(content) {
|
|
31
|
+
const images = content.some(part => part.type === 'image');
|
|
31
32
|
if (!images) {
|
|
32
|
-
return
|
|
33
|
+
return content.map(part => part.type === 'text' ? part.text : '').join('');
|
|
33
34
|
}
|
|
34
|
-
return
|
|
35
|
+
return content.map((part) => {
|
|
35
36
|
if (part.type === 'text')
|
|
36
37
|
return { type: 'input_text', text: part.text };
|
|
37
38
|
if (part.type === 'image' && 'dataBase64' in part)
|
|
@@ -67,6 +68,29 @@ export function toResponsesInput(messages, system) {
|
|
|
67
68
|
input.push({ type: 'message', role: message.role, content });
|
|
68
69
|
content = [];
|
|
69
70
|
};
|
|
71
|
+
// Handle tool messages separately - they have no content blocks, just top-level fields
|
|
72
|
+
if (message.role === 'tool') {
|
|
73
|
+
if (message.toolCallId === undefined) {
|
|
74
|
+
throw fixedError('native Codex tool message requires toolCallId', 'INVALID_ARGS');
|
|
75
|
+
}
|
|
76
|
+
input.push({
|
|
77
|
+
type: 'function_call_output',
|
|
78
|
+
call_id: String(message.toolCallId),
|
|
79
|
+
output: toolOutput(message.content),
|
|
80
|
+
...message.isError === true ? { is_error: true } : {},
|
|
81
|
+
});
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
// Handle developer messages - map to developer role in Responses API
|
|
85
|
+
if (message.role === 'developer') {
|
|
86
|
+
for (const block of message.content) {
|
|
87
|
+
if (block.type === 'text') {
|
|
88
|
+
content.push({ type: 'input_text', text: block.text });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
flush();
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
70
94
|
for (const block of message.content) {
|
|
71
95
|
if (message.role === 'system') {
|
|
72
96
|
if (block.type === 'text')
|
|
@@ -89,18 +113,15 @@ export function toResponsesInput(messages, system) {
|
|
|
89
113
|
content.push(imageItem(block));
|
|
90
114
|
break;
|
|
91
115
|
case 'tool-call':
|
|
116
|
+
if (message.role !== 'assistant') {
|
|
117
|
+
throw fixedError('native Codex tool calls require assistant messages', 'INVALID_ARGS');
|
|
118
|
+
}
|
|
92
119
|
flush();
|
|
93
120
|
input.push({
|
|
94
121
|
type: 'function_call', call_id: String(block.id),
|
|
95
122
|
name: block.name, arguments: block.arguments,
|
|
96
123
|
});
|
|
97
124
|
break;
|
|
98
|
-
case 'tool-result':
|
|
99
|
-
flush();
|
|
100
|
-
input.push({
|
|
101
|
-
type: 'function_call_output', call_id: String(block.toolCallId), output: toolOutput(block),
|
|
102
|
-
});
|
|
103
|
-
break;
|
|
104
125
|
case 'reasoning':
|
|
105
126
|
break; // Visible reasoning is never replayed; M4 may add encrypted items.
|
|
106
127
|
default:
|
|
@@ -191,11 +212,21 @@ export function codexRequestBody(options, messages, mode = {}) {
|
|
|
191
212
|
...mode.serviceTier === undefined ? {} : { service_tier: mode.serviceTier },
|
|
192
213
|
...options.sessionId === undefined ? {} : { prompt_cache_key: String(options.sessionId) },
|
|
193
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
|
+
}];
|
|
194
226
|
if (mode.responsesLite === undefined) {
|
|
195
227
|
return {
|
|
196
228
|
...common,
|
|
197
|
-
|
|
198
|
-
input,
|
|
229
|
+
input: [...instructionPrefix, ...input],
|
|
199
230
|
...options.tools !== undefined && options.tools.length > 0
|
|
200
231
|
? { tools: toResponsesTools(options.tools) } : {},
|
|
201
232
|
parallel_tool_calls: true,
|
|
@@ -204,7 +235,6 @@ export function codexRequestBody(options, messages, mode = {}) {
|
|
|
204
235
|
};
|
|
205
236
|
}
|
|
206
237
|
const tools = toResponsesLiteTools(options.tools ?? []);
|
|
207
|
-
const baseInstructions = mode.responsesLite.instructionsTemplate ?? instructions;
|
|
208
238
|
const contextualInstructions = mode.responsesLite.instructionsTemplate !== undefined
|
|
209
239
|
&& resolved.instructions !== undefined && resolved.instructions !== baseInstructions
|
|
210
240
|
? [{
|
|
@@ -212,21 +242,12 @@ export function codexRequestBody(options, messages, mode = {}) {
|
|
|
212
242
|
content: [{ type: 'input_text', text: resolved.instructions }],
|
|
213
243
|
}]
|
|
214
244
|
: [];
|
|
215
|
-
const prefixNamespace = uuidBytes(uuidV5(UUID_NAMESPACE_OID, String(options.sessionId ?? options.model)));
|
|
216
245
|
const prefix = [{
|
|
217
246
|
type: 'additional_tools',
|
|
218
247
|
id: `at_${uuidV5(prefixNamespace, JSON.stringify(tools))}`,
|
|
219
248
|
role: 'developer',
|
|
220
249
|
tools,
|
|
221
|
-
},
|
|
222
|
-
type: 'message',
|
|
223
|
-
id: `msg_${uuidV5(prefixNamespace, baseInstructions)}`,
|
|
224
|
-
role: 'developer',
|
|
225
|
-
content: [{ type: 'input_text', text: baseInstructions }],
|
|
226
|
-
internal_chat_message_metadata_passthrough: {
|
|
227
|
-
content_item_kinds: ['model.base_instructions'],
|
|
228
|
-
},
|
|
229
|
-
}];
|
|
250
|
+
}, ...instructionPrefix];
|
|
230
251
|
return {
|
|
231
252
|
...common,
|
|
232
253
|
input: [...prefix, ...contextualInstructions, ...input],
|
|
@@ -273,10 +294,11 @@ function retryDelay(message) {
|
|
|
273
294
|
const value = Number(match[1]);
|
|
274
295
|
if (!Number.isFinite(value) || value <= 0)
|
|
275
296
|
return undefined;
|
|
276
|
-
|
|
297
|
+
const delay = match[2]?.toLowerCase() === 'ms' ? value : value * 1000;
|
|
298
|
+
return Number.isFinite(delay) && delay <= Number.MAX_SAFE_INTEGER ? delay : undefined;
|
|
277
299
|
}
|
|
278
300
|
/** Classify in-band failure data without reflecting provider text. */
|
|
279
|
-
export function responsesFailure(code, message) {
|
|
301
|
+
export function responsesFailure(code, message, headers) {
|
|
280
302
|
const detail = `${code ?? ''} ${message ?? ''}`;
|
|
281
303
|
if (code === 'context_length_exceeded' || code === 'context_window_exceeded'
|
|
282
304
|
|| isContextWindowExceededError(detail)) {
|
|
@@ -288,8 +310,8 @@ export function responsesFailure(code, message) {
|
|
|
288
310
|
return fixedError('native Codex account quota is exhausted', QUOTA_EXCEEDED_CODE);
|
|
289
311
|
}
|
|
290
312
|
if (code === 'rate_limit_exceeded' || code === 'slow_down') {
|
|
291
|
-
const delay = retryDelay(message);
|
|
292
|
-
return
|
|
313
|
+
const delay = jsonRetryAfterMs(headers) ?? retryDelay(message);
|
|
314
|
+
return retryError('native Codex request was rate limited', 'RATE_LIMIT', {
|
|
293
315
|
...delay === undefined ? {} : { providerRetryAfterMs: delay },
|
|
294
316
|
});
|
|
295
317
|
}
|
|
@@ -299,7 +321,10 @@ export function responsesFailure(code, message) {
|
|
|
299
321
|
if (code === 'bio_policy') {
|
|
300
322
|
return fixedError('native Codex rejected the request', 'INVALID_REQUEST');
|
|
301
323
|
}
|
|
302
|
-
|
|
324
|
+
const delay = jsonRetryAfterMs(headers);
|
|
325
|
+
return retryError('native Codex reported a failed response', 'SERVER', {
|
|
326
|
+
...delay === undefined ? {} : { providerRetryAfterMs: delay },
|
|
327
|
+
});
|
|
303
328
|
}
|
|
304
329
|
function eventItemId(event) {
|
|
305
330
|
if (typeof event.item_id === 'string' && event.item_id.length > 0)
|
|
@@ -323,6 +348,7 @@ export class ResponsesStreamTranslator {
|
|
|
323
348
|
replayContext;
|
|
324
349
|
blocks = new Map();
|
|
325
350
|
order = [];
|
|
351
|
+
messagePhases = new Map();
|
|
326
352
|
replayCapture;
|
|
327
353
|
nextIndex = 0;
|
|
328
354
|
sawToolCall = false;
|
|
@@ -333,6 +359,18 @@ export class ResponsesStreamTranslator {
|
|
|
333
359
|
? undefined
|
|
334
360
|
: new NativeCodexReplayCapture(replayContext.provider, replayContext.model);
|
|
335
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
|
+
}
|
|
336
374
|
append(block, delta) {
|
|
337
375
|
block.text += delta;
|
|
338
376
|
}
|
|
@@ -380,6 +418,8 @@ export class ResponsesStreamTranslator {
|
|
|
380
418
|
switch (event.type) {
|
|
381
419
|
case 'response.output_item.added': {
|
|
382
420
|
const item = event.item;
|
|
421
|
+
if (item?.type === 'message')
|
|
422
|
+
this.captureMessagePhase(item);
|
|
383
423
|
if (item?.type !== 'function_call')
|
|
384
424
|
return chunks;
|
|
385
425
|
if (item.id === undefined || item.id.length === 0
|
|
@@ -463,6 +503,8 @@ export class ResponsesStreamTranslator {
|
|
|
463
503
|
});
|
|
464
504
|
}
|
|
465
505
|
else if (item.type === 'message') {
|
|
506
|
+
this.captureMessagePhase(item);
|
|
507
|
+
const phase = this.messagePhase(item.id);
|
|
466
508
|
if (!Array.isArray(item.content)) {
|
|
467
509
|
throw fixedError('native Codex message item has invalid content', 'MALFORMED_RESPONSE');
|
|
468
510
|
}
|
|
@@ -487,6 +529,7 @@ export class ResponsesStreamTranslator {
|
|
|
487
529
|
if (this.replayContext !== undefined)
|
|
488
530
|
this.replayCapture?.add({
|
|
489
531
|
type: 'message', ...(replayId === undefined ? {} : { id: replayId }), blocks: refs,
|
|
532
|
+
...(phase === undefined ? {} : { phase }),
|
|
490
533
|
});
|
|
491
534
|
}
|
|
492
535
|
else if (item.type === 'reasoning') {
|
|
@@ -575,7 +618,7 @@ export class ResponsesStreamTranslator {
|
|
|
575
618
|
return chunks;
|
|
576
619
|
}
|
|
577
620
|
case 'response.failed':
|
|
578
|
-
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);
|
|
579
622
|
case 'error':
|
|
580
623
|
throw responsesFailure(event.code, event.message);
|
|
581
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
|
+
}
|