@pure01fx/dsh-openai-codex-auth 0.5.0 → 0.6.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 +42 -0
- package/README.md +33 -5
- package/client.js +186 -16
- package/cordis.patch.yml +2 -6
- package/lib/catalog.d.ts +77 -0
- package/lib/catalog.js +383 -0
- package/lib/endpoint.d.ts +2 -0
- package/lib/endpoint.js +38 -0
- package/lib/index.d.ts +63 -17
- package/lib/index.js +535 -130
- package/lib/native-adapter.d.ts +43 -0
- package/lib/native-adapter.js +246 -0
- package/lib/native-http.d.ts +61 -0
- package/lib/native-http.js +602 -0
- package/lib/native-websocket-session.d.ts +14 -0
- package/lib/native-websocket-session.js +109 -0
- package/lib/native-websocket-socket.d.ts +28 -0
- package/lib/native-websocket-socket.js +215 -0
- package/lib/native-websocket.d.ts +45 -0
- package/lib/native-websocket.js +615 -0
- package/lib/rate-limits.d.ts +35 -0
- package/lib/rate-limits.js +189 -0
- package/lib/replay.d.ts +49 -0
- package/lib/replay.js +240 -0
- package/lib/response-usage.d.ts +14 -0
- package/lib/response-usage.js +35 -0
- package/lib/responses.d.ts +126 -0
- package/lib/responses.js +572 -0
- package/lib/sse.d.ts +13 -0
- package/lib/sse.js +81 -0
- package/lib/upstream.d.ts +6 -0
- package/lib/upstream.js +6 -0
- package/lib/usage.d.ts +29 -0
- package/lib/usage.js +160 -0
- package/package.json +47 -11
package/lib/responses.js
ADDED
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
/** Pure DSH-to-Codex Responses request and stream translation. */
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { CallId, CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, isContextWindowExceededError, isQuotaExceededError, LlmError, QUOTA_EXCEEDED_CODE, } from '@deepseek-ai/dsh-llm';
|
|
4
|
+
import { parseSse } from './sse.js';
|
|
5
|
+
import { NativeCodexReplayCapture, replayAssistantInput, replayableItemId, } from './replay.js';
|
|
6
|
+
export const DEFAULT_CODEX_INSTRUCTIONS = 'You are Codex, an AI coding agent. Help the user with software engineering tasks.';
|
|
7
|
+
const CALL_ID_MAX_LENGTH = 64;
|
|
8
|
+
const CALL_ID_PREFIX = 'call_';
|
|
9
|
+
function fixedError(message, code) { return new LlmError(message, code); }
|
|
10
|
+
function imageItem(image) {
|
|
11
|
+
if (!/^image[/][a-z0-9.+-]+$/i.test(image.mediaType) || image.dataBase64.length === 0) {
|
|
12
|
+
throw fixedError('native Codex request contains invalid resolved image data', 'MALFORMED_REQUEST');
|
|
13
|
+
}
|
|
14
|
+
return { type: 'input_image', image_url: `data:${image.mediaType};base64,${image.dataBase64}` };
|
|
15
|
+
}
|
|
16
|
+
function toolOutput(block) {
|
|
17
|
+
const images = block.content.some(part => part.type === 'image');
|
|
18
|
+
if (!images) {
|
|
19
|
+
return block.content.map(part => part.type === 'text' ? part.text : '').join('');
|
|
20
|
+
}
|
|
21
|
+
return block.content.map((part) => {
|
|
22
|
+
if (part.type === 'text')
|
|
23
|
+
return { type: 'input_text', text: part.text };
|
|
24
|
+
if (part.type === 'image' && 'dataBase64' in part)
|
|
25
|
+
return imageItem(part);
|
|
26
|
+
throw fixedError('native Codex tool output contains an unsupported content block', 'UNSUPPORTED');
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
export function toResponsesTools(tools) {
|
|
30
|
+
return tools.map(tool => ({
|
|
31
|
+
type: 'function', name: tool.name, description: tool.description, parameters: tool.parameters,
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
/** Convert resolved DSH messages into Responses instructions and ordered input items. */
|
|
35
|
+
export function toResponsesInput(messages, system) {
|
|
36
|
+
const input = [];
|
|
37
|
+
const systemTexts = [];
|
|
38
|
+
for (const message of messages) {
|
|
39
|
+
if (message.role === 'assistant' && message.replaySource !== undefined) {
|
|
40
|
+
input.push(...replayAssistantInput(message.content, message.replaySource));
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
let content = [];
|
|
44
|
+
const flush = () => {
|
|
45
|
+
if (content.length === 0)
|
|
46
|
+
return;
|
|
47
|
+
input.push({ type: 'message', role: message.role, content });
|
|
48
|
+
content = [];
|
|
49
|
+
};
|
|
50
|
+
for (const block of message.content) {
|
|
51
|
+
if (message.role === 'system') {
|
|
52
|
+
if (block.type === 'text')
|
|
53
|
+
systemTexts.push(block.text);
|
|
54
|
+
else if (block.type === 'image') {
|
|
55
|
+
throw fixedError('native Codex does not support images in system messages', 'UNSUPPORTED');
|
|
56
|
+
}
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
switch (block.type) {
|
|
60
|
+
case 'text':
|
|
61
|
+
content.push({
|
|
62
|
+
type: message.role === 'assistant' ? 'output_text' : 'input_text', text: block.text,
|
|
63
|
+
});
|
|
64
|
+
break;
|
|
65
|
+
case 'image':
|
|
66
|
+
if (message.role === 'assistant') {
|
|
67
|
+
throw fixedError('native Codex does not support assistant image history', 'UNSUPPORTED');
|
|
68
|
+
}
|
|
69
|
+
content.push(imageItem(block));
|
|
70
|
+
break;
|
|
71
|
+
case 'tool-call':
|
|
72
|
+
flush();
|
|
73
|
+
input.push({
|
|
74
|
+
type: 'function_call', call_id: String(block.id),
|
|
75
|
+
name: block.name, arguments: block.arguments,
|
|
76
|
+
});
|
|
77
|
+
break;
|
|
78
|
+
case 'tool-result':
|
|
79
|
+
flush();
|
|
80
|
+
input.push({
|
|
81
|
+
type: 'function_call_output', call_id: String(block.toolCallId), output: toolOutput(block),
|
|
82
|
+
});
|
|
83
|
+
break;
|
|
84
|
+
case 'reasoning':
|
|
85
|
+
break; // Visible reasoning is never replayed; M4 may add encrypted items.
|
|
86
|
+
default:
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
flush();
|
|
91
|
+
}
|
|
92
|
+
const instructions = system ?? (systemTexts.length === 0 ? undefined : systemTexts.join('\n\n'));
|
|
93
|
+
return { ...instructions === undefined ? {} : { instructions }, input };
|
|
94
|
+
}
|
|
95
|
+
/** Bound call ids while preserving every function call/result correlation. */
|
|
96
|
+
export function normalizeCodexCallIds(input) {
|
|
97
|
+
const mapping = new Map();
|
|
98
|
+
const used = new Set();
|
|
99
|
+
const callId = (item) => (item.type === 'function_call' || item.type === 'function_call_output')
|
|
100
|
+
&& typeof item.call_id === 'string' ? item.call_id : undefined;
|
|
101
|
+
for (const item of input) {
|
|
102
|
+
const id = callId(item);
|
|
103
|
+
if (id !== undefined && id.length <= CALL_ID_MAX_LENGTH) {
|
|
104
|
+
mapping.set(id, id);
|
|
105
|
+
used.add(id);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
for (const item of input) {
|
|
109
|
+
const id = callId(item);
|
|
110
|
+
if (id === undefined || mapping.has(id))
|
|
111
|
+
continue;
|
|
112
|
+
let attempt = 0;
|
|
113
|
+
let normalized;
|
|
114
|
+
do {
|
|
115
|
+
const hash = createHash('sha256');
|
|
116
|
+
if (attempt > 0)
|
|
117
|
+
hash.update(String(attempt)).update(String.fromCharCode(0));
|
|
118
|
+
normalized = `${CALL_ID_PREFIX}${hash.update(id).digest('hex').slice(0, CALL_ID_MAX_LENGTH - CALL_ID_PREFIX.length)}`;
|
|
119
|
+
attempt += 1;
|
|
120
|
+
} while (used.has(normalized));
|
|
121
|
+
mapping.set(id, normalized);
|
|
122
|
+
used.add(normalized);
|
|
123
|
+
}
|
|
124
|
+
return input.map((item) => {
|
|
125
|
+
const id = callId(item);
|
|
126
|
+
const normalized = id === undefined ? undefined : mapping.get(id);
|
|
127
|
+
return normalized === undefined || normalized === id ? item : { ...item, call_id: normalized };
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
function assertSupportedOptions(options) {
|
|
131
|
+
if (options.temperature !== undefined) {
|
|
132
|
+
throw fixedError('native Codex does not support temperature', 'UNSUPPORTED');
|
|
133
|
+
}
|
|
134
|
+
if (options.maxTokens !== undefined) {
|
|
135
|
+
throw fixedError('native Codex does not support maxTokens', 'UNSUPPORTED');
|
|
136
|
+
}
|
|
137
|
+
if (options.stop !== undefined) {
|
|
138
|
+
throw fixedError('native Codex does not support stop sequences', 'UNSUPPORTED');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/** Build the canonical Standard/Fast HTTP Responses body. */
|
|
142
|
+
export function codexRequestBody(options, messages, mode = {}) {
|
|
143
|
+
assertSupportedOptions(options);
|
|
144
|
+
if (mode.serviceTier !== undefined && mode.serviceTier !== 'priority') {
|
|
145
|
+
throw fixedError('native Codex service tier is invalid', 'INVALID_ARGS');
|
|
146
|
+
}
|
|
147
|
+
const resolved = toResponsesInput(messages, options.system);
|
|
148
|
+
return {
|
|
149
|
+
model: options.model,
|
|
150
|
+
instructions: resolved.instructions ?? DEFAULT_CODEX_INSTRUCTIONS,
|
|
151
|
+
input: normalizeCodexCallIds(resolved.input),
|
|
152
|
+
...options.tools !== undefined && options.tools.length > 0
|
|
153
|
+
? { tools: toResponsesTools(options.tools) } : {},
|
|
154
|
+
tool_choice: 'auto',
|
|
155
|
+
parallel_tool_calls: true,
|
|
156
|
+
...options.reasoningEffort === undefined ? {}
|
|
157
|
+
: { reasoning: { effort: String(options.reasoningEffort), summary: 'auto' } },
|
|
158
|
+
store: false,
|
|
159
|
+
stream: true,
|
|
160
|
+
include: ['reasoning.encrypted_content'],
|
|
161
|
+
...mode.serviceTier === undefined ? {} : { service_tier: mode.serviceTier },
|
|
162
|
+
...options.sessionId === undefined ? {} : { prompt_cache_key: String(options.sessionId) },
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function tokenCount(value, field, fallback) {
|
|
166
|
+
if (value === undefined && fallback !== undefined)
|
|
167
|
+
return fallback;
|
|
168
|
+
if (value === undefined || !Number.isSafeInteger(value) || value < 0) {
|
|
169
|
+
throw fixedError(`native Codex response contains invalid ${field} usage`, 'MALFORMED_RESPONSE');
|
|
170
|
+
}
|
|
171
|
+
return value;
|
|
172
|
+
}
|
|
173
|
+
/** Map provider totals to DSH's strict disjoint token counts. */
|
|
174
|
+
export function mapResponsesUsage(usage) {
|
|
175
|
+
const totalInput = tokenCount(usage.input_tokens, 'input token');
|
|
176
|
+
const output = tokenCount(usage.output_tokens, 'output token');
|
|
177
|
+
const cached = tokenCount(usage.input_tokens_details?.cached_tokens, 'cached token', 0);
|
|
178
|
+
const written = tokenCount(usage.input_tokens_details?.cache_write_tokens, 'cache-write token', 0);
|
|
179
|
+
const reasoning = tokenCount(usage.output_tokens_details?.reasoning_tokens, 'reasoning token', 0);
|
|
180
|
+
const uncached = totalInput - cached - written;
|
|
181
|
+
if (uncached < 0) {
|
|
182
|
+
throw fixedError('native Codex response contains inconsistent input usage', 'MALFORMED_RESPONSE');
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
inputTokens: uncached, outputTokens: output,
|
|
186
|
+
...cached === 0 ? {} : { cacheReadTokens: cached },
|
|
187
|
+
...written === 0 ? {} : { cacheWriteTokens: written },
|
|
188
|
+
...reasoning === 0 ? {} : { reasoningTokens: reasoning },
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function retryDelay(message) {
|
|
192
|
+
const match = message?.match(/try again in[ ]*([0-9]+(?:[.][0-9]+)?)[ ]*(ms|s|seconds?)/i);
|
|
193
|
+
if (match === null || match === undefined)
|
|
194
|
+
return undefined;
|
|
195
|
+
const value = Number(match[1]);
|
|
196
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
197
|
+
return undefined;
|
|
198
|
+
return Math.min(match[2]?.toLowerCase() === 'ms' ? value : value * 1000, 600_000);
|
|
199
|
+
}
|
|
200
|
+
/** Classify in-band failure data without reflecting provider text. */
|
|
201
|
+
export function responsesFailure(code, message) {
|
|
202
|
+
const detail = `${code ?? ''} ${message ?? ''}`;
|
|
203
|
+
if (code === 'context_length_exceeded' || code === 'context_window_exceeded'
|
|
204
|
+
|| isContextWindowExceededError(detail)) {
|
|
205
|
+
return fixedError('native Codex request exceeded the model context window', CONTEXT_WINDOW_EXCEEDED_CODE);
|
|
206
|
+
}
|
|
207
|
+
if (code === 'insufficient_quota' || isQuotaExceededError(detail)) {
|
|
208
|
+
return fixedError('native Codex account quota is exhausted', QUOTA_EXCEEDED_CODE);
|
|
209
|
+
}
|
|
210
|
+
if (code === 'rate_limit_exceeded') {
|
|
211
|
+
const delay = retryDelay(message);
|
|
212
|
+
return new LlmError('native Codex request was rate limited', 'RATE_LIMIT', {
|
|
213
|
+
...delay === undefined ? {} : { providerRetryAfterMs: delay },
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
if (code === 'invalid_prompt' || code === 'bio_policy') {
|
|
217
|
+
return fixedError('native Codex rejected the request', 'INVALID_REQUEST');
|
|
218
|
+
}
|
|
219
|
+
return fixedError('native Codex reported a failed response', 'SERVER');
|
|
220
|
+
}
|
|
221
|
+
function eventItemId(event) {
|
|
222
|
+
if (typeof event.item_id === 'string' && event.item_id.length > 0)
|
|
223
|
+
return event.item_id;
|
|
224
|
+
throw fixedError('native Codex SSE event has no item identity', 'MALFORMED_RESPONSE');
|
|
225
|
+
}
|
|
226
|
+
function eventDelta(event) {
|
|
227
|
+
if (typeof event.delta === 'string')
|
|
228
|
+
return event.delta;
|
|
229
|
+
throw fixedError('native Codex SSE event has invalid delta text', 'MALFORMED_RESPONSE');
|
|
230
|
+
}
|
|
231
|
+
function closeBlock(block) {
|
|
232
|
+
if (block.kind === 'text')
|
|
233
|
+
return { type: 'text', text: block.text };
|
|
234
|
+
if (block.kind === 'reasoning')
|
|
235
|
+
return { type: 'reasoning', text: block.text };
|
|
236
|
+
return { type: 'tool-call', id: CallId(block.callId), name: block.name ?? '', arguments: block.text };
|
|
237
|
+
}
|
|
238
|
+
/** Stateful, transport-free Responses event to DSH chunk translator. */
|
|
239
|
+
export class ResponsesStreamTranslator {
|
|
240
|
+
replayContext;
|
|
241
|
+
blocks = new Map();
|
|
242
|
+
order = [];
|
|
243
|
+
replayCapture;
|
|
244
|
+
nextIndex = 0;
|
|
245
|
+
sawToolCall = false;
|
|
246
|
+
terminated = false;
|
|
247
|
+
constructor(replayContext) {
|
|
248
|
+
this.replayContext = replayContext;
|
|
249
|
+
this.replayCapture = replayContext === undefined
|
|
250
|
+
? undefined
|
|
251
|
+
: new NativeCodexReplayCapture(replayContext.provider, replayContext.model);
|
|
252
|
+
}
|
|
253
|
+
open(key, kind, chunks, callId = '', name) {
|
|
254
|
+
const block = {
|
|
255
|
+
index: this.nextIndex++, kind, text: '', callId,
|
|
256
|
+
...name === undefined ? {} : { name },
|
|
257
|
+
};
|
|
258
|
+
this.blocks.set(key, block);
|
|
259
|
+
this.order.push(block);
|
|
260
|
+
chunks.push({ type: 'block-start', index: block.index, blockType: kind });
|
|
261
|
+
return block;
|
|
262
|
+
}
|
|
263
|
+
close(key, chunks) {
|
|
264
|
+
const block = this.blocks.get(key);
|
|
265
|
+
if (block === undefined)
|
|
266
|
+
return;
|
|
267
|
+
this.blocks.delete(key);
|
|
268
|
+
chunks.push({ type: 'block-end', index: block.index, block: closeBlock(block) });
|
|
269
|
+
}
|
|
270
|
+
closeItem(id, chunks) {
|
|
271
|
+
for (const key of [...this.blocks.keys()])
|
|
272
|
+
if (key.startsWith(`${id}:`))
|
|
273
|
+
this.close(key, chunks);
|
|
274
|
+
}
|
|
275
|
+
closeAll(chunks) {
|
|
276
|
+
for (const block of this.order) {
|
|
277
|
+
for (const [key, candidate] of this.blocks) {
|
|
278
|
+
if (candidate === block) {
|
|
279
|
+
this.close(key, chunks);
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
push(event) {
|
|
286
|
+
if (this.terminated)
|
|
287
|
+
return [];
|
|
288
|
+
const chunks = [];
|
|
289
|
+
switch (event.type) {
|
|
290
|
+
case 'response.output_item.added': {
|
|
291
|
+
const item = event.item;
|
|
292
|
+
if (item?.type !== 'function_call')
|
|
293
|
+
return chunks;
|
|
294
|
+
if (item.id === undefined || item.id.length === 0
|
|
295
|
+
|| item.call_id === undefined || item.call_id.length === 0
|
|
296
|
+
|| item.name === undefined || item.name.length === 0) {
|
|
297
|
+
throw fixedError('native Codex function call has invalid identity', 'MALFORMED_RESPONSE');
|
|
298
|
+
}
|
|
299
|
+
this.sawToolCall = true;
|
|
300
|
+
const block = this.open(`${item.id}:call`, 'tool-call', chunks, item.call_id, item.name);
|
|
301
|
+
chunks.push({
|
|
302
|
+
type: 'tool-call-delta', index: block.index, id: CallId(block.callId),
|
|
303
|
+
name: item.name, argumentsDelta: '',
|
|
304
|
+
});
|
|
305
|
+
return chunks;
|
|
306
|
+
}
|
|
307
|
+
case 'response.output_text.delta': {
|
|
308
|
+
const key = `${eventItemId(event)}:text:${String(event.content_index ?? 0)}`;
|
|
309
|
+
const block = this.blocks.get(key) ?? this.open(key, 'text', chunks);
|
|
310
|
+
const delta = eventDelta(event);
|
|
311
|
+
block.text += delta;
|
|
312
|
+
chunks.push({ type: 'text-delta', index: block.index, text: delta });
|
|
313
|
+
return chunks;
|
|
314
|
+
}
|
|
315
|
+
case 'response.reasoning_summary_text.delta': {
|
|
316
|
+
const key = `${eventItemId(event)}:summary:${String(event.summary_index ?? 0)}`;
|
|
317
|
+
const block = this.blocks.get(key) ?? this.open(key, 'reasoning', chunks);
|
|
318
|
+
const delta = eventDelta(event);
|
|
319
|
+
block.text += delta;
|
|
320
|
+
chunks.push({ type: 'reasoning-delta', index: block.index, text: delta });
|
|
321
|
+
return chunks;
|
|
322
|
+
}
|
|
323
|
+
case 'response.reasoning_text.delta':
|
|
324
|
+
return chunks;
|
|
325
|
+
case 'response.function_call_arguments.delta': {
|
|
326
|
+
const key = `${eventItemId(event)}:call`;
|
|
327
|
+
const block = this.blocks.get(key);
|
|
328
|
+
if (block === undefined || block.kind !== 'tool-call') {
|
|
329
|
+
throw fixedError('native Codex function arguments have no open call', 'MALFORMED_RESPONSE');
|
|
330
|
+
}
|
|
331
|
+
const delta = eventDelta(event);
|
|
332
|
+
block.text += delta;
|
|
333
|
+
chunks.push({
|
|
334
|
+
type: 'tool-call-delta', index: block.index, id: CallId(block.callId),
|
|
335
|
+
...block.name === undefined ? {} : { name: block.name }, argumentsDelta: delta,
|
|
336
|
+
});
|
|
337
|
+
return chunks;
|
|
338
|
+
}
|
|
339
|
+
case 'response.output_item.done': {
|
|
340
|
+
const item = event.item;
|
|
341
|
+
if (item?.id === undefined || item.id.length === 0) {
|
|
342
|
+
throw fixedError('native Codex completed item has no identity', 'MALFORMED_RESPONSE');
|
|
343
|
+
}
|
|
344
|
+
const replayId = this.replayContext === undefined ? undefined : replayableItemId(item.id);
|
|
345
|
+
if (item.type === 'function_call') {
|
|
346
|
+
if (item.call_id === undefined || item.call_id.length === 0
|
|
347
|
+
|| item.name === undefined || item.name.length === 0
|
|
348
|
+
|| typeof item.arguments !== 'string') {
|
|
349
|
+
throw fixedError('native Codex function call has invalid content', 'MALFORMED_RESPONSE');
|
|
350
|
+
}
|
|
351
|
+
const key = `${item.id}:call`;
|
|
352
|
+
let block = this.blocks.get(key);
|
|
353
|
+
if (block === undefined) {
|
|
354
|
+
this.sawToolCall = true;
|
|
355
|
+
block = this.open(key, 'tool-call', chunks, item.call_id, item.name);
|
|
356
|
+
}
|
|
357
|
+
else if (block.callId !== item.call_id || block.name !== item.name
|
|
358
|
+
|| (block.text.length > 0 && block.text !== item.arguments)) {
|
|
359
|
+
throw fixedError('native Codex function call changed during streaming', 'MALFORMED_RESPONSE');
|
|
360
|
+
}
|
|
361
|
+
block.callId = item.call_id;
|
|
362
|
+
block.name = item.name;
|
|
363
|
+
if (block.text.length === 0)
|
|
364
|
+
block.text = item.arguments;
|
|
365
|
+
this.close(key, chunks);
|
|
366
|
+
if (this.replayContext !== undefined)
|
|
367
|
+
this.replayCapture?.add({
|
|
368
|
+
type: 'function_call', ...(replayId === undefined ? {} : { id: replayId }),
|
|
369
|
+
block: block.index,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
else if (item.type === 'message') {
|
|
373
|
+
if (!Array.isArray(item.content)) {
|
|
374
|
+
throw fixedError('native Codex message item has invalid content', 'MALFORMED_RESPONSE');
|
|
375
|
+
}
|
|
376
|
+
const refs = [];
|
|
377
|
+
const expected = new Set();
|
|
378
|
+
for (const [index, part] of item.content.entries()) {
|
|
379
|
+
if (part.type !== 'output_text' || typeof part.text !== 'string' || part.text.length === 0) {
|
|
380
|
+
throw fixedError('native Codex message item has unsupported content', 'MALFORMED_RESPONSE');
|
|
381
|
+
}
|
|
382
|
+
const key = `${item.id}:text:${String(index)}`;
|
|
383
|
+
expected.add(key);
|
|
384
|
+
const block = this.blocks.get(key) ?? this.open(key, 'text', chunks);
|
|
385
|
+
if (block.text.length > 0 && block.text !== part.text) {
|
|
386
|
+
throw fixedError('native Codex text changed during streaming', 'MALFORMED_RESPONSE');
|
|
387
|
+
}
|
|
388
|
+
if (block.text.length === 0)
|
|
389
|
+
block.text = part.text;
|
|
390
|
+
refs.push(block.index);
|
|
391
|
+
this.close(key, chunks);
|
|
392
|
+
}
|
|
393
|
+
if ([...this.blocks.keys()].some(key => key.startsWith(`${item.id}:text:`) && !expected.has(key)))
|
|
394
|
+
throw fixedError('native Codex message item has unmatched text', 'MALFORMED_RESPONSE');
|
|
395
|
+
if (this.replayContext !== undefined)
|
|
396
|
+
this.replayCapture?.add({
|
|
397
|
+
type: 'message', ...(replayId === undefined ? {} : { id: replayId }), blocks: refs,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
else if (item.type === 'reasoning') {
|
|
401
|
+
if (!Array.isArray(item.summary)) {
|
|
402
|
+
throw fixedError('native Codex reasoning summary is invalid', 'MALFORMED_RESPONSE');
|
|
403
|
+
}
|
|
404
|
+
const refs = [];
|
|
405
|
+
const expected = new Set();
|
|
406
|
+
for (const [index, part] of item.summary.entries()) {
|
|
407
|
+
if (typeof part !== 'object' || part === null
|
|
408
|
+
|| part.type !== 'summary_text'
|
|
409
|
+
|| typeof part.text !== 'string'
|
|
410
|
+
|| part.text.length === 0) {
|
|
411
|
+
throw fixedError('native Codex reasoning summary is invalid', 'MALFORMED_RESPONSE');
|
|
412
|
+
}
|
|
413
|
+
const text = part.text;
|
|
414
|
+
const key = `${item.id}:summary:${String(index)}`;
|
|
415
|
+
expected.add(key);
|
|
416
|
+
const block = this.blocks.get(key) ?? this.open(key, 'reasoning', chunks);
|
|
417
|
+
if (block.text.length > 0 && block.text !== text) {
|
|
418
|
+
throw fixedError('native Codex reasoning summary changed during streaming', 'MALFORMED_RESPONSE');
|
|
419
|
+
}
|
|
420
|
+
if (block.text.length === 0)
|
|
421
|
+
block.text = text;
|
|
422
|
+
refs.push(block.index);
|
|
423
|
+
this.close(key, chunks);
|
|
424
|
+
}
|
|
425
|
+
if ([...this.blocks.keys()].some(key => key.startsWith(`${item.id}:summary:`) && !expected.has(key)))
|
|
426
|
+
throw fixedError('native Codex reasoning item has unmatched summary', 'MALFORMED_RESPONSE');
|
|
427
|
+
const encryptedContent = item.encrypted_content;
|
|
428
|
+
if (encryptedContent !== undefined && encryptedContent !== null
|
|
429
|
+
&& (typeof encryptedContent !== 'string' || encryptedContent.length === 0)) {
|
|
430
|
+
throw fixedError('native Codex encrypted reasoning is invalid', 'MALFORMED_RESPONSE');
|
|
431
|
+
}
|
|
432
|
+
if (this.replayContext !== undefined)
|
|
433
|
+
this.replayCapture?.add({
|
|
434
|
+
type: 'reasoning', ...(replayId === undefined ? {} : { id: replayId }), blocks: refs,
|
|
435
|
+
...typeof encryptedContent === 'string'
|
|
436
|
+
? { encryptedContent } : {},
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
throw fixedError('native Codex completed item type is unsupported', 'UNSUPPORTED');
|
|
441
|
+
}
|
|
442
|
+
return chunks;
|
|
443
|
+
}
|
|
444
|
+
case 'response.completed': {
|
|
445
|
+
this.terminated = true;
|
|
446
|
+
this.closeAll(chunks);
|
|
447
|
+
if (event.response?.usage !== undefined) {
|
|
448
|
+
chunks.push({ type: 'usage', usage: mapResponsesUsage(event.response.usage) });
|
|
449
|
+
}
|
|
450
|
+
const replayState = this.order.length === 0
|
|
451
|
+
? undefined
|
|
452
|
+
: this.replayCapture?.finish();
|
|
453
|
+
if (this.order.length > 0 && this.replayContext !== undefined) {
|
|
454
|
+
if (replayState === undefined) {
|
|
455
|
+
throw fixedError('native Codex completed response has no replay descriptors', 'MALFORMED_RESPONSE');
|
|
456
|
+
}
|
|
457
|
+
replayAssistantInput(this.order.map(closeBlock), {
|
|
458
|
+
...this.replayContext,
|
|
459
|
+
replayState,
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
chunks.push(this.order.length === 0
|
|
463
|
+
? { type: 'finish', reason: { kind: 'error', failure: {
|
|
464
|
+
message: 'native Codex returned a completed response with no content',
|
|
465
|
+
code: EMPTY_RESPONSE_CODE,
|
|
466
|
+
} } }
|
|
467
|
+
: {
|
|
468
|
+
type: 'finish', reason: { kind: this.sawToolCall ? 'tool-calls' : 'stop' },
|
|
469
|
+
...(replayState === undefined ? {} : { replayState }),
|
|
470
|
+
});
|
|
471
|
+
return chunks;
|
|
472
|
+
}
|
|
473
|
+
case 'response.incomplete': {
|
|
474
|
+
const reason = event.response?.incomplete_details?.reason;
|
|
475
|
+
if (reason !== 'max_output_tokens') {
|
|
476
|
+
throw responsesFailure(reason, event.response?.error?.message);
|
|
477
|
+
}
|
|
478
|
+
this.terminated = true;
|
|
479
|
+
this.closeAll(chunks);
|
|
480
|
+
if (event.response?.usage !== undefined) {
|
|
481
|
+
chunks.push({ type: 'usage', usage: mapResponsesUsage(event.response.usage) });
|
|
482
|
+
}
|
|
483
|
+
chunks.push({ type: 'finish', reason: { kind: 'max-tokens' } });
|
|
484
|
+
return chunks;
|
|
485
|
+
}
|
|
486
|
+
case 'response.failed':
|
|
487
|
+
throw responsesFailure(event.response?.error?.code, event.response?.error?.message);
|
|
488
|
+
case 'error':
|
|
489
|
+
throw responsesFailure(event.code, event.message);
|
|
490
|
+
default:
|
|
491
|
+
return chunks;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
endOfStream() {
|
|
495
|
+
throw fixedError('native Codex SSE stream ended before response.completed', 'STREAM_CLOSED');
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
/** Validate one opaque sticky turn token before retaining or forwarding it. */
|
|
499
|
+
export function boundedCodexTurnState(value) {
|
|
500
|
+
let candidate = value;
|
|
501
|
+
for (let depth = 0; depth < 8 && Array.isArray(candidate); depth++)
|
|
502
|
+
candidate = candidate[0];
|
|
503
|
+
return typeof candidate === 'string' && candidate.length > 0
|
|
504
|
+
&& Buffer.byteLength(candidate) <= 4096 && !/[\r\n\0]/u.test(candidate)
|
|
505
|
+
? candidate : undefined;
|
|
506
|
+
}
|
|
507
|
+
/** Extract the bounded sticky turn token from provider metadata/event shapes. */
|
|
508
|
+
export function codexResponseTurnState(event) {
|
|
509
|
+
const row = event;
|
|
510
|
+
const response = typeof row.response === 'object' && row.response !== null
|
|
511
|
+
? row.response : undefined;
|
|
512
|
+
const direct = row.turn_state ?? response?.turn_state;
|
|
513
|
+
const boundedDirect = boundedCodexTurnState(direct);
|
|
514
|
+
if (boundedDirect !== undefined)
|
|
515
|
+
return boundedDirect;
|
|
516
|
+
const headers = typeof row.headers === 'object' && row.headers !== null
|
|
517
|
+
? row.headers : undefined;
|
|
518
|
+
if (headers === undefined)
|
|
519
|
+
return undefined;
|
|
520
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
521
|
+
if (key.toLowerCase() === 'x-codex-turn-state') {
|
|
522
|
+
const bounded = boundedCodexTurnState(value);
|
|
523
|
+
if (bounded !== undefined)
|
|
524
|
+
return bounded;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return undefined;
|
|
528
|
+
}
|
|
529
|
+
/** Consume framed SSE JSON into DSH chunks. */
|
|
530
|
+
export async function* streamResponses(stream, options = {}) {
|
|
531
|
+
const byteLimit = options.maxResponseBytes ?? 24 * 1024 * 1024;
|
|
532
|
+
const eventLimit = options.maxResponseEvents ?? 4096;
|
|
533
|
+
if (!Number.isSafeInteger(byteLimit) || byteLimit <= 0 || byteLimit > 24 * 1024 * 1024
|
|
534
|
+
|| !Number.isSafeInteger(eventLimit) || eventLimit <= 0 || eventLimit > 4096) {
|
|
535
|
+
throw fixedError('native Codex response limit is invalid', 'INVALID_CONFIG');
|
|
536
|
+
}
|
|
537
|
+
let responseBytes = 0;
|
|
538
|
+
let responseEvents = 0;
|
|
539
|
+
const translator = new ResponsesStreamTranslator(options.replayContext);
|
|
540
|
+
for await (const frame of parseSse(stream, {
|
|
541
|
+
...options,
|
|
542
|
+
onBytes: (bytes) => {
|
|
543
|
+
options.onBytes?.(bytes);
|
|
544
|
+
responseBytes += bytes;
|
|
545
|
+
if (responseBytes > byteLimit) {
|
|
546
|
+
throw fixedError('native Codex response exceeded the size limit', 'RESPONSE_TOO_LARGE');
|
|
547
|
+
}
|
|
548
|
+
},
|
|
549
|
+
})) {
|
|
550
|
+
responseEvents += 1;
|
|
551
|
+
if (responseEvents > eventLimit) {
|
|
552
|
+
throw fixedError('native Codex response had too many events', 'RESPONSE_TOO_LARGE');
|
|
553
|
+
}
|
|
554
|
+
let event;
|
|
555
|
+
try {
|
|
556
|
+
event = JSON.parse(frame.data);
|
|
557
|
+
}
|
|
558
|
+
catch {
|
|
559
|
+
options.onMalformedEvent?.();
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
if (typeof event !== 'object' || event === null || typeof event.type !== 'string') {
|
|
563
|
+
options.onMalformedEvent?.();
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
options.onEvent?.(event);
|
|
567
|
+
yield* translator.push(event);
|
|
568
|
+
if (translator.terminated)
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
translator.endOfStream();
|
|
572
|
+
}
|
package/lib/sse.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const DEFAULT_MAX_SSE_EVENT_BYTES: number;
|
|
2
|
+
export interface SseEvent {
|
|
3
|
+
data: string;
|
|
4
|
+
event?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ParseSseOptions {
|
|
7
|
+
signal?: AbortSignal;
|
|
8
|
+
onActivity?: () => void;
|
|
9
|
+
onBytes?: (bytes: number) => void;
|
|
10
|
+
maxEventBytes?: number;
|
|
11
|
+
}
|
|
12
|
+
/** Decode a byte stream into bounded SSE frames. */
|
|
13
|
+
export declare function parseSse(stream: ReadableStream<Uint8Array>, options?: ParseSseOptions): AsyncGenerator<SseEvent>;
|
package/lib/sse.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/** Bounded, cancellable Server-Sent Events byte framing. */
|
|
2
|
+
import { LlmError } from '@deepseek-ai/dsh-llm';
|
|
3
|
+
export const DEFAULT_MAX_SSE_EVENT_BYTES = 1024 * 1024;
|
|
4
|
+
function aborted() {
|
|
5
|
+
return new LlmError('native Codex SSE stream was cancelled', 'ABORTED');
|
|
6
|
+
}
|
|
7
|
+
function tooLarge() {
|
|
8
|
+
return new LlmError('native Codex SSE event exceeded the size limit', 'SSE_EVENT_TOO_LARGE');
|
|
9
|
+
}
|
|
10
|
+
/** Decode a byte stream into bounded SSE frames. */
|
|
11
|
+
export async function* parseSse(stream, options = {}) {
|
|
12
|
+
const limit = options.maxEventBytes ?? DEFAULT_MAX_SSE_EVENT_BYTES;
|
|
13
|
+
if (!Number.isSafeInteger(limit) || limit <= 0) {
|
|
14
|
+
throw new LlmError('native Codex SSE size limit is invalid', 'INVALID_CONFIG');
|
|
15
|
+
}
|
|
16
|
+
if (options.signal?.aborted === true)
|
|
17
|
+
throw aborted();
|
|
18
|
+
const reader = stream.getReader();
|
|
19
|
+
const decoder = new TextDecoder();
|
|
20
|
+
let pending = '';
|
|
21
|
+
let dataLines = [];
|
|
22
|
+
let eventName;
|
|
23
|
+
let eventBytes = 0;
|
|
24
|
+
let cancelled = false;
|
|
25
|
+
const onAbort = () => {
|
|
26
|
+
cancelled = true;
|
|
27
|
+
void reader.cancel(options.signal?.reason).catch(() => { });
|
|
28
|
+
};
|
|
29
|
+
options.signal?.addEventListener('abort', onAbort, { once: true });
|
|
30
|
+
try {
|
|
31
|
+
while (true) {
|
|
32
|
+
if (cancelled || Boolean(options.signal?.aborted))
|
|
33
|
+
throw aborted();
|
|
34
|
+
const { done, value } = await reader.read();
|
|
35
|
+
if (cancelled || Boolean(options.signal?.aborted))
|
|
36
|
+
throw aborted();
|
|
37
|
+
if (done)
|
|
38
|
+
return;
|
|
39
|
+
options.onActivity?.();
|
|
40
|
+
options.onBytes?.(value.byteLength);
|
|
41
|
+
pending += decoder.decode(value, { stream: true });
|
|
42
|
+
if (Buffer.byteLength(pending) > limit && !pending.includes('\n'))
|
|
43
|
+
throw tooLarge();
|
|
44
|
+
let newline = pending.indexOf('\n');
|
|
45
|
+
while (newline >= 0) {
|
|
46
|
+
let line = pending.slice(0, newline);
|
|
47
|
+
pending = pending.slice(newline + 1);
|
|
48
|
+
newline = pending.indexOf('\n');
|
|
49
|
+
if (line.charCodeAt(line.length - 1) === 13)
|
|
50
|
+
line = line.slice(0, -1);
|
|
51
|
+
if (line.length === 0) {
|
|
52
|
+
if (dataLines.length > 0) {
|
|
53
|
+
yield {
|
|
54
|
+
data: dataLines.join('\n'),
|
|
55
|
+
...eventName === undefined ? {} : { event: eventName },
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
dataLines = [];
|
|
59
|
+
eventName = undefined;
|
|
60
|
+
eventBytes = 0;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
eventBytes += Buffer.byteLength(line) + 1;
|
|
64
|
+
if (eventBytes > limit)
|
|
65
|
+
throw tooLarge();
|
|
66
|
+
if (line.startsWith(':'))
|
|
67
|
+
options.onActivity?.();
|
|
68
|
+
else if (line.startsWith('data:'))
|
|
69
|
+
dataLines.push(line.slice(5).replace(/^ /, ''));
|
|
70
|
+
else if (line.startsWith('event:'))
|
|
71
|
+
eventName = line.slice(6).replace(/^ /, '');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
options.signal?.removeEventListener('abort', onAbort);
|
|
77
|
+
if (!cancelled)
|
|
78
|
+
await reader.cancel().catch(() => { });
|
|
79
|
+
reader.releaseLock();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Single source of truth for the OpenAI Codex revision this adapter tracks. */
|
|
2
|
+
export declare const TRACKED_CODEX_REPOSITORY = "https://github.com/openai/codex.git";
|
|
3
|
+
export declare const TRACKED_CODEX_COMMIT = "6478a751fde8884b2fdc76486fe23175a8e795d4";
|
|
4
|
+
export declare const TRACKED_CODEX_RELEASE = "rust-v0.151.0";
|
|
5
|
+
/** Whole release version sent to the Codex model-catalog endpoint. */
|
|
6
|
+
export declare const CODEX_CLIENT_VERSION = "0.151.0";
|
package/lib/upstream.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Single source of truth for the OpenAI Codex revision this adapter tracks. */
|
|
2
|
+
export const TRACKED_CODEX_REPOSITORY = 'https://github.com/openai/codex.git';
|
|
3
|
+
export const TRACKED_CODEX_COMMIT = '6478a751fde8884b2fdc76486fe23175a8e795d4';
|
|
4
|
+
export const TRACKED_CODEX_RELEASE = 'rust-v0.151.0';
|
|
5
|
+
/** Whole release version sent to the Codex model-catalog endpoint. */
|
|
6
|
+
export const CODEX_CLIENT_VERSION = '0.151.0';
|