@gajae-code/ai 0.15.4 → 0.15.6

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.
Files changed (77) hide show
  1. package/CHANGELOG.md +27 -1
  2. package/dist/types/auth-broker/client.d.ts +6 -2
  3. package/dist/types/auth-broker/remote-store.d.ts +14 -2
  4. package/dist/types/auth-broker/types.d.ts +6 -0
  5. package/dist/types/auth-broker/wire-schemas.d.ts +19 -0
  6. package/dist/types/auth-gateway/server.d.ts +39 -5
  7. package/dist/types/auth-gateway/types.d.ts +16 -2
  8. package/dist/types/auth-storage.d.ts +116 -34
  9. package/dist/types/provider-models/openai-compat.d.ts +1 -0
  10. package/dist/types/provider-models/special.d.ts +2 -1
  11. package/dist/types/providers/kiro-api-key.d.ts +50 -0
  12. package/dist/types/providers/kiro-codewhisperer.d.ts +3 -0
  13. package/dist/types/providers/register-builtins.d.ts +12 -12
  14. package/dist/types/stream.d.ts +2 -1
  15. package/dist/types/types.d.ts +35 -24
  16. package/dist/types/utils/fallback-transport.d.ts +7 -0
  17. package/dist/types/utils/json-parse.d.ts +5 -3
  18. package/dist/types/utils/oauth/api-key-login.d.ts +4 -1
  19. package/dist/types/utils/oauth/api-key-validation.d.ts +12 -6
  20. package/dist/types/utils/oauth/commandcode.d.ts +1 -0
  21. package/dist/types/utils/oauth/types.d.ts +1 -1
  22. package/dist/types/utils/retry.d.ts +2 -0
  23. package/dist/types/utils/tool-call-healing.d.ts +4 -4
  24. package/package.json +3 -3
  25. package/src/auth-broker/client.ts +41 -13
  26. package/src/auth-broker/redact.ts +25 -1
  27. package/src/auth-broker/remote-store.ts +374 -115
  28. package/src/auth-broker/server.ts +131 -91
  29. package/src/auth-broker/types.ts +6 -0
  30. package/src/auth-broker/wire-schemas.ts +6 -0
  31. package/src/auth-gateway/server.ts +447 -79
  32. package/src/auth-gateway/types.ts +28 -2
  33. package/src/auth-storage.ts +742 -157
  34. package/src/cli.ts +1 -0
  35. package/src/model-thinking.ts +16 -0
  36. package/src/models.json +1054 -0
  37. package/src/models.ts +9 -1
  38. package/src/provider-models/descriptors.ts +3 -1
  39. package/src/provider-models/openai-compat.ts +41 -1
  40. package/src/provider-models/special.ts +15 -3
  41. package/src/providers/anthropic.ts +7 -1
  42. package/src/providers/azure-openai-responses.ts +4 -1
  43. package/src/providers/cursor.ts +256 -101
  44. package/src/providers/gitlab-duo.ts +18 -1
  45. package/src/providers/google-gemini-cli.ts +3 -0
  46. package/src/providers/google-shared.ts +3 -0
  47. package/src/providers/kiro-api-key.d.ts +50 -0
  48. package/src/providers/kiro-api-key.ts +786 -0
  49. package/src/providers/kiro-codewhisperer.d.ts +3 -0
  50. package/src/providers/kiro-codewhisperer.ts +34 -9
  51. package/src/providers/ollama.ts +3 -0
  52. package/src/providers/openai-codex-responses.ts +24 -6
  53. package/src/providers/openai-completions.ts +11 -1
  54. package/src/providers/openai-responses-shared.ts +23 -2
  55. package/src/providers/openai-responses.ts +10 -1
  56. package/src/providers/pi-native-client.ts +1 -0
  57. package/src/providers/pi-native-server.ts +24 -0
  58. package/src/providers/register-builtins.d.ts +12 -12
  59. package/src/providers/register-builtins.ts +16 -3
  60. package/src/stream.d.ts +2 -1
  61. package/src/stream.ts +180 -70
  62. package/src/types.d.ts +35 -24
  63. package/src/types.ts +40 -23
  64. package/src/utils/fallback-transport.d.ts +7 -0
  65. package/src/utils/fallback-transport.ts +21 -4
  66. package/src/utils/json-parse.d.ts +5 -3
  67. package/src/utils/json-parse.ts +6 -6
  68. package/src/utils/oauth/api-key-login.ts +13 -2
  69. package/src/utils/oauth/api-key-validation.ts +242 -41
  70. package/src/utils/oauth/commandcode.ts +17 -0
  71. package/src/utils/oauth/index.ts +20 -5
  72. package/src/utils/oauth/types.d.ts +1 -1
  73. package/src/utils/oauth/types.ts +1 -0
  74. package/src/utils/retry.d.ts +2 -0
  75. package/src/utils/retry.ts +15 -2
  76. package/src/utils/tool-call-healing.d.ts +4 -4
  77. package/src/utils/tool-call-healing.ts +4 -4
@@ -1,5 +1,8 @@
1
+ import type { Effort } from "../model-thinking";
1
2
  import type { StreamFunction, StreamOptions } from "../types";
2
3
  export interface KiroCodeWhispererOptions extends StreamOptions {
4
+ /** Effort level for Kiro API-key reasoning. */
5
+ reasoning?: Effort | boolean;
3
6
  /** AWS region for the CodeWhisperer streaming endpoint. */
4
7
  region?: string;
5
8
  /** Profile ARN for enterprise IAM Identity Center accounts. */
@@ -11,6 +11,7 @@
11
11
  * not from any AGPL reference implementation.
12
12
  */
13
13
  import { $credentialEnv, $env, extractHttpStatusFromError } from "@gajae-code/utils";
14
+ import type { Effort } from "../model-thinking";
14
15
  import type {
15
16
  Api,
16
17
  AssistantMessage,
@@ -28,12 +29,15 @@ import { transportFailureFacts } from "../utils/fallback-transport";
28
29
  import { withHttpStatus } from "../utils/http-inspector";
29
30
  import { captureUnicodeEscapeEvidence } from "../utils/json-parse";
30
31
  import { decodeEventStream } from "./aws-eventstream";
32
+ import { isKiroApiKey, streamKiroApiKey, toKiroModelId } from "./kiro-api-key";
31
33
 
32
34
  // ─────────────────────────────────────────────────────────────────────────────
33
35
  // Provider options
34
36
  // ─────────────────────────────────────────────────────────────────────────────
35
37
 
36
38
  export interface KiroCodeWhispererOptions extends StreamOptions {
39
+ /** Effort level for Kiro API-key reasoning. */
40
+ reasoning?: Effort | boolean;
37
41
  /** AWS region for the CodeWhisperer streaming endpoint. */
38
42
  region?: string;
39
43
  /** Profile ARN for enterprise IAM Identity Center accounts. */
@@ -63,6 +67,7 @@ interface WireToolResult {
63
67
  interface WireUserMessage {
64
68
  userInputMessage: {
65
69
  content: string;
70
+ modelId?: string;
66
71
  userInputMessageContext?: {
67
72
  tools?: { tools: WireToolSpec[] };
68
73
  toolResults?: { toolResults: WireToolResult[][] };
@@ -143,6 +148,11 @@ export const streamKiroCodeWhisperer: StreamFunction<"kiro-codewhisperer-stream"
143
148
  context: Context,
144
149
  options: KiroCodeWhispererOptions,
145
150
  ): AssistantMessageEventStream => {
151
+ const token = resolveBearerToken(options.apiKey);
152
+ if (isKiroApiKey(token)) {
153
+ return streamKiroApiKey(model, context, { ...options, apiKey: token });
154
+ }
155
+
146
156
  const stream = new AssistantMessageEventStream();
147
157
 
148
158
  (async () => {
@@ -175,7 +185,7 @@ export const streamKiroCodeWhisperer: StreamFunction<"kiro-codewhisperer-stream"
175
185
  const bearerToken = resolveBearerToken(options.apiKey);
176
186
  if (!bearerToken) {
177
187
  throw new Error(
178
- "No Kiro credentials found. Run 'gjc auth-broker login kiro' to authenticate via AWS Builder ID, or set AWS_BEARER_TOKEN_KIRO.",
188
+ "No Kiro credentials found. Set KIRO_API_KEY (ksk_ from https://app.kiro.dev/settings/api-keys) or run 'gjc auth-broker login kiro'.",
179
189
  );
180
190
  }
181
191
 
@@ -344,7 +354,7 @@ export const streamKiroCodeWhisperer: StreamFunction<"kiro-codewhisperer-stream"
344
354
 
345
355
  function buildConversationState(
346
356
  context: Context,
347
- _model: Model<"kiro-codewhisperer-stream">,
357
+ model: Model<"kiro-codewhisperer-stream">,
348
358
  options: KiroCodeWhispererOptions,
349
359
  ): ConversationState {
350
360
  const messages = context.messages;
@@ -352,18 +362,24 @@ function buildConversationState(
352
362
  throw new Error("Kiro CodeWhisperer requires at least one message");
353
363
  }
354
364
 
365
+ // Normalize the local dashed selector/wire id (e.g. "claude-haiku-4-5") to
366
+ // the canonical dotted upstream Kiro model id (e.g. "claude-haiku-4.5"),
367
+ // matching the sibling ksk_ API-key transport (kiro-api-key.ts) so both
368
+ // auth methods send the same wire form for the same catalog entry.
369
+ const modelId = toKiroModelId(model.wireModelId || model.id);
370
+
355
371
  // Build history from all messages except the last
356
372
  const history: WireHistoryMessage[] = [];
357
373
  const systemPrompt = context.systemPrompt?.join("\n") ?? "";
358
374
 
359
375
  for (let i = 0; i < messages.length - 1; i++) {
360
376
  const msg = messages[i];
361
- history.push(convertToWireMessage(msg, i === 0 ? systemPrompt : undefined));
377
+ history.push(convertToWireMessage(msg, modelId, i === 0 ? systemPrompt : undefined));
362
378
  }
363
379
 
364
380
  // Convert the last message as currentMessage
365
381
  const lastMsg = messages[messages.length - 1];
366
- const currentMessage = convertToWireUserMessage(lastMsg, systemPrompt);
382
+ const currentMessage = convertToWireUserMessage(lastMsg, modelId, systemPrompt);
367
383
 
368
384
  // Add tools to the current message context
369
385
  if (context.tools && context.tools.length > 0) {
@@ -383,12 +399,16 @@ function buildConversationState(
383
399
  };
384
400
  }
385
401
 
386
- function convertToWireMessage(msg: Context["messages"][number], systemPrompt?: string): WireHistoryMessage {
402
+ function convertToWireMessage(
403
+ msg: Context["messages"][number],
404
+ modelId: string,
405
+ systemPrompt?: string,
406
+ ): WireHistoryMessage {
387
407
  if (msg.role === "user") {
388
- return convertToWireUserMessage(msg, systemPrompt);
408
+ return convertToWireUserMessage(msg, modelId, systemPrompt);
389
409
  }
390
410
  if (msg.role === "toolResult") {
391
- return convertToWireUserMessage(msg, systemPrompt);
411
+ return convertToWireUserMessage(msg, modelId, systemPrompt);
392
412
  }
393
413
  // assistant → assistant response
394
414
  const textParts: string[] = [];
@@ -408,7 +428,11 @@ function convertToWireMessage(msg: Context["messages"][number], systemPrompt?: s
408
428
  };
409
429
  }
410
430
 
411
- function convertToWireUserMessage(msg: Context["messages"][number], systemPrompt?: string): WireUserMessage {
431
+ function convertToWireUserMessage(
432
+ msg: Context["messages"][number],
433
+ modelId: string,
434
+ systemPrompt?: string,
435
+ ): WireUserMessage {
412
436
  let content = extractTextContent(msg);
413
437
  if (systemPrompt) {
414
438
  content = `${systemPrompt}\n\n${content}`;
@@ -417,6 +441,7 @@ function convertToWireUserMessage(msg: Context["messages"][number], systemPrompt
417
441
  const userMsg: WireUserMessage = {
418
442
  userInputMessage: {
419
443
  content,
444
+ modelId,
420
445
  },
421
446
  };
422
447
 
@@ -541,7 +566,7 @@ function handleToolUseEvent(
541
566
 
542
567
  function resolveBearerToken(apiKey: string | undefined): string | undefined {
543
568
  if (!apiKey) {
544
- return $credentialEnv("AWS_BEARER_TOKEN_KIRO") ?? undefined;
569
+ return $credentialEnv("KIRO_API_KEY") ?? $credentialEnv("AWS_BEARER_TOKEN_KIRO") ?? undefined;
545
570
  }
546
571
 
547
572
  // Structured API key (from getOAuthApiKey) contains the access token as JSON
@@ -419,6 +419,7 @@ export const streamOllama: StreamFunction<"ollama-chat"> = (
419
419
  url: `${baseUrl}/api/chat`,
420
420
  body,
421
421
  };
422
+ options?.onStreamCreated?.();
422
423
  let response = await fetchWithRetry(`${baseUrl}/api/chat`, {
423
424
  method: "POST",
424
425
  headers: {
@@ -441,6 +442,7 @@ export const streamOllama: StreamFunction<"ollama-chat"> = (
441
442
  if (
442
443
  firstTokenTime === undefined &&
443
444
  !options.fallbackManaged &&
445
+ !options.disableProviderRetries &&
444
446
  isForcedToolChoiceUnsupportedError(error, true)
445
447
  ) {
446
448
  markToolChoiceIncapability(model, "auto", error.message);
@@ -457,6 +459,7 @@ export const streamOllama: StreamFunction<"ollama-chat"> = (
457
459
  body = { ...body };
458
460
  delete (body as { tool_choice?: unknown }).tool_choice;
459
461
  rawRequestDump = { ...rawRequestDump, body };
462
+ options?.onStreamCreated?.();
460
463
  response = await fetchWithRetry(`${baseUrl}/api/chat`, {
461
464
  method: "POST",
462
465
  headers: {
@@ -844,6 +844,7 @@ async function openInitialCodexEventStream(
844
844
  if (websocketState && shouldUseCodexWebSocket(model, websocketState, options?.preferWebsockets)) {
845
845
  const websocketRetryBudget = getCodexWebSocketRetryBudget(options);
846
846
  let websocketRetries = 0;
847
+ let lastWebsocketError: Error | undefined;
847
848
  while (true) {
848
849
  try {
849
850
  return await openCodexWebSocketTransport(
@@ -855,6 +856,7 @@ async function openInitialCodexEventStream(
855
856
  );
856
857
  } catch (error) {
857
858
  const websocketError = error instanceof Error ? error : new Error(String(error));
859
+ lastWebsocketError = websocketError;
858
860
  const isFatal = isCodexWebSocketFatalError(websocketError);
859
861
  const activateFallback = isFatal || websocketRetries >= websocketRetryBudget;
860
862
  recordCodexWebSocketFailure(websocketState, activateFallback);
@@ -875,6 +877,9 @@ async function openInitialCodexEventStream(
875
877
  break;
876
878
  }
877
879
  }
880
+ if (options?.fallbackManaged || options?.disableProviderRetries) {
881
+ throw lastWebsocketError ?? new Error("Codex websocket transport failed");
882
+ }
878
883
  }
879
884
  return openCodexSseTransport(model, requestContext, requestSetup, options, websocketState, transformedBody);
880
885
  }
@@ -1571,6 +1576,8 @@ function handleOutputItemDone(
1571
1576
  arguments: terminalArguments as Record<string, unknown>,
1572
1577
  };
1573
1578
  captureUnicodeEscapeEvidence(toolCall, item.arguments);
1579
+ delete runtime.currentBlock.escapedNonAsciiArguments;
1580
+ delete runtime.currentBlock.escapedUnicodeArgumentEvidence;
1574
1581
  Object.assign(runtime.currentBlock, toolCall);
1575
1582
  captureUnicodeEscapeEvidence(runtime.currentBlock, item.arguments);
1576
1583
  delete (runtime.currentBlock as { partialJson?: string }).partialJson;
@@ -1625,6 +1632,8 @@ function handleOutputItemDone(
1625
1632
  arguments: { input: terminalInput },
1626
1633
  customWireName: item.name,
1627
1634
  };
1635
+ delete runtime.currentBlock.escapedNonAsciiArguments;
1636
+ delete runtime.currentBlock.escapedUnicodeArgumentEvidence;
1628
1637
  Object.assign(runtime.currentBlock, toolCall);
1629
1638
  delete (runtime.currentBlock as { partialJson?: string }).partialJson;
1630
1639
  delete (runtime.currentBlock as { doneInput?: string }).doneInput;
@@ -1735,6 +1744,7 @@ async function tryRetryWithoutForcedToolChoice(
1735
1744
  ): Promise<boolean> {
1736
1745
  if (
1737
1746
  context.options?.fallbackManaged ||
1747
+ context.options?.disableProviderRetries ||
1738
1748
  runtime.toolChoiceFallbackAttempted ||
1739
1749
  context.output.content.length > 0 ||
1740
1750
  context.firstTokenTime !== undefined ||
@@ -1838,7 +1848,8 @@ async function tryReconnectCodexWebSocketOnConnectionLimit(
1838
1848
  !websocketState ||
1839
1849
  runtime.transport !== "websocket" ||
1840
1850
  context.options?.signal?.aborted ||
1841
- context.options?.fallbackManaged
1851
+ context.options?.fallbackManaged ||
1852
+ context.options?.disableProviderRetries
1842
1853
  ) {
1843
1854
  return false;
1844
1855
  }
@@ -1901,6 +1912,7 @@ async function tryRecoverCodexPreviousResponseNotFound(
1901
1912
  runtime.previousResponseRecoveryAttempted ||
1902
1913
  !websocketState ||
1903
1914
  context.options?.fallbackManaged ||
1915
+ context.options?.disableProviderRetries ||
1904
1916
  runtime.transport !== "websocket" ||
1905
1917
  context.output.content.length > 0 ||
1906
1918
  context.options?.signal?.aborted ||
@@ -1941,7 +1953,8 @@ async function tryReplayWebsocketFailureOverSse(
1941
1953
  runtime.canSafelyReplayWebsocketOverSse &&
1942
1954
  !runtime.sawTerminalEvent &&
1943
1955
  !context.options?.signal?.aborted &&
1944
- !context.options?.fallbackManaged;
1956
+ !context.options?.fallbackManaged &&
1957
+ !context.options?.disableProviderRetries;
1945
1958
  if (!canReplay) return false;
1946
1959
 
1947
1960
  const state = websocketState;
@@ -1995,7 +2008,8 @@ async function tryRetryCodexProviderError(
1995
2008
  context.output.content.length > 0 ||
1996
2009
  runtime.providerRetryAttempt >= resolveRetryBudget(context.options?.streamMaxRetries, CODEX_MAX_RETRIES) ||
1997
2010
  context.options?.signal?.aborted ||
1998
- context.options?.fallbackManaged
2011
+ context.options?.fallbackManaged ||
2012
+ context.options?.disableProviderRetries
1999
2013
  ) {
2000
2014
  return false;
2001
2015
  }
@@ -2115,7 +2129,7 @@ export const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses"
2115
2129
  try {
2116
2130
  initialTransport = await openInitialCodexEventStream(model, streamOptions, requestSetup, requestContext);
2117
2131
  } catch (error) {
2118
- if (streamOptions.fallbackManaged) throw error;
2132
+ if (streamOptions.fallbackManaged || streamOptions.disableProviderRetries) throw error;
2119
2133
  initialTransport = await retryCodexInitialTransportWithoutToolChoice(
2120
2134
  model,
2121
2135
  streamOptions,
@@ -2629,6 +2643,7 @@ class CodexWebSocketConnection {
2629
2643
  signal?: AbortSignal,
2630
2644
  firstEventTimeoutMs?: number,
2631
2645
  idleTimeoutMs = this.#idleTimeoutMs,
2646
+ onStreamCreated?: () => void,
2632
2647
  ): AsyncGenerator<Record<string, unknown>> {
2633
2648
  if (!this.#socket || this.#socket.readyState !== WebSocket.OPEN) {
2634
2649
  throw createCodexWebSocketTransportError("websocket connection is unavailable");
@@ -2650,6 +2665,7 @@ class CodexWebSocketConnection {
2650
2665
  }
2651
2666
 
2652
2667
  try {
2668
+ onStreamCreated?.();
2653
2669
  this.#socket.send(JSON.stringify(request));
2654
2670
  let sawFirstProgress = false;
2655
2671
  const startedAt = Date.now();
@@ -2791,7 +2807,7 @@ async function openCodexSseEventStream(
2791
2807
  signal?: AbortSignal,
2792
2808
  onSseEvent?: OpenAICodexResponsesOptions["onSseEvent"],
2793
2809
  fetchOverride?: FetchImpl,
2794
- options?: Pick<OpenAICodexResponsesOptions, "requestMaxRetries">,
2810
+ options?: Pick<OpenAICodexResponsesOptions, "requestMaxRetries" | "onStreamCreated">,
2795
2811
  ): Promise<AsyncGenerator<Record<string, unknown>>> {
2796
2812
  const headers = createCodexHeaders(requestHeaders, accountId, apiKey, sessionId, "sse", state);
2797
2813
  logCodexDebug("codex request", {
@@ -2801,6 +2817,7 @@ async function openCodexSseEventStream(
2801
2817
  sentTurnStateHeader: headers.has(X_CODEX_TURN_STATE_HEADER),
2802
2818
  sentModelsEtagHeader: headers.has(X_MODELS_ETAG_HEADER),
2803
2819
  });
2820
+ options?.onStreamCreated?.();
2804
2821
  const response = await fetchWithRetry(url, {
2805
2822
  method: "POST",
2806
2823
  headers,
@@ -2841,7 +2858,7 @@ async function openCodexWebSocketEventStream(
2841
2858
  request: Record<string, unknown>,
2842
2859
  state: CodexWebSocketSessionState,
2843
2860
  signal?: AbortSignal,
2844
- options?: Pick<OpenAICodexResponsesOptions, "streamFirstEventTimeoutMs" | "streamIdleTimeoutMs">,
2861
+ options?: Pick<OpenAICodexResponsesOptions, "streamFirstEventTimeoutMs" | "streamIdleTimeoutMs" | "onStreamCreated">,
2845
2862
  firstEventTimeoutMs?: number,
2846
2863
  ): Promise<AsyncGenerator<Record<string, unknown>>> {
2847
2864
  const connection = await getOrCreateCodexWebSocketConnection(state, url, headers, signal, options);
@@ -2850,6 +2867,7 @@ async function openCodexWebSocketEventStream(
2850
2867
  signal,
2851
2868
  firstEventTimeoutMs,
2852
2869
  getCodexWebSocketIdleTimeoutMs(options?.streamIdleTimeoutMs),
2870
+ options?.onStreamCreated,
2853
2871
  );
2854
2872
  }
2855
2873
 
@@ -636,6 +636,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
636
636
  headers: requestHeaders,
637
637
  body: params,
638
638
  };
639
+ options?.onStreamCreated?.();
639
640
  const { data, response, request_id } = await client.chat.completions
640
641
  .create(params as OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming, { signal: requestSignal })
641
642
  .withResponse();
@@ -648,6 +649,8 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
648
649
  provider: model.provider,
649
650
  signal: requestSignal,
650
651
  fallbackManaged: options?.fallbackManaged,
652
+ requestMaxRetries: options?.requestMaxRetries,
653
+ disableProviderRetries: options?.disableProviderRetries,
651
654
  });
652
655
  } catch (error) {
653
656
  const capturedErrorResponse = getCapturedErrorResponse();
@@ -656,6 +659,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
656
659
  );
657
660
  if (
658
661
  !options?.fallbackManaged &&
662
+ !options?.disableProviderRetries &&
659
663
  firstTokenTime === undefined &&
660
664
  isForcedToolChoiceUnsupportedError(error, sentForcedToolChoice)
661
665
  ) {
@@ -675,6 +679,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
675
679
  openaiStream = await createCompletionsStream();
676
680
  } else if (
677
681
  !options?.fallbackManaged &&
682
+ !options?.disableProviderRetries &&
678
683
  isOpenRouterAnthropicModel(model) &&
679
684
  !disableStrictTools &&
680
685
  isCompiledGrammarTooLargeStrictError(error, capturedErrorResponse)
@@ -689,6 +694,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
689
694
  } else {
690
695
  if (
691
696
  options?.fallbackManaged ||
697
+ options?.disableProviderRetries ||
692
698
  !shouldRetryWithoutStrictTools(error, capturedErrorResponse, appliedToolStrictMode, context.tools)
693
699
  ) {
694
700
  throw error;
@@ -733,7 +739,11 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
733
739
  continue;
734
740
  }
735
741
  replaySafeChunks.push(chunk);
736
- if (hasNetworkErrorFinishReason(chunk) && !options?.fallbackManaged) {
742
+ if (
743
+ hasNetworkErrorFinishReason(chunk) &&
744
+ !options?.fallbackManaged &&
745
+ !options?.disableProviderRetries
746
+ ) {
737
747
  retryNetworkError = true;
738
748
  break;
739
749
  }
@@ -31,6 +31,7 @@ import {
31
31
  } from "../types";
32
32
  import { normalizeResponsesToolCallId, sanitizeJsonStrings } from "../utils";
33
33
  import type { AssistantMessageEventStream } from "../utils/event-stream";
34
+ import { SERVER_OVERLOADED_PROVIDER_CODE } from "../utils/fallback-transport";
34
35
  import { captureUnicodeEscapeEvidence, isCompleteJson, parseStreamingJson } from "../utils/json-parse";
35
36
  import { areJsonValuesEqual } from "../utils/schema";
36
37
  import { joinTextWithImagePlaceholder, NON_VISION_IMAGE_PLACEHOLDER, partitionVisionContent } from "./vision-guard";
@@ -981,7 +982,10 @@ export async function processResponsesStream<TApi extends Api>(
981
982
  : typeof statusDetailsReason === "string" && statusDetailsReason.length > 0
982
983
  ? `status_details: ${statusDetailsReason}`
983
984
  : "Unknown error (no error details in response)";
984
- throw new Error(message);
985
+ // A `cancelled` response is not a capacity rejection and may already
986
+ // have produced observable work, so only the `failed` status carries
987
+ // the typed code onward.
988
+ throw createResponsesFailedError(message, response.status === "failed" ? error?.code : undefined);
985
989
  }
986
990
  // A response cut short for length (`incomplete`) may have stopped
987
991
  // mid-tool-call. Any tool-call item still tracked in `items` never
@@ -1003,11 +1007,28 @@ export async function processResponsesStream<TApi extends Api>(
1003
1007
  : details?.reason
1004
1008
  ? `incomplete: ${details.reason}`
1005
1009
  : "Unknown error (no error details in response)";
1006
- throw new Error(message);
1010
+ throw createResponsesFailedError(message, error?.code);
1007
1011
  }
1008
1012
  }
1009
1013
  }
1010
1014
 
1015
+ /**
1016
+ * A terminal failure envelope arrives inside an HTTP 200 stream — as
1017
+ * `response.failed`, or as `response.completed` with a `failed` response status
1018
+ * — so the typed `error.code` is the only structured evidence the transport can
1019
+ * keep. Both shapes carry the same structured failure and are typed identically.
1020
+ * Exactly OpenAI's capacity-overload code is carried through as transport facts,
1021
+ * matched case-sensitively; every other failure stays a plain error, so an
1022
+ * untyped, cased, or malformed code can never reach a typed retry admission. The
1023
+ * display message is unchanged either way.
1024
+ */
1025
+ function createResponsesFailedError(message: string, code: string | undefined): Error {
1026
+ if (code !== SERVER_OVERLOADED_PROVIDER_CODE) return new Error(message);
1027
+ const error = new Error(message) as Error & { openaiErrorCode?: string };
1028
+ error.openaiErrorCode = SERVER_OVERLOADED_PROVIDER_CODE;
1029
+ return error;
1030
+ }
1031
+
1011
1032
  /**
1012
1033
  * Whether two raw JSON argument strings decode to the same value. Used to tell a
1013
1034
  * relay's re-serialization of the same tool arguments apart from a genuine
@@ -402,16 +402,24 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
402
402
  };
403
403
  const openaiStream = await callWithCopilotModelRetry(
404
404
  async () => {
405
+ options?.onStreamCreated?.();
405
406
  const { data, response, request_id } = await client.responses
406
407
  .create(params, { signal: requestSignal })
407
408
  .withResponse();
408
409
  await notifyProviderResponse(options, response, model, request_id);
409
410
  return data;
410
411
  },
411
- { provider: model.provider, signal: requestSignal, fallbackManaged: options?.fallbackManaged },
412
+ {
413
+ provider: model.provider,
414
+ signal: requestSignal,
415
+ fallbackManaged: options?.fallbackManaged,
416
+ requestMaxRetries: options?.requestMaxRetries,
417
+ disableProviderRetries: options?.disableProviderRetries,
418
+ },
412
419
  ).catch(async error => {
413
420
  if (
414
421
  options?.fallbackManaged ||
422
+ options?.disableProviderRetries ||
415
423
  !isForcedToolChoiceUnsupportedError(error, isForcedOpenAIResponsesToolChoice(params.tool_choice))
416
424
  ) {
417
425
  throw error;
@@ -431,6 +439,7 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
431
439
  });
432
440
  delete params.tool_choice;
433
441
  if (rawRequestDump) rawRequestDump.body = params;
442
+ options?.onStreamCreated?.();
434
443
  const { data, response, request_id } = await client.responses
435
444
  .create(params, { signal: requestSignal })
436
445
  .withResponse();
@@ -37,6 +37,7 @@ import { AssistantMessageEventStream } from "../utils/event-stream";
37
37
  const NON_WIRE_KEYS = new Set<keyof SimpleStreamOptions>([
38
38
  "signal",
39
39
  "apiKey",
40
+ "onStreamCreated",
40
41
  "fetch",
41
42
  "onPayload",
42
43
  "onResponse",
@@ -78,6 +78,21 @@ const ALLOWED_OPTION_KEYS: ReadonlySet<keyof SimpleStreamOptions> = new Set([
78
78
  "preferWebsockets",
79
79
  ] as const satisfies readonly (keyof SimpleStreamOptions)[]);
80
80
 
81
+ const CREDENTIAL_HEADER_NAMES = new Set([
82
+ "authorization",
83
+ "proxy-authorization",
84
+ "cookie",
85
+ "set-cookie",
86
+ "api-key",
87
+ "x-api-key",
88
+ "x-goog-api-key",
89
+ "anthropic-api-key",
90
+ "x-auth-token",
91
+ "x-access-token",
92
+ "x-api-token",
93
+ "x-client-secret",
94
+ ]);
95
+
81
96
  // ---------------------------------------------------------------------------
82
97
  // parseRequest
83
98
  // ---------------------------------------------------------------------------
@@ -132,6 +147,15 @@ export function parseRequest(body: unknown, _headers?: Headers): PiNativeParsedR
132
147
  for (const [k, v] of Object.entries(rawOpts)) {
133
148
  if (v === undefined || v === null) continue;
134
149
  if (!ALLOWED_OPTION_KEYS.has(k as keyof SimpleStreamOptions)) continue;
150
+ if (k === "headers" && typeof v === "object" && v !== null && !Array.isArray(v)) {
151
+ const safeHeaders: Record<string, string> = {};
152
+ for (const [name, value] of Object.entries(v)) {
153
+ if (CREDENTIAL_HEADER_NAMES.has(name.toLowerCase())) continue;
154
+ if (typeof value === "string") safeHeaders[name] = value;
155
+ }
156
+ optsBag[k] = safeHeaders;
157
+ continue;
158
+ }
135
159
  optsBag[k] = v;
136
160
  }
137
161
  }
@@ -44,16 +44,16 @@ export declare function resolveLazyStreamFirstEventFallbackMs(provider: string,
44
44
  export declare const PROVIDER_RUNTIME_DESCRIPTORS: readonly ProviderRuntimeDescriptor<Api, unknown>[];
45
45
  /** Return the lazy descriptor for a built-in API, if one is registered. */
46
46
  export declare function getProviderRuntimeDescriptor<TApi extends Api>(api: TApi): ProviderRuntimeDescriptor<TApi, unknown> | undefined;
47
- export declare const streamAnthropic: (model: Model<"anthropic-messages">, context: Context, options: OptionsForApi<"anthropic-messages">) => EventStreamImpl;
48
- export declare const streamAzureOpenAIResponses: (model: Model<"azure-openai-responses">, context: Context, options: OptionsForApi<"azure-openai-responses">) => EventStreamImpl;
49
- export declare const streamGoogle: (model: Model<"google-generative-ai">, context: Context, options: OptionsForApi<"google-generative-ai">) => EventStreamImpl;
50
- export declare const streamGoogleGeminiCli: (model: Model<"google-gemini-cli">, context: Context, options: OptionsForApi<"google-gemini-cli">) => EventStreamImpl;
51
- export declare const streamGoogleVertex: (model: Model<"google-vertex">, context: Context, options: OptionsForApi<"google-vertex">) => EventStreamImpl;
52
- export declare const streamOpenAICodexResponses: (model: Model<"openai-codex-responses">, context: Context, options: OptionsForApi<"openai-codex-responses">) => EventStreamImpl;
53
- export declare const streamOpenAICompletions: (model: Model<"openai-completions">, context: Context, options: OptionsForApi<"openai-completions">) => EventStreamImpl;
54
- export declare const streamOpenAIResponses: (model: Model<"openai-responses">, context: Context, options: OptionsForApi<"openai-responses">) => EventStreamImpl;
55
- export declare const streamCursor: (model: Model<"cursor-agent">, context: Context, options: OptionsForApi<"cursor-agent">) => EventStreamImpl;
56
- export declare const streamOllama: (model: Model<"ollama-chat">, context: Context, options: OptionsForApi<"ollama-chat">) => EventStreamImpl;
57
- export declare const streamBedrock: (model: Model<"bedrock-converse-stream">, context: Context, options: OptionsForApi<"bedrock-converse-stream">) => EventStreamImpl;
58
- export declare const streamKiroCodeWhisperer: (model: Model<"kiro-codewhisperer-stream">, context: Context, options: OptionsForApi<"kiro-codewhisperer-stream">) => EventStreamImpl;
47
+ export declare const streamAnthropic: (model: Model<"anthropic-messages">, context: Context, options: OptionsForApi<"anthropic-messages">, onStreamCreated?: () => void) => EventStreamImpl;
48
+ export declare const streamAzureOpenAIResponses: (model: Model<"azure-openai-responses">, context: Context, options: OptionsForApi<"azure-openai-responses">, onStreamCreated?: () => void) => EventStreamImpl;
49
+ export declare const streamGoogle: (model: Model<"google-generative-ai">, context: Context, options: OptionsForApi<"google-generative-ai">, onStreamCreated?: () => void) => EventStreamImpl;
50
+ export declare const streamGoogleGeminiCli: (model: Model<"google-gemini-cli">, context: Context, options: OptionsForApi<"google-gemini-cli">, onStreamCreated?: () => void) => EventStreamImpl;
51
+ export declare const streamGoogleVertex: (model: Model<"google-vertex">, context: Context, options: OptionsForApi<"google-vertex">, onStreamCreated?: () => void) => EventStreamImpl;
52
+ export declare const streamOpenAICodexResponses: (model: Model<"openai-codex-responses">, context: Context, options: OptionsForApi<"openai-codex-responses">, onStreamCreated?: () => void) => EventStreamImpl;
53
+ export declare const streamOpenAICompletions: (model: Model<"openai-completions">, context: Context, options: OptionsForApi<"openai-completions">, onStreamCreated?: () => void) => EventStreamImpl;
54
+ export declare const streamOpenAIResponses: (model: Model<"openai-responses">, context: Context, options: OptionsForApi<"openai-responses">, onStreamCreated?: () => void) => EventStreamImpl;
55
+ export declare const streamCursor: (model: Model<"cursor-agent">, context: Context, options: OptionsForApi<"cursor-agent">, onStreamCreated?: () => void) => EventStreamImpl;
56
+ export declare const streamOllama: (model: Model<"ollama-chat">, context: Context, options: OptionsForApi<"ollama-chat">, onStreamCreated?: () => void) => EventStreamImpl;
57
+ export declare const streamBedrock: (model: Model<"bedrock-converse-stream">, context: Context, options: OptionsForApi<"bedrock-converse-stream">, onStreamCreated?: () => void) => EventStreamImpl;
58
+ export declare const streamKiroCodeWhisperer: (model: Model<"kiro-codewhisperer-stream">, context: Context, options: OptionsForApi<"kiro-codewhisperer-stream">, onStreamCreated?: () => void) => EventStreamImpl;
59
59
  export {};
@@ -252,10 +252,17 @@ function forwardStream<TApi extends Api>(
252
252
  model: Model<TApi>,
253
253
  options: OptionsForApi<TApi>,
254
254
  abortTracker: AbortSourceTracker,
255
+ onStreamCreated?: () => void,
255
256
  limits?: LazyStreamLimits,
256
257
  ): void {
257
258
  (async () => {
258
259
  try {
260
+ let admitted = false;
261
+ const markAdmission = (): void => {
262
+ if (admitted) return;
263
+ admitted = true;
264
+ onStreamCreated?.();
265
+ };
259
266
  let watchedSource = source;
260
267
  if (!limits?.providerOwnsWatchdog) {
261
268
  const idleTimeoutMs = options.streamIdleTimeoutMs ?? getStreamIdleTimeoutMs(limits?.defaultIdleTimeoutMs);
@@ -286,6 +293,7 @@ function forwardStream<TApi extends Api>(
286
293
  }
287
294
 
288
295
  for await (const event of watchedSource) {
296
+ if (event.type !== "start") markAdmission();
289
297
  target.push(event);
290
298
  }
291
299
  if (hasFinalResult(source)) {
@@ -337,8 +345,13 @@ function createLazyLoadErrorMessage<TApi extends Api>(
337
345
  function createLazyStream<TApi extends Api>(
338
346
  loadModule: () => Promise<LazyProviderModule<TApi>>,
339
347
  limits?: LazyStreamLimits,
340
- ): (model: Model<TApi>, context: Context, options: OptionsForApi<TApi>) => EventStreamImpl {
341
- return (model, context, options) => {
348
+ ): (
349
+ model: Model<TApi>,
350
+ context: Context,
351
+ options: OptionsForApi<TApi>,
352
+ onStreamCreated?: () => void,
353
+ ) => EventStreamImpl {
354
+ return (model, context, options, onStreamCreated) => {
342
355
  let abortTracker: AbortSourceTracker | undefined;
343
356
  const outer = new EventStreamImpl(() =>
344
357
  abortTracker?.abortLocally(new Error("Provider stream consumer stopped before completion")),
@@ -350,7 +363,7 @@ function createLazyStream<TApi extends Api>(
350
363
  abortTracker = createAbortSourceTracker(streamOptions.signal);
351
364
  const providerOptions = { ...streamOptions, signal: abortTracker.requestSignal } as OptionsForApi<TApi>;
352
365
  const inner = module.stream(model, context, providerOptions);
353
- forwardStream(outer, inner, model, streamOptions, abortTracker, limits);
366
+ forwardStream(outer, inner, model, streamOptions, abortTracker, onStreamCreated, limits);
354
367
  })
355
368
  .catch(error => {
356
369
  const message = createLazyLoadErrorMessage(model, error);
package/src/stream.d.ts CHANGED
@@ -29,12 +29,13 @@ export declare function listProvidersWithEnvKey(): string[];
29
29
  * handling, so callers can append it unconditionally.
30
30
  */
31
31
  export declare function formatProviderCredentialHint(provider: string): string;
32
+ export declare function streamFromLazyImport(createInner: () => Promise<AssistantMessageEventStream>, signal?: AbortSignal, onStreamCreated?: () => void): AssistantMessageEventStream;
32
33
  /**
33
34
  * Build an actionable "missing API key" error for a provider, used by the
34
35
  * low-level `stream`/`complete` entry points (#755).
35
36
  */
36
37
  export declare function formatMissingApiKeyError(provider: string): string;
37
- export declare function stream<TApi extends Api>(model: Model<TApi>, context: Context, options?: OptionsForApi<TApi>): AssistantMessageEventStream;
38
+ export declare function stream<TApi extends Api>(model: Model<TApi>, context: Context, options?: OptionsForApi<TApi>, onStreamCreated?: () => void): AssistantMessageEventStream;
38
39
  export declare function complete<TApi extends Api>(model: Model<TApi>, context: Context, options?: OptionsForApi<TApi>): Promise<AssistantMessage>;
39
40
  export declare function streamSimple<TApi extends Api>(model: Model<TApi>, context: Context, options?: SimpleStreamOptions): AssistantMessageEventStream;
40
41
  export declare function completeSimple<TApi extends Api>(model: Model<TApi>, context: Context, options?: SimpleStreamOptions): Promise<AssistantMessage>;