@gajae-code/ai 0.2.2 → 0.2.4

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.2.4] - 2026-06-02
6
+
7
+ ### Added
8
+
9
+ - Added configurable provider request and stream retry budgets so hosts can bound transient upstream/server retry behavior separately from session-level retries.
10
+
5
11
  ## [0.2.2] - 2026-05-31
6
12
 
7
13
  ### Fixed
@@ -118,6 +118,7 @@ export type AnthropicClientOptionsArgs = {
118
118
  hasTools?: boolean;
119
119
  onSseEvent?: AnthropicOptions["onSseEvent"];
120
120
  fetch?: FetchImpl;
121
+ requestMaxRetries?: number;
121
122
  };
122
123
  export type AnthropicClientOptionsResult = {
123
124
  isOAuthToken: boolean;
@@ -178,6 +178,16 @@ export interface StreamOptions {
178
178
  * Default: 60000 (60 seconds). Set to 0 to disable the cap.
179
179
  */
180
180
  maxRetryDelayMs?: number;
181
+ /**
182
+ * Maximum provider request retries for transports/SDKs that retry before a stream is established.
183
+ * Counts retries only, not the initial attempt. Providers keep their built-in default when unset.
184
+ */
185
+ requestMaxRetries?: number;
186
+ /**
187
+ * Maximum provider stream replay retries after a replay-safe transient stream failure.
188
+ * Counts retries only, not the initial stream attempt. Providers keep their built-in default when unset.
189
+ */
190
+ streamMaxRetries?: number;
181
191
  /**
182
192
  * Optional metadata to include in API requests.
183
193
  * Providers extract the fields they understand and ignore the rest.
@@ -0,0 +1 @@
1
+ export declare function resolveRetryBudget(value: number | undefined, fallback: number): number;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/ai",
4
- "version": "0.2.2",
4
+ "version": "0.2.4",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://gaebal-gajae.dev",
7
7
  "author": "Yeachan-Heo",
@@ -43,7 +43,7 @@
43
43
  "dependencies": {
44
44
  "@anthropic-ai/sdk": "^0.94.0",
45
45
  "@bufbuild/protobuf": "^2.12.0",
46
- "@gajae-code/utils": "0.2.2",
46
+ "@gajae-code/utils": "0.2.4",
47
47
  "openai": "^6.36.0",
48
48
  "partial-json": "^0.1.7",
49
49
  "zod": "4.4.3"
@@ -31,6 +31,7 @@ import { normalizeToolCallId, resolveCacheRetention } from "../utils";
31
31
  import { AssistantMessageEventStream } from "../utils/event-stream";
32
32
  import { appendRawHttpRequestDumpFor400, type RawHttpRequestDump, withHttpStatus } from "../utils/http-inspector";
33
33
  import { parseStreamingJson } from "../utils/json-parse";
34
+ import { resolveRetryBudget } from "../utils/retry-budget";
34
35
  import { toolWireSchema } from "../utils/schema/wire";
35
36
  import { resolveAwsCredentials } from "./aws-credentials";
36
37
  import { decodeEventStream } from "./aws-eventstream";
@@ -259,6 +260,7 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream"> = (
259
260
  headers: requestHeaders,
260
261
  body,
261
262
  signal: options.signal,
263
+ maxAttempts: resolveRetryBudget(options.requestMaxRetries, 4) + 1,
262
264
  });
263
265
 
264
266
  if (!response.ok) {
@@ -61,6 +61,7 @@ import { parseJsonWithRepair, parseStreamingJson } from "../utils/json-parse";
61
61
  import { parseGitHubCopilotApiKey } from "../utils/oauth/github-copilot";
62
62
  import { notifyProviderResponse } from "../utils/provider-response";
63
63
  import { isCopilotTransientModelError } from "../utils/retry";
64
+ import { resolveRetryBudget } from "../utils/retry-budget";
64
65
  import { COMBINATOR_KEYS, NO_STRICT, toolWireSchema } from "../utils/schema";
65
66
  import { spillToDescription } from "../utils/schema/spill";
66
67
  import { notifyRawSseEvent, wrapFetchForSseDebug } from "../utils/sse-debug";
@@ -183,12 +184,14 @@ export function buildAnthropicHeaders(options: AnthropicHeaderOptions): Record<s
183
184
  "User-Agent": userAgent,
184
185
  };
185
186
  } else if (!isAnthropicApiBaseUrl(options.baseUrl)) {
187
+ const incomingUserAgent = getHeaderCaseInsensitive(options.modelHeaders, "User-Agent");
186
188
  return {
187
189
  ...modelHeaders,
188
190
  Accept: acceptHeader,
189
191
  Authorization: `Bearer ${options.apiKey}`,
190
192
  ...sharedHeaders,
191
193
  "Anthropic-Beta": betaHeader,
194
+ ...(incomingUserAgent ? { "User-Agent": incomingUserAgent } : {}),
192
195
  };
193
196
  } else {
194
197
  return {
@@ -626,6 +629,7 @@ export type AnthropicClientOptionsArgs = {
626
629
  hasTools?: boolean;
627
630
  onSseEvent?: AnthropicOptions["onSseEvent"];
628
631
  fetch?: FetchImpl;
632
+ requestMaxRetries?: number;
629
633
  };
630
634
 
631
635
  export type AnthropicClientOptionsResult = {
@@ -1057,6 +1061,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1057
1061
  hasTools: !!context.tools?.length,
1058
1062
  onSseEvent: options?.onSseEvent,
1059
1063
  fetch: options?.fetch,
1064
+ requestMaxRetries: options?.requestMaxRetries,
1060
1065
  });
1061
1066
  client = created.client;
1062
1067
  isOAuthToken = created.isOAuthToken;
@@ -1445,7 +1450,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1445
1450
  firstTokenTime === undefined && isProviderRetryableError(streamFailure, model.provider);
1446
1451
  if (
1447
1452
  activeAbortTracker.wasCallerAbort() ||
1448
- providerRetryAttempt >= PROVIDER_MAX_RETRIES ||
1453
+ providerRetryAttempt >= resolveRetryBudget(options?.streamMaxRetries, PROVIDER_MAX_RETRIES) ||
1449
1454
  (!canRetryTransientEnvelopeFailure && !canRetryProviderFailure)
1450
1455
  ) {
1451
1456
  throw streamFailure;
@@ -1604,7 +1609,7 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
1604
1609
  apiKey: null,
1605
1610
  authToken: copilotApiKey,
1606
1611
  baseURL: baseUrl,
1607
- maxRetries: 5,
1612
+ maxRetries: resolveRetryBudget(args.requestMaxRetries, 5),
1608
1613
  dangerouslyAllowBrowser: true,
1609
1614
  defaultHeaders,
1610
1615
  logLevel: ANTHROPIC_SDK_LOG_LEVEL,
@@ -1637,7 +1642,7 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
1637
1642
  apiKey: null,
1638
1643
  authToken: null,
1639
1644
  baseURL: baseUrl,
1640
- maxRetries: 5,
1645
+ maxRetries: resolveRetryBudget(args.requestMaxRetries, 5),
1641
1646
  dangerouslyAllowBrowser: true,
1642
1647
  defaultHeaders,
1643
1648
  logLevel: ANTHROPIC_SDK_LOG_LEVEL,
@@ -1650,7 +1655,7 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
1650
1655
  apiKey: oauthToken ? null : apiKey,
1651
1656
  authToken: oauthToken ? apiKey : undefined,
1652
1657
  baseURL: baseUrl,
1653
- maxRetries: 5,
1658
+ maxRetries: resolveRetryBudget(args.requestMaxRetries, 5),
1654
1659
  dangerouslyAllowBrowser: true,
1655
1660
  defaultHeaders,
1656
1661
  logLevel: ANTHROPIC_SDK_LOG_LEVEL,
@@ -26,6 +26,7 @@ import {
26
26
  getStreamFirstEventTimeoutMs,
27
27
  iterateWithIdleTimeout,
28
28
  } from "../utils/idle-iterator";
29
+ import { resolveRetryBudget } from "../utils/retry-budget";
29
30
  import { sanitizeSchemaForOpenAIResponses, toolWireSchema } from "../utils/schema";
30
31
  import { wrapFetchForSseDebug } from "../utils/sse-debug";
31
32
  import { mapToOpenAIResponsesToolChoice } from "../utils/tool-choice";
@@ -249,7 +250,7 @@ function createClient(model: Model<"azure-openai-responses">, apiKey: string, op
249
250
  apiKey,
250
251
  apiVersion,
251
252
  dangerouslyAllowBrowser: true,
252
- maxRetries: 5,
253
+ maxRetries: resolveRetryBudget(options?.requestMaxRetries, 5),
253
254
  defaultHeaders: headers,
254
255
  baseURL: baseUrl,
255
256
  fetch: onSseEvent ? wrapFetchForSseDebug(baseFetch, event => onSseEvent(event, model)) : baseFetch,
@@ -21,6 +21,7 @@ import type {
21
21
  import { normalizeSystemPrompts } from "../utils";
22
22
  import { AssistantMessageEventStream } from "../utils/event-stream";
23
23
  import { appendRawHttpRequestDumpFor400, type RawHttpRequestDump, withHttpStatus } from "../utils/http-inspector";
24
+ import { resolveRetryBudget } from "../utils/retry-budget";
24
25
  // Refresh is the sole responsibility of AuthStorage (broker-aware, single-flighted);
25
26
  // the stream provider trusts the access token threaded through `options.apiKey`.
26
27
  import { normalizeSchemaForCCA } from "../utils/schema";
@@ -340,7 +341,7 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
340
341
  headers: requestHeaders,
341
342
  body: requestBodyJson,
342
343
  signal: options?.signal,
343
- maxAttempts: MAX_RETRIES + 1,
344
+ maxAttempts: resolveRetryBudget(options?.requestMaxRetries, MAX_RETRIES) + 1,
344
345
  defaultDelayMs: attempt => BASE_DELAY_MS * 2 ** attempt,
345
346
  maxDelayMs: options?.maxRetryDelayMs ?? RATE_LIMIT_BUDGET_MS,
346
347
  fetch: options?.fetch,
@@ -509,7 +510,8 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
509
510
  let receivedContent = false;
510
511
  let currentResponse = response;
511
512
 
512
- for (let emptyAttempt = 0; emptyAttempt <= MAX_EMPTY_STREAM_RETRIES; emptyAttempt++) {
513
+ const emptyStreamRetryBudget = resolveRetryBudget(options?.streamMaxRetries, MAX_EMPTY_STREAM_RETRIES);
514
+ for (let emptyAttempt = 0; emptyAttempt <= emptyStreamRetryBudget; emptyAttempt++) {
513
515
  if (options?.signal?.aborted) {
514
516
  throw new Error("Request was aborted");
515
517
  }
@@ -549,7 +551,7 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
549
551
  break;
550
552
  }
551
553
 
552
- if (emptyAttempt < MAX_EMPTY_STREAM_RETRIES) {
554
+ if (emptyAttempt < emptyStreamRetryBudget) {
553
555
  resetOutput();
554
556
  }
555
557
  }
@@ -18,6 +18,7 @@ import { normalizeSystemPrompts } from "../utils";
18
18
  import { AssistantMessageEventStream } from "../utils/event-stream";
19
19
  import { finalizeErrorMessage, type RawHttpRequestDump } from "../utils/http-inspector";
20
20
  import { parseStreamingJson } from "../utils/json-parse";
21
+ import { resolveRetryBudget } from "../utils/retry-budget";
21
22
  import { toolWireSchema } from "../utils/schema/wire";
22
23
  import { transformMessages } from "./transform-messages";
23
24
 
@@ -402,6 +403,7 @@ export const streamOllama: StreamFunction<"ollama-chat"> = (
402
403
  },
403
404
  body: JSON.stringify(body),
404
405
  signal: options.signal,
406
+ maxAttempts: resolveRetryBudget(options.requestMaxRetries, 4) + 1,
405
407
  defaultDelayMs: OLLAMA_RETRY_DELAYS_MS,
406
408
  fetch: options.fetch,
407
409
  });
@@ -49,6 +49,7 @@ import { AssistantMessageEventStream } from "../utils/event-stream";
49
49
  import { finalizeErrorMessage, type RawHttpRequestDump } from "../utils/http-inspector";
50
50
  import { getOpenAIStreamIdleTimeoutMs, iterateWithIdleTimeout } from "../utils/idle-iterator";
51
51
  import { parseStreamingJson } from "../utils/json-parse";
52
+ import { resolveRetryBudget } from "../utils/retry-budget";
52
53
  import { adaptSchemaForStrict, NO_STRICT, sanitizeSchemaForOpenAIResponses, toolWireSchema } from "../utils/schema";
53
54
  import { compactGrammarDefinition } from "./grammar";
54
55
  import { CODEX_BASE_URL, getCodexAccountId, OPENAI_HEADER_VALUES, OPENAI_HEADERS } from "./openai-codex/constants";
@@ -227,7 +228,10 @@ function isCodexWebSocketEnvEnabled(): boolean {
227
228
  return $flag("PI_CODEX_WEBSOCKET");
228
229
  }
229
230
 
230
- function getCodexWebSocketRetryBudget(): number {
231
+ function getCodexWebSocketRetryBudget(options?: Pick<OpenAICodexResponsesOptions, "streamMaxRetries">): number {
232
+ if (options?.streamMaxRetries !== undefined) {
233
+ return resolveRetryBudget(options.streamMaxRetries, CODEX_WEBSOCKET_RETRY_BUDGET);
234
+ }
231
235
  return parseCodexNonNegativeInteger($env.PI_CODEX_WEBSOCKET_RETRY_BUDGET, CODEX_WEBSOCKET_RETRY_BUDGET);
232
236
  }
233
237
 
@@ -641,7 +645,7 @@ async function openInitialCodexEventStream(
641
645
  }> {
642
646
  const { transformedBody, websocketState } = requestContext;
643
647
  if (websocketState && shouldUseCodexWebSocket(model, websocketState, options?.preferWebsockets)) {
644
- const websocketRetryBudget = getCodexWebSocketRetryBudget();
648
+ const websocketRetryBudget = getCodexWebSocketRetryBudget(options);
645
649
  let websocketRetries = 0;
646
650
  while (true) {
647
651
  try {
@@ -707,7 +711,7 @@ async function openCodexWebSocketTransport(
707
711
  sentModelsEtagHeader: websocketHeaders.has(X_MODELS_ETAG_HEADER),
708
712
  requestType: websocketRequest.type,
709
713
  retry,
710
- retryBudget: getCodexWebSocketRetryBudget(),
714
+ retryBudget: getCodexWebSocketRetryBudget(options),
711
715
  });
712
716
  const eventStream = await openCodexWebSocketEventStream(
713
717
  toWebSocketUrl(requestContext.url),
@@ -744,6 +748,7 @@ async function openCodexSseTransport(
744
748
  requestSetup.requestSignal,
745
749
  event => options?.onSseEvent?.(event, model),
746
750
  options?.fetch,
751
+ options,
747
752
  ),
748
753
  );
749
754
  return { eventStream, requestBodyForState: structuredCloneJSON(body), transport: "sse" };
@@ -1348,7 +1353,7 @@ async function tryRecoverCodexPreviousResponseNotFound(
1348
1353
  runtime.transport !== "websocket" ||
1349
1354
  context.output.content.length > 0 ||
1350
1355
  context.options?.signal?.aborted ||
1351
- runtime.providerRetryAttempt >= CODEX_MAX_RETRIES
1356
+ runtime.providerRetryAttempt >= resolveRetryBudget(context.options?.streamMaxRetries, CODEX_MAX_RETRIES)
1352
1357
  ) {
1353
1358
  return false;
1354
1359
  }
@@ -1390,12 +1395,14 @@ async function tryReplayWebsocketFailureOverSse(
1390
1395
  const replayingBufferedOutputOverSse = context.output.content.length > 0;
1391
1396
  const isFatal = isCodexWebSocketFatalError(streamError);
1392
1397
  const activateFallback =
1393
- replayingBufferedOutputOverSse || isFatal || runtime.websocketStreamRetries >= getCodexWebSocketRetryBudget();
1398
+ replayingBufferedOutputOverSse ||
1399
+ isFatal ||
1400
+ runtime.websocketStreamRetries >= getCodexWebSocketRetryBudget(context.options);
1394
1401
  recordCodexWebSocketFailure(state, activateFallback);
1395
1402
  logCodexDebug("codex websocket stream fallback", {
1396
1403
  error: streamError.message,
1397
1404
  retry: runtime.websocketStreamRetries,
1398
- retryBudget: getCodexWebSocketRetryBudget(),
1405
+ retryBudget: getCodexWebSocketRetryBudget(context.options),
1399
1406
  activated: activateFallback,
1400
1407
  fatal: isFatal,
1401
1408
  replayedBufferedOutput: replayingBufferedOutputOverSse,
@@ -1431,7 +1438,7 @@ async function tryRetryCodexProviderError(
1431
1438
  if (
1432
1439
  !isRetryableCodexProviderError(error) ||
1433
1440
  context.output.content.length > 0 ||
1434
- runtime.providerRetryAttempt >= CODEX_MAX_RETRIES ||
1441
+ runtime.providerRetryAttempt >= resolveRetryBudget(context.options?.streamMaxRetries, CODEX_MAX_RETRIES) ||
1435
1442
  context.options?.signal?.aborted
1436
1443
  ) {
1437
1444
  return false;
@@ -1447,7 +1454,7 @@ async function tryRetryCodexProviderError(
1447
1454
  logCodexDebug("retrying codex provider stream error", {
1448
1455
  error: error instanceof Error ? error.message : String(error),
1449
1456
  retry: runtime.providerRetryAttempt,
1450
- retryBudget: CODEX_MAX_RETRIES,
1457
+ retryBudget: resolveRetryBudget(context.options?.streamMaxRetries, CODEX_MAX_RETRIES),
1451
1458
  transport: runtime.transport,
1452
1459
  });
1453
1460
 
@@ -2187,6 +2194,7 @@ async function openCodexSseEventStream(
2187
2194
  signal?: AbortSignal,
2188
2195
  onSseEvent?: OpenAICodexResponsesOptions["onSseEvent"],
2189
2196
  fetchOverride?: FetchImpl,
2197
+ options?: Pick<OpenAICodexResponsesOptions, "requestMaxRetries">,
2190
2198
  ): Promise<AsyncGenerator<Record<string, unknown>>> {
2191
2199
  const headers = createCodexHeaders(requestHeaders, accountId, apiKey, sessionId, "sse", state);
2192
2200
  logCodexDebug("codex request", {
@@ -2201,7 +2209,7 @@ async function openCodexSseEventStream(
2201
2209
  headers,
2202
2210
  body: JSON.stringify(body),
2203
2211
  signal,
2204
- maxAttempts: CODEX_MAX_RETRIES + 1,
2212
+ maxAttempts: resolveRetryBudget(options?.requestMaxRetries, CODEX_MAX_RETRIES) + 1,
2205
2213
  defaultDelayMs: attempt => CODEX_RETRY_DELAY_MS * (attempt + 1),
2206
2214
  maxDelayMs: CODEX_RATE_LIMIT_BUDGET_MS,
2207
2215
  fetch: fetchOverride,
@@ -56,6 +56,7 @@ import { parseGitHubCopilotApiKey } from "../utils/oauth/github-copilot";
56
56
  import { getKimiCommonHeaders } from "../utils/oauth/kimi";
57
57
  import { notifyProviderResponse } from "../utils/provider-response";
58
58
  import { callWithCopilotModelRetry } from "../utils/retry";
59
+ import { resolveRetryBudget } from "../utils/retry-budget";
59
60
  import { adaptSchemaForStrict, NO_STRICT, toolWireSchema } from "../utils/schema";
60
61
  import { wrapFetchForSseDebug } from "../utils/sse-debug";
61
62
  import { type HealedToolCall, modelMayLeakKimiToolCalls, ToolCallHealer } from "../utils/tool-call-healing";
@@ -444,6 +445,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
444
445
  options?.fetch,
445
446
  options?.streamFirstEventTimeoutMs,
446
447
  options?.authCredentialType,
448
+ options?.requestMaxRetries,
447
449
  );
448
450
  const premiumRequestsTotal = copilotPremiumRequests;
449
451
  getCapturedErrorResponse = captureErrorResponse;
@@ -920,6 +922,7 @@ async function createClient(
920
922
  fetchOverride?: FetchImpl,
921
923
  streamFirstEventTimeoutOverride?: number,
922
924
  authCredentialType?: OpenAICompletionsOptions["authCredentialType"],
925
+ requestMaxRetries?: number,
923
926
  ): Promise<{
924
927
  client: OpenAI;
925
928
  copilotPremiumRequests: number | undefined;
@@ -1051,7 +1054,7 @@ async function createClient(
1051
1054
  apiKey,
1052
1055
  baseURL: baseUrl,
1053
1056
  dangerouslyAllowBrowser: true,
1054
- maxRetries: 5,
1057
+ maxRetries: resolveRetryBudget(requestMaxRetries, 5),
1055
1058
  defaultHeaders: headers,
1056
1059
  defaultQuery: azureDefaultQuery,
1057
1060
  fetch: debugFetch,
@@ -42,6 +42,7 @@ import {
42
42
  import { parseGitHubCopilotApiKey } from "../utils/oauth/github-copilot";
43
43
  import { notifyProviderResponse } from "../utils/provider-response";
44
44
  import { callWithCopilotModelRetry } from "../utils/retry";
45
+ import { resolveRetryBudget } from "../utils/retry-budget";
45
46
  import { adaptSchemaForStrict, NO_STRICT, sanitizeSchemaForOpenAIResponses, toolWireSchema } from "../utils/schema";
46
47
  import { wrapFetchForSseDebug } from "../utils/sse-debug";
47
48
  import { mapToOpenAIResponsesToolChoice, type OpenAIResponsesToolChoice } from "../utils/tool-choice";
@@ -253,6 +254,7 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
253
254
  options?.onSseEvent,
254
255
  options?.fetch,
255
256
  options?.authCredentialType,
257
+ options?.requestMaxRetries,
256
258
  );
257
259
  const premiumRequestsTotal = copilotPremiumRequests;
258
260
  const providerSessionState = getOpenAIResponsesProviderSessionState(model, options?.providerSessionState);
@@ -354,6 +356,7 @@ function createClient(
354
356
  onSseEvent?: OpenAIResponsesOptions["onSseEvent"],
355
357
  fetchOverride?: FetchImpl,
356
358
  authCredentialType?: OpenAIResponsesOptions["authCredentialType"],
359
+ requestMaxRetries?: number,
357
360
  ): {
358
361
  client: OpenAI;
359
362
  copilotPremiumRequests: number | undefined;
@@ -410,7 +413,7 @@ function createClient(
410
413
  apiKey,
411
414
  baseURL: baseUrl,
412
415
  dangerouslyAllowBrowser: true,
413
- maxRetries: 5,
416
+ maxRetries: resolveRetryBudget(requestMaxRetries, 5),
414
417
  defaultHeaders: headers,
415
418
  fetch: onSseEvent
416
419
  ? wrapFetchForSseDebug(transformedFetch, event => onSseEvent(event, model))
package/src/stream.ts CHANGED
@@ -563,6 +563,8 @@ function mapOptionsForApi<TApi extends Api>(
563
563
  headers: options?.headers,
564
564
  initiatorOverride: options?.initiatorOverride,
565
565
  maxRetryDelayMs: options?.maxRetryDelayMs,
566
+ requestMaxRetries: options?.requestMaxRetries,
567
+ streamMaxRetries: options?.streamMaxRetries,
566
568
  metadata: options?.metadata,
567
569
  sessionId: options?.sessionId,
568
570
  providerSessionState: options?.providerSessionState,
package/src/types.ts CHANGED
@@ -310,6 +310,16 @@ export interface StreamOptions {
310
310
  * Default: 60000 (60 seconds). Set to 0 to disable the cap.
311
311
  */
312
312
  maxRetryDelayMs?: number;
313
+ /**
314
+ * Maximum provider request retries for transports/SDKs that retry before a stream is established.
315
+ * Counts retries only, not the initial attempt. Providers keep their built-in default when unset.
316
+ */
317
+ requestMaxRetries?: number;
318
+ /**
319
+ * Maximum provider stream replay retries after a replay-safe transient stream failure.
320
+ * Counts retries only, not the initial stream attempt. Providers keep their built-in default when unset.
321
+ */
322
+ streamMaxRetries?: number;
313
323
  /**
314
324
  * Optional metadata to include in API requests.
315
325
  * Providers extract the fields they understand and ignore the rest.
@@ -0,0 +1,4 @@
1
+ export function resolveRetryBudget(value: number | undefined, fallback: number): number {
2
+ if (value === undefined || !Number.isFinite(value)) return fallback;
3
+ return Math.max(0, Math.floor(value));
4
+ }