@gajae-code/ai 0.13.2 → 0.14.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.
- package/CHANGELOG.md +61 -2
- package/dist/types/auth-broker/client.d.ts +9 -1
- package/dist/types/auth-broker/redact.d.ts +7 -0
- package/dist/types/auth-broker/remote-store.d.ts +50 -9
- package/dist/types/auth-broker/types.d.ts +14 -0
- package/dist/types/auth-broker/wire-schemas.d.ts +25 -0
- package/dist/types/auth-storage.d.ts +200 -6
- package/dist/types/core.d.ts +1 -0
- package/dist/types/model-cache.d.ts +4 -1
- package/dist/types/model-manager.d.ts +11 -0
- package/dist/types/provider-models/openai-compat.d.ts +5 -0
- package/dist/types/provider-models/special.d.ts +3 -0
- package/dist/types/providers/anthropic.d.ts +31 -0
- package/dist/types/providers/cursor.d.ts +9 -1
- package/dist/types/providers/kiro-codewhisperer.d.ts +8 -0
- package/dist/types/providers/mock.d.ts +8 -0
- package/dist/types/providers/register-builtins.d.ts +1 -0
- package/dist/types/providers/transform-messages.d.ts +18 -0
- package/dist/types/types.d.ts +34 -8
- package/dist/types/usage/grok-cli.d.ts +5 -0
- package/dist/types/usage.d.ts +6 -0
- package/dist/types/utils/discovery/openai-compatible.d.ts +5 -0
- package/dist/types/utils/event-stream.d.ts +4 -2
- package/dist/types/utils/fallback-transport.d.ts +10 -0
- package/dist/types/utils/http-inspector.d.ts +1 -0
- package/dist/types/utils/idle-iterator.d.ts +13 -1
- package/dist/types/utils/json-parse.d.ts +19 -0
- package/dist/types/utils/oauth/callback-server.d.ts +13 -0
- package/dist/types/utils/oauth/kiro.d.ts +71 -0
- package/dist/types/utils/oauth/types.d.ts +1 -1
- package/dist/types/utils/parse-bind.d.ts +8 -5
- package/dist/types/utils/tool-call-healing.d.ts +7 -0
- package/dist/types/utils/tool-choice-capability.d.ts +11 -0
- package/package.json +3 -2
- package/src/auth-broker/client.ts +30 -0
- package/src/auth-broker/redact.ts +15 -0
- package/src/auth-broker/refresher.ts +4 -2
- package/src/auth-broker/remote-store.ts +693 -70
- package/src/auth-broker/server.ts +57 -12
- package/src/auth-broker/types.ts +16 -0
- package/src/auth-broker/wire-schemas.ts +21 -0
- package/src/auth-gateway/server.ts +84 -19
- package/src/auth-storage.ts +985 -41
- package/src/core.ts +1 -0
- package/src/model-cache.ts +23 -4
- package/src/model-manager.ts +70 -11
- package/src/model-thinking.ts +45 -1
- package/src/models.json +9604 -1932
- package/src/openai-completions-compat.ts +2 -1
- package/src/provider-models/descriptors.ts +7 -1
- package/src/provider-models/openai-compat.ts +52 -28
- package/src/provider-models/special.ts +12 -0
- package/src/providers/amazon-bedrock.ts +2 -1
- package/src/providers/anthropic.ts +831 -27
- package/src/providers/cursor.ts +83 -3
- package/src/providers/kiro-codewhisperer.ts +572 -0
- package/src/providers/mock.ts +15 -2
- package/src/providers/ollama.ts +9 -2
- package/src/providers/openai-codex-responses.ts +16 -9
- package/src/providers/openai-completions.ts +6 -1
- package/src/providers/openai-responses-shared.ts +180 -18
- package/src/providers/register-builtins.ts +24 -2
- package/src/providers/transform-messages.ts +64 -1
- package/src/stream.ts +25 -2
- package/src/types.ts +36 -7
- package/src/usage/grok-cli.ts +86 -1
- package/src/usage.ts +7 -0
- package/src/utils/discovery/openai-compatible.ts +89 -4
- package/src/utils/event-stream.ts +11 -2
- package/src/utils/fallback-transport.ts +44 -2
- package/src/utils/http-inspector.ts +1 -0
- package/src/utils/idle-iterator.ts +29 -6
- package/src/utils/json-parse.ts +80 -0
- package/src/utils/oauth/callback-server.ts +31 -1
- package/src/utils/oauth/index.ts +14 -1
- package/src/utils/oauth/kiro.ts +448 -0
- package/src/utils/oauth/synthetic.ts +2 -3
- package/src/utils/oauth/types.ts +1 -0
- package/src/utils/parse-bind.ts +27 -0
- package/src/utils/tool-call-healing.ts +13 -2
- package/src/utils/tool-choice-capability.ts +386 -6
package/src/providers/cursor.ts
CHANGED
|
@@ -27,7 +27,7 @@ import type {
|
|
|
27
27
|
} from "../types";
|
|
28
28
|
import { normalizeSystemPrompts } from "../utils";
|
|
29
29
|
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
30
|
-
import { parseStreamingJson } from "../utils/json-parse";
|
|
30
|
+
import { findUnnecessaryUnicodeEscape, parseStreamingJson } from "../utils/json-parse";
|
|
31
31
|
import { formatErrorMessageWithRetryAfter } from "../utils/retry-after";
|
|
32
32
|
import { flattenToolRootCombinators, toolWireSchema } from "../utils/schema";
|
|
33
33
|
import { CURSOR_COMPOSER_EDIT_DISCIPLINE_PROMPT, isComposerHarnessModel } from "./composer-discipline";
|
|
@@ -564,6 +564,9 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
|
|
|
564
564
|
if (state.currentToolCall) {
|
|
565
565
|
const idx = output.content.indexOf(state.currentToolCall);
|
|
566
566
|
state.currentToolCall.arguments = parseStreamingJson(state.currentToolCall.partialJson);
|
|
567
|
+
if (findUnnecessaryUnicodeEscape(state.currentToolCall.partialJson ?? "")) {
|
|
568
|
+
state.currentToolCall.escapedNonAsciiArguments = true;
|
|
569
|
+
}
|
|
567
570
|
delete (state.currentToolCall as any).partialJson;
|
|
568
571
|
delete (state.currentToolCall as any).index;
|
|
569
572
|
stream.push({
|
|
@@ -1913,7 +1916,75 @@ function cursorNativeToolName(kindKey: string): string {
|
|
|
1913
1916
|
// do not otherwise handle (everything except mcpToolCall / updateTodosToolCall), so
|
|
1914
1917
|
// without this they are silently dropped and never render. Build a generic toolCall
|
|
1915
1918
|
// block from whichever *ToolCall field is set so the call (and its result) is shown.
|
|
1916
|
-
|
|
1919
|
+
|
|
1920
|
+
/** Hard node budget for one native-payload conversion; bounds hostile or cyclic graphs. */
|
|
1921
|
+
const CURSOR_JSON_SAFE_MAX_NODES = 10_000;
|
|
1922
|
+
const CURSOR_JSON_SAFE_MAX_DEPTH = 100;
|
|
1923
|
+
|
|
1924
|
+
/**
|
|
1925
|
+
* Total conversion of a Cursor protobuf payload into plain JSON-safe data.
|
|
1926
|
+
*
|
|
1927
|
+
* protobuf-es v2 messages are plain objects, but they carry `$typeName`
|
|
1928
|
+
* markers, `bigint` fields (e.g. `fileSize`, `durationMs`, `timestampMs`,
|
|
1929
|
+
* `fileOutputThresholdBytes`), and `Uint8Array` blobs. None of those may leak
|
|
1930
|
+
* into assistant message content: toolCall `arguments` are staged into managed
|
|
1931
|
+
* snapshots, persisted to the JSONL transcript, and replayed to providers —
|
|
1932
|
+
* all of which require `JSON.stringify`-safe values. Attaching the raw payload
|
|
1933
|
+
* is exactly the local-snapshot producer defect class behind issue #4578.
|
|
1934
|
+
*
|
|
1935
|
+
* Rules: `$typeName` is stripped, safe-range bigints become numbers (decimal
|
|
1936
|
+
* strings beyond `Number.MAX_SAFE_INTEGER`), byte arrays become base64
|
|
1937
|
+
* strings, dates become ISO strings, functions/symbols are dropped, cycles
|
|
1938
|
+
* and over-depth values collapse to null, and containers stop accepting
|
|
1939
|
+
* entries once the shared node budget is exhausted.
|
|
1940
|
+
*/
|
|
1941
|
+
function cursorJsonSafeValue(value: unknown, path?: Set<object>, budget?: { remaining: number }, depth = 0): unknown {
|
|
1942
|
+
const seen = path ?? new Set<object>();
|
|
1943
|
+
const nodes = budget ?? { remaining: CURSOR_JSON_SAFE_MAX_NODES };
|
|
1944
|
+
if (nodes.remaining-- <= 0) return null;
|
|
1945
|
+
if (depth >= CURSOR_JSON_SAFE_MAX_DEPTH) return null;
|
|
1946
|
+
if (typeof value === "bigint") {
|
|
1947
|
+
return value <= BigInt(Number.MAX_SAFE_INTEGER) && value >= BigInt(-Number.MAX_SAFE_INTEGER)
|
|
1948
|
+
? Number(value)
|
|
1949
|
+
: value.toString();
|
|
1950
|
+
}
|
|
1951
|
+
if (typeof value === "function" || typeof value === "symbol" || value === undefined) return null;
|
|
1952
|
+
if (typeof value === "number" && !Number.isFinite(value)) return null;
|
|
1953
|
+
if (value === null || typeof value !== "object") return value;
|
|
1954
|
+
if (seen.has(value)) return null;
|
|
1955
|
+
if (value instanceof Uint8Array)
|
|
1956
|
+
return Buffer.from(value.buffer, value.byteOffset, value.byteLength).toString("base64");
|
|
1957
|
+
if (value instanceof Date) return Number.isFinite(value.getTime()) ? value.toISOString() : null;
|
|
1958
|
+
seen.add(value);
|
|
1959
|
+
try {
|
|
1960
|
+
if (Array.isArray(value)) {
|
|
1961
|
+
const array: unknown[] = [];
|
|
1962
|
+
for (const entry of value) {
|
|
1963
|
+
if (nodes.remaining <= 0) break;
|
|
1964
|
+
array.push(cursorJsonSafeValue(entry, seen, nodes, depth + 1));
|
|
1965
|
+
}
|
|
1966
|
+
return array;
|
|
1967
|
+
}
|
|
1968
|
+
const record: Record<string, unknown> = {};
|
|
1969
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
1970
|
+
if (key === "$typeName") continue;
|
|
1971
|
+
if (nodes.remaining <= 0) break;
|
|
1972
|
+
record[key] = cursorJsonSafeValue(entry, seen, nodes, depth + 1);
|
|
1973
|
+
}
|
|
1974
|
+
return record;
|
|
1975
|
+
} catch {
|
|
1976
|
+
return null;
|
|
1977
|
+
} finally {
|
|
1978
|
+
seen.delete(value);
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
/** Exported for direct regression coverage of the JSON-safety boundary. */
|
|
1983
|
+
export function cursorJsonSafeValueForTest(value: unknown): unknown {
|
|
1984
|
+
return cursorJsonSafeValue(value);
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
export function buildNativeToolCallBlock(
|
|
1917
1988
|
toolCall: Record<string, unknown>,
|
|
1918
1989
|
callId: string,
|
|
1919
1990
|
index: number,
|
|
@@ -1922,11 +1993,20 @@ function buildNativeToolCallBlock(
|
|
|
1922
1993
|
if (!/ToolCall$/.test(key) || !payload || typeof payload !== "object") continue;
|
|
1923
1994
|
if (key === "mcpToolCall" || key === "updateTodosToolCall") continue;
|
|
1924
1995
|
const args = (payload as { args?: unknown }).args;
|
|
1996
|
+
const hasObjectArgs = args !== null && typeof args === "object";
|
|
1997
|
+
const convertedArgs = hasObjectArgs ? cursorJsonSafeValue(args) : undefined;
|
|
1998
|
+
const safeArguments =
|
|
1999
|
+
convertedArgs !== undefined &&
|
|
2000
|
+
convertedArgs !== null &&
|
|
2001
|
+
typeof convertedArgs === "object" &&
|
|
2002
|
+
!Array.isArray(convertedArgs)
|
|
2003
|
+
? (convertedArgs as Record<string, unknown>)
|
|
2004
|
+
: { raw: hasObjectArgs ? convertedArgs : cursorJsonSafeValue(payload) };
|
|
1925
2005
|
return {
|
|
1926
2006
|
type: "toolCall",
|
|
1927
2007
|
id: callId,
|
|
1928
2008
|
name: cursorNativeToolName(key),
|
|
1929
|
-
arguments:
|
|
2009
|
+
arguments: safeArguments,
|
|
1930
2010
|
index,
|
|
1931
2011
|
kind: "native",
|
|
1932
2012
|
};
|
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kiro / Amazon Q Developer / CodeWhisperer streaming transport.
|
|
3
|
+
*
|
|
4
|
+
* Talks directly to the CodeWhisperer streaming service over HTTPS using
|
|
5
|
+
* a bearer token from AWS SSO OIDC. The response is an
|
|
6
|
+
* `application/vnd.amazon.eventstream`, decoded by the shared
|
|
7
|
+
* `decodeEventStream` primitive from `aws-eventstream.ts`.
|
|
8
|
+
*
|
|
9
|
+
* Clean-room: derived from published Amazon service model shapes
|
|
10
|
+
* (aws-toolkit-vscode CodeWhisperer streaming + codewhispererruntime-2022-11-11),
|
|
11
|
+
* not from any AGPL reference implementation.
|
|
12
|
+
*/
|
|
13
|
+
import { $credentialEnv, $env, extractHttpStatusFromError } from "@gajae-code/utils";
|
|
14
|
+
import type {
|
|
15
|
+
Api,
|
|
16
|
+
AssistantMessage,
|
|
17
|
+
Context,
|
|
18
|
+
Model,
|
|
19
|
+
StreamFunction,
|
|
20
|
+
StreamOptions,
|
|
21
|
+
TextContent,
|
|
22
|
+
Tool,
|
|
23
|
+
ToolCall,
|
|
24
|
+
ToolResultMessage,
|
|
25
|
+
} from "../types";
|
|
26
|
+
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
27
|
+
import { transportFailureFacts } from "../utils/fallback-transport";
|
|
28
|
+
import { withHttpStatus } from "../utils/http-inspector";
|
|
29
|
+
import { decodeEventStream } from "./aws-eventstream";
|
|
30
|
+
|
|
31
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
32
|
+
// Provider options
|
|
33
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
export interface KiroCodeWhispererOptions extends StreamOptions {
|
|
36
|
+
/** AWS region for the CodeWhisperer streaming endpoint. */
|
|
37
|
+
region?: string;
|
|
38
|
+
/** Profile ARN for enterprise IAM Identity Center accounts. */
|
|
39
|
+
profileArn?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
43
|
+
// CodeWhisperer streaming wire types
|
|
44
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
interface WireToolSpec {
|
|
47
|
+
toolSpecification: {
|
|
48
|
+
name: string;
|
|
49
|
+
description?: string;
|
|
50
|
+
inputSchema: { json: unknown };
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface WireToolResult {
|
|
55
|
+
toolResultMessage: {
|
|
56
|
+
content: string;
|
|
57
|
+
toolUseId: string;
|
|
58
|
+
status?: "success" | "error";
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface WireUserMessage {
|
|
63
|
+
userInputMessage: {
|
|
64
|
+
content: string;
|
|
65
|
+
userInputMessageContext?: {
|
|
66
|
+
tools?: { tools: WireToolSpec[] };
|
|
67
|
+
toolResults?: { toolResults: WireToolResult[][] };
|
|
68
|
+
editorStateContext?: Record<string, unknown>;
|
|
69
|
+
};
|
|
70
|
+
origin?: string;
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface WireAssistantMessage {
|
|
75
|
+
assistantResponseMessage: {
|
|
76
|
+
content: string;
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
type WireHistoryMessage = WireUserMessage | WireAssistantMessage;
|
|
81
|
+
|
|
82
|
+
interface ConversationState {
|
|
83
|
+
chatTriggerType: "MANUAL";
|
|
84
|
+
currentMessage: WireUserMessage;
|
|
85
|
+
history?: WireHistoryMessage[];
|
|
86
|
+
profileArn?: string;
|
|
87
|
+
customizationArn?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
interface GenerateAssistantResponseRequest {
|
|
91
|
+
conversationState: ConversationState;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
95
|
+
// Response eventstream types (ChatResponseStream union members)
|
|
96
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
interface AssistantResponseEvent {
|
|
99
|
+
assistantResponseEvent?: {
|
|
100
|
+
content?: string;
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
interface ToolUseEventPayload {
|
|
105
|
+
toolUseEvent?: {
|
|
106
|
+
toolUseId?: string;
|
|
107
|
+
name?: string;
|
|
108
|
+
input?: unknown;
|
|
109
|
+
stop?: { stopReason?: string };
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
interface MessageMetadataEvent {
|
|
114
|
+
messageMetadataEvent?: {
|
|
115
|
+
conversationId?: string;
|
|
116
|
+
utteranceId?: string;
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
interface ErrorPayload {
|
|
121
|
+
error?: {
|
|
122
|
+
message?: string;
|
|
123
|
+
code?: string;
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
128
|
+
// Constants
|
|
129
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
const DEFAULT_REGION = "us-east-1";
|
|
132
|
+
const STREAMING_SERVICE_NAME = "amazoncodewhispererstreamingservice";
|
|
133
|
+
|
|
134
|
+
type Block = (TextContent | ToolCall) & { index?: number; partialJson?: string };
|
|
135
|
+
|
|
136
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
137
|
+
// Stream function
|
|
138
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
export const streamKiroCodeWhisperer: StreamFunction<"kiro-codewhisperer-stream"> = (
|
|
141
|
+
model: Model<"kiro-codewhisperer-stream">,
|
|
142
|
+
context: Context,
|
|
143
|
+
options: KiroCodeWhispererOptions,
|
|
144
|
+
): AssistantMessageEventStream => {
|
|
145
|
+
const stream = new AssistantMessageEventStream();
|
|
146
|
+
|
|
147
|
+
(async () => {
|
|
148
|
+
const startTime = Date.now();
|
|
149
|
+
let firstTokenTime: number | undefined;
|
|
150
|
+
|
|
151
|
+
const output: AssistantMessage = {
|
|
152
|
+
role: "assistant",
|
|
153
|
+
content: [],
|
|
154
|
+
api: "kiro-codewhisperer-stream" as Api,
|
|
155
|
+
provider: model.provider,
|
|
156
|
+
model: model.id,
|
|
157
|
+
usage: {
|
|
158
|
+
input: 0,
|
|
159
|
+
output: 0,
|
|
160
|
+
cacheRead: 0,
|
|
161
|
+
cacheWrite: 0,
|
|
162
|
+
totalTokens: 0,
|
|
163
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
164
|
+
},
|
|
165
|
+
stopReason: "stop",
|
|
166
|
+
timestamp: Date.now(),
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const blocks = output.content as Block[];
|
|
170
|
+
const region = options.region || $env.KIRO_REGION || $env.AWS_REGION || $env.AWS_DEFAULT_REGION || DEFAULT_REGION;
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
// Resolve bearer token
|
|
174
|
+
const bearerToken = resolveBearerToken(options.apiKey);
|
|
175
|
+
if (!bearerToken) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
"No Kiro credentials found. Run 'gjc auth-broker login kiro' to authenticate via AWS Builder ID, or set AWS_BEARER_TOKEN_KIRO.",
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Build request
|
|
182
|
+
const conversationState = buildConversationState(context, model, options);
|
|
183
|
+
const requestBody: GenerateAssistantResponseRequest = {
|
|
184
|
+
conversationState,
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
options?.onPayload?.(requestBody, model, options?.attemptScope);
|
|
188
|
+
|
|
189
|
+
const host = `${STREAMING_SERVICE_NAME}.${region}.amazonaws.com`;
|
|
190
|
+
const url = `https://${host}/`;
|
|
191
|
+
|
|
192
|
+
const bodyText = JSON.stringify(requestBody);
|
|
193
|
+
const body = new TextEncoder().encode(bodyText);
|
|
194
|
+
const requestHeaders: Record<string, string> = {
|
|
195
|
+
"content-type": "application/json",
|
|
196
|
+
accept: "application/vnd.amazon.eventstream",
|
|
197
|
+
authorization: `Bearer ${bearerToken}`,
|
|
198
|
+
"amzn-X-amz-target": "AmazonCodeWhispererService.GenerateAssistantResponse",
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
if (options.profileArn) {
|
|
202
|
+
requestHeaders["x-amzn-codewhisperer-proflearn"] = options.profileArn;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Merge user-provided headers
|
|
206
|
+
if (options.headers) {
|
|
207
|
+
Object.assign(requestHeaders, options.headers);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const response = await fetch(url, {
|
|
211
|
+
method: "POST",
|
|
212
|
+
headers: requestHeaders,
|
|
213
|
+
body,
|
|
214
|
+
signal: options.signal,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
if (!response.ok) {
|
|
218
|
+
const errBody = await response.text().catch(() => "");
|
|
219
|
+
throw withHttpStatus(
|
|
220
|
+
new Error(`Kiro CodeWhisperer HTTP ${response.status}: ${errBody.slice(0, 1000)}`),
|
|
221
|
+
response.status,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (!response.body) throw new Error("Kiro CodeWhisperer response has no body");
|
|
226
|
+
|
|
227
|
+
// Decode eventstream
|
|
228
|
+
for await (const message of decodeEventStream(response.body)) {
|
|
229
|
+
if (options.signal?.aborted) break;
|
|
230
|
+
|
|
231
|
+
const messageType = message.headers[":message-type"];
|
|
232
|
+
const eventType = message.headers[":event-type"];
|
|
233
|
+
|
|
234
|
+
if (messageType === "exception") {
|
|
235
|
+
const exceptionType = message.headers[":exception-type"] || "Exception";
|
|
236
|
+
const payload = safeParsePayload(message.payload) as { message?: string } | undefined;
|
|
237
|
+
const errorMessage = payload?.message || new TextDecoder().decode(message.payload);
|
|
238
|
+
const status = exceptionType === "ValidationException" ? 400 : 0;
|
|
239
|
+
const err = new Error(`${exceptionType}: ${errorMessage}`);
|
|
240
|
+
throw status ? withHttpStatus(err, status) : err;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (messageType === "error") {
|
|
244
|
+
const code = message.headers[":error-code"] || "UnknownError";
|
|
245
|
+
const errorMessage = message.headers[":error-message"] || new TextDecoder().decode(message.payload);
|
|
246
|
+
throw new Error(`${code}: ${errorMessage}`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (messageType !== "event") continue;
|
|
250
|
+
|
|
251
|
+
const payload = safeParsePayload(message.payload);
|
|
252
|
+
if (!payload) continue;
|
|
253
|
+
|
|
254
|
+
switch (eventType) {
|
|
255
|
+
case "assistantResponseEvent": {
|
|
256
|
+
const ev = payload as AssistantResponseEvent;
|
|
257
|
+
const content = ev.assistantResponseEvent?.content;
|
|
258
|
+
if (content) {
|
|
259
|
+
if (!firstTokenTime) firstTokenTime = Date.now();
|
|
260
|
+
if (blocks.length === 0) {
|
|
261
|
+
stream.push({ type: "start", partial: output });
|
|
262
|
+
}
|
|
263
|
+
handleTextDelta(content, blocks, output, stream);
|
|
264
|
+
}
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
case "toolUseEvent": {
|
|
268
|
+
const ev = payload as ToolUseEventPayload;
|
|
269
|
+
if (!firstTokenTime) firstTokenTime = Date.now();
|
|
270
|
+
if (blocks.length === 0) {
|
|
271
|
+
stream.push({ type: "start", partial: output });
|
|
272
|
+
}
|
|
273
|
+
handleToolUseEvent(ev, blocks, output, stream);
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
case "messageMetadataEvent": {
|
|
277
|
+
const ev = payload as MessageMetadataEvent;
|
|
278
|
+
if (ev.messageMetadataEvent?.conversationId) {
|
|
279
|
+
output.responseId = ev.messageMetadataEvent.conversationId;
|
|
280
|
+
}
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
case "codeReferenceEvent":
|
|
284
|
+
case "supplementaryWebLinksEvent":
|
|
285
|
+
case "followupPromptEvent":
|
|
286
|
+
case "dryRunSucceedEvent":
|
|
287
|
+
case "citationEvent":
|
|
288
|
+
case "intentsEvent":
|
|
289
|
+
case "interactionComponentsEvent":
|
|
290
|
+
case "invalidStateEvent":
|
|
291
|
+
// Known but unhandled events — ignore gracefully
|
|
292
|
+
break;
|
|
293
|
+
default:
|
|
294
|
+
// Unknown event types — ignore (forward compatibility)
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const errorPayload = payload as ErrorPayload;
|
|
299
|
+
if (errorPayload.error?.message) {
|
|
300
|
+
throw new Error(`Kiro CodeWhisperer stream error: ${errorPayload.error.message}`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (options.signal?.aborted) throw new Error("Request was aborted");
|
|
305
|
+
|
|
306
|
+
// Finalize blocks
|
|
307
|
+
for (const block of blocks) {
|
|
308
|
+
delete block.index;
|
|
309
|
+
delete block.partialJson;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Determine stop reason
|
|
313
|
+
const hasToolCall = blocks.some(b => b.type === "toolCall");
|
|
314
|
+
output.stopReason = hasToolCall ? "toolUse" : "stop";
|
|
315
|
+
|
|
316
|
+
output.duration = Date.now() - startTime;
|
|
317
|
+
if (firstTokenTime) output.ttft = firstTokenTime - startTime;
|
|
318
|
+
stream.push({ type: "done", reason: output.stopReason as "stop" | "length" | "toolUse", message: output });
|
|
319
|
+
stream.end();
|
|
320
|
+
} catch (error) {
|
|
321
|
+
for (const block of output.content) {
|
|
322
|
+
delete (block as Block).index;
|
|
323
|
+
delete (block as Block).partialJson;
|
|
324
|
+
}
|
|
325
|
+
output.stopReason = options.signal?.aborted ? "aborted" : "error";
|
|
326
|
+
output.errorStatus = extractHttpStatusFromError(error);
|
|
327
|
+
output.transportFailure = transportFailureFacts(error);
|
|
328
|
+
const baseMessage = error instanceof Error ? error.message : JSON.stringify(error);
|
|
329
|
+
output.errorMessage = baseMessage;
|
|
330
|
+
output.duration = Date.now() - startTime;
|
|
331
|
+
if (firstTokenTime) output.ttft = firstTokenTime - startTime;
|
|
332
|
+
stream.push({ type: "error", reason: output.stopReason, error: output });
|
|
333
|
+
stream.end();
|
|
334
|
+
}
|
|
335
|
+
})();
|
|
336
|
+
|
|
337
|
+
return stream;
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
341
|
+
// Request building
|
|
342
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
343
|
+
|
|
344
|
+
function buildConversationState(
|
|
345
|
+
context: Context,
|
|
346
|
+
_model: Model<"kiro-codewhisperer-stream">,
|
|
347
|
+
options: KiroCodeWhispererOptions,
|
|
348
|
+
): ConversationState {
|
|
349
|
+
const messages = context.messages;
|
|
350
|
+
if (messages.length === 0) {
|
|
351
|
+
throw new Error("Kiro CodeWhisperer requires at least one message");
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Build history from all messages except the last
|
|
355
|
+
const history: WireHistoryMessage[] = [];
|
|
356
|
+
const systemPrompt = context.systemPrompt?.join("\n") ?? "";
|
|
357
|
+
|
|
358
|
+
for (let i = 0; i < messages.length - 1; i++) {
|
|
359
|
+
const msg = messages[i];
|
|
360
|
+
history.push(convertToWireMessage(msg, i === 0 ? systemPrompt : undefined));
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Convert the last message as currentMessage
|
|
364
|
+
const lastMsg = messages[messages.length - 1];
|
|
365
|
+
const currentMessage = convertToWireUserMessage(lastMsg, systemPrompt);
|
|
366
|
+
|
|
367
|
+
// Add tools to the current message context
|
|
368
|
+
if (context.tools && context.tools.length > 0) {
|
|
369
|
+
if (!currentMessage.userInputMessage.userInputMessageContext) {
|
|
370
|
+
currentMessage.userInputMessage.userInputMessageContext = {};
|
|
371
|
+
}
|
|
372
|
+
currentMessage.userInputMessage.userInputMessageContext.tools = {
|
|
373
|
+
tools: convertTools(context.tools),
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return {
|
|
378
|
+
chatTriggerType: "MANUAL",
|
|
379
|
+
currentMessage,
|
|
380
|
+
history: history.length > 0 ? history : undefined,
|
|
381
|
+
profileArn: options.profileArn,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function convertToWireMessage(msg: Context["messages"][number], systemPrompt?: string): WireHistoryMessage {
|
|
386
|
+
if (msg.role === "user") {
|
|
387
|
+
return convertToWireUserMessage(msg, systemPrompt);
|
|
388
|
+
}
|
|
389
|
+
if (msg.role === "toolResult") {
|
|
390
|
+
return convertToWireUserMessage(msg, systemPrompt);
|
|
391
|
+
}
|
|
392
|
+
// assistant → assistant response
|
|
393
|
+
const textParts: string[] = [];
|
|
394
|
+
for (const block of msg.content) {
|
|
395
|
+
if (typeof block === "string") {
|
|
396
|
+
textParts.push(block);
|
|
397
|
+
} else if (block.type === "text") {
|
|
398
|
+
textParts.push(block.text);
|
|
399
|
+
} else if (block.type === "toolCall") {
|
|
400
|
+
textParts.push(JSON.stringify({ toolUseId: block.id, name: block.name, input: block.arguments }));
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return {
|
|
404
|
+
assistantResponseMessage: {
|
|
405
|
+
content: textParts.join("\n") || "",
|
|
406
|
+
},
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function convertToWireUserMessage(msg: Context["messages"][number], systemPrompt?: string): WireUserMessage {
|
|
411
|
+
let content = extractTextContent(msg);
|
|
412
|
+
if (systemPrompt) {
|
|
413
|
+
content = `${systemPrompt}\n\n${content}`;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const userMsg: WireUserMessage = {
|
|
417
|
+
userInputMessage: {
|
|
418
|
+
content,
|
|
419
|
+
},
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
// Handle tool results
|
|
423
|
+
if (msg.role === "toolResult") {
|
|
424
|
+
const toolResultMsg = msg as ToolResultMessage;
|
|
425
|
+
const toolResults = (toolResultMsg.content ?? []).map(detail => ({
|
|
426
|
+
toolResultMessage: {
|
|
427
|
+
content: detail.type === "text" ? detail.text : "",
|
|
428
|
+
toolUseId: toolResultMsg.toolCallId,
|
|
429
|
+
status: toolResultMsg.isError ? ("error" as const) : ("success" as const),
|
|
430
|
+
},
|
|
431
|
+
}));
|
|
432
|
+
if (toolResults.length > 0) {
|
|
433
|
+
if (!userMsg.userInputMessage.userInputMessageContext) {
|
|
434
|
+
userMsg.userInputMessage.userInputMessageContext = {};
|
|
435
|
+
}
|
|
436
|
+
userMsg.userInputMessage.userInputMessageContext.toolResults = { toolResults: [toolResults] };
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return userMsg;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function extractTextContent(msg: Context["messages"][number]): string {
|
|
444
|
+
if (typeof msg.content === "string") return msg.content;
|
|
445
|
+
if (Array.isArray(msg.content)) {
|
|
446
|
+
return msg.content
|
|
447
|
+
.map(block => {
|
|
448
|
+
if (typeof block === "string") return block;
|
|
449
|
+
if (block.type === "text") return block.text;
|
|
450
|
+
if (block.type === "image") return ""; // Images not supported in text field
|
|
451
|
+
return "";
|
|
452
|
+
})
|
|
453
|
+
.join("");
|
|
454
|
+
}
|
|
455
|
+
return "";
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function convertTools(tools: Tool[]): WireToolSpec[] {
|
|
459
|
+
return tools.map(tool => ({
|
|
460
|
+
toolSpecification: {
|
|
461
|
+
name: tool.name,
|
|
462
|
+
description: tool.description ?? "",
|
|
463
|
+
inputSchema: {
|
|
464
|
+
json: tool.parameters ?? {},
|
|
465
|
+
},
|
|
466
|
+
},
|
|
467
|
+
}));
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
471
|
+
// Event handling
|
|
472
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
473
|
+
|
|
474
|
+
function handleTextDelta(
|
|
475
|
+
delta: string,
|
|
476
|
+
blocks: Block[],
|
|
477
|
+
output: AssistantMessage,
|
|
478
|
+
stream: AssistantMessageEventStream,
|
|
479
|
+
): void {
|
|
480
|
+
// Find or create the last text block
|
|
481
|
+
let lastBlock = blocks[blocks.length - 1];
|
|
482
|
+
if (lastBlock?.type !== "text") {
|
|
483
|
+
const newBlock: Block = { type: "text", text: "", index: blocks.length };
|
|
484
|
+
blocks.push(newBlock);
|
|
485
|
+
lastBlock = newBlock;
|
|
486
|
+
stream.push({ type: "text_start", contentIndex: newBlock.index!, partial: output });
|
|
487
|
+
}
|
|
488
|
+
lastBlock.text += delta;
|
|
489
|
+
stream.push({ type: "text_delta", contentIndex: lastBlock.index!, delta, partial: output });
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function handleToolUseEvent(
|
|
493
|
+
ev: ToolUseEventPayload,
|
|
494
|
+
blocks: Block[],
|
|
495
|
+
output: AssistantMessage,
|
|
496
|
+
stream: AssistantMessageEventStream,
|
|
497
|
+
): void {
|
|
498
|
+
const toolEvent = ev.toolUseEvent;
|
|
499
|
+
if (!toolEvent) return;
|
|
500
|
+
|
|
501
|
+
const toolUseId = toolEvent.toolUseId ?? "";
|
|
502
|
+
const name = toolEvent.name ?? "";
|
|
503
|
+
|
|
504
|
+
// If input is provided as a complete object, emit toolcall_end
|
|
505
|
+
if (toolEvent.input !== undefined && toolEvent.input !== null) {
|
|
506
|
+
const inputStr = typeof toolEvent.input === "string" ? toolEvent.input : JSON.stringify(toolEvent.input);
|
|
507
|
+
const toolCall: ToolCall = {
|
|
508
|
+
type: "toolCall",
|
|
509
|
+
id: toolUseId,
|
|
510
|
+
name,
|
|
511
|
+
arguments: safeParseJson(inputStr) as Record<string, any>,
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
const newBlock: Block = { ...toolCall, index: blocks.length };
|
|
515
|
+
blocks.push(newBlock);
|
|
516
|
+
stream.push({ type: "toolcall_end", contentIndex: newBlock.index!, toolCall, partial: output });
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Otherwise, accumulate partial input (if the service streams it in chunks)
|
|
521
|
+
// The published model does not document chunked tool input for CodeWhisperer,
|
|
522
|
+
// so this path handles the case defensively but expects complete input per event.
|
|
523
|
+
const toolCall: ToolCall = {
|
|
524
|
+
type: "toolCall",
|
|
525
|
+
id: toolUseId,
|
|
526
|
+
name,
|
|
527
|
+
arguments: {},
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
const newBlock: Block = { ...toolCall, index: blocks.length };
|
|
531
|
+
blocks.push(newBlock);
|
|
532
|
+
stream.push({ type: "toolcall_end", contentIndex: newBlock.index!, toolCall, partial: output });
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
536
|
+
// Helpers
|
|
537
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
538
|
+
|
|
539
|
+
function resolveBearerToken(apiKey: string | undefined): string | undefined {
|
|
540
|
+
if (!apiKey) {
|
|
541
|
+
return $credentialEnv("AWS_BEARER_TOKEN_KIRO") ?? undefined;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Structured API key (from getOAuthApiKey) contains the access token as JSON
|
|
545
|
+
try {
|
|
546
|
+
const parsed = JSON.parse(apiKey) as { token?: string };
|
|
547
|
+
if (parsed.token) return parsed.token;
|
|
548
|
+
} catch {
|
|
549
|
+
// Plain bearer token
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return apiKey;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function safeParsePayload(payload: Uint8Array): unknown {
|
|
556
|
+
if (payload.length === 0) return {};
|
|
557
|
+
try {
|
|
558
|
+
const text = new TextDecoder().decode(payload);
|
|
559
|
+
return JSON.parse(text);
|
|
560
|
+
} catch {
|
|
561
|
+
return undefined;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function safeParseJson(str: string): unknown {
|
|
566
|
+
if (!str) return {};
|
|
567
|
+
try {
|
|
568
|
+
return JSON.parse(str);
|
|
569
|
+
} catch {
|
|
570
|
+
return str;
|
|
571
|
+
}
|
|
572
|
+
}
|