@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.
- package/CHANGELOG.md +43 -0
- package/dist/types/adapter-internals/aws-region.d.ts +7 -0
- package/dist/types/core.d.ts +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/provider-models/openai-compat.d.ts +1 -0
- package/dist/types/providers/anthropic.d.ts +1 -1
- package/dist/types/providers/cursor.d.ts +27 -1
- package/dist/types/providers/google-gemini-headers.d.ts +1 -1
- package/dist/types/providers/openai-codex-responses.d.ts +6 -0
- package/dist/types/utils/discovery/openai-compatible.d.ts +21 -0
- package/dist/types/utils/h2-fetch.d.ts +7 -0
- package/dist/types/utils/schema/normalize.d.ts +0 -5
- package/dist/types/utils/sqlite-errors.d.ts +4 -0
- package/package.json +3 -3
- package/src/adapter-internals/aws-region.d.ts +7 -0
- package/src/adapter-internals/aws-region.ts +14 -0
- package/src/auth-broker/server.ts +10 -1
- package/src/auth-storage.ts +14 -14
- package/src/core.ts +1 -0
- package/src/index.ts +1 -0
- package/src/model-thinking.ts +8 -0
- package/src/models.json +201 -3
- package/src/provider-models/openai-compat.ts +93 -8
- package/src/providers/amazon-bedrock.ts +5 -1
- package/src/providers/anthropic.d.ts +1 -1
- package/src/providers/anthropic.ts +1 -1
- package/src/providers/aws-credentials.ts +6 -0
- package/src/providers/cursor.d.ts +27 -1
- package/src/providers/cursor.ts +234 -17
- package/src/providers/google-gemini-headers.d.ts +1 -1
- package/src/providers/google-gemini-headers.ts +1 -1
- package/src/providers/kiro-api-key.ts +33 -8
- package/src/providers/kiro-codewhisperer.ts +4 -1
- package/src/providers/openai-codex-responses.d.ts +6 -0
- package/src/providers/openai-codex-responses.ts +17 -2
- package/src/providers/pi-native-client.ts +24 -1
- package/src/utils/discovery/antigravity.ts +10 -1
- package/src/utils/discovery/openai-compatible.ts +38 -0
- package/src/utils/h2-fetch.ts +10 -0
- package/src/utils/oauth/callback-server.ts +8 -1
- package/src/utils/oauth/glm-zcode.ts +1 -1
- package/src/utils/oauth/kiro.ts +91 -22
- package/src/utils/schema/dereference.ts +169 -49
- package/src/utils/schema/draft.ts +46 -23
- package/src/utils/schema/normalize.d.ts +0 -5
- package/src/utils/schema/normalize.ts +396 -119
- package/src/utils/schema/types.ts +3 -1
- package/src/utils/schema/zod-decontaminate.ts +83 -29
- package/src/utils/sqlite-errors.d.ts +4 -0
- package/src/utils/sqlite-errors.ts +13 -0
- package/src/utils/tool-choice-capability.ts +2 -3
package/src/providers/cursor.ts
CHANGED
|
@@ -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
|
-
|
|
666
|
-
conversationBlobStores.
|
|
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:
|
|
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 =>
|
|
728
|
-
|
|
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
|
|
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
|
-
|
|
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 (
|
|
2998
|
+
if (!checkpoint.tokenDetails) {
|
|
2863
2999
|
return;
|
|
2864
3000
|
}
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
589
|
-
const
|
|
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
|
|
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
|
|
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() ||
|
|
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
|
|