@pure01fx/dsh-openai-codex-auth 0.7.0 → 0.7.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 CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.7.1
6
+
7
+ ### Native stream compatibility
8
+
9
+ - Aligns long native Codex streams with codex-rs by removing aggregate event/wire-byte, output-item, and replay-item ceilings, using a 300-second WebSocket idle timeout, and retaining 64 MiB single-event/frame, queued-byte, translated-content, retained-output, and replay-state safeguards.
10
+
5
11
  ## 0.7.0
6
12
 
7
13
  ### DSH 0.1.1 compatibility
@@ -1,7 +1,6 @@
1
1
  /** Pure WebSocket v2 previous-response and incremental suffix state. */
2
2
  import { createHash } from 'node:crypto';
3
3
  import { LlmError } from '@deepseek-ai/dsh-llm';
4
- const MAX_OUTPUT_ITEMS = 2048;
5
4
  const MAX_RESPONSE_ID_BYTES = 256;
6
5
  const IGNORED_REUSE_FIELDS = new Set([
7
6
  'input', 'previous_response_id', 'generate', 'client_metadata',
@@ -90,10 +89,6 @@ export class NativeCodexWebSocketSessionState {
90
89
  this.reset();
91
90
  throw failure('native Codex WebSocket completion identity is invalid');
92
91
  }
93
- if (outputItems.length > MAX_OUTPUT_ITEMS) {
94
- this.reset();
95
- throw failure('native Codex WebSocket response has too many items');
96
- }
97
92
  this.completed = {
98
93
  propertyHash: this.pending.propertyHash,
99
94
  contextLength: this.pending.inputLength + outputItems.length,
@@ -4,9 +4,8 @@ import { nativeCodexEndpoint } from './endpoint.js';
4
4
  import { NATIVE_CODEX_CONNECTION_FAILED_CODE, isNativeCodexConnectionFailure, } from './native-adapter.js';
5
5
  import WebSocket from 'ws';
6
6
  const DEFAULT_CONNECT_TIMEOUT_MS = 10_000;
7
- const DEFAULT_MAX_FRAME_BYTES = 1024 * 1024;
8
- const MAX_QUEUED_FRAMES = 4096;
9
- const MAX_QUEUED_BYTES = 24 * 1024 * 1024;
7
+ const DEFAULT_MAX_FRAME_BYTES = 64 * 1024 * 1024;
8
+ const MAX_QUEUED_BYTES = 64 * 1024 * 1024;
10
9
  function failure(message, code, cause) {
11
10
  return new LlmError(message, code, cause === undefined ? undefined : { cause });
12
11
  }
@@ -83,7 +82,7 @@ class NodeNativeCodexWebSocket {
83
82
  waiter.resolve(value);
84
83
  return;
85
84
  }
86
- if (this.queue.length >= MAX_QUEUED_FRAMES || this.queuedBytes + bytes > MAX_QUEUED_BYTES) {
85
+ if (this.queuedBytes + bytes > MAX_QUEUED_BYTES) {
87
86
  this.fail(failure('native Codex WebSocket queued too much response data', 'WS_RESPONSE_TOO_LARGE'));
88
87
  return;
89
88
  }
@@ -11,8 +11,8 @@ import { ResponsesStreamTranslator, codexResponseTurnState, } from './responses.
11
11
  import { NodeNativeCodexWebSocketFactory, } from './native-websocket-socket.js';
12
12
  import { NativeCodexWebSocketSessionState } from './native-websocket-session.js';
13
13
  const WS_BETA = 'responses_websockets=2026-02-06';
14
- const DEFAULT_IDLE_TIMEOUT_MS = 30_000;
15
- const DEFAULT_MAX_FRAME_BYTES = 1024 * 1024;
14
+ const DEFAULT_IDLE_TIMEOUT_MS = 300_000;
15
+ const DEFAULT_MAX_FRAME_BYTES = 64 * 1024 * 1024;
16
16
  const DEFAULT_MAX_SESSIONS = 32;
17
17
  const DEFAULT_SESSION_IDLE_MS = 30 * 60_000;
18
18
  const DEFAULT_MAX_RECONNECTS = 5;
@@ -21,9 +21,7 @@ const DEFAULT_MAX_RETRY_DELAY_MS = 10_000;
21
21
  const INITIAL_CONNECTION_RETRY_DELAY_MS = 5_000;
22
22
  const MAX_CONNECTION_RETRY_DELAY_MS = 60_000;
23
23
  const MAX_TURN_STATE_BYTES = 4096;
24
- const MAX_EVENTS_PER_RESPONSE = 4096;
25
- const MAX_OUTPUT_ITEMS_PER_RESPONSE = 2048;
26
- const MAX_RESPONSE_BYTES = 24 * 1024 * 1024;
24
+ const MAX_RETAINED_OUTPUT_BYTES = 64 * 1024 * 1024;
27
25
  function failure(message, code, cause) {
28
26
  return new LlmError(message, code, cause === undefined ? undefined : { cause });
29
27
  }
@@ -221,7 +219,7 @@ export class NativeCodexWebSocketTransport {
221
219
  this.factory = options.webSocketFactory ?? new NodeNativeCodexWebSocketFactory();
222
220
  this.connectTimeoutMs = boundedPositive(options.webSocketConnectTimeoutMs, 10_000, 120_000, 'WebSocket connect timeout');
223
221
  this.idleTimeoutMs = boundedPositive(options.webSocketIdleTimeoutMs, DEFAULT_IDLE_TIMEOUT_MS, 60 * 60_000, 'WebSocket idle timeout');
224
- this.maxFrameBytes = boundedPositive(options.maxWebSocketFrameBytes, DEFAULT_MAX_FRAME_BYTES, MAX_RESPONSE_BYTES, 'WebSocket frame limit');
222
+ this.maxFrameBytes = boundedPositive(options.maxWebSocketFrameBytes, DEFAULT_MAX_FRAME_BYTES, DEFAULT_MAX_FRAME_BYTES, 'WebSocket frame limit');
225
223
  this.maxSessions = boundedPositive(options.maxWebSocketSessions, DEFAULT_MAX_SESSIONS, 256, 'WebSocket session limit');
226
224
  this.sessionIdleMs = boundedPositive(options.webSocketSessionIdleMs, DEFAULT_SESSION_IDLE_MS, 24 * 60 * 60_000, 'WebSocket session idle limit');
227
225
  this.maxReconnects = retryCount(options.maxWebSocketReconnects);
@@ -385,14 +383,9 @@ export class NativeCodexWebSocketTransport {
385
383
  model: mode.publicModel ?? generation.model,
386
384
  });
387
385
  const outputItems = [];
388
- let events = 0;
389
- let responseBytes = 0;
390
- while (events++ < MAX_EVENTS_PER_RESPONSE) {
386
+ let outputBytes = 0;
387
+ while (true) {
391
388
  const text = await this.receive(entry, signal);
392
- responseBytes += Buffer.byteLength(text);
393
- if (responseBytes > MAX_RESPONSE_BYTES) {
394
- throw failure('native Codex WebSocket response exceeded the size limit', 'WS_RESPONSE_TOO_LARGE');
395
- }
396
389
  let event;
397
390
  try {
398
391
  event = JSON.parse(text);
@@ -421,10 +414,12 @@ export class NativeCodexWebSocketTransport {
421
414
  }
422
415
  const output = normalizedOutputItem(event);
423
416
  if (output !== undefined) {
424
- if (outputItems.length >= MAX_OUTPUT_ITEMS_PER_RESPONSE) {
425
- throw failure('native Codex WebSocket response had too many output items', 'WS_RESPONSE_TOO_LARGE');
417
+ const nextOutputBytes = outputBytes + Buffer.byteLength(JSON.stringify(output));
418
+ if (nextOutputBytes > MAX_RETAINED_OUTPUT_BYTES) {
419
+ throw failure('native Codex WebSocket retained output exceeded the size limit', 'WS_RESPONSE_TOO_LARGE');
426
420
  }
427
421
  outputItems.push(output);
422
+ outputBytes = nextOutputBytes;
428
423
  }
429
424
  if (event.type === 'response.completed') {
430
425
  const response = typeof event.response === 'object'
@@ -444,7 +439,6 @@ export class NativeCodexWebSocketTransport {
444
439
  return;
445
440
  }
446
441
  }
447
- throw failure('native Codex WebSocket response had too many events', 'WS_PROTOCOL_ERROR');
448
442
  }
449
443
  async *attempt(entry, prepared, credential, signal) {
450
444
  await this.ensureSocket(entry, prepared, credential, signal);
package/lib/replay.d.ts CHANGED
@@ -32,12 +32,11 @@ export interface NativeCodexReplaySource {
32
32
  export declare function replayableItemId(value: string | undefined): string | undefined;
33
33
  /** True only for legacy raw state or an rc.2 envelope emitted by this package. */
34
34
  export declare function hasNativeCodexReplayKind(value: unknown): boolean;
35
- /** Attempt-local bounded accumulator; no ciphertext can grow unchecked before completion. */
35
+ /** Attempt-local byte-bounded accumulator; no ciphertext can grow unchecked before completion. */
36
36
  export declare class NativeCodexReplayCapture {
37
37
  private readonly provider;
38
38
  private readonly model;
39
39
  private readonly descriptors;
40
- private references;
41
40
  private stateBytes;
42
41
  constructor(provider: string, model: string);
43
42
  add(item: NativeCodexReplayDescriptor): void;
package/lib/replay.js CHANGED
@@ -2,11 +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
- const MAX_REPLAY_DESCRIPTORS = 128;
6
- const MAX_REPLAY_BLOCK_REFS = 256;
7
5
  const MAX_REPLAY_ITEM_ID_BYTES = 256;
8
- const MAX_REPLAY_CIPHERTEXT_BYTES = 1024 * 1024;
9
- const MAX_REPLAY_STATE_BYTES = 4 * 1024 * 1024;
6
+ const MAX_REPLAY_CIPHERTEXT_BYTES = 64 * 1024 * 1024;
7
+ const MAX_REPLAY_STATE_BYTES = 64 * 1024 * 1024;
10
8
  function failure(message, code = 'INVALID_REPLAY_STATE') {
11
9
  return new LlmError(message, code);
12
10
  }
@@ -105,8 +103,9 @@ export function hasNativeCodexReplayKind(value) {
105
103
  return object(replayPayload(value))?.kind === NATIVE_CODEX_REPLAY_KIND;
106
104
  }
107
105
  function parseState(value) {
108
- safeStateSize(value, 'INVALID_REPLAY_STATE');
109
- const row = object(replayPayload(value));
106
+ const payload = replayPayload(value);
107
+ safeStateSize(payload, 'INVALID_REPLAY_STATE');
108
+ const row = object(payload);
110
109
  if (row === undefined || row.kind !== NATIVE_CODEX_REPLAY_KIND
111
110
  || row.version !== NATIVE_CODEX_REPLAY_VERSION
112
111
  || !onlyKeys(row, ['kind', 'version', 'provider', 'model', 'items'])) {
@@ -115,13 +114,10 @@ function parseState(value) {
115
114
  const provider = boundedString(row.provider);
116
115
  const model = boundedString(row.model, 512);
117
116
  if (provider === undefined || model === undefined || !Array.isArray(row.items)
118
- || row.items.length === 0 || row.items.length > MAX_REPLAY_DESCRIPTORS) {
117
+ || row.items.length === 0) {
119
118
  throw failure('native Codex replay state metadata is invalid');
120
119
  }
121
120
  const items = row.items.map(parseDescriptor);
122
- const refs = items.reduce((total, item) => total + (item.type === 'function_call' ? 1 : item.blocks.length), 0);
123
- if (refs > MAX_REPLAY_BLOCK_REFS)
124
- throw failure('native Codex replay state has too many block references');
125
121
  return {
126
122
  kind: NATIVE_CODEX_REPLAY_KIND,
127
123
  version: NATIVE_CODEX_REPLAY_VERSION,
@@ -130,12 +126,11 @@ function parseState(value) {
130
126
  items,
131
127
  };
132
128
  }
133
- /** Attempt-local bounded accumulator; no ciphertext can grow unchecked before completion. */
129
+ /** Attempt-local byte-bounded accumulator; no ciphertext can grow unchecked before completion. */
134
130
  export class NativeCodexReplayCapture {
135
131
  provider;
136
132
  model;
137
133
  descriptors = [];
138
- references = 0;
139
134
  stateBytes;
140
135
  constructor(provider, model) {
141
136
  this.provider = provider;
@@ -149,13 +144,6 @@ export class NativeCodexReplayCapture {
149
144
  }));
150
145
  }
151
146
  add(item) {
152
- if (this.descriptors.length >= MAX_REPLAY_DESCRIPTORS) {
153
- throw failure('native Codex response has too many replay descriptors', 'MALFORMED_RESPONSE');
154
- }
155
- const addedReferences = item.type === 'function_call' ? 1 : item.blocks.length;
156
- if (this.references + addedReferences > MAX_REPLAY_BLOCK_REFS) {
157
- throw failure('native Codex response has too many replay block references', 'MALFORMED_RESPONSE');
158
- }
159
147
  if (item.type === 'reasoning' && item.encryptedContent !== undefined
160
148
  && Buffer.byteLength(item.encryptedContent) > MAX_REPLAY_CIPHERTEXT_BYTES) {
161
149
  throw failure('native Codex encrypted reasoning exceeded the replay limit', 'MALFORMED_RESPONSE');
@@ -166,7 +154,6 @@ export class NativeCodexReplayCapture {
166
154
  throw failure('native Codex replay state exceeded the size limit', 'REPLAY_STATE_TOO_LARGE');
167
155
  }
168
156
  this.descriptors.push(item);
169
- this.references += addedReferences;
170
157
  this.stateBytes = nextBytes;
171
158
  }
172
159
  finish() {
@@ -100,9 +100,13 @@ export declare class ResponsesStreamTranslator {
100
100
  private readonly order;
101
101
  private readonly replayCapture;
102
102
  private nextIndex;
103
+ private retainedBytes;
103
104
  private sawToolCall;
104
105
  terminated: boolean;
105
106
  constructor(replayContext?: ResponsesReplayContext | undefined);
107
+ private reserve;
108
+ private append;
109
+ private fill;
106
110
  private open;
107
111
  private close;
108
112
  private closeItem;
@@ -114,8 +118,6 @@ export interface StreamResponsesOptions extends ParseSseOptions {
114
118
  onMalformedEvent?: () => void;
115
119
  onEvent?: (event: ResponsesStreamEvent) => void;
116
120
  replayContext?: ResponsesReplayContext;
117
- maxResponseBytes?: number;
118
- maxResponseEvents?: number;
119
121
  }
120
122
  /** Validate one opaque sticky turn token before retaining or forwarding it. */
121
123
  export declare function boundedCodexTurnState(value: unknown): string | undefined;
package/lib/responses.js CHANGED
@@ -6,6 +6,7 @@ import { NativeCodexReplayCapture, replayAssistantInput, replayableItemId, } fro
6
6
  export const DEFAULT_CODEX_INSTRUCTIONS = 'You are Codex, an AI coding agent. Help the user with software engineering tasks.';
7
7
  const CALL_ID_MAX_LENGTH = 64;
8
8
  const CALL_ID_PREFIX = 'call_';
9
+ const MAX_RETAINED_RESPONSE_BYTES = 64 * 1024 * 1024;
9
10
  function fixedError(message, code) { return new LlmError(message, code); }
10
11
  function imageItem(image) {
11
12
  if (!/^image[/][a-z0-9.+-]+$/i.test(image.mediaType) || image.dataBase64.length === 0) {
@@ -242,6 +243,7 @@ export class ResponsesStreamTranslator {
242
243
  order = [];
243
244
  replayCapture;
244
245
  nextIndex = 0;
246
+ retainedBytes = 0;
245
247
  sawToolCall = false;
246
248
  terminated = false;
247
249
  constructor(replayContext) {
@@ -250,7 +252,26 @@ export class ResponsesStreamTranslator {
250
252
  ? undefined
251
253
  : new NativeCodexReplayCapture(replayContext.provider, replayContext.model);
252
254
  }
255
+ reserve(bytes) {
256
+ const nextBytes = this.retainedBytes + bytes;
257
+ if (!Number.isSafeInteger(nextBytes) || nextBytes > MAX_RETAINED_RESPONSE_BYTES) {
258
+ throw fixedError('native Codex response retained content exceeded the size limit', 'RESPONSE_TOO_LARGE');
259
+ }
260
+ this.retainedBytes = nextBytes;
261
+ }
262
+ append(block, delta) {
263
+ this.reserve(Buffer.byteLength(delta));
264
+ block.text += delta;
265
+ }
266
+ fill(block, text) {
267
+ if (block.text.length > 0)
268
+ return;
269
+ this.reserve(Buffer.byteLength(text));
270
+ block.text = text;
271
+ }
253
272
  open(key, kind, chunks, callId = '', name) {
273
+ this.reserve(128 + Buffer.byteLength(key) + Buffer.byteLength(callId)
274
+ + (name === undefined ? 0 : Buffer.byteLength(name)));
254
275
  const block = {
255
276
  index: this.nextIndex++, kind, text: '', callId,
256
277
  ...name === undefined ? {} : { name },
@@ -308,7 +329,7 @@ export class ResponsesStreamTranslator {
308
329
  const key = `${eventItemId(event)}:text:${String(event.content_index ?? 0)}`;
309
330
  const block = this.blocks.get(key) ?? this.open(key, 'text', chunks);
310
331
  const delta = eventDelta(event);
311
- block.text += delta;
332
+ this.append(block, delta);
312
333
  chunks.push({ type: 'text-delta', index: block.index, text: delta });
313
334
  return chunks;
314
335
  }
@@ -316,7 +337,7 @@ export class ResponsesStreamTranslator {
316
337
  const key = `${eventItemId(event)}:summary:${String(event.summary_index ?? 0)}`;
317
338
  const block = this.blocks.get(key) ?? this.open(key, 'reasoning', chunks);
318
339
  const delta = eventDelta(event);
319
- block.text += delta;
340
+ this.append(block, delta);
320
341
  chunks.push({ type: 'reasoning-delta', index: block.index, text: delta });
321
342
  return chunks;
322
343
  }
@@ -329,7 +350,7 @@ export class ResponsesStreamTranslator {
329
350
  throw fixedError('native Codex function arguments have no open call', 'MALFORMED_RESPONSE');
330
351
  }
331
352
  const delta = eventDelta(event);
332
- block.text += delta;
353
+ this.append(block, delta);
333
354
  chunks.push({
334
355
  type: 'tool-call-delta', index: block.index, id: CallId(block.callId),
335
356
  ...block.name === undefined ? {} : { name: block.name }, argumentsDelta: delta,
@@ -360,8 +381,7 @@ export class ResponsesStreamTranslator {
360
381
  }
361
382
  block.callId = item.call_id;
362
383
  block.name = item.name;
363
- if (block.text.length === 0)
364
- block.text = item.arguments;
384
+ this.fill(block, item.arguments);
365
385
  this.close(key, chunks);
366
386
  if (this.replayContext !== undefined)
367
387
  this.replayCapture?.add({
@@ -385,8 +405,7 @@ export class ResponsesStreamTranslator {
385
405
  if (block.text.length > 0 && block.text !== part.text) {
386
406
  throw fixedError('native Codex text changed during streaming', 'MALFORMED_RESPONSE');
387
407
  }
388
- if (block.text.length === 0)
389
- block.text = part.text;
408
+ this.fill(block, part.text);
390
409
  refs.push(block.index);
391
410
  this.close(key, chunks);
392
411
  }
@@ -417,8 +436,7 @@ export class ResponsesStreamTranslator {
417
436
  if (block.text.length > 0 && block.text !== text) {
418
437
  throw fixedError('native Codex reasoning summary changed during streaming', 'MALFORMED_RESPONSE');
419
438
  }
420
- if (block.text.length === 0)
421
- block.text = text;
439
+ this.fill(block, text);
422
440
  refs.push(block.index);
423
441
  this.close(key, chunks);
424
442
  }
@@ -528,29 +546,8 @@ export function codexResponseTurnState(event) {
528
546
  }
529
547
  /** Consume framed SSE JSON into DSH chunks. */
530
548
  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
549
  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
- }
550
+ for await (const frame of parseSse(stream, options)) {
554
551
  let event;
555
552
  try {
556
553
  event = JSON.parse(frame.data);
package/lib/sse.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /** Bounded, cancellable Server-Sent Events byte framing. */
2
2
  import { LlmError } from '@deepseek-ai/dsh-llm';
3
- export const DEFAULT_MAX_SSE_EVENT_BYTES = 1024 * 1024;
3
+ export const DEFAULT_MAX_SSE_EVENT_BYTES = 64 * 1024 * 1024;
4
4
  function aborted() {
5
5
  return new LlmError('native Codex SSE stream was cancelled', 'ABORTED');
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pure01fx/dsh-openai-codex-auth",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "Native ChatGPT Codex provider, device-code-first login, and same-origin Web integration for DeepSeek Harness",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -69,6 +69,11 @@
69
69
  "CHANGELOG.md",
70
70
  "LICENSE"
71
71
  ],
72
+ "scripts": {
73
+ "build": "tsc",
74
+ "test": "vitest run",
75
+ "prepack": "pnpm build"
76
+ },
72
77
  "dsh": {
73
78
  "engines": {
74
79
  "dsh": "0.1.1-rc.2"
@@ -107,9 +112,5 @@
107
112
  "@types/node": "^22.20.0",
108
113
  "typescript": "^6.0.3",
109
114
  "vitest": "^4.1.8"
110
- },
111
- "scripts": {
112
- "build": "tsc",
113
- "test": "vitest run"
114
115
  }
115
- }
116
+ }