@gajae-code/ai 0.13.2 → 0.13.3
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 +16 -0
- package/dist/types/provider-models/special.d.ts +3 -0
- package/dist/types/providers/kiro-codewhisperer.d.ts +8 -0
- package/dist/types/providers/mock.d.ts +2 -0
- package/dist/types/providers/register-builtins.d.ts +1 -0
- package/dist/types/types.d.ts +14 -2
- package/dist/types/utils/json-parse.d.ts +19 -0
- package/dist/types/utils/oauth/kiro.d.ts +71 -0
- package/dist/types/utils/oauth/types.d.ts +1 -1
- package/package.json +2 -2
- package/src/model-thinking.ts +24 -0
- package/src/models.json +7753 -1422
- package/src/openai-completions-compat.ts +2 -1
- package/src/provider-models/descriptors.ts +2 -0
- package/src/provider-models/special.ts +12 -0
- package/src/providers/anthropic.ts +10 -1
- package/src/providers/kiro-codewhisperer.ts +572 -0
- package/src/providers/mock.ts +3 -0
- package/src/providers/openai-completions.ts +4 -1
- package/src/providers/openai-responses-shared.ts +12 -4
- package/src/providers/register-builtins.ts +19 -0
- package/src/stream.ts +13 -0
- package/src/types.ts +15 -1
- package/src/utils/json-parse.ts +80 -0
- 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
|
@@ -125,6 +125,7 @@ export function detectOpenAICompat(model: Model<"openai-completions">, resolvedB
|
|
|
125
125
|
baseUrl.includes("fireworks.ai") ||
|
|
126
126
|
isDirectDeepseekApi;
|
|
127
127
|
const isGrok = provider === "xai" || baseUrl.includes("api.x.ai");
|
|
128
|
+
const isDirectXaiReasoningEffortModel = provider === "xai" && (model.id === "grok-4.5" || model.id === "grok-4.6");
|
|
128
129
|
const isMistral = provider === "mistral" || baseUrl.includes("mistral.ai");
|
|
129
130
|
|
|
130
131
|
// Hosts whose chat-completions endpoints are known to accept multiple
|
|
@@ -215,7 +216,7 @@ export function detectOpenAICompat(model: Model<"openai-completions">, resolvedB
|
|
|
215
216
|
sendSessionHeaders: false,
|
|
216
217
|
supportsResponsesSessionAffinity: false,
|
|
217
218
|
supportsMultipleSystemMessages: supportsMultipleSystemMessagesDefault,
|
|
218
|
-
supportsReasoningEffort: !isGrok && !isZai,
|
|
219
|
+
supportsReasoningEffort: (!isGrok && !isZai) || isDirectXaiReasoningEffortModel,
|
|
219
220
|
reasoningEffortMap,
|
|
220
221
|
supportsUsageInStreaming: !isCerebras,
|
|
221
222
|
disableReasoningOnForcedToolChoice: isKimiModel || isAnthropicModel || isOpenCodeGoReasoning,
|
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
cursorModelManagerOptions,
|
|
53
53
|
glmZcodeModelManagerOptions,
|
|
54
54
|
jetbrainsJunieModelManagerOptions,
|
|
55
|
+
kiroModelManagerOptions,
|
|
55
56
|
openCodexModelManagerOptions,
|
|
56
57
|
zaiModelManagerOptions,
|
|
57
58
|
} from "./special";
|
|
@@ -356,6 +357,7 @@ export const PROVIDER_DESCRIPTORS: readonly ProviderDescriptor[] = [
|
|
|
356
357
|
config => cursorModelManagerOptions(config),
|
|
357
358
|
catalog("Cursor", ["CURSOR_API_KEY"], { oauthProvider: "cursor" }),
|
|
358
359
|
),
|
|
360
|
+
descriptor("kiro", "CLAUDE_3_7_SONNET_20250219_V1_0", config => kiroModelManagerOptions(config)),
|
|
359
361
|
] as const;
|
|
360
362
|
|
|
361
363
|
/** Default model IDs for all known providers, built from descriptors + special providers. */
|
|
@@ -96,3 +96,15 @@ export function jetbrainsJunieModelManagerOptions(
|
|
|
96
96
|
): ModelManagerOptions<"anthropic-messages"> {
|
|
97
97
|
return { providerId: "jetbrains-junie" };
|
|
98
98
|
}
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// Kiro (Amazon Q Developer / CodeWhisperer)
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
export interface KiroModelManagerConfig {}
|
|
105
|
+
|
|
106
|
+
export function kiroModelManagerOptions(
|
|
107
|
+
_config: KiroModelManagerConfig = {},
|
|
108
|
+
): ModelManagerOptions<"kiro-codewhisperer-stream"> {
|
|
109
|
+
return { providerId: "kiro" };
|
|
110
|
+
}
|
|
@@ -73,7 +73,12 @@ import {
|
|
|
73
73
|
getStreamIdleTimeoutMs,
|
|
74
74
|
iterateWithIdleTimeout,
|
|
75
75
|
} from "../utils/idle-iterator";
|
|
76
|
-
import {
|
|
76
|
+
import {
|
|
77
|
+
findUnnecessaryUnicodeEscape,
|
|
78
|
+
isCompleteJson,
|
|
79
|
+
parseJsonWithRepair,
|
|
80
|
+
parseStreamingJson,
|
|
81
|
+
} from "../utils/json-parse";
|
|
77
82
|
import { parseGitHubCopilotApiKey } from "../utils/oauth/github-copilot";
|
|
78
83
|
import { notifyProviderResponse } from "../utils/provider-response";
|
|
79
84
|
import { isCopilotTransientModelError } from "../utils/retry";
|
|
@@ -1835,6 +1840,9 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
1835
1840
|
if (!isCompleteJson(block.partialJson)) truncatedToolCalls.add(block);
|
|
1836
1841
|
if (block.partialJson.trim()) {
|
|
1837
1842
|
block.arguments = parseStreamingJson(block.partialJson);
|
|
1843
|
+
// Raw-wire check: after decode the escape is invisible.
|
|
1844
|
+
if (findUnnecessaryUnicodeEscape(block.partialJson))
|
|
1845
|
+
block.escapedNonAsciiArguments = true;
|
|
1838
1846
|
}
|
|
1839
1847
|
delete (block as { partialJson?: string }).partialJson;
|
|
1840
1848
|
stream.push({
|
|
@@ -2114,6 +2122,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
2114
2122
|
truncatedToolCalls.add(block);
|
|
2115
2123
|
if (block.partialJson.trim()) {
|
|
2116
2124
|
block.arguments = parseStreamingJson(block.partialJson);
|
|
2125
|
+
if (findUnnecessaryUnicodeEscape(block.partialJson)) block.escapedNonAsciiArguments = true;
|
|
2117
2126
|
}
|
|
2118
2127
|
delete (block as { partialJson?: string }).partialJson;
|
|
2119
2128
|
}
|
|
@@ -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
|
+
}
|
package/src/providers/mock.ts
CHANGED
|
@@ -75,6 +75,8 @@ export type MockContent =
|
|
|
75
75
|
arguments: Record<string, unknown> | string;
|
|
76
76
|
/** Simulate a provider-flagged truncated call (cut off mid-arguments). */
|
|
77
77
|
incompleteArguments?: boolean;
|
|
78
|
+
/** Simulate a provider-flagged `\uXXXX`-escaped-arguments call. */
|
|
79
|
+
escapedNonAsciiArguments?: boolean;
|
|
78
80
|
};
|
|
79
81
|
|
|
80
82
|
/** One scripted response. */
|
|
@@ -423,6 +425,7 @@ function normalizeContent(input: MockContent, state: MockModel): TextContent | T
|
|
|
423
425
|
name: input.name,
|
|
424
426
|
arguments: typeof input.arguments === "string" ? input.arguments : { ...input.arguments },
|
|
425
427
|
...(input.incompleteArguments ? { incompleteArguments: true } : {}),
|
|
428
|
+
...(input.escapedNonAsciiArguments ? { escapedNonAsciiArguments: true } : {}),
|
|
426
429
|
} as ToolCall;
|
|
427
430
|
}
|
|
428
431
|
return input;
|
|
@@ -54,7 +54,7 @@ import {
|
|
|
54
54
|
iterateWithIdleTimeout,
|
|
55
55
|
resolveOpenAISdkRequestTimeoutMs,
|
|
56
56
|
} from "../utils/idle-iterator";
|
|
57
|
-
import { isCompleteJson, parseStreamingJson } from "../utils/json-parse";
|
|
57
|
+
import { findUnnecessaryUnicodeEscape, isCompleteJson, parseStreamingJson } from "../utils/json-parse";
|
|
58
58
|
import { parseGitHubCopilotApiKey } from "../utils/oauth/github-copilot";
|
|
59
59
|
import { getKimiCommonHeaders } from "../utils/oauth/kimi";
|
|
60
60
|
import { notifyProviderResponse } from "../utils/provider-response";
|
|
@@ -676,6 +676,8 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
|
|
|
676
676
|
return;
|
|
677
677
|
}
|
|
678
678
|
block.arguments = parseStreamingJson(block.partialArgs);
|
|
679
|
+
// Raw-wire check: after decode the escape is invisible.
|
|
680
|
+
if (findUnnecessaryUnicodeEscape(block.partialArgs)) block.escapedNonAsciiArguments = true;
|
|
679
681
|
delete (block as { partialArgs?: string }).partialArgs;
|
|
680
682
|
stream.push({ type: "toolcall_end", contentIndex, toolCall: block, partial: output });
|
|
681
683
|
};
|
|
@@ -804,6 +806,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
|
|
|
804
806
|
partialArgs: call.arguments,
|
|
805
807
|
};
|
|
806
808
|
block.arguments = parseStreamingJson(call.arguments);
|
|
809
|
+
if (findUnnecessaryUnicodeEscape(call.arguments)) block.escapedNonAsciiArguments = true;
|
|
807
810
|
currentBlock = block;
|
|
808
811
|
output.content.push(block);
|
|
809
812
|
stream.push({ type: "toolcall_start", contentIndex: blockIndex(block), partial: output });
|