@gajae-code/ai 0.15.5 → 0.16.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.
Files changed (88) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/dist/types/adapter-internals/aws-region.d.ts +7 -0
  3. package/dist/types/auth-broker/client.d.ts +6 -2
  4. package/dist/types/auth-broker/remote-store.d.ts +14 -2
  5. package/dist/types/auth-broker/types.d.ts +6 -0
  6. package/dist/types/auth-broker/wire-schemas.d.ts +19 -0
  7. package/dist/types/auth-gateway/server.d.ts +39 -5
  8. package/dist/types/auth-gateway/types.d.ts +16 -2
  9. package/dist/types/auth-storage.d.ts +92 -24
  10. package/dist/types/core.d.ts +1 -0
  11. package/dist/types/index.d.ts +1 -0
  12. package/dist/types/provider-models/openai-compat.d.ts +1 -0
  13. package/dist/types/providers/anthropic.d.ts +1 -1
  14. package/dist/types/providers/google-gemini-headers.d.ts +1 -1
  15. package/dist/types/providers/openai-codex-responses.d.ts +6 -0
  16. package/dist/types/providers/register-builtins.d.ts +12 -12
  17. package/dist/types/stream.d.ts +2 -1
  18. package/dist/types/types.d.ts +22 -2
  19. package/dist/types/utils/oauth/api-key-login.d.ts +4 -1
  20. package/dist/types/utils/oauth/api-key-validation.d.ts +12 -6
  21. package/dist/types/utils/oauth/commandcode.d.ts +1 -0
  22. package/dist/types/utils/oauth/types.d.ts +1 -1
  23. package/dist/types/utils/retry.d.ts +2 -0
  24. package/dist/types/utils/schema/normalize.d.ts +0 -5
  25. package/dist/types/utils/sqlite-errors.d.ts +4 -0
  26. package/package.json +3 -3
  27. package/src/adapter-internals/aws-region.d.ts +7 -0
  28. package/src/adapter-internals/aws-region.ts +14 -0
  29. package/src/auth-broker/client.ts +41 -13
  30. package/src/auth-broker/redact.ts +25 -1
  31. package/src/auth-broker/remote-store.ts +374 -115
  32. package/src/auth-broker/server.ts +131 -91
  33. package/src/auth-broker/types.ts +6 -0
  34. package/src/auth-broker/wire-schemas.ts +6 -0
  35. package/src/auth-gateway/server.ts +447 -79
  36. package/src/auth-gateway/types.ts +28 -2
  37. package/src/auth-storage.ts +672 -168
  38. package/src/cli.ts +1 -0
  39. package/src/core.ts +1 -0
  40. package/src/index.ts +1 -0
  41. package/src/model-thinking.ts +8 -0
  42. package/src/models.json +1224 -3
  43. package/src/provider-models/descriptors.ts +3 -1
  44. package/src/provider-models/openai-compat.ts +102 -1
  45. package/src/providers/amazon-bedrock.ts +5 -1
  46. package/src/providers/anthropic.d.ts +1 -1
  47. package/src/providers/anthropic.ts +8 -2
  48. package/src/providers/aws-credentials.ts +6 -0
  49. package/src/providers/azure-openai-responses.ts +4 -1
  50. package/src/providers/cursor.ts +256 -101
  51. package/src/providers/gitlab-duo.ts +18 -1
  52. package/src/providers/google-gemini-cli.ts +3 -0
  53. package/src/providers/google-gemini-headers.d.ts +1 -1
  54. package/src/providers/google-gemini-headers.ts +1 -1
  55. package/src/providers/google-shared.ts +3 -0
  56. package/src/providers/kiro-api-key.ts +33 -8
  57. package/src/providers/kiro-codewhisperer.ts +28 -9
  58. package/src/providers/ollama.ts +3 -0
  59. package/src/providers/openai-codex-responses.d.ts +6 -0
  60. package/src/providers/openai-codex-responses.ts +37 -8
  61. package/src/providers/openai-completions.ts +11 -1
  62. package/src/providers/openai-responses.ts +10 -1
  63. package/src/providers/pi-native-client.ts +25 -1
  64. package/src/providers/pi-native-server.ts +24 -0
  65. package/src/providers/register-builtins.d.ts +12 -12
  66. package/src/providers/register-builtins.ts +16 -3
  67. package/src/stream.d.ts +2 -1
  68. package/src/stream.ts +175 -67
  69. package/src/types.d.ts +22 -2
  70. package/src/types.ts +27 -1
  71. package/src/utils/oauth/api-key-login.ts +13 -2
  72. package/src/utils/oauth/api-key-validation.ts +242 -41
  73. package/src/utils/oauth/commandcode.ts +17 -0
  74. package/src/utils/oauth/index.ts +20 -5
  75. package/src/utils/oauth/kiro.ts +91 -22
  76. package/src/utils/oauth/types.d.ts +1 -1
  77. package/src/utils/oauth/types.ts +1 -0
  78. package/src/utils/retry.d.ts +2 -0
  79. package/src/utils/retry.ts +15 -2
  80. package/src/utils/schema/dereference.ts +169 -49
  81. package/src/utils/schema/draft.ts +46 -23
  82. package/src/utils/schema/normalize.d.ts +0 -5
  83. package/src/utils/schema/normalize.ts +396 -119
  84. package/src/utils/schema/types.ts +3 -1
  85. package/src/utils/schema/zod-decontaminate.ts +83 -29
  86. package/src/utils/sqlite-errors.d.ts +4 -0
  87. package/src/utils/sqlite-errors.ts +13 -0
  88. package/src/utils/tool-choice-capability.ts +2 -3
@@ -5,7 +5,7 @@
5
5
  */
6
6
  export const GEMINI_CLI_VERSION_ENV = "GJC_AI_GEMINI_CLI_VERSION";
7
7
  export const LEGACY_GEMINI_CLI_VERSION_ENV = "PI_AI_GEMINI_CLI_VERSION";
8
- export const DEFAULT_GEMINI_CLI_VERSION = "0.52.0";
8
+ export const DEFAULT_GEMINI_CLI_VERSION = "0.58.0";
9
9
 
10
10
  export function getGeminiCliUserAgent(modelId = "gemini-3.1-pro-preview"): string {
11
11
  const version =
@@ -921,6 +921,7 @@ export function streamGoogleGenAI<T extends "google-generative-ai" | "google-ver
921
921
  options.toolChoice !== "auto" &&
922
922
  options.toolChoice !== "none";
923
923
  const fetchImpl = plan.fetch ?? options?.fetch ?? (globalThis.fetch.bind(globalThis) as FetchImpl);
924
+ options?.onStreamCreated?.();
924
925
  let response = await fetchImpl(plan.url, {
925
926
  method: "POST",
926
927
  headers: { ...plan.headers, "Content-Type": "application/json", Accept: "text/event-stream" },
@@ -935,6 +936,7 @@ export function streamGoogleGenAI<T extends "google-generative-ai" | "google-ver
935
936
  );
936
937
  if (
937
938
  !options?.fallbackManaged &&
939
+ !options?.disableProviderRetries &&
938
940
  firstTokenTime === undefined &&
939
941
  isForcedToolChoiceUnsupportedError(error, true)
940
942
  ) {
@@ -957,6 +959,7 @@ export function streamGoogleGenAI<T extends "google-generative-ai" | "google-ver
957
959
  params = retryParams;
958
960
  rawRequestDump = { ...rawRequestDump, body: params };
959
961
  wireBody = paramsToWireBody(params);
962
+ options?.onStreamCreated?.();
960
963
  response = await fetchImpl(plan.url, {
961
964
  method: "POST",
962
965
  headers: { ...plan.headers, "Content-Type": "application/json", Accept: "text/event-stream" },
@@ -6,6 +6,7 @@
6
6
  * AWS SSO OIDC / CodeWhisperer streaming path used by `gjc auth-broker login kiro`.
7
7
  */
8
8
  import { $env } from "@gajae-code/utils";
9
+ import { assertAwsRegionLabel } from "../adapter-internals/aws-region";
9
10
  import { Effort } from "../model-thinking";
10
11
  import type {
11
12
  Api,
@@ -53,19 +54,40 @@ export function isKiroApiKey(value: string | undefined): value is string {
53
54
 
54
55
  export function kiroApiRegion(options?: { region?: string }): string {
55
56
  return (
56
- options?.region ||
57
- $env.KIRO_API_REGION ||
58
- $env.KIRO_REGION ||
59
- $env.AWS_REGION ||
60
- $env.AWS_DEFAULT_REGION ||
57
+ options?.region ??
58
+ $env.KIRO_API_REGION ??
59
+ $env.KIRO_REGION ??
60
+ $env.AWS_REGION ??
61
+ $env.AWS_DEFAULT_REGION ??
61
62
  DEFAULT_REGION
62
63
  );
63
64
  }
64
65
 
65
66
  export function kiroApiBaseUrl(region: string): string {
67
+ assertAwsRegionLabel(region);
66
68
  return `https://q.${region}.amazonaws.com/`;
67
69
  }
68
70
 
71
+ function isRegionDerivedKiroApiBaseUrl(baseUrl: string): boolean {
72
+ try {
73
+ const url = new URL(baseUrl);
74
+ const match = /^q\.([a-z0-9-]+)\.amazonaws\.com$/.exec(url.hostname);
75
+ if (!match) return false;
76
+ assertAwsRegionLabel(match[1]);
77
+ return (
78
+ url.protocol === "https:" &&
79
+ url.username === "" &&
80
+ url.password === "" &&
81
+ url.port === "" &&
82
+ url.pathname === "/" &&
83
+ url.search === "" &&
84
+ url.hash === ""
85
+ );
86
+ } catch {
87
+ return false;
88
+ }
89
+ }
90
+
69
91
  export function toKiroModelId(modelId: string): string {
70
92
  return modelId.replace(/(\d)-(\d)/g, "$1.$2");
71
93
  }
@@ -279,12 +301,13 @@ export async function fetchKiroApiModels(
279
301
  apiKey: string,
280
302
  region?: string,
281
303
  ): Promise<Model<"kiro-codewhisperer-stream">[]> {
282
- const resolvedRegion = region || kiroApiRegion();
304
+ const resolvedRegion = region ?? kiroApiRegion();
283
305
  const baseUrl = kiroApiBaseUrl(resolvedRegion);
284
306
  const response = await fetch(baseUrl, {
285
307
  method: "POST",
286
308
  headers: kiroApiHeaders(apiKey, LIST_TARGET),
287
309
  body: JSON.stringify({ origin: KIRO_ORIGIN }),
310
+ redirect: "error",
288
311
  signal: AbortSignal.timeout(15_000),
289
312
  });
290
313
  if (!response.ok) {
@@ -585,8 +608,9 @@ export const streamKiroApiKey: StreamFunction<"kiro-codewhisperer-stream"> = (
585
608
  "Kiro API key missing. Set KIRO_API_KEY to a ksk_ key from https://app.kiro.dev/settings/api-keys.",
586
609
  );
587
610
  }
588
- const region = kiroApiRegion(options);
589
- const endpoint = model.baseUrl || kiroApiBaseUrl(region);
611
+ const configuredBaseUrl = model.baseUrl;
612
+ const usesExplicitBaseUrl = Boolean(configuredBaseUrl) && !isRegionDerivedKiroApiBaseUrl(configuredBaseUrl);
613
+ const endpoint = configuredBaseUrl || kiroApiBaseUrl(kiroApiRegion(options));
590
614
  const request = buildApiKeyRequest(model, context, options);
591
615
  options?.onPayload?.(request, model, options?.attemptScope);
592
616
 
@@ -594,6 +618,7 @@ export const streamKiroApiKey: StreamFunction<"kiro-codewhisperer-stream"> = (
594
618
  method: "POST",
595
619
  headers: { ...kiroApiHeaders(apiKey, CHAT_TARGET), ...(options.headers ?? {}) },
596
620
  body: JSON.stringify(request),
621
+ ...(usesExplicitBaseUrl ? {} : { redirect: "error" as const }),
597
622
  signal: options.signal,
598
623
  });
599
624
  if (!response.ok) {
@@ -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 { assertAwsRegionLabel } from "../adapter-internals/aws-region";
14
15
  import type { Effort } from "../model-thinking";
15
16
  import type {
16
17
  Api,
@@ -29,7 +30,7 @@ import { transportFailureFacts } from "../utils/fallback-transport";
29
30
  import { withHttpStatus } from "../utils/http-inspector";
30
31
  import { captureUnicodeEscapeEvidence } from "../utils/json-parse";
31
32
  import { decodeEventStream } from "./aws-eventstream";
32
- import { isKiroApiKey, streamKiroApiKey } from "./kiro-api-key";
33
+ import { isKiroApiKey, streamKiroApiKey, toKiroModelId } from "./kiro-api-key";
33
34
 
34
35
  // ─────────────────────────────────────────────────────────────────────────────
35
36
  // Provider options
@@ -67,6 +68,7 @@ interface WireToolResult {
67
68
  interface WireUserMessage {
68
69
  userInputMessage: {
69
70
  content: string;
71
+ modelId?: string;
70
72
  userInputMessageContext?: {
71
73
  tools?: { tools: WireToolSpec[] };
72
74
  toolResults?: { toolResults: WireToolResult[][] };
@@ -177,9 +179,10 @@ export const streamKiroCodeWhisperer: StreamFunction<"kiro-codewhisperer-stream"
177
179
  };
178
180
 
179
181
  const blocks = output.content as Block[];
180
- const region = options.region || $env.KIRO_REGION || $env.AWS_REGION || $env.AWS_DEFAULT_REGION || DEFAULT_REGION;
182
+ const region = options.region ?? $env.KIRO_REGION ?? $env.AWS_REGION ?? $env.AWS_DEFAULT_REGION ?? DEFAULT_REGION;
181
183
 
182
184
  try {
185
+ assertAwsRegionLabel(region);
183
186
  // Resolve bearer token
184
187
  const bearerToken = resolveBearerToken(options.apiKey);
185
188
  if (!bearerToken) {
@@ -221,6 +224,7 @@ export const streamKiroCodeWhisperer: StreamFunction<"kiro-codewhisperer-stream"
221
224
  method: "POST",
222
225
  headers: requestHeaders,
223
226
  body,
227
+ redirect: "error",
224
228
  signal: options.signal,
225
229
  });
226
230
 
@@ -353,7 +357,7 @@ export const streamKiroCodeWhisperer: StreamFunction<"kiro-codewhisperer-stream"
353
357
 
354
358
  function buildConversationState(
355
359
  context: Context,
356
- _model: Model<"kiro-codewhisperer-stream">,
360
+ model: Model<"kiro-codewhisperer-stream">,
357
361
  options: KiroCodeWhispererOptions,
358
362
  ): ConversationState {
359
363
  const messages = context.messages;
@@ -361,18 +365,24 @@ function buildConversationState(
361
365
  throw new Error("Kiro CodeWhisperer requires at least one message");
362
366
  }
363
367
 
368
+ // Normalize the local dashed selector/wire id (e.g. "claude-haiku-4-5") to
369
+ // the canonical dotted upstream Kiro model id (e.g. "claude-haiku-4.5"),
370
+ // matching the sibling ksk_ API-key transport (kiro-api-key.ts) so both
371
+ // auth methods send the same wire form for the same catalog entry.
372
+ const modelId = toKiroModelId(model.wireModelId || model.id);
373
+
364
374
  // Build history from all messages except the last
365
375
  const history: WireHistoryMessage[] = [];
366
376
  const systemPrompt = context.systemPrompt?.join("\n") ?? "";
367
377
 
368
378
  for (let i = 0; i < messages.length - 1; i++) {
369
379
  const msg = messages[i];
370
- history.push(convertToWireMessage(msg, i === 0 ? systemPrompt : undefined));
380
+ history.push(convertToWireMessage(msg, modelId, i === 0 ? systemPrompt : undefined));
371
381
  }
372
382
 
373
383
  // Convert the last message as currentMessage
374
384
  const lastMsg = messages[messages.length - 1];
375
- const currentMessage = convertToWireUserMessage(lastMsg, systemPrompt);
385
+ const currentMessage = convertToWireUserMessage(lastMsg, modelId, systemPrompt);
376
386
 
377
387
  // Add tools to the current message context
378
388
  if (context.tools && context.tools.length > 0) {
@@ -392,12 +402,16 @@ function buildConversationState(
392
402
  };
393
403
  }
394
404
 
395
- function convertToWireMessage(msg: Context["messages"][number], systemPrompt?: string): WireHistoryMessage {
405
+ function convertToWireMessage(
406
+ msg: Context["messages"][number],
407
+ modelId: string,
408
+ systemPrompt?: string,
409
+ ): WireHistoryMessage {
396
410
  if (msg.role === "user") {
397
- return convertToWireUserMessage(msg, systemPrompt);
411
+ return convertToWireUserMessage(msg, modelId, systemPrompt);
398
412
  }
399
413
  if (msg.role === "toolResult") {
400
- return convertToWireUserMessage(msg, systemPrompt);
414
+ return convertToWireUserMessage(msg, modelId, systemPrompt);
401
415
  }
402
416
  // assistant → assistant response
403
417
  const textParts: string[] = [];
@@ -417,7 +431,11 @@ function convertToWireMessage(msg: Context["messages"][number], systemPrompt?: s
417
431
  };
418
432
  }
419
433
 
420
- function convertToWireUserMessage(msg: Context["messages"][number], systemPrompt?: string): WireUserMessage {
434
+ function convertToWireUserMessage(
435
+ msg: Context["messages"][number],
436
+ modelId: string,
437
+ systemPrompt?: string,
438
+ ): WireUserMessage {
421
439
  let content = extractTextContent(msg);
422
440
  if (systemPrompt) {
423
441
  content = `${systemPrompt}\n\n${content}`;
@@ -426,6 +444,7 @@ function convertToWireUserMessage(msg: Context["messages"][number], systemPrompt
426
444
  const userMsg: WireUserMessage = {
427
445
  userInputMessage: {
428
446
  content,
447
+ modelId,
429
448
  },
430
449
  };
431
450
 
@@ -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: {
@@ -25,6 +25,12 @@ export declare function normalizeCodexToolChoice(choice: ToolChoice | undefined,
25
25
  export declare function formatCodexUserAgent(platform: string, release: string, arch: string): string;
26
26
  export declare const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses">;
27
27
  export declare function prewarmOpenAICodexResponses(model: Model<"openai-codex-responses">, options?: Pick<OpenAICodexResponsesOptions, "apiKey" | "headers" | "sessionId" | "signal" | "preferWebsockets" | "providerSessionState">): Promise<void>;
28
+ /**
29
+ * Bun 1.4.0's Windows WebSocket client can segfault during TLS handshakes.
30
+ * Keep the model's websocket preference available on other platforms, while
31
+ * requiring an explicit opt-in on Windows until the bundled runtime is fixed.
32
+ */
33
+ export declare function isCodexWebSocketSafeByDefault(platform?: NodeJS.Platform): boolean;
28
34
  export interface OpenAICodexTransportDetails {
29
35
  websocketPreferred: boolean;
30
36
  lastTransport?: CodexTransport;
@@ -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
  }
@@ -1739,6 +1744,7 @@ async function tryRetryWithoutForcedToolChoice(
1739
1744
  ): Promise<boolean> {
1740
1745
  if (
1741
1746
  context.options?.fallbackManaged ||
1747
+ context.options?.disableProviderRetries ||
1742
1748
  runtime.toolChoiceFallbackAttempted ||
1743
1749
  context.output.content.length > 0 ||
1744
1750
  context.firstTokenTime !== undefined ||
@@ -1842,7 +1848,8 @@ async function tryReconnectCodexWebSocketOnConnectionLimit(
1842
1848
  !websocketState ||
1843
1849
  runtime.transport !== "websocket" ||
1844
1850
  context.options?.signal?.aborted ||
1845
- context.options?.fallbackManaged
1851
+ context.options?.fallbackManaged ||
1852
+ context.options?.disableProviderRetries
1846
1853
  ) {
1847
1854
  return false;
1848
1855
  }
@@ -1905,6 +1912,7 @@ async function tryRecoverCodexPreviousResponseNotFound(
1905
1912
  runtime.previousResponseRecoveryAttempted ||
1906
1913
  !websocketState ||
1907
1914
  context.options?.fallbackManaged ||
1915
+ context.options?.disableProviderRetries ||
1908
1916
  runtime.transport !== "websocket" ||
1909
1917
  context.output.content.length > 0 ||
1910
1918
  context.options?.signal?.aborted ||
@@ -1945,7 +1953,8 @@ async function tryReplayWebsocketFailureOverSse(
1945
1953
  runtime.canSafelyReplayWebsocketOverSse &&
1946
1954
  !runtime.sawTerminalEvent &&
1947
1955
  !context.options?.signal?.aborted &&
1948
- !context.options?.fallbackManaged;
1956
+ !context.options?.fallbackManaged &&
1957
+ !context.options?.disableProviderRetries;
1949
1958
  if (!canReplay) return false;
1950
1959
 
1951
1960
  const state = websocketState;
@@ -1999,7 +2008,8 @@ async function tryRetryCodexProviderError(
1999
2008
  context.output.content.length > 0 ||
2000
2009
  runtime.providerRetryAttempt >= resolveRetryBudget(context.options?.streamMaxRetries, CODEX_MAX_RETRIES) ||
2001
2010
  context.options?.signal?.aborted ||
2002
- context.options?.fallbackManaged
2011
+ context.options?.fallbackManaged ||
2012
+ context.options?.disableProviderRetries
2003
2013
  ) {
2004
2014
  return false;
2005
2015
  }
@@ -2119,7 +2129,7 @@ export const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses"
2119
2129
  try {
2120
2130
  initialTransport = await openInitialCodexEventStream(model, streamOptions, requestSetup, requestContext);
2121
2131
  } catch (error) {
2122
- if (streamOptions.fallbackManaged) throw error;
2132
+ if (streamOptions.fallbackManaged || streamOptions.disableProviderRetries) throw error;
2123
2133
  initialTransport = await retryCodexInitialTransportWithoutToolChoice(
2124
2134
  model,
2125
2135
  streamOptions,
@@ -2297,6 +2307,15 @@ function recordCodexWebSocketFailure(state: CodexWebSocketSessionState, activate
2297
2307
  }
2298
2308
  }
2299
2309
 
2310
+ /**
2311
+ * Bun 1.4.0's Windows WebSocket client can segfault during TLS handshakes.
2312
+ * Keep the model's websocket preference available on other platforms, while
2313
+ * requiring an explicit opt-in on Windows until the bundled runtime is fixed.
2314
+ */
2315
+ export function isCodexWebSocketSafeByDefault(platform: NodeJS.Platform = process.platform): boolean {
2316
+ return platform !== "win32";
2317
+ }
2318
+
2300
2319
  function shouldUseCodexWebSocket(
2301
2320
  model: Model<"openai-codex-responses">,
2302
2321
  state: CodexWebSocketSessionState | undefined,
@@ -2304,7 +2323,11 @@ function shouldUseCodexWebSocket(
2304
2323
  ): boolean {
2305
2324
  if (!state || state.disableWebsocket) return false;
2306
2325
  if (preferWebsockets === false) return false;
2307
- return isCodexWebSocketEnvEnabled() || preferWebsockets === true || model.preferWebsockets === true;
2326
+ return (
2327
+ isCodexWebSocketEnvEnabled() ||
2328
+ preferWebsockets === true ||
2329
+ (isCodexWebSocketSafeByDefault() && model.preferWebsockets === true)
2330
+ );
2308
2331
  }
2309
2332
 
2310
2333
  export interface OpenAICodexTransportDetails {
@@ -2362,7 +2385,9 @@ export function getOpenAICodexTransportDetails(
2362
2385
  const websocketPreferred =
2363
2386
  options?.preferWebsockets === false
2364
2387
  ? false
2365
- : isCodexWebSocketEnvEnabled() || options?.preferWebsockets === true || model.preferWebsockets === true;
2388
+ : isCodexWebSocketEnvEnabled() ||
2389
+ options?.preferWebsockets === true ||
2390
+ (isCodexWebSocketSafeByDefault() && model.preferWebsockets === true);
2366
2391
  const state = getCodexWebSocketStateForPublicSession(model, options);
2367
2392
 
2368
2393
  return {
@@ -2633,6 +2658,7 @@ class CodexWebSocketConnection {
2633
2658
  signal?: AbortSignal,
2634
2659
  firstEventTimeoutMs?: number,
2635
2660
  idleTimeoutMs = this.#idleTimeoutMs,
2661
+ onStreamCreated?: () => void,
2636
2662
  ): AsyncGenerator<Record<string, unknown>> {
2637
2663
  if (!this.#socket || this.#socket.readyState !== WebSocket.OPEN) {
2638
2664
  throw createCodexWebSocketTransportError("websocket connection is unavailable");
@@ -2654,6 +2680,7 @@ class CodexWebSocketConnection {
2654
2680
  }
2655
2681
 
2656
2682
  try {
2683
+ onStreamCreated?.();
2657
2684
  this.#socket.send(JSON.stringify(request));
2658
2685
  let sawFirstProgress = false;
2659
2686
  const startedAt = Date.now();
@@ -2795,7 +2822,7 @@ async function openCodexSseEventStream(
2795
2822
  signal?: AbortSignal,
2796
2823
  onSseEvent?: OpenAICodexResponsesOptions["onSseEvent"],
2797
2824
  fetchOverride?: FetchImpl,
2798
- options?: Pick<OpenAICodexResponsesOptions, "requestMaxRetries">,
2825
+ options?: Pick<OpenAICodexResponsesOptions, "requestMaxRetries" | "onStreamCreated">,
2799
2826
  ): Promise<AsyncGenerator<Record<string, unknown>>> {
2800
2827
  const headers = createCodexHeaders(requestHeaders, accountId, apiKey, sessionId, "sse", state);
2801
2828
  logCodexDebug("codex request", {
@@ -2805,6 +2832,7 @@ async function openCodexSseEventStream(
2805
2832
  sentTurnStateHeader: headers.has(X_CODEX_TURN_STATE_HEADER),
2806
2833
  sentModelsEtagHeader: headers.has(X_MODELS_ETAG_HEADER),
2807
2834
  });
2835
+ options?.onStreamCreated?.();
2808
2836
  const response = await fetchWithRetry(url, {
2809
2837
  method: "POST",
2810
2838
  headers,
@@ -2845,7 +2873,7 @@ async function openCodexWebSocketEventStream(
2845
2873
  request: Record<string, unknown>,
2846
2874
  state: CodexWebSocketSessionState,
2847
2875
  signal?: AbortSignal,
2848
- options?: Pick<OpenAICodexResponsesOptions, "streamFirstEventTimeoutMs" | "streamIdleTimeoutMs">,
2876
+ options?: Pick<OpenAICodexResponsesOptions, "streamFirstEventTimeoutMs" | "streamIdleTimeoutMs" | "onStreamCreated">,
2849
2877
  firstEventTimeoutMs?: number,
2850
2878
  ): Promise<AsyncGenerator<Record<string, unknown>>> {
2851
2879
  const connection = await getOrCreateCodexWebSocketConnection(state, url, headers, signal, options);
@@ -2854,6 +2882,7 @@ async function openCodexWebSocketEventStream(
2854
2882
  signal,
2855
2883
  firstEventTimeoutMs,
2856
2884
  getCodexWebSocketIdleTimeoutMs(options?.streamIdleTimeoutMs),
2885
+ options?.onStreamCreated,
2857
2886
  );
2858
2887
  }
2859
2888
 
@@ -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
  }
@@ -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",
@@ -48,6 +49,29 @@ const NON_WIRE_KEYS = new Set<keyof SimpleStreamOptions>([
48
49
  "fallbackAttempt",
49
50
  ]);
50
51
 
52
+ /**
53
+ * Project the caller's {@link Context} onto the wire schema. Runtime tool
54
+ * objects routinely carry harness-only state (runners, session managers,
55
+ * fs-stat BigInts) that must never be serialized: BigInt fields make
56
+ * `JSON.stringify` throw outright, and the rest is dead weight the gateway
57
+ * re-derives from its own tool registry. Only the protocol-meaningful,
58
+ * JSON-safe `Tool` fields cross the wire.
59
+ */
60
+ function buildWireContext(context: Context): Context {
61
+ if (!context.tools || context.tools.length === 0) return context;
62
+ return {
63
+ ...context,
64
+ tools: context.tools.map(tool => ({
65
+ name: tool.name,
66
+ description: tool.description,
67
+ parameters: tool.parameters,
68
+ ...(tool.strict !== undefined ? { strict: tool.strict } : {}),
69
+ ...(tool.customFormat !== undefined ? { customFormat: tool.customFormat } : {}),
70
+ ...(tool.customWireName !== undefined ? { customWireName: tool.customWireName } : {}),
71
+ })),
72
+ };
73
+ }
74
+
51
75
  function buildWireOptions(options: SimpleStreamOptions | undefined): Record<string, unknown> {
52
76
  if (!options) return {};
53
77
  const wire: Record<string, unknown> = {};
@@ -165,7 +189,7 @@ export function streamPiNative<TApi extends Api>(
165
189
  const headers = buildHeaders(model as Model<Api>, options?.apiKey);
166
190
  const body = JSON.stringify({
167
191
  modelId: model.id,
168
- context,
192
+ context: buildWireContext(context),
169
193
  options: buildWireOptions(options),
170
194
  stream: true,
171
195
  });
@@ -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 {};