@ottocode/sdk 0.1.281 → 0.1.283

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ottocode/sdk",
3
- "version": "0.1.281",
3
+ "version": "0.1.283",
4
4
  "description": "AI agent SDK for building intelligent assistants - tree-shakable and comprehensive",
5
5
  "author": "nitishxyz",
6
6
  "license": "MIT",
@@ -15,8 +15,10 @@ const TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1000;
15
15
  const TOKEN_REFRESH_MAX_RETRIES = 2;
16
16
  const TOKEN_REFRESH_RETRY_DELAY_MS = 1000;
17
17
  const CODEX_INSTALLATION_ID = crypto.randomUUID();
18
- const CODEX_REQUEST_TIMEOUT_MS = 120_000;
19
- const CODEX_STREAM_IDLE_TIMEOUT_MS = 120_000;
18
+ const CODEX_REQUEST_TIMEOUT_MS = 20_000;
19
+ const CODEX_STREAM_IDLE_TIMEOUT_MS = 20_000;
20
+ const CODEX_REQUEST_MAX_RETRIES = 3;
21
+ const CODEX_REQUEST_RETRY_DELAY_MS = 500;
20
22
 
21
23
  type OpenAIOAuthSessionState = {
22
24
  responseId?: string;
@@ -110,6 +112,20 @@ function getCodexStreamIdleTimeoutMs() {
110
112
  );
111
113
  }
112
114
 
115
+ function getCodexRequestMaxRetries() {
116
+ return parsePositiveIntegerEnv(
117
+ 'OTTO_OPENAI_OAUTH_REQUEST_MAX_RETRIES',
118
+ CODEX_REQUEST_MAX_RETRIES,
119
+ );
120
+ }
121
+
122
+ function getCodexRequestRetryDelayMs() {
123
+ return parsePositiveIntegerEnv(
124
+ 'OTTO_OPENAI_OAUTH_REQUEST_RETRY_DELAY_MS',
125
+ CODEX_REQUEST_RETRY_DELAY_MS,
126
+ );
127
+ }
128
+
113
129
  export function clearOpenAIOAuthSessionState(sessionId?: string) {
114
130
  if (sessionId) {
115
131
  openAIOAuthSessionState.delete(sessionId);
@@ -452,6 +468,108 @@ function trackResponsesStream(
452
468
  });
453
469
  }
454
470
 
471
+ async function waitForCodexStreamStart(
472
+ response: Response,
473
+ args: {
474
+ sessionId?: string;
475
+ model?: string;
476
+ parentSignal?: AbortSignal | null;
477
+ },
478
+ ): Promise<Response> {
479
+ if (!response.ok || !response.body) {
480
+ return response;
481
+ }
482
+
483
+ const reader = response.body.getReader();
484
+ const idleTimeoutMs = getCodexStreamIdleTimeoutMs();
485
+ let timeout: Timer | undefined;
486
+ let removeAbortListener: (() => void) | undefined;
487
+ const cleanup = () => {
488
+ if (timeout) clearTimeout(timeout);
489
+ timeout = undefined;
490
+ removeAbortListener?.();
491
+ removeAbortListener = undefined;
492
+ };
493
+
494
+ try {
495
+ const first = await Promise.race([
496
+ reader.read(),
497
+ new Promise<never>((_resolve, reject) => {
498
+ timeout = setTimeout(() => {
499
+ reject(
500
+ new Error(
501
+ `OpenAI OAuth Codex stream idle timeout before first chunk after ${idleTimeoutMs}ms`,
502
+ ),
503
+ );
504
+ }, idleTimeoutMs);
505
+
506
+ if (args.parentSignal) {
507
+ const onAbort = () => {
508
+ reject(args.parentSignal?.reason ?? new Error('Request aborted'));
509
+ };
510
+ if (args.parentSignal.aborted) {
511
+ onAbort();
512
+ } else {
513
+ args.parentSignal.addEventListener('abort', onAbort, {
514
+ once: true,
515
+ });
516
+ removeAbortListener = () =>
517
+ args.parentSignal?.removeEventListener('abort', onAbort);
518
+ }
519
+ }
520
+ }),
521
+ ]);
522
+ cleanup();
523
+
524
+ if (first.done) {
525
+ return new Response(null, {
526
+ status: response.status,
527
+ statusText: response.statusText,
528
+ headers: response.headers,
529
+ });
530
+ }
531
+
532
+ const body = new ReadableStream<Uint8Array>({
533
+ start(controller) {
534
+ controller.enqueue(first.value);
535
+ },
536
+ async pull(controller) {
537
+ try {
538
+ const next = await reader.read();
539
+ if (next.done) {
540
+ controller.close();
541
+ return;
542
+ }
543
+ controller.enqueue(next.value);
544
+ } catch (error) {
545
+ controller.error(error);
546
+ }
547
+ },
548
+ async cancel(reason) {
549
+ await reader.cancel(reason);
550
+ },
551
+ });
552
+
553
+ return new Response(body, {
554
+ status: response.status,
555
+ statusText: response.statusText,
556
+ headers: response.headers,
557
+ });
558
+ } catch (error) {
559
+ cleanup();
560
+ loggerWarn('[openai-oauth] response stream did not start before timeout', {
561
+ sessionId: args.sessionId,
562
+ model: args.model,
563
+ timeoutMs: idleTimeoutMs,
564
+ error: summarizeError(error),
565
+ });
566
+ try {
567
+ await reader.cancel(error);
568
+ } catch {}
569
+ throw error;
570
+ }
571
+ }
572
+
455
573
  async function fetchWithCodexRequestTimeout(
456
574
  url: string,
457
575
  init: RequestInit,
@@ -470,6 +588,55 @@ async function fetchWithCodexRequestTimeout(
470
588
  });
471
589
  }
472
590
 
591
+ const maxRetries = getCodexRequestMaxRetries();
592
+ let lastError: unknown;
593
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
594
+ const attemptStartedAt = Date.now();
595
+ try {
596
+ const response = await fetchCodexRequestAttemptWithTimeout(url, init, {
597
+ ...args,
598
+ requestStartedAt: attemptStartedAt,
599
+ });
600
+ return await waitForCodexStreamStart(response, {
601
+ sessionId: args.sessionId,
602
+ model: args.model,
603
+ parentSignal: init.signal,
604
+ });
605
+ } catch (error) {
606
+ lastError = error;
607
+ if (init.signal?.aborted || attempt >= maxRetries) {
608
+ throw error;
609
+ }
610
+
611
+ const retryDelayMs = getCodexRequestRetryDelayMs() * (attempt + 1);
612
+ loggerWarn('[openai-oauth] request attempt failed before stream start', {
613
+ sessionId: args.sessionId,
614
+ model: args.model,
615
+ attempt: attempt + 1,
616
+ maxRetries,
617
+ nextAttempt: attempt + 2,
618
+ requestTimeoutMs: getCodexRequestTimeoutMs(),
619
+ streamIdleTimeoutMs: getCodexStreamIdleTimeoutMs(),
620
+ durationMs: Date.now() - attemptStartedAt,
621
+ retryDelayMs,
622
+ error: summarizeError(error),
623
+ });
624
+ await sleep(retryDelayMs);
625
+ }
626
+ }
627
+
628
+ throw lastError;
629
+ }
630
+
631
+ async function fetchCodexRequestAttemptWithTimeout(
632
+ url: string,
633
+ init: RequestInit,
634
+ args: {
635
+ sessionId?: string;
636
+ model?: string;
637
+ requestStartedAt: number;
638
+ },
639
+ ) {
473
640
  const timeoutMs = getCodexRequestTimeoutMs();
474
641
  const controller = new AbortController();
475
642
  const timeout = setTimeout(() => {