@gajae-code/ai 0.15.6 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/types/adapter-internals/aws-region.d.ts +7 -0
  3. package/dist/types/core.d.ts +1 -0
  4. package/dist/types/index.d.ts +1 -0
  5. package/dist/types/provider-models/openai-compat.d.ts +1 -0
  6. package/dist/types/providers/anthropic.d.ts +1 -1
  7. package/dist/types/providers/cursor.d.ts +27 -1
  8. package/dist/types/providers/google-gemini-headers.d.ts +1 -1
  9. package/dist/types/providers/openai-codex-responses.d.ts +6 -0
  10. package/dist/types/utils/discovery/openai-compatible.d.ts +21 -0
  11. package/dist/types/utils/h2-fetch.d.ts +7 -0
  12. package/dist/types/utils/schema/normalize.d.ts +0 -5
  13. package/dist/types/utils/sqlite-errors.d.ts +4 -0
  14. package/package.json +3 -3
  15. package/src/adapter-internals/aws-region.d.ts +7 -0
  16. package/src/adapter-internals/aws-region.ts +14 -0
  17. package/src/auth-broker/server.ts +10 -1
  18. package/src/auth-storage.ts +14 -14
  19. package/src/core.ts +1 -0
  20. package/src/index.ts +1 -0
  21. package/src/model-thinking.ts +8 -0
  22. package/src/models.json +201 -3
  23. package/src/provider-models/openai-compat.ts +93 -8
  24. package/src/providers/amazon-bedrock.ts +5 -1
  25. package/src/providers/anthropic.d.ts +1 -1
  26. package/src/providers/anthropic.ts +1 -1
  27. package/src/providers/aws-credentials.ts +6 -0
  28. package/src/providers/cursor.d.ts +27 -1
  29. package/src/providers/cursor.ts +234 -17
  30. package/src/providers/google-gemini-headers.d.ts +1 -1
  31. package/src/providers/google-gemini-headers.ts +1 -1
  32. package/src/providers/kiro-api-key.ts +33 -8
  33. package/src/providers/kiro-codewhisperer.ts +4 -1
  34. package/src/providers/openai-codex-responses.d.ts +6 -0
  35. package/src/providers/openai-codex-responses.ts +17 -2
  36. package/src/providers/pi-native-client.ts +24 -1
  37. package/src/utils/discovery/antigravity.ts +10 -1
  38. package/src/utils/discovery/openai-compatible.ts +38 -0
  39. package/src/utils/h2-fetch.ts +10 -0
  40. package/src/utils/oauth/callback-server.ts +8 -1
  41. package/src/utils/oauth/glm-zcode.ts +1 -1
  42. package/src/utils/oauth/kiro.ts +91 -22
  43. package/src/utils/schema/dereference.ts +169 -49
  44. package/src/utils/schema/draft.ts +46 -23
  45. package/src/utils/schema/normalize.d.ts +0 -5
  46. package/src/utils/schema/normalize.ts +396 -119
  47. package/src/utils/schema/types.ts +3 -1
  48. package/src/utils/schema/zod-decontaminate.ts +83 -29
  49. package/src/utils/sqlite-errors.d.ts +4 -0
  50. package/src/utils/sqlite-errors.ts +13 -0
  51. package/src/utils/tool-choice-capability.ts +2 -3
@@ -24,6 +24,7 @@ import type {
24
24
  Tool,
25
25
  ToolCall,
26
26
  ToolResultMessage,
27
+ Usage,
27
28
  } from "../types";
28
29
  import { normalizeSystemPrompts } from "../utils";
29
30
  import { kCursorExecResolved } from "../utils/block-symbols";
@@ -76,6 +77,7 @@ import {
76
77
  type ConversationStateStructure,
77
78
  ConversationStateStructureSchema,
78
79
  ConversationStepSchema,
80
+ ConversationTokenDetailsSchema,
79
81
  ConversationTurnStructureSchema,
80
82
  CursorRuleSchema,
81
83
  CursorRuleSource,
@@ -175,6 +177,7 @@ export { CURSOR_CLIENT_VERSION };
175
177
 
176
178
  const conversationStateCache = new Map<string, ConversationStateStructure>();
177
179
  const conversationBlobStores = new Map<string, Map<string, Uint8Array>>();
180
+ const conversationUsageContextCache = new Map<string, CursorUsageContext>();
178
181
 
179
182
  // F15: bound the module-global conversation caches so long-lived / many-session use cannot
180
183
  // grow them without limit. LRU by conversation count + TTL on idle conversations.
@@ -186,6 +189,7 @@ const conversationLastAccess = new Map<string, number>();
186
189
  export function disposeCursorConversation(conversationId: string): void {
187
190
  conversationStateCache.delete(conversationId);
188
191
  conversationBlobStores.delete(conversationId);
192
+ conversationUsageContextCache.delete(conversationId);
189
193
  conversationLastAccess.delete(conversationId);
190
194
  }
191
195
 
@@ -337,9 +341,18 @@ class CursorRequestCoordinator implements CursorRequestWriter {
337
341
 
338
342
  admit(taskFactory: () => Promise<void>): void {
339
343
  if (!this.canAdmitTask()) return;
344
+ this.#admitOrdered(taskFactory, false);
345
+ }
346
+
347
+ admitCheckpoint(taskFactory: () => Promise<void>): Promise<void> {
348
+ if (this.#state === "failed") return Promise.reject(this.#failure ?? new Error("Cursor request failed"));
349
+ return this.#admitOrdered(taskFactory, true);
350
+ }
351
+
352
+ #admitOrdered(taskFactory: () => Promise<void>, allowAfterSuccess: boolean): Promise<void> {
340
353
  const orderedTask = this.#hasAdmittedTask
341
354
  ? this.#taskChain.then(() => {
342
- if (this.#state === "failed" || this.#state === "succeeded") return;
355
+ if (this.#state === "failed" || (!allowAfterSuccess && this.#state === "succeeded")) return;
343
356
  return taskFactory();
344
357
  })
345
358
  : taskFactory();
@@ -355,6 +368,7 @@ class CursorRequestCoordinator implements CursorRequestWriter {
355
368
  () => this.#tasks.delete(orderedTask),
356
369
  () => this.#tasks.delete(orderedTask),
357
370
  );
371
+ return orderedTask;
358
372
  }
359
373
 
360
374
  turnEnded(): void {
@@ -651,6 +665,9 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
651
665
  let onAbort: (() => void) | undefined;
652
666
  let coordinator: CursorRequestCoordinator = undefined!;
653
667
  const baseUrl = model.baseUrl || CURSOR_API_URL;
668
+ let activeConversationId: string | undefined;
669
+ let previousConversationState: ConversationStateStructure | undefined;
670
+ let previousUsageContext: CursorUsageContext | undefined;
654
671
 
655
672
  try {
656
673
  const apiKey = options?.apiKey;
@@ -662,16 +679,22 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
662
679
  }
663
680
 
664
681
  const conversationId = options?.conversationId ?? options?.sessionId ?? crypto.randomUUID();
665
- const blobStore = conversationBlobStores.get(conversationId) ?? new Map<string, Uint8Array>();
666
- conversationBlobStores.set(conversationId, blobStore);
682
+ activeConversationId = conversationId;
683
+ const cachedBlobStore = conversationBlobStores.get(conversationId);
684
+ const blobStore = new Map(cachedBlobStore);
667
685
  const cachedState = conversationStateCache.get(conversationId);
686
+ previousConversationState = cachedState;
687
+ const usageContext = buildCursorUsageContext(context, model, options);
688
+ previousUsageContext = conversationUsageContextCache.get(conversationId);
689
+ conversationUsageContextCache.set(conversationId, usageContext);
690
+ const reusableCachedState =
691
+ cachedState && canReuseCursorUsageContext(previousUsageContext, usageContext) ? cachedState : undefined;
668
692
  const { requestBytes, conversationState } = buildGrpcRequest(model, context, options, {
669
693
  conversationId,
670
694
  blobStore,
671
- conversationState: cachedState,
695
+ conversationState: reusableCachedState,
672
696
  });
673
697
  conversationStateCache.set(conversationId, conversationState);
674
- touchCursorConversation(conversationId);
675
698
  const requestContextTools = buildMcpToolDefinitions(context.tools);
676
699
  const targetUrl = new URL(baseUrl);
677
700
  const proxyUrl = getProxyForUrl(model.provider, targetUrl);
@@ -709,6 +732,28 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
709
732
  };
710
733
  let resolveH2: (() => void) | undefined;
711
734
  let rejectH2: ((error: Error) => void) | undefined;
735
+ const inboundEnd = Promise.withResolvers<void>();
736
+ let inboundSettled = false;
737
+ let inboundTimeout: NodeJS.Timeout | undefined;
738
+ const settleInbound = (error?: Error) => {
739
+ if (inboundSettled) return;
740
+ inboundSettled = true;
741
+ if (inboundTimeout) {
742
+ clearTimeout(inboundTimeout);
743
+ inboundTimeout = undefined;
744
+ }
745
+ if (error) inboundEnd.reject(error);
746
+ else inboundEnd.resolve();
747
+ };
748
+ void inboundEnd.promise.catch(() => {});
749
+ const armInboundTimeout = () => {
750
+ const timeoutMs = options?.streamIdleTimeoutMs ?? 0;
751
+ if (timeoutMs <= 0 || inboundSettled) return;
752
+ inboundTimeout = setTimeout(
753
+ () => settleInbound(new Error("Cursor stream did not reach its inbound terminal frame")),
754
+ timeoutMs,
755
+ );
756
+ };
712
757
  coordinator = new CursorRequestCoordinator(
713
758
  h2Request,
714
759
  stopHeartbeat,
@@ -724,16 +769,32 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
724
769
  },
725
770
  options?.streamIdleTimeoutMs ?? getStreamIdleTimeoutMs(),
726
771
  );
727
- h2Client.on("error", error => coordinator.fail(error));
728
- h2Request.on("error", error => coordinator.fail(error));
772
+ h2Client.on("error", error => {
773
+ settleInbound(error);
774
+ coordinator.fail(error);
775
+ });
776
+ h2Request.on("error", error => {
777
+ settleInbound(error);
778
+ coordinator.fail(error);
779
+ });
729
780
 
730
781
  stream.push({ type: "start", partial: output });
731
782
 
732
783
  let pendingBuffer = Buffer.alloc(0);
784
+ const checkpointTasks: Promise<void>[] = [];
733
785
  let currentTextBlock: (TextContent & { index: number }) | null = null;
734
786
  let currentThinkingBlock: (ThinkingContent & { index: number }) | null = null;
735
787
  let currentToolCall: ToolCallState | null = null;
736
- const usageState: UsageState = { sawTokenDelta: false };
788
+ const cachedConversationUsedTokens =
789
+ conversationState.tokenDetails && canReuseCursorUsageContext(previousUsageContext, usageContext)
790
+ ? conversationState.tokenDetails.usedTokens
791
+ : 0;
792
+ const usageState: UsageState = {
793
+ sawTokenDelta: false,
794
+ conversationUsedTokens: cachedConversationUsedTokens,
795
+ checkpointOutputTokens: 0,
796
+ hasConversationCheckpoint: false,
797
+ };
737
798
 
738
799
  const state: BlockState = {
739
800
  get currentTextBlock() {
@@ -763,8 +824,7 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
763
824
  };
764
825
 
765
826
  const onConversationCheckpoint = (checkpoint: ConversationStateStructure) => {
766
- conversationStateCache.set(conversationId, checkpoint);
767
- touchCursorConversation(conversationId);
827
+ usageState.pendingCheckpoint = checkpoint;
768
828
  };
769
829
 
770
830
  h2Request.on("trailers", trailers => {
@@ -775,11 +835,18 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
775
835
  }
776
836
  });
777
837
  h2Request.on("end", () => {
838
+ settleInbound();
778
839
  if (!coordinator.hasTurnEnded()) {
779
840
  coordinator.fail(new Error("Cursor stream ended before turnEnded"));
780
841
  }
781
842
  });
843
+ h2Request.on("close", () => {
844
+ const error = new Error("Cursor stream closed before inbound completion");
845
+ if (!inboundSettled) settleInbound(error);
846
+ coordinator.fail(error);
847
+ });
782
848
  onAbort = () => {
849
+ settleInbound(new Error("Request was aborted"));
783
850
  coordinator.fail(new Error("Request was aborted"));
784
851
  };
785
852
  if (options?.signal) {
@@ -813,6 +880,21 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
813
880
  const isTurnEnded =
814
881
  serverMessage.message.case === "interactionUpdate" &&
815
882
  serverMessage.message.value.message?.case === "turnEnded";
883
+ const isConversationCheckpoint = serverMessage.message.case === "conversationCheckpointUpdate";
884
+ if (isConversationCheckpoint) {
885
+ checkpointTasks.push(
886
+ coordinator.admitCheckpoint(() => {
887
+ handleConversationCheckpointUpdate(
888
+ serverMessage.message.value as ConversationStateStructure,
889
+ output,
890
+ usageState,
891
+ onConversationCheckpoint,
892
+ );
893
+ return Promise.resolve();
894
+ }),
895
+ );
896
+ continue;
897
+ }
816
898
  // Serialize handlers: exec messages can be asynchronous, and resolving the
817
899
  // request on turnEnded before prior handlers finish loses their responses.
818
900
  if (!coordinator.canAdmitTask()) continue;
@@ -866,6 +948,9 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
866
948
  resolve();
867
949
  }
868
950
  });
951
+ armInboundTimeout();
952
+ await inboundEnd.promise;
953
+ await Promise.all(checkpointTasks);
869
954
 
870
955
  if (state.currentTextBlock) {
871
956
  const idx = output.content.indexOf(state.currentTextBlock);
@@ -899,6 +984,36 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
899
984
  });
900
985
  }
901
986
 
987
+ finalizeCursorUsage(output, usageState);
988
+ const stateToCommit =
989
+ usageState.pendingCheckpoint ?? conversationStateCache.get(conversationId) ?? conversationState;
990
+ if (
991
+ usageState.pendingCheckpoint ||
992
+ usageState.hasConversationCheckpoint ||
993
+ usageState.conversationUsedTokens > 0
994
+ ) {
995
+ conversationStateCache.set(
996
+ conversationId,
997
+ create(ConversationStateStructureSchema, {
998
+ ...stateToCommit,
999
+ ...(usageState.hasConversationCheckpoint || usageState.conversationUsedTokens > 0
1000
+ ? {
1001
+ tokenDetails: create(ConversationTokenDetailsSchema, {
1002
+ usedTokens: output.usage.totalTokens,
1003
+ maxTokens: stateToCommit.tokenDetails?.maxTokens ?? 0,
1004
+ }),
1005
+ }
1006
+ : {}),
1007
+ }),
1008
+ );
1009
+ touchCursorConversation(conversationId);
1010
+ }
1011
+ conversationUsageContextCache.set(conversationId, {
1012
+ ...usageContext,
1013
+ messageKeys: [...usageContext.messageKeys, hashCursorUsageMessage(output)],
1014
+ });
1015
+ conversationBlobStores.set(conversationId, blobStore);
1016
+ touchCursorConversation(conversationId);
902
1017
  calculateCost(model, output.usage);
903
1018
 
904
1019
  output.duration = Date.now() - startTime;
@@ -910,6 +1025,12 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
910
1025
  });
911
1026
  stream.end();
912
1027
  } catch (error) {
1028
+ if (activeConversationId) {
1029
+ if (previousConversationState) conversationStateCache.set(activeConversationId, previousConversationState);
1030
+ else conversationStateCache.delete(activeConversationId);
1031
+ if (previousUsageContext) conversationUsageContextCache.set(activeConversationId, previousUsageContext);
1032
+ else conversationUsageContextCache.delete(activeConversationId);
1033
+ }
913
1034
  // Keep the completion promise terminal even for synchronous setup/write
914
1035
  // failures that may not emit a separate HTTP/2 error event.
915
1036
  const mappedError = mapH2TransportError(coordinator?.failureError() ?? error, baseUrl);
@@ -959,6 +1080,24 @@ interface BlockState {
959
1080
 
960
1081
  interface UsageState {
961
1082
  sawTokenDelta: boolean;
1083
+ /**
1084
+ * Latest `ConversationTokenDetails.used_tokens`: the whole conversation's
1085
+ * token consumption as counted by Cursor, not this turn's output.
1086
+ */
1087
+ conversationUsedTokens: number;
1088
+ /** Output tokens already included in the latest checkpoint snapshot. */
1089
+ checkpointOutputTokens: number;
1090
+ /** Whether the current stream received a checkpoint, including an explicit zero. */
1091
+ hasConversationCheckpoint: boolean;
1092
+ pendingCheckpoint?: ConversationStateStructure;
1093
+ }
1094
+
1095
+ interface CursorUsageContext {
1096
+ modelKey: string;
1097
+ systemPromptKey: string;
1098
+ customSystemPromptKey: string;
1099
+ toolsKey: string;
1100
+ messageKeys: string[];
962
1101
  }
963
1102
 
964
1103
  async function handleServerMessage(
@@ -2855,17 +2994,58 @@ function handleConversationCheckpointUpdate(
2855
2994
  onConversationCheckpoint?: (checkpoint: ConversationStateStructure) => void,
2856
2995
  ): void {
2857
2996
  onConversationCheckpoint?.(checkpoint);
2858
- if (usageState.sawTokenDelta) {
2859
- return;
2860
- }
2861
2997
  const usedTokens = checkpoint.tokenDetails?.usedTokens ?? 0;
2862
- if (usedTokens <= 0) {
2998
+ if (!checkpoint.tokenDetails) {
2863
2999
  return;
2864
3000
  }
2865
- if (output.usage.output !== usedTokens) {
2866
- output.usage.output = usedTokens;
2867
- output.usage.totalTokens = output.usage.input + output.usage.output;
3001
+ const previousUsedTokens = usageState.conversationUsedTokens;
3002
+ // `used_tokens` counts the whole conversation, so it is prompt-side usage and
3003
+ // must not be attributed to this turn's output. Checkpoints can arrive while
3004
+ // output is still streaming; the split is applied once the stream finalizes.
3005
+ usageState.conversationUsedTokens = usedTokens;
3006
+ usageState.checkpointOutputTokens =
3007
+ usageState.hasConversationCheckpoint && usedTokens < previousUsedTokens ? 0 : output.usage.output;
3008
+ usageState.hasConversationCheckpoint = true;
3009
+ }
3010
+
3011
+ /**
3012
+ * Cursor streams output tokens as deltas and reports whole-conversation
3013
+ * consumption separately as `ConversationTokenDetails.used_tokens`. Derive
3014
+ * prompt tokens from the difference so context accounting and compaction see a
3015
+ * real prompt size instead of zero.
3016
+ */
3017
+ export function finalizeCursorUsage(output: AssistantMessage, usageState: UsageState): void {
3018
+ const used = usageState.conversationUsedTokens;
3019
+ if (!usageState.hasConversationCheckpoint && used <= 0) {
3020
+ return;
2868
3021
  }
3022
+ const outputIncludedInSnapshot = usageState.hasConversationCheckpoint ? usageState.checkpointOutputTokens : 0;
3023
+ output.usage.input = Math.max(0, used - outputIncludedInSnapshot);
3024
+ output.usage.totalTokens = output.usage.input + output.usage.output;
3025
+ }
3026
+
3027
+ /** Exposes {@link finalizeCursorUsage} for tests without a live HTTP/2 stream. */
3028
+ export function finalizeCursorUsageForTest(
3029
+ usedTokens: number,
3030
+ outputTokens: number,
3031
+ options: { checkpointOutputTokens?: number; hasConversationCheckpoint?: boolean } = {},
3032
+ ): Usage {
3033
+ const usage: Usage = {
3034
+ input: 0,
3035
+ output: outputTokens,
3036
+ cacheRead: 0,
3037
+ cacheWrite: 0,
3038
+ totalTokens: outputTokens,
3039
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
3040
+ };
3041
+ finalizeCursorUsage({ usage } as AssistantMessage, {
3042
+ sawTokenDelta: true,
3043
+ conversationUsedTokens: usedTokens,
3044
+ checkpointOutputTokens:
3045
+ options.checkpointOutputTokens ?? ((options.hasConversationCheckpoint ?? usedTokens > 0) ? outputTokens : 0),
3046
+ hasConversationCheckpoint: options.hasConversationCheckpoint ?? usedTokens > 0,
3047
+ });
3048
+ return usage;
2869
3049
  }
2870
3050
 
2871
3051
  function createBlobId(data: Uint8Array): Uint8Array {
@@ -3188,6 +3368,43 @@ function buildConversationTurns(messages: Message[], blobStore: Map<string, Uint
3188
3368
  return turns;
3189
3369
  }
3190
3370
 
3371
+ function buildCursorUsageContext(
3372
+ context: Context,
3373
+ model: Model<"cursor-agent">,
3374
+ options: CursorOptions | undefined,
3375
+ ): CursorUsageContext {
3376
+ return {
3377
+ modelKey: hashCursorUsageValue({ provider: model.provider, id: model.id, wireModelId: model.wireModelId }),
3378
+ systemPromptKey: hashCursorUsageValue(context.systemPrompt ?? []),
3379
+ customSystemPromptKey: hashCursorUsageValue(options?.customSystemPrompt ?? ""),
3380
+ toolsKey: hashCursorUsageValue(context.tools ?? []),
3381
+ messageKeys: context.messages.map(message => hashCursorUsageMessage(message)),
3382
+ };
3383
+ }
3384
+
3385
+ function hashCursorUsageMessage(message: { role: string; content: unknown }): string {
3386
+ return hashCursorUsageValue({ role: message.role, content: message.content });
3387
+ }
3388
+
3389
+ function hashCursorUsageValue(value: unknown): string {
3390
+ return createHash("sha256")
3391
+ .update(JSON.stringify(value) ?? "")
3392
+ .digest("hex");
3393
+ }
3394
+
3395
+ function canReuseCursorUsageContext(previous: CursorUsageContext | undefined, current: CursorUsageContext): boolean {
3396
+ if (
3397
+ !previous ||
3398
+ previous.modelKey !== current.modelKey ||
3399
+ previous.systemPromptKey !== current.systemPromptKey ||
3400
+ previous.customSystemPromptKey !== current.customSystemPromptKey ||
3401
+ previous.toolsKey !== current.toolsKey
3402
+ )
3403
+ return false;
3404
+ if (previous.messageKeys.length > current.messageKeys.length) return false;
3405
+ return previous.messageKeys.every((key, index) => key === current.messageKeys[index]);
3406
+ }
3407
+
3191
3408
  /** Exported for tests: decodes Cursor history blobs built from conversation messages. */
3192
3409
  export function buildCursorHistoryForTest(messages: Message[]): {
3193
3410
  rootPromptMessagesJson: unknown[];
@@ -5,7 +5,7 @@
5
5
  */
6
6
  export declare const GEMINI_CLI_VERSION_ENV = "GJC_AI_GEMINI_CLI_VERSION";
7
7
  export declare const LEGACY_GEMINI_CLI_VERSION_ENV = "PI_AI_GEMINI_CLI_VERSION";
8
- export declare const DEFAULT_GEMINI_CLI_VERSION = "0.52.0";
8
+ export declare const DEFAULT_GEMINI_CLI_VERSION = "0.58.0";
9
9
  export declare function getGeminiCliUserAgent(modelId?: string): string;
10
10
  export declare const getGeminiCliHeaders: (modelId?: string) => {
11
11
  "User-Agent": string;
@@ -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 =
@@ -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,
@@ -178,9 +179,10 @@ export const streamKiroCodeWhisperer: StreamFunction<"kiro-codewhisperer-stream"
178
179
  };
179
180
 
180
181
  const blocks = output.content as Block[];
181
- 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;
182
183
 
183
184
  try {
185
+ assertAwsRegionLabel(region);
184
186
  // Resolve bearer token
185
187
  const bearerToken = resolveBearerToken(options.apiKey);
186
188
  if (!bearerToken) {
@@ -222,6 +224,7 @@ export const streamKiroCodeWhisperer: StreamFunction<"kiro-codewhisperer-stream"
222
224
  method: "POST",
223
225
  headers: requestHeaders,
224
226
  body,
227
+ redirect: "error",
225
228
  signal: options.signal,
226
229
  });
227
230
 
@@ -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;
@@ -2307,6 +2307,15 @@ function recordCodexWebSocketFailure(state: CodexWebSocketSessionState, activate
2307
2307
  }
2308
2308
  }
2309
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
+
2310
2319
  function shouldUseCodexWebSocket(
2311
2320
  model: Model<"openai-codex-responses">,
2312
2321
  state: CodexWebSocketSessionState | undefined,
@@ -2314,7 +2323,11 @@ function shouldUseCodexWebSocket(
2314
2323
  ): boolean {
2315
2324
  if (!state || state.disableWebsocket) return false;
2316
2325
  if (preferWebsockets === false) return false;
2317
- return isCodexWebSocketEnvEnabled() || preferWebsockets === true || model.preferWebsockets === true;
2326
+ return (
2327
+ isCodexWebSocketEnvEnabled() ||
2328
+ preferWebsockets === true ||
2329
+ (isCodexWebSocketSafeByDefault() && model.preferWebsockets === true)
2330
+ );
2318
2331
  }
2319
2332
 
2320
2333
  export interface OpenAICodexTransportDetails {
@@ -2372,7 +2385,9 @@ export function getOpenAICodexTransportDetails(
2372
2385
  const websocketPreferred =
2373
2386
  options?.preferWebsockets === false
2374
2387
  ? false
2375
- : isCodexWebSocketEnvEnabled() || options?.preferWebsockets === true || model.preferWebsockets === true;
2388
+ : isCodexWebSocketEnvEnabled() ||
2389
+ options?.preferWebsockets === true ||
2390
+ (isCodexWebSocketSafeByDefault() && model.preferWebsockets === true);
2376
2391
  const state = getCodexWebSocketStateForPublicSession(model, options);
2377
2392
 
2378
2393
  return {
@@ -49,6 +49,29 @@ const NON_WIRE_KEYS = new Set<keyof SimpleStreamOptions>([
49
49
  "fallbackAttempt",
50
50
  ]);
51
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
+
52
75
  function buildWireOptions(options: SimpleStreamOptions | undefined): Record<string, unknown> {
53
76
  if (!options) return {};
54
77
  const wire: Record<string, unknown> = {};
@@ -166,7 +189,7 @@ export function streamPiNative<TApi extends Api>(
166
189
  const headers = buildHeaders(model as Model<Api>, options?.apiKey);
167
190
  const body = JSON.stringify({
168
191
  modelId: model.id,
169
- context,
192
+ context: buildWireContext(context),
170
193
  options: buildWireOptions(options),
171
194
  stream: true,
172
195
  });
@@ -218,13 +218,22 @@ export async function fetchAntigravityDiscoveryModels(
218
218
  continue;
219
219
  }
220
220
 
221
+ const surfacedModelIds = new Set<string>();
222
+ for (const sort of parsed.agentModelSorts ?? []) {
223
+ for (const group of sort.groups ?? []) {
224
+ for (const modelId of group.modelIds ?? []) {
225
+ surfacedModelIds.add(modelId);
226
+ }
227
+ }
228
+ }
229
+
221
230
  const models: Model<"google-gemini-cli">[] = [];
222
231
 
223
232
  for (const [modelId, model] of Object.entries(parsed.models ?? {})) {
224
233
  if (ANTIGRAVITY_DISCOVERY_DENYLIST.has(modelId) || isRetiredModelKey(targetProvider, modelId)) {
225
234
  continue;
226
235
  }
227
- if (model.isInternal === true) {
236
+ if (model.isInternal === true && !surfacedModelIds.has(modelId)) {
228
237
  continue;
229
238
  }
230
239