@lightharu/krouter 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/LICENSE +679 -0
  2. package/README.md +238 -0
  3. package/dist-web/assets/index-CM4-0adf.css +1 -0
  4. package/dist-web/assets/index-DCslvfUR.js +139 -0
  5. package/dist-web/favicon.svg +9 -0
  6. package/dist-web/icon.svg +9 -0
  7. package/dist-web/index.html +19 -0
  8. package/out-server/main/kiroAuthSync.js +249 -0
  9. package/out-server/main/kproxy/certManager.js +262 -0
  10. package/out-server/main/kproxy/index.js +254 -0
  11. package/out-server/main/kproxy/mitmProxy.js +475 -0
  12. package/out-server/main/kproxy/types.js +23 -0
  13. package/out-server/main/proxy/accountPool.js +543 -0
  14. package/out-server/main/proxy/clientConfig.js +596 -0
  15. package/out-server/main/proxy/index.js +25 -0
  16. package/out-server/main/proxy/kiroApi.js +1996 -0
  17. package/out-server/main/proxy/logger.js +407 -0
  18. package/out-server/main/proxy/modelCatalog.js +75 -0
  19. package/out-server/main/proxy/promptCacheTracker.js +301 -0
  20. package/out-server/main/proxy/proxyServer.js +3543 -0
  21. package/out-server/main/proxy/selfSignedCert.js +179 -0
  22. package/out-server/main/proxy/systemProxy.js +250 -0
  23. package/out-server/main/proxy/tokenCounter.js +164 -0
  24. package/out-server/main/proxy/toolNameRegistry.js +57 -0
  25. package/out-server/main/proxy/translator.js +1084 -0
  26. package/out-server/main/proxy/types.js +3 -0
  27. package/out-server/main/registration/browser-identity.js +184 -0
  28. package/out-server/main/registration/chainProxy.js +349 -0
  29. package/out-server/main/registration/config.js +58 -0
  30. package/out-server/main/registration/email-service.js +801 -0
  31. package/out-server/main/registration/fingerprint.js +352 -0
  32. package/out-server/main/registration/http-utils.js +148 -0
  33. package/out-server/main/registration/jwe.js +74 -0
  34. package/out-server/main/registration/names.js +142 -0
  35. package/out-server/main/registration/proton-mail-window.js +339 -0
  36. package/out-server/main/registration/registrar.js +1715 -0
  37. package/out-server/main/registration/tlsClientPool.js +70 -0
  38. package/out-server/main/registration/xxtea.js +161 -0
  39. package/out-server/main/runtimePaths.js +19 -0
  40. package/out-server/main/utils/redact.js +95 -0
  41. package/out-server/server/index.js +1272 -0
  42. package/out-server/server/services/accountExtras.js +105 -0
  43. package/out-server/server/services/accountProfileHydration.js +95 -0
  44. package/out-server/server/services/authFlows.js +509 -0
  45. package/out-server/server/services/dashboardTunnel.js +315 -0
  46. package/out-server/server/services/diagnostics.js +326 -0
  47. package/out-server/server/services/kiroAccounts.js +431 -0
  48. package/out-server/server/services/kiroSettings.js +260 -0
  49. package/out-server/server/services/kproxyRuntime.js +264 -0
  50. package/out-server/server/services/localKiroCredentials.js +320 -0
  51. package/out-server/server/services/machineIdRuntime.js +327 -0
  52. package/out-server/server/services/protonBrowserRuntime.js +724 -0
  53. package/out-server/server/services/proxyRuntime.js +523 -0
  54. package/out-server/server/services/registrationRuntime.js +106 -0
  55. package/out-server/server/store.js +266 -0
  56. package/package.json +113 -0
  57. package/resources/tls-client-xgo-1.14.0-windows-amd64.dll +0 -0
  58. package/scripts/kiro-manager-cli.cjs +3 -0
  59. package/scripts/krouter-cli.cjs +509 -0
  60. package/src/renderer/src/assets/krouter-logo.svg +11 -0
  61. package/src/renderer/src/assets/krouter-mark.svg +9 -0
@@ -0,0 +1,1996 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.KIRO_SOCIAL_PROFILE_ARN = exports.isPlaceholderProfileArn = exports.KIRO_BUILDER_ID_PROFILE_ARN = exports.KIRO_BUILDER_ID_PLACEHOLDER_ARN = exports.getModelContextWindow = exports.setModelContextWindow = void 0;
4
+ exports.setUseKProxyForApiInProxy = setUseKProxyForApiInProxy;
5
+ exports.setLogStreamEvents = setLogStreamEvents;
6
+ exports.setPayloadSizeLimitKB = setPayloadSizeLimitKB;
7
+ exports.setEnableTokenBufferReserve = setEnableTokenBufferReserve;
8
+ exports.getEnableTokenBufferReserve = getEnableTokenBufferReserve;
9
+ exports.setTokenBufferReserve = setTokenBufferReserve;
10
+ exports.getTokenBufferReserve = getTokenBufferReserve;
11
+ exports.resolveProfileArn = resolveProfileArn;
12
+ exports.mapModelId = mapModelId;
13
+ exports.isAgenticRequest = isAgenticRequest;
14
+ exports.isThinkingEnabled = isThinkingEnabled;
15
+ exports.injectSystemPrompts = injectSystemPrompts;
16
+ exports.buildKiroPayload = buildKiroPayload;
17
+ exports.clearAllCaches = clearAllCaches;
18
+ exports.callKiroApiStream = callKiroApiStream;
19
+ exports.estimateTokens = estimateTokens;
20
+ exports.callKiroApi = callKiroApi;
21
+ exports.fetchKiroModels = fetchKiroModels;
22
+ exports.fetchAvailableSubscriptions = fetchAvailableSubscriptions;
23
+ exports.fetchSubscriptionToken = fetchSubscriptionToken;
24
+ exports.setUserPreference = setUserPreference;
25
+ // Kiro API 调用核心模块
26
+ const uuid_1 = require("uuid");
27
+ const undici_1 = require("undici");
28
+ const logger_1 = require("./logger");
29
+ const kproxy_1 = require("../kproxy");
30
+ const systemProxy_1 = require("./systemProxy");
31
+ const tokenCounter_1 = require("./tokenCounter");
32
+ Object.defineProperty(exports, "setModelContextWindow", { enumerable: true, get: function () { return tokenCounter_1.setModelContextWindow; } });
33
+ Object.defineProperty(exports, "getModelContextWindow", { enumerable: true, get: function () { return tokenCounter_1.getModelContextWindow; } });
34
+ // 是否使用 K-Proxy 代理发送 API 请求(从主进程导入)
35
+ let useKProxyForApi = false;
36
+ let logStreamEvents = false;
37
+ function setUseKProxyForApiInProxy(enabled) {
38
+ useKProxyForApi = enabled;
39
+ }
40
+ function setLogStreamEvents(enabled) {
41
+ logStreamEvents = enabled;
42
+ }
43
+ // Payload 大小限制(KB),用户可在高级设置中调整
44
+ let payloadSizeLimitKB = 1536; // 默认 1.5MB
45
+ function setPayloadSizeLimitKB(limitKB) {
46
+ payloadSizeLimitKB = Math.max(256, Math.min(10240, limitKB));
47
+ }
48
+ // Token buffer reserve 开关(默认 false = 完全跳过 trimHistoryByTokens)
49
+ // 关闭时后端不再裁剪任何旧消息,超出 context window 由 Kiro 后端原样返回错误
50
+ let enableTokenBufferReserve = false;
51
+ function setEnableTokenBufferReserve(enabled) {
52
+ enableTokenBufferReserve = !!enabled;
53
+ }
54
+ function getEnableTokenBufferReserve() {
55
+ return enableTokenBufferReserve;
56
+ }
57
+ // Token buffer reserve(仅在 enableTokenBufferReserve=true 时生效)
58
+ // 为 model context window 预留的余量,覆盖 system + tools + current + output + 估算偏差 + schema 开销
59
+ // 默认 20K:开关启用后的合理初始值(200K → effective 180K, 1M → effective 980K)
60
+ let tokenBufferReserve = 20000;
61
+ function setTokenBufferReserve(tokens) {
62
+ tokenBufferReserve = Math.max(5000, Math.min(150000, tokens));
63
+ }
64
+ function getTokenBufferReserve() {
65
+ return tokenBufferReserve;
66
+ }
67
+ // 根据 modelId 和 buffer 计算 effective token limit
68
+ // 仅在 enableTokenBufferReserve=true 时被调用
69
+ // 查不到 model 时 fallback 到 200K context (Claude 默认)
70
+ function getEffectiveTokenLimit(modelId) {
71
+ // 复用 getModelContextLength(支持 cache 命中 → 模糊匹配 → 关键词兜底)
72
+ const ctx = modelId ? (0, tokenCounter_1.getModelContextLength)(modelId) : 200000;
73
+ return Math.max(8000, ctx - tokenBufferReserve);
74
+ }
75
+ // Token 估算 (UTF-8 字节数 / 3.5,对中英混合场景做安全偏保守估算)
76
+ // 比真实 cl100k_base tokenizer 略偏高 (10-20%), 用于触发裁剪阈值是安全的
77
+ function estimateTokensFromString(str) {
78
+ return Math.ceil(Buffer.byteLength(str, 'utf-8') / 3.5);
79
+ }
80
+ function estimatePayloadTokens(payload) {
81
+ return estimateTokensFromString(JSON.stringify(payload));
82
+ }
83
+ /**
84
+ * 获取网络代理 agent
85
+ * 优先级(从高到低):
86
+ * 1. 账号自身绑定的 proxyUrl(实现"N 个号一个 IP"分桶反代)
87
+ * 2. K-Proxy(如果启用)
88
+ * 3. 环境变量代理
89
+ * 4. 系统代理
90
+ *
91
+ * 传入 account 让账号级代理覆盖全局;不传则走全局逻辑。
92
+ */
93
+ function getNetworkAgent(account) {
94
+ // 1. 账号专属代理:实现"N 个账号共用 1 个 IP"的分桶反代
95
+ if (account?.proxyUrl) {
96
+ const agent = (0, systemProxy_1.safeCreateProxyAgent)(account.proxyUrl);
97
+ if (agent) {
98
+ logger_1.proxyLogger.debug('KiroAPI', `Using account-bound proxy for ${account.email || account.id}`);
99
+ return agent;
100
+ }
101
+ }
102
+ // 2. K-Proxy
103
+ if (useKProxyForApi) {
104
+ const kproxyService = (0, kproxy_1.getKProxyService)();
105
+ if (kproxyService?.isRunning()) {
106
+ const config = kproxyService.getConfig();
107
+ const proxyUrl = `http://${config.host}:${config.port}`;
108
+ const agent = (0, systemProxy_1.safeCreateProxyAgent)(proxyUrl);
109
+ if (agent)
110
+ return agent;
111
+ }
112
+ }
113
+ // 3. 环境变量
114
+ const envProxy = process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy;
115
+ const envAgent = (0, systemProxy_1.safeCreateProxyAgent)(envProxy);
116
+ if (envAgent)
117
+ return envAgent;
118
+ // 4. 系统代理
119
+ return (0, systemProxy_1.safeCreateProxyAgent)((0, systemProxy_1.getSystemProxy)());
120
+ }
121
+ /**
122
+ * 使用代理的 fetch 函数
123
+ * 传入 account 时会优先使用账号绑定的代理(账号-代理 N:1 分桶)
124
+ */
125
+ async function fetchWithProxy(url, options, account) {
126
+ const agent = getNetworkAgent(account);
127
+ if (agent) {
128
+ logger_1.proxyLogger.debug('KiroAPI', `Using proxy agent: ${agent.constructor.name}`);
129
+ return await (0, undici_1.fetch)(url, { ...options, dispatcher: agent });
130
+ }
131
+ return await fetch(url, options);
132
+ }
133
+ // Kiro API 端点配置
134
+ const KIRO_ENDPOINTS = [
135
+ {
136
+ url: 'https://codewhisperer.us-east-1.amazonaws.com/generateAssistantResponse',
137
+ origin: 'AI_EDITOR',
138
+ amzTarget: 'AmazonCodeWhispererStreamingService.GenerateAssistantResponse',
139
+ name: 'CodeWhisperer',
140
+ protocol: 'generateAssistantResponse'
141
+ },
142
+ {
143
+ url: 'https://q.us-east-1.amazonaws.com/generateAssistantResponse',
144
+ origin: 'AI_EDITOR',
145
+ amzTarget: 'AmazonCodeWhispererStreamingService.GenerateAssistantResponse',
146
+ name: 'AmazonQ',
147
+ protocol: 'generateAssistantResponse'
148
+ },
149
+ {
150
+ url: 'https://q.us-east-1.amazonaws.com/SendMessageStreaming',
151
+ origin: 'CLI',
152
+ amzTarget: 'AmazonQDeveloperStreamingService.SendMessage',
153
+ name: 'AmazonQCLI'
154
+ }
155
+ ];
156
+ // Kiro 版本号(跟随官方 IDE 更新)
157
+ const KIRO_VERSION = '0.12.155';
158
+ const AWS_SDK_VERSION = '1.0.34';
159
+ const AWS_STREAMING_API_VERSION = '1.0.34';
160
+ const OS_PLATFORM = process.platform === 'win32' ? 'win32' : process.platform === 'darwin' ? 'macos' : 'linux';
161
+ const OS_RELEASE = (() => { try {
162
+ return require('os').release();
163
+ }
164
+ catch {
165
+ return '10.0.0';
166
+ } })();
167
+ const NODE_VERSION = process.versions.node || '22.22.0';
168
+ function getKiroUserAgent(machineId) {
169
+ const suffix = machineId ? `KiroIDE-${KIRO_VERSION}-${machineId}` : `KiroIDE-${KIRO_VERSION}`;
170
+ return `aws-sdk-js/${AWS_SDK_VERSION} ua/2.1 os/${OS_PLATFORM}#${OS_RELEASE} lang/js md/nodejs#${NODE_VERSION} api/codewhispererstreaming#${AWS_STREAMING_API_VERSION} m/E ${suffix}`;
171
+ }
172
+ function getKiroAmzUserAgent(machineId) {
173
+ const suffix = machineId ? `KiroIDE ${KIRO_VERSION} ${machineId}` : `KiroIDE-${KIRO_VERSION}`;
174
+ return `aws-sdk-js/${AWS_SDK_VERSION} ${suffix}`;
175
+ }
176
+ // profileArn 决策中心已迁移到 ../kiroAuthSync,反代和账号管理器主进程共用同一份定义,
177
+ // 防止多处常量漂移。注意 KIRO_BUILDER_ID_PLACEHOLDER_ARN 仍以本模块为出口 re-export,
178
+ // 这样 main/index.ts 等老 import 路径不需要改。
179
+ const kiroAuthSync_1 = require("../kiroAuthSync");
180
+ Object.defineProperty(exports, "KIRO_SOCIAL_PROFILE_ARN", { enumerable: true, get: function () { return kiroAuthSync_1.KIRO_SOCIAL_PROFILE_ARN; } });
181
+ exports.KIRO_BUILDER_ID_PLACEHOLDER_ARN = kiroAuthSync_1.KIRO_BUILDER_ID_PLACEHOLDER_ARN;
182
+ exports.KIRO_BUILDER_ID_PROFILE_ARN = exports.KIRO_BUILDER_ID_PLACEHOLDER_ARN;
183
+ exports.isPlaceholderProfileArn = kiroAuthSync_1.isPlaceholderProfileArn;
184
+ /**
185
+ * 反代调 Kiro API 时使用的 profileArn 决策。
186
+ * BuilderId 使用占位符 ARN,Social 使用固定 ARN。
187
+ * Streaming endpoints currently require the Builder ID placeholder ARN on
188
+ * some deployments and reject it on others. callKiroApiStream handles the
189
+ * compatibility retry.
190
+ */
191
+ function resolveProfileArn(account) {
192
+ const explicit = account.profileArn?.trim();
193
+ if (explicit && !(0, exports.isPlaceholderProfileArn)(explicit)) {
194
+ return explicit;
195
+ }
196
+ const authMethod = account.authMethod?.toLowerCase();
197
+ const provider = account.provider?.toLowerCase();
198
+ if (authMethod === 'social' || provider === 'github' || provider === 'google') {
199
+ return kiroAuthSync_1.KIRO_SOCIAL_PROFILE_ARN;
200
+ }
201
+ if (provider === 'enterprise' || provider === 'iam_sso' || provider === 'awsidc' || authMethod === 'external_idp') {
202
+ return undefined;
203
+ }
204
+ return exports.KIRO_BUILDER_ID_PLACEHOLDER_ARN;
205
+ }
206
+ // Agentic 模式系统提示 - 防止大文件写入超时
207
+ const AGENTIC_SYSTEM_PROMPT = `# CRITICAL: CHUNKED WRITE PROTOCOL (MANDATORY)
208
+
209
+ You MUST follow these rules for ALL file operations. Violation causes server timeouts and task failure.
210
+
211
+ ## ABSOLUTE LIMITS
212
+ - **MAXIMUM 350 LINES** per single write/edit operation - NO EXCEPTIONS
213
+ - **RECOMMENDED 300 LINES** or less for optimal performance
214
+ - **NEVER** write entire files in one operation if >300 lines
215
+
216
+ ## MANDATORY CHUNKED WRITE STRATEGY
217
+
218
+ ### For NEW FILES (>300 lines total):
219
+ 1. FIRST: Write initial chunk (first 250-300 lines) using write_to_file/fsWrite
220
+ 2. THEN: Append remaining content in 250-300 line chunks using file append operations
221
+ 3. REPEAT: Continue appending until complete
222
+
223
+ ### For EDITING EXISTING FILES:
224
+ 1. Use surgical edits (apply_diff/targeted edits) - change ONLY what's needed
225
+ 2. NEVER rewrite entire files - use incremental modifications
226
+ 3. Split large refactors into multiple small, focused edits
227
+
228
+ REMEMBER: When in doubt, write LESS per operation. Multiple small operations > one large operation.`;
229
+ // Thinking 模式标签
230
+ const THINKING_MODE_PROMPT = `<thinking_mode>enabled</thinking_mode>
231
+ <max_thinking_length>200000</max_thinking_length>`;
232
+ const CODEWHISPERER_DEFAULT_MODEL_ID = 'CLAUDE_SONNET_4_20250514_V1_0';
233
+ const CODEWHISPERER_MODEL_CACHE_TTL = 5 * 60 * 1000;
234
+ const codeWhispererModelCache = new Map();
235
+ // 模型 ID 映射
236
+ const MODEL_ID_MAP = {
237
+ // Claude 4.5 系列
238
+ 'claude-sonnet-4-5': 'claude-sonnet-4.5',
239
+ 'claude-sonnet-4.5': 'claude-sonnet-4.5',
240
+ 'claude-haiku-4-5': 'claude-haiku-4.5',
241
+ 'claude-haiku-4.5': 'claude-haiku-4.5',
242
+ 'claude-opus-4-5': 'claude-opus-4.5',
243
+ 'claude-opus-4.5': 'claude-opus-4.5',
244
+ // Claude 4 系列
245
+ 'claude-sonnet-4': 'claude-sonnet-4',
246
+ 'claude-sonnet-4-20250514': 'claude-sonnet-4',
247
+ // Claude 3.5 系列 (映射到 Sonnet 4.5)
248
+ 'claude-3-5-sonnet': 'claude-sonnet-4.5',
249
+ 'claude-3-opus': 'claude-sonnet-4.5',
250
+ 'claude-3-sonnet': 'claude-sonnet-4',
251
+ 'claude-3-haiku': 'claude-haiku-4.5',
252
+ // GPT 兼容映射 (映射到 Sonnet 4.5)
253
+ 'gpt-4': 'claude-sonnet-4.5',
254
+ 'gpt-4o': 'claude-sonnet-4.5',
255
+ 'gpt-4-turbo': 'claude-sonnet-4.5',
256
+ 'gpt-3.5-turbo': 'claude-sonnet-4.5',
257
+ 'default': 'claude-sonnet-4.5'
258
+ };
259
+ /**
260
+ * 归一化 Claude 版本号:把版本号里的短横线转成点号。
261
+ *
262
+ * 背景:部分客户端(如 Claude Code)不允许模型名里出现 ".",会把 "claude-opus-4.6"
263
+ * 写成 "claude-opus-4-6",若原样透传给 Kiro 会被解析成 "claude-opus-4"(丢掉 minor),
264
+ * 导致 1M 上下文等特性设置失败。这里把 claude-{family}-{major}-{minor} 的最后一段
265
+ * 版本短横转成点号,兼容未来任意新版本(4.6 / 4.7 / 5.0 ...)。
266
+ *
267
+ * 仅当 minor 是 1~2 位数字且其后不是更多数字时才转换,避免误伤日期快照后缀
268
+ * (如 claude-sonnet-4-20250514 不会被改)。
269
+ */
270
+ function normalizeClaudeVersion(modelId) {
271
+ return modelId.replace(/^(claude-(?:sonnet|haiku|opus))-(\d+)-(\d{1,2})(?=$|[^\d])/i, '$1-$2.$3');
272
+ }
273
+ function mapModelId(model) {
274
+ let modelId = model.trim();
275
+ if (!modelId)
276
+ return MODEL_ID_MAP.default;
277
+ if (isCodeWhispererModelId(modelId))
278
+ return modelId;
279
+ // 0) 归一化版本号短横 → 点号(claude-opus-4-6 → claude-opus-4.6),兼容不支持 "." 的客户端
280
+ modelId = normalizeClaudeVersion(modelId);
281
+ const lower = modelId.toLowerCase();
282
+ // 1) 显式 alias 映射优先
283
+ if (MODEL_ID_MAP[lower])
284
+ return MODEL_ID_MAP[lower];
285
+ // 2) 看似 Kiro 支持的 Claude 模型格式 (claude-{sonnet|haiku|opus}-{ver}),原样透传
286
+ // 用于向前兼容尚未加入 MODEL_ID_MAP 的新发布模型
287
+ if (/^claude-(sonnet|haiku|opus)-/.test(lower))
288
+ return modelId;
289
+ // 3) 完全未知的 model(用户拼错/不存在),兜底到 default 避免直接 400
290
+ console.warn(`[Kiro API] Unknown model "${modelId}" → fallback to "${MODEL_ID_MAP.default}"`);
291
+ return MODEL_ID_MAP.default;
292
+ }
293
+ function clonePayload(payload) {
294
+ return JSON.parse(JSON.stringify(payload));
295
+ }
296
+ function normalizeModelKey(value) {
297
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, '');
298
+ }
299
+ function modelTokens(value) {
300
+ return value.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
301
+ }
302
+ function matchesRequestedModel(model, requestedModelId) {
303
+ // 1. modelId 级精确匹配(去除符号后比较)
304
+ const requestedKey = normalizeModelKey(requestedModelId);
305
+ const modelIdKey = normalizeModelKey(model.modelId);
306
+ if (modelIdKey === requestedKey || modelIdKey.includes(requestedKey))
307
+ return true;
308
+ // 2. modelName 精确匹配
309
+ if (model.modelName && normalizeModelKey(model.modelName).includes(requestedKey))
310
+ return true;
311
+ // 3. token 匹配(所有请求 token 必须在 modelId+modelName 中命中,不搜索 description 避免误匹配)
312
+ const tokens = modelTokens(requestedModelId).filter(token => token !== 'latest' && token !== 'model');
313
+ if (tokens.length === 0)
314
+ return false;
315
+ const candidateTokens = new Set(modelTokens(`${model.modelId} ${model.modelName || ''}`));
316
+ // 必须全部 token 命中
317
+ if (!tokens.every(token => candidateTokens.has(token)))
318
+ return false;
319
+ // 防止模型家族冲突:如果请求包含 opus/sonnet/haiku,候选必须也包含对应的
320
+ const families = ['opus', 'sonnet', 'haiku'];
321
+ for (const family of families) {
322
+ if (tokens.includes(family) && !candidateTokens.has(family))
323
+ return false;
324
+ if (!tokens.includes(family) && candidateTokens.has(family))
325
+ return false;
326
+ }
327
+ return true;
328
+ }
329
+ function isCodeWhispererModelId(modelId) {
330
+ return /^[A-Z0-9_]+$/.test(modelId) && modelId.includes('_');
331
+ }
332
+ function getModelCacheKey(account) {
333
+ return `${account.id}:${account.region || 'us-east-1'}:${resolveProfileArn(account) ?? 'no-arn'}`;
334
+ }
335
+ async function getCachedCodeWhispererModels(account, signal) {
336
+ const key = getModelCacheKey(account);
337
+ const cached = codeWhispererModelCache.get(key);
338
+ if (cached && Date.now() - cached.timestamp < CODEWHISPERER_MODEL_CACHE_TTL)
339
+ return cached.models;
340
+ const models = await fetchKiroModels(account, signal);
341
+ codeWhispererModelCache.set(key, { models, timestamp: Date.now() });
342
+ return models;
343
+ }
344
+ async function resolveCodeWhispererModelId(account, requestedModelId, signal) {
345
+ const modelId = requestedModelId?.trim();
346
+ if (!modelId)
347
+ return CODEWHISPERER_DEFAULT_MODEL_ID;
348
+ if (isCodeWhispererModelId(modelId))
349
+ return modelId;
350
+ const models = await getCachedCodeWhispererModels(account, signal);
351
+ return models.find(model => matchesRequestedModel(model, modelId))?.modelId || CODEWHISPERER_DEFAULT_MODEL_ID;
352
+ }
353
+ function getPayloadModelId(payload) {
354
+ const currentModelId = payload.conversationState.currentMessage.userInputMessage.modelId;
355
+ if (currentModelId)
356
+ return currentModelId;
357
+ return payload.conversationState.history?.find(message => message.userInputMessage?.modelId)?.userInputMessage?.modelId;
358
+ }
359
+ function applyPayloadModelId(payload, modelId) {
360
+ payload.conversationState.currentMessage.userInputMessage.modelId = modelId;
361
+ for (const message of payload.conversationState.history ?? []) {
362
+ if (message.userInputMessage)
363
+ message.userInputMessage.modelId = modelId;
364
+ }
365
+ }
366
+ function applyPayloadOrigin(payload, origin) {
367
+ payload.conversationState.currentMessage.userInputMessage.origin = origin;
368
+ for (const message of payload.conversationState.history ?? []) {
369
+ if (message.userInputMessage)
370
+ message.userInputMessage.origin = origin;
371
+ }
372
+ }
373
+ // 检测是否为 Agentic 模式请求
374
+ function isAgenticRequest(model, tools) {
375
+ const lower = model.toLowerCase();
376
+ // 模型名称包含 -agentic 或有工具调用
377
+ return lower.includes('-agentic') || lower.includes('agentic') || Boolean(tools && tools.length > 0);
378
+ }
379
+ // 检测是否启用 Thinking 模式
380
+ function isThinkingEnabled(headers) {
381
+ if (!headers)
382
+ return false;
383
+ // 检查 Anthropic-Beta 头是否包含 thinking
384
+ const betaHeader = headers['anthropic-beta'] || headers['Anthropic-Beta'] || '';
385
+ return betaHeader.toLowerCase().includes('thinking');
386
+ }
387
+ // 注入系统提示
388
+ function injectSystemPrompts(content, isAgentic, thinkingEnabled) {
389
+ let result = content;
390
+ // 注入时间戳
391
+ const timestamp = new Date().toISOString();
392
+ const timestampPrompt = `Current time: ${timestamp}`;
393
+ // 注入 Thinking 模式(必须在最前面)
394
+ if (thinkingEnabled) {
395
+ result = THINKING_MODE_PROMPT + '\n\n' + result;
396
+ }
397
+ // 注入 Agentic 模式提示
398
+ if (isAgentic) {
399
+ result = result + '\n\n' + AGENTIC_SYSTEM_PROMPT;
400
+ }
401
+ // 注入时间戳
402
+ result = timestampPrompt + '\n\n' + result;
403
+ return result;
404
+ }
405
+ // ============= 消息清理逻辑(参考 Kiro 官方实现)=============
406
+ // 占位消息
407
+ const HELLO_MESSAGE = {
408
+ userInputMessage: { content: 'Hello', origin: 'AI_EDITOR' }
409
+ };
410
+ const CONTINUE_MESSAGE = {
411
+ userInputMessage: { content: 'Continue', origin: 'AI_EDITOR' }
412
+ };
413
+ const UNDERSTOOD_MESSAGE = {
414
+ assistantResponseMessage: { content: 'understood' }
415
+ };
416
+ // 创建失败的工具结果消息
417
+ function createFailedToolUseMessage(toolUseIds) {
418
+ return {
419
+ userInputMessage: {
420
+ content: '',
421
+ origin: 'AI_EDITOR',
422
+ userInputMessageContext: {
423
+ toolResults: toolUseIds.map(createFailedToolResult)
424
+ }
425
+ }
426
+ };
427
+ }
428
+ // 类型检查函数
429
+ function isUserInputMessage(message) {
430
+ return message != null && 'userInputMessage' in message && message.userInputMessage != null;
431
+ }
432
+ function isAssistantResponseMessage(message) {
433
+ return message != null && 'assistantResponseMessage' in message && message.assistantResponseMessage != null;
434
+ }
435
+ function hasToolResults(message) {
436
+ return !!(message.userInputMessage?.userInputMessageContext?.toolResults?.length);
437
+ }
438
+ function hasToolUses(message) {
439
+ return !!(message.assistantResponseMessage?.toolUses?.length);
440
+ }
441
+ function hasMatchingToolResults(toolUses, toolResults) {
442
+ if (!toolUses || !toolUses.length)
443
+ return true;
444
+ if (!toolResults || !toolResults.length)
445
+ return false;
446
+ const allToolUsesHaveResults = toolUses.every(toolUse => toolResults.some(result => result.toolUseId === toolUse.toolUseId));
447
+ const allToolResultsHaveUses = toolResults.every(result => toolUses.some(toolUse => result.toolUseId === toolUse.toolUseId));
448
+ return allToolUsesHaveResults && allToolResultsHaveUses;
449
+ }
450
+ function createFailedToolResult(toolUseId) {
451
+ return {
452
+ toolUseId,
453
+ content: [{ text: 'Tool execution failed' }],
454
+ status: 'error'
455
+ };
456
+ }
457
+ function stripInvalidToolResults(message) {
458
+ if (message.userInputMessage?.content?.trim()) {
459
+ return {
460
+ userInputMessage: {
461
+ ...message.userInputMessage,
462
+ userInputMessageContext: undefined
463
+ }
464
+ };
465
+ }
466
+ return null;
467
+ }
468
+ // 确保以 user 消息开始
469
+ function ensureStartsWithUserMessage(messages) {
470
+ if (messages.length === 0 || isUserInputMessage(messages[0])) {
471
+ return messages;
472
+ }
473
+ return [HELLO_MESSAGE, ...messages];
474
+ }
475
+ // 确保以 user 消息结束
476
+ function ensureEndsWithUserMessage(messages) {
477
+ if (messages.length === 0)
478
+ return [HELLO_MESSAGE];
479
+ if (isUserInputMessage(messages[messages.length - 1]))
480
+ return messages;
481
+ return [...messages, CONTINUE_MESSAGE];
482
+ }
483
+ // 确保消息交替
484
+ function ensureAlternatingMessages(messages) {
485
+ if (messages.length <= 1)
486
+ return messages;
487
+ const result = [messages[0]];
488
+ for (let i = 1; i < messages.length; i++) {
489
+ const prevMessage = result[result.length - 1];
490
+ const currentMessage = messages[i];
491
+ if (isUserInputMessage(prevMessage) && isUserInputMessage(currentMessage)) {
492
+ result.push(UNDERSTOOD_MESSAGE);
493
+ }
494
+ else if (isAssistantResponseMessage(prevMessage) && isAssistantResponseMessage(currentMessage)) {
495
+ result.push(CONTINUE_MESSAGE);
496
+ }
497
+ result.push(currentMessage);
498
+ }
499
+ return result;
500
+ }
501
+ function relocateToolResultMessages(messages) {
502
+ const assistantToolUseIndexes = [];
503
+ const toolResultIndexById = new Map();
504
+ for (let i = 0; i < messages.length; i++) {
505
+ const message = messages[i];
506
+ if (isAssistantResponseMessage(message) && hasToolUses(message)) {
507
+ assistantToolUseIndexes.push(i);
508
+ }
509
+ else if (isUserInputMessage(message) && hasToolResults(message)) {
510
+ for (const toolResult of message.userInputMessage?.userInputMessageContext?.toolResults ?? []) {
511
+ if (toolResult.toolUseId && !toolResultIndexById.has(toolResult.toolUseId)) {
512
+ toolResultIndexById.set(toolResult.toolUseId, i);
513
+ }
514
+ }
515
+ }
516
+ }
517
+ if (assistantToolUseIndexes.length === 0)
518
+ return messages;
519
+ const result = [];
520
+ const usedIndexes = new Set();
521
+ for (let i = 0; i < messages.length; i++) {
522
+ if (usedIndexes.has(i))
523
+ continue;
524
+ const message = messages[i];
525
+ result.push(message);
526
+ usedIndexes.add(i);
527
+ if (isAssistantResponseMessage(message) && hasToolUses(message)) {
528
+ for (const toolUse of message.assistantResponseMessage?.toolUses ?? []) {
529
+ const toolResultIndex = toolResultIndexById.get(toolUse.toolUseId);
530
+ if (toolResultIndex !== undefined && toolResultIndex !== i + 1 && !usedIndexes.has(toolResultIndex)) {
531
+ const toolResultMessage = messages[toolResultIndex];
532
+ if (toolResultMessage) {
533
+ result.push(toolResultMessage);
534
+ usedIndexes.add(toolResultIndex);
535
+ }
536
+ }
537
+ }
538
+ }
539
+ }
540
+ return result;
541
+ }
542
+ function removeInvalidToolResultMessages(messages) {
543
+ const result = [];
544
+ for (let i = 0; i < messages.length; i++) {
545
+ const message = messages[i];
546
+ const previousMessage = i > 0 ? messages[i - 1] : null;
547
+ if (!isUserInputMessage(message) || !hasToolResults(message)) {
548
+ result.push(message);
549
+ continue;
550
+ }
551
+ if (!previousMessage || !isAssistantResponseMessage(previousMessage) || !hasToolUses(previousMessage)) {
552
+ const stripped = stripInvalidToolResults(message);
553
+ if (stripped)
554
+ result.push(stripped);
555
+ continue;
556
+ }
557
+ const validToolUseIds = new Set((previousMessage.assistantResponseMessage?.toolUses ?? []).map(toolUse => toolUse.toolUseId).filter(Boolean));
558
+ const seenToolUseIds = new Set();
559
+ const toolResults = message.userInputMessage?.userInputMessageContext?.toolResults ?? [];
560
+ const filteredToolResults = toolResults.filter(toolResult => {
561
+ if (!toolResult.toolUseId || !validToolUseIds.has(toolResult.toolUseId) || seenToolUseIds.has(toolResult.toolUseId))
562
+ return false;
563
+ seenToolUseIds.add(toolResult.toolUseId);
564
+ return true;
565
+ });
566
+ if (filteredToolResults.length === toolResults.length) {
567
+ result.push(message);
568
+ }
569
+ else if (filteredToolResults.length > 0) {
570
+ result.push({
571
+ userInputMessage: {
572
+ ...message.userInputMessage,
573
+ userInputMessageContext: {
574
+ ...message.userInputMessage.userInputMessageContext,
575
+ toolResults: filteredToolResults
576
+ }
577
+ }
578
+ });
579
+ }
580
+ else {
581
+ const stripped = stripInvalidToolResults(message);
582
+ if (stripped)
583
+ result.push(stripped);
584
+ }
585
+ }
586
+ return result;
587
+ }
588
+ // 确保工具调用有对应结果
589
+ function ensureValidToolUsesAndResults(messages) {
590
+ const result = [];
591
+ for (let i = 0; i < messages.length; i++) {
592
+ const message = messages[i];
593
+ result.push(message);
594
+ if (isAssistantResponseMessage(message) && hasToolUses(message)) {
595
+ const nextMessage = i + 1 < messages.length ? messages[i + 1] : null;
596
+ const toolUses = message.assistantResponseMessage?.toolUses ?? [];
597
+ const toolUseIds = toolUses.map((tu, idx) => tu.toolUseId ?? `toolUse_${idx + 1}`);
598
+ if (!nextMessage || !isUserInputMessage(nextMessage) || !hasToolResults(nextMessage)) {
599
+ // 没有对应的工具结果,添加失败消息
600
+ result.push(createFailedToolUseMessage(toolUseIds));
601
+ }
602
+ else if (!hasMatchingToolResults(message.assistantResponseMessage?.toolUses, nextMessage.userInputMessage?.userInputMessageContext?.toolResults) && !messages.some((candidate, index) => (index !== i
603
+ && isAssistantResponseMessage(candidate)
604
+ && hasToolUses(candidate)
605
+ && hasMatchingToolResults(candidate.assistantResponseMessage?.toolUses, nextMessage.userInputMessage?.userInputMessageContext?.toolResults)))) {
606
+ // 工具结果不匹配,添加失败消息
607
+ const existingToolResults = nextMessage.userInputMessage?.userInputMessageContext?.toolResults ?? [];
608
+ const validToolUseIds = new Set(toolUseIds);
609
+ const usedToolUseIds = new Set();
610
+ const completedToolResults = existingToolResults.filter(toolResult => {
611
+ if (!toolResult.toolUseId || !validToolUseIds.has(toolResult.toolUseId) || usedToolUseIds.has(toolResult.toolUseId))
612
+ return false;
613
+ usedToolUseIds.add(toolResult.toolUseId);
614
+ return true;
615
+ });
616
+ for (const toolUseId of toolUseIds) {
617
+ if (!usedToolUseIds.has(toolUseId))
618
+ completedToolResults.push(createFailedToolResult(toolUseId));
619
+ }
620
+ result.push({
621
+ userInputMessage: {
622
+ ...nextMessage.userInputMessage,
623
+ userInputMessageContext: {
624
+ ...nextMessage.userInputMessage.userInputMessageContext,
625
+ toolResults: completedToolResults
626
+ }
627
+ }
628
+ });
629
+ i++;
630
+ }
631
+ }
632
+ }
633
+ return result;
634
+ }
635
+ // 移除空的 user 消息
636
+ function removeEmptyUserMessages(messages) {
637
+ if (messages.length <= 1)
638
+ return messages;
639
+ const firstUserMessageIndex = messages.findIndex(isUserInputMessage);
640
+ return messages.filter((message, index) => {
641
+ if (isAssistantResponseMessage(message))
642
+ return true;
643
+ if (isUserInputMessage(message) && index === firstUserMessageIndex)
644
+ return true;
645
+ if (isUserInputMessage(message)) {
646
+ const hasContent = message.userInputMessage?.content?.trim() !== '';
647
+ return hasContent || hasToolResults(message);
648
+ }
649
+ return true;
650
+ });
651
+ }
652
+ function validateConversation(messages) {
653
+ const errors = [];
654
+ if (messages.length === 0 || !isUserInputMessage(messages[0])) {
655
+ errors.push('STARTS_WITH_USER_MESSAGE:index=0');
656
+ }
657
+ if (messages.length === 0 || !isUserInputMessage(messages[messages.length - 1])) {
658
+ errors.push(`ENDS_WITH_USER_MESSAGE:index=${Math.max(messages.length - 1, 0)}`);
659
+ }
660
+ for (let i = 1; i < messages.length; i++) {
661
+ const previousMessage = messages[i - 1];
662
+ const currentMessage = messages[i];
663
+ if (isUserInputMessage(previousMessage) && isUserInputMessage(currentMessage)) {
664
+ errors.push(`ALTERNATING_MESSAGES:index=${i}`);
665
+ break;
666
+ }
667
+ if (isAssistantResponseMessage(previousMessage) && isAssistantResponseMessage(currentMessage)) {
668
+ errors.push(`ALTERNATING_MESSAGES:index=${i}`);
669
+ break;
670
+ }
671
+ }
672
+ for (let i = 0; i < messages.length - 1; i++) {
673
+ const message = messages[i];
674
+ const nextMessage = messages[i + 1];
675
+ if (isAssistantResponseMessage(message) && hasToolUses(message) && (!isUserInputMessage(nextMessage) || !hasMatchingToolResults(message.assistantResponseMessage?.toolUses, nextMessage?.userInputMessage?.userInputMessageContext?.toolResults))) {
676
+ errors.push(`TOOL_USES_AND_RESULTS:index=${i + 1}`);
677
+ break;
678
+ }
679
+ if (isAssistantResponseMessage(message) && !hasToolUses(message) && isUserInputMessage(nextMessage) && hasToolResults(nextMessage)) {
680
+ errors.push(`TOOL_RESULTS_AND_NO_USES:index=${i}`);
681
+ break;
682
+ }
683
+ }
684
+ for (let i = 1; i < messages.length; i++) {
685
+ const previousMessage = messages[i - 1];
686
+ const currentMessage = messages[i];
687
+ if (!isAssistantResponseMessage(previousMessage) || !hasToolUses(previousMessage) || !isUserInputMessage(currentMessage) || !hasToolResults(currentMessage))
688
+ continue;
689
+ const toolUseIds = new Set((previousMessage.assistantResponseMessage?.toolUses ?? []).map(toolUse => toolUse.toolUseId).filter(Boolean));
690
+ const seenToolUseIds = new Set();
691
+ const hasInvalidToolResult = (currentMessage.userInputMessage?.userInputMessageContext?.toolResults ?? []).some(toolResult => {
692
+ if (!toolResult.toolUseId || !toolUseIds.has(toolResult.toolUseId) || seenToolUseIds.has(toolResult.toolUseId))
693
+ return true;
694
+ seenToolUseIds.add(toolResult.toolUseId);
695
+ return false;
696
+ });
697
+ if (hasInvalidToolResult) {
698
+ errors.push(`TOOL_RESULTS_ORPHAN_IDS:index=${i}`);
699
+ break;
700
+ }
701
+ }
702
+ for (let i = 0; i < messages.length; i++) {
703
+ const message = messages[i];
704
+ if (isUserInputMessage(message) && !message.userInputMessage?.content?.trim() && !hasToolResults(message)) {
705
+ errors.push(`NON_EMPTY_USER_MESSAGE:index=${i}`);
706
+ break;
707
+ }
708
+ }
709
+ return errors;
710
+ }
711
+ function getToolNames(tools) {
712
+ return new Set(tools.flatMap(tool => 'toolSpecification' in tool ? [tool.toolSpecification.name] : []));
713
+ }
714
+ function stringifyToolInput(input) {
715
+ if (input === undefined)
716
+ return '';
717
+ if (typeof input === 'string')
718
+ return input;
719
+ try {
720
+ return JSON.stringify(input);
721
+ }
722
+ catch {
723
+ return String(input);
724
+ }
725
+ }
726
+ function flattenContent(content, extra) {
727
+ const trimmedContent = content.trim();
728
+ if (!trimmedContent)
729
+ return extra;
730
+ if (!extra)
731
+ return trimmedContent;
732
+ return `${trimmedContent}\n\n${extra}`;
733
+ }
734
+ function formatToolUses(toolUses) {
735
+ return toolUses.map(toolUse => [
736
+ `<tool_use id="${toolUse.toolUseId}" name="${toolUse.name}">`,
737
+ stringifyToolInput(toolUse.input),
738
+ '</tool_use>'
739
+ ].filter(Boolean).join('\n')).join('\n\n');
740
+ }
741
+ function formatToolResults(toolResults) {
742
+ return toolResults.map(toolResult => [
743
+ `<tool_result id="${toolResult.toolUseId}" status="${toolResult.status}">`,
744
+ toolResult.content.map(content => content.text).join('\n'),
745
+ '</tool_result>'
746
+ ].filter(Boolean).join('\n')).join('\n\n');
747
+ }
748
+ function normalizeToolHistory(messages, tools) {
749
+ const toolNames = getToolNames(tools);
750
+ const hasUnknownToolUse = messages.some(message => (message.assistantResponseMessage?.toolUses?.some(toolUse => !toolNames.has(toolUse.name)) ?? false));
751
+ if (!hasUnknownToolUse)
752
+ return messages;
753
+ return messages.map(message => {
754
+ if (message.assistantResponseMessage?.toolUses?.length) {
755
+ return {
756
+ assistantResponseMessage: {
757
+ ...message.assistantResponseMessage,
758
+ content: flattenContent(message.assistantResponseMessage.content, formatToolUses(message.assistantResponseMessage.toolUses)),
759
+ toolUses: undefined
760
+ }
761
+ };
762
+ }
763
+ if (message.userInputMessage?.userInputMessageContext?.toolResults?.length) {
764
+ return {
765
+ userInputMessage: {
766
+ ...message.userInputMessage,
767
+ content: flattenContent(message.userInputMessage.content, formatToolResults(message.userInputMessage.userInputMessageContext.toolResults)),
768
+ userInputMessageContext: {
769
+ ...message.userInputMessage.userInputMessageContext,
770
+ toolResults: undefined
771
+ }
772
+ }
773
+ };
774
+ }
775
+ return message;
776
+ });
777
+ }
778
+ // 清理会话消息(参考 Kiro 官方实现)
779
+ function sanitizeConversation(messages) {
780
+ let sanitized = [...messages];
781
+ sanitized = ensureStartsWithUserMessage(sanitized);
782
+ sanitized = removeEmptyUserMessages(sanitized);
783
+ sanitized = relocateToolResultMessages(sanitized);
784
+ sanitized = removeInvalidToolResultMessages(sanitized);
785
+ sanitized = ensureValidToolUsesAndResults(sanitized);
786
+ sanitized = ensureAlternatingMessages(sanitized);
787
+ sanitized = ensureEndsWithUserMessage(sanitized);
788
+ const validationErrors = validateConversation(sanitized);
789
+ if (validationErrors.length > 0) {
790
+ throw new Error(`Invalid Kiro conversation after sanitization: ${validationErrors.join(', ')}`);
791
+ }
792
+ return sanitized;
793
+ }
794
+ // 按 token 估算成对裁剪 history 最旧消息 (避免后端 CONTENT_LENGTH_EXCEEDS_THRESHOLD)
795
+ // 切点保证不破坏 toolUse↔toolResult 配对:assistant(toolUse) 必须连同后续 user(toolResult) 一起裁
796
+ // 裁剪后用 ensureStartsWithUserMessage 兜底重新规范化
797
+ function trimHistoryByTokens(payload, maxTokens) {
798
+ let history = payload.conversationState.history;
799
+ if (!history || history.length === 0) {
800
+ return { trimmed: 0, finalTokens: estimatePayloadTokens(payload), iterations: 0 };
801
+ }
802
+ let totalTrimmed = 0;
803
+ let iterations = 0;
804
+ let currentTokens = estimatePayloadTokens(payload);
805
+ const MAX_ITERATIONS = 100; // 防止极端情况死循环
806
+ while (currentTokens > maxTokens && history.length >= 4 && iterations < MAX_ITERATIONS) {
807
+ iterations++;
808
+ // 计算安全切点:从 index 0 开始至少裁掉 1 组 (user+assistant),并连带 toolUse/toolResult 配对
809
+ let cutAt = 0;
810
+ while (cutAt < history.length - 2) {
811
+ const msg = history[cutAt];
812
+ // assistant(toolUse) → 下一条 user(toolResult) 必须一起裁,避免配对断裂
813
+ if (isAssistantResponseMessage(msg) && hasToolUses(msg)) {
814
+ cutAt += 2;
815
+ }
816
+ else {
817
+ cutAt += 1;
818
+ }
819
+ if (cutAt >= 2)
820
+ break;
821
+ }
822
+ if (cutAt === 0)
823
+ break; // 无法继续裁剪
824
+ history = history.slice(cutAt);
825
+ totalTrimmed += cutAt;
826
+ // 裁剪后 history 可能以 assistant 起头 → 补 HELLO 重新规范
827
+ history = ensureStartsWithUserMessage(history);
828
+ payload.conversationState.history = history;
829
+ currentTokens = estimatePayloadTokens(payload);
830
+ }
831
+ return { trimmed: totalTrimmed, finalTokens: currentTokens, iterations };
832
+ }
833
+ // ============= 构建 Kiro API 请求负载(参考 Kiro 官方实现)=============
834
+ function buildKiroPayload(content, modelId, origin, history = [], tools = [], toolResults = [], images = [], profileArn, inferenceConfig, messageOptions, additionalModelRequestFields) {
835
+ // 构建当前消息
836
+ const finalContent = content.trim() || (toolResults.length > 0 ? '' : 'Continue');
837
+ const currentUserInputMessage = {
838
+ content: finalContent,
839
+ modelId,
840
+ origin
841
+ };
842
+ if (images.length > 0) {
843
+ currentUserInputMessage.images = images;
844
+ }
845
+ if (messageOptions?.documents?.length) {
846
+ currentUserInputMessage.documents = messageOptions.documents;
847
+ }
848
+ if (messageOptions?.cachePoint) {
849
+ currentUserInputMessage.cachePoint = messageOptions.cachePoint;
850
+ }
851
+ if (messageOptions?.clientCacheConfig !== undefined) {
852
+ currentUserInputMessage.clientCacheConfig = messageOptions.clientCacheConfig;
853
+ }
854
+ // 构建 userInputMessageContext(包含 tools 和 toolResults)
855
+ // 注意:tools 只放在最后一条消息(currentMessage)的 userInputMessageContext 中
856
+ if (tools.length > 0 || toolResults.length > 0) {
857
+ currentUserInputMessage.userInputMessageContext = {};
858
+ if (tools.length > 0) {
859
+ currentUserInputMessage.userInputMessageContext.tools = tools;
860
+ }
861
+ if (toolResults.length > 0) {
862
+ currentUserInputMessage.userInputMessageContext.toolResults = toolResults;
863
+ }
864
+ }
865
+ if (messageOptions?.context) {
866
+ currentUserInputMessage.userInputMessageContext = {
867
+ ...currentUserInputMessage.userInputMessageContext,
868
+ ...(messageOptions.context.editorState !== undefined ? { editorState: messageOptions.context.editorState } : {}),
869
+ ...(messageOptions.context.shellState !== undefined ? { shellState: messageOptions.context.shellState } : {}),
870
+ ...(messageOptions.context.gitState !== undefined ? { gitState: messageOptions.context.gitState } : {}),
871
+ ...(messageOptions.context.envState !== undefined ? { envState: messageOptions.context.envState } : {}),
872
+ ...(messageOptions.context.additionalContext !== undefined ? { additionalContext: messageOptions.context.additionalContext } : {})
873
+ };
874
+ }
875
+ // 构建 currentMessage
876
+ const currentMessage = {
877
+ userInputMessage: currentUserInputMessage
878
+ };
879
+ // 清理并准备所有消息(history + currentMessage)
880
+ const allMessages = [...history, currentMessage];
881
+ const sanitizedMessages = sanitizeConversation(normalizeToolHistory(allMessages, tools));
882
+ // 分离 history 和 currentMessage
883
+ // currentMessage 是最后一条消息,history 是其余的
884
+ const sanitizedHistory = sanitizedMessages.slice(0, -1);
885
+ let finalCurrentMessage = sanitizedMessages.at(-1);
886
+ // 确保 currentMessage 是 user 消息(sanitizeConversation 保证以 user 消息结束)
887
+ // 并确保包含 tools
888
+ if (!finalCurrentMessage.userInputMessage) {
889
+ // 如果清理后最后一条不是 user 消息,创建一个新的
890
+ finalCurrentMessage = {
891
+ userInputMessage: {
892
+ content: finalContent || 'Continue',
893
+ modelId,
894
+ origin
895
+ }
896
+ };
897
+ }
898
+ finalCurrentMessage.userInputMessage.userInputMessageContext = {
899
+ ...finalCurrentMessage.userInputMessage.userInputMessageContext,
900
+ ...(tools.length > 0 ? { tools } : {})
901
+ };
902
+ // conversationId 稳定化:同一会话的多轮请求复用同一个 conversationId
903
+ // 优先级:客户端显式 conversation_id → sessionHint(header 提取)→ history fingerprint → 新 UUID
904
+ const conversationId = resolveConversationId(history, messageOptions?.conversationId);
905
+ const payload = {
906
+ conversationState: {
907
+ agentContinuationId: (0, uuid_1.v4)(),
908
+ agentTaskType: 'vibe',
909
+ chatTriggerType: 'MANUAL',
910
+ conversationId,
911
+ currentMessage: {
912
+ userInputMessage: finalCurrentMessage.userInputMessage
913
+ },
914
+ history: sanitizedHistory.length > 0 ? sanitizedHistory : undefined
915
+ }
916
+ };
917
+ if (profileArn !== undefined) {
918
+ payload.profileArn = profileArn;
919
+ }
920
+ if (inferenceConfig && (inferenceConfig.maxTokens || inferenceConfig.temperature !== undefined || inferenceConfig.topP !== undefined)) {
921
+ payload.inferenceConfig = {};
922
+ if (inferenceConfig.maxTokens) {
923
+ payload.inferenceConfig.maxTokens = inferenceConfig.maxTokens;
924
+ }
925
+ if (inferenceConfig.temperature !== undefined) {
926
+ payload.inferenceConfig.temperature = inferenceConfig.temperature;
927
+ }
928
+ if (inferenceConfig.topP !== undefined) {
929
+ payload.inferenceConfig.topP = inferenceConfig.topP;
930
+ }
931
+ }
932
+ // additionalModelRequestFields(thinking 等模型级参数)
933
+ if (additionalModelRequestFields && Object.keys(additionalModelRequestFields).length > 0) {
934
+ payload.additionalModelRequestFields = additionalModelRequestFields;
935
+ }
936
+ // ====== 第一阶段:按 token 估算成对裁剪旧 history ======
937
+ // 避免 Kiro 后端 CONTENT_LENGTH_EXCEEDS_THRESHOLD(token 维度的拒绝)
938
+ // 注意:byte size 充足但 token 超限是常见情况(长对话+大量小消息)
939
+ // effectiveLimit 按模型 context window 自动算:ctx - tokenBufferReserve(开关启用时,默认 20K)
940
+ // 例:sonnet-4.5 (200K) → 180K, sonnet-4.5 with 1M beta → 980K
941
+ // 开关关闭时完全跳过,超出 context window 由 Kiro 后端原样返回错误
942
+ if (enableTokenBufferReserve) {
943
+ const effectiveTokenLimit = getEffectiveTokenLimit(modelId);
944
+ const tokenTrimResult = trimHistoryByTokens(payload, effectiveTokenLimit);
945
+ if (tokenTrimResult.trimmed > 0) {
946
+ const modelCtx = (0, tokenCounter_1.getModelContextLength)(modelId);
947
+ console.log(`[KiroPayload] Trimmed ${tokenTrimResult.trimmed} oldest history messages by token estimate (≈${tokenTrimResult.finalTokens.toLocaleString()} / ${effectiveTokenLimit.toLocaleString()} tokens [model ctx ${modelCtx.toLocaleString()} - buffer ${tokenBufferReserve.toLocaleString()}], ${tokenTrimResult.iterations} iter)`);
948
+ }
949
+ }
950
+ // ====== 第二阶段:按 byte 截断 tool result 内容 ======
951
+ // 避免 HTTP body 过大被 Kiro 网关拒绝
952
+ // 用户可在高级设置中调整限制值(默认 1536KB = 1.5MB)
953
+ const PAYLOAD_SIZE_LIMIT = (payloadSizeLimitKB || 1536) * 1024;
954
+ const TOOL_RESULT_TRUNCATE_LENGTH = 4000;
955
+ let initialPayloadSize = JSON.stringify(payload).length;
956
+ if (initialPayloadSize > PAYLOAD_SIZE_LIMIT && payload.conversationState.history) {
957
+ const historyMessages = payload.conversationState.history;
958
+ let truncatedCount = 0;
959
+ for (const message of historyMessages) {
960
+ if (initialPayloadSize <= PAYLOAD_SIZE_LIMIT)
961
+ break;
962
+ const userToolResults = message.userInputMessage?.userInputMessageContext?.toolResults;
963
+ if (!userToolResults)
964
+ continue;
965
+ for (const toolResult of userToolResults) {
966
+ if (initialPayloadSize <= PAYLOAD_SIZE_LIMIT)
967
+ break;
968
+ if (!toolResult.content)
969
+ continue;
970
+ for (const contentItem of toolResult.content) {
971
+ if (initialPayloadSize <= PAYLOAD_SIZE_LIMIT)
972
+ break;
973
+ if (contentItem.text && contentItem.text.length > TOOL_RESULT_TRUNCATE_LENGTH) {
974
+ const originalLen = contentItem.text.length;
975
+ contentItem.text = `${contentItem.text.slice(0, TOOL_RESULT_TRUNCATE_LENGTH)}\n\n[Truncated by proxy: original ${originalLen} chars]`;
976
+ truncatedCount++;
977
+ initialPayloadSize = JSON.stringify(payload).length;
978
+ }
979
+ }
980
+ }
981
+ }
982
+ if (truncatedCount > 0) {
983
+ console.log(`[KiroPayload] Truncated ${truncatedCount} large tool results to fit payload size limit (final size: ${initialPayloadSize} bytes)`);
984
+ }
985
+ }
986
+ // 调试日志
987
+ console.log(`[KiroPayload] Built payload (native history mode):`, {
988
+ contentLength: finalContent.length,
989
+ originalHistoryLength: history.length,
990
+ sanitizedHistoryLength: sanitizedHistory.length,
991
+ toolsCount: tools.length,
992
+ toolResultsCount: toolResults.length,
993
+ hasProfileArn: payload.profileArn !== undefined,
994
+ hasThinking: !!additionalModelRequestFields?.thinking,
995
+ payloadSize: initialPayloadSize
996
+ });
997
+ return payload;
998
+ }
999
+ // conversationId 稳定化:同一会话的多轮请求复用同一个 conversationId
1000
+ // 策略:sessionHint(由 proxyServer 从 header/body 提取)→ 稳定映射到固定 conversationId
1001
+ // 无 sessionHint 时用 history fingerprint 兜底
1002
+ const conversationCache = new Map();
1003
+ const CONVERSATION_CACHE_TTL = 2 * 60 * 60 * 1000; // 2 小时
1004
+ const CONVERSATION_CACHE_MAX = 1000;
1005
+ function resolveConversationId(history, sessionHint) {
1006
+ // sessionHint 已包含 API Key hash 前缀(由 proxyServer 注入),天然隔离不同用户
1007
+ const key = sessionHint || fingerprintFromHistory(history);
1008
+ if (!key)
1009
+ return (0, uuid_1.v4)();
1010
+ const now = Date.now();
1011
+ const cached = conversationCache.get(key);
1012
+ if (cached) {
1013
+ cached.timestamp = now;
1014
+ return cached.id;
1015
+ }
1016
+ // 清理过期缓存
1017
+ if (conversationCache.size > CONVERSATION_CACHE_MAX) {
1018
+ const cutoff = now - CONVERSATION_CACHE_TTL;
1019
+ for (const [k, v] of conversationCache) {
1020
+ if (v.timestamp < cutoff)
1021
+ conversationCache.delete(k);
1022
+ }
1023
+ }
1024
+ const id = (0, uuid_1.v4)();
1025
+ conversationCache.set(key, { id, timestamp: now });
1026
+ return id;
1027
+ }
1028
+ function fingerprintFromHistory(history) {
1029
+ if (history.length === 0)
1030
+ return undefined;
1031
+ const fp = history.slice(0, 2).map(msg => `${msg.userInputMessage?.content || ''}|${msg.assistantResponseMessage?.content || ''}`).join('::');
1032
+ const crypto = require('crypto');
1033
+ return crypto.createHash('sha256').update(fp).digest('hex').slice(0, 32);
1034
+ }
1035
+ // 清除所有内存缓存
1036
+ function clearAllCaches() {
1037
+ const conversationCount = conversationCache.size;
1038
+ const modelCount = codeWhispererModelCache.size;
1039
+ conversationCache.clear();
1040
+ codeWhispererModelCache.clear();
1041
+ return { conversation: conversationCount, model: modelCount };
1042
+ }
1043
+ // machineId 稳定生成缓存(用于无绑定 machineId 且 K-Proxy 不可用时的兆底)
1044
+ const fallbackMachineIds = new Map();
1045
+ function generateStableMachineId(accountId) {
1046
+ const cached = fallbackMachineIds.get(accountId);
1047
+ if (cached)
1048
+ return cached;
1049
+ const crypto = require('crypto');
1050
+ const hash = crypto.createHash('sha256').update(`kiro-device-${accountId}`).digest('hex');
1051
+ fallbackMachineIds.set(accountId, hash);
1052
+ return hash;
1053
+ }
1054
+ // 获取账号绑定的 Machine ID(保证永远不为空)
1055
+ function getAccountMachineId(accountId, accountMachineId) {
1056
+ if (accountMachineId)
1057
+ return accountMachineId;
1058
+ const kproxyService = (0, kproxy_1.getKProxyService)();
1059
+ if (kproxyService) {
1060
+ const deviceId = kproxyService.getDeviceIdForAccount(accountId);
1061
+ if (deviceId)
1062
+ return deviceId;
1063
+ }
1064
+ return generateStableMachineId(accountId);
1065
+ }
1066
+ // 获取认证方式对应的请求头
1067
+ function getAuthHeaders(account, _endpoint, agentMode = 'vibe') {
1068
+ const machineId = getAccountMachineId(account.id, account.machineId);
1069
+ // authMethod controls token refresh only. Official Kiro IDE uses its IDE
1070
+ // identity for Builder ID, social, and enterprise requests alike.
1071
+ const headers = {
1072
+ 'content-type': 'application/json',
1073
+ 'x-amzn-kiro-agent-mode': agentMode,
1074
+ 'x-amz-user-agent': getKiroAmzUserAgent(machineId),
1075
+ 'user-agent': getKiroUserAgent(machineId),
1076
+ 'amz-sdk-invocation-id': (0, uuid_1.v4)(),
1077
+ 'amz-sdk-request': 'attempt=1; max=3',
1078
+ 'Authorization': `Bearer ${account.accessToken}`
1079
+ };
1080
+ if (account.authMethod === 'external_idp')
1081
+ headers.TokenType = 'EXTERNAL_IDP';
1082
+ return headers;
1083
+ }
1084
+ // 获取排序后的端点列表(根据首选端点配置)
1085
+ function getSortedEndpoints(preferredEndpoint) {
1086
+ if (!preferredEndpoint)
1087
+ return KIRO_ENDPOINTS.filter(ep => ep.name !== 'AmazonQCLI');
1088
+ // AmazonQ CLI 模式:只用这一个端点,失败不回退
1089
+ if (preferredEndpoint === 'amazonq-cli') {
1090
+ return KIRO_ENDPOINTS.filter(ep => ep.name === 'AmazonQCLI');
1091
+ }
1092
+ const preferredName = preferredEndpoint === 'codewhisperer' ? 'CodeWhisperer' : 'AmazonQ';
1093
+ const sorted = KIRO_ENDPOINTS.filter(ep => ep.name !== 'AmazonQCLI');
1094
+ sorted.sort((a, b) => {
1095
+ if (a.name === preferredName)
1096
+ return -1;
1097
+ if (b.name === preferredName)
1098
+ return 1;
1099
+ return 0;
1100
+ });
1101
+ return sorted;
1102
+ }
1103
+ function getAbortError(signal) {
1104
+ if (signal?.reason instanceof Error)
1105
+ return signal.reason;
1106
+ if (signal?.reason)
1107
+ return new Error(String(signal.reason));
1108
+ return new Error('Request aborted');
1109
+ }
1110
+ function throwIfAborted(signal) {
1111
+ if (signal?.aborted)
1112
+ throw getAbortError(signal);
1113
+ }
1114
+ function waitForRetry(ms, signal) {
1115
+ throwIfAborted(signal);
1116
+ return new Promise((resolve, reject) => {
1117
+ const timeout = setTimeout(() => {
1118
+ signal?.removeEventListener('abort', abort);
1119
+ resolve();
1120
+ }, ms);
1121
+ const abort = () => {
1122
+ clearTimeout(timeout);
1123
+ reject(getAbortError(signal));
1124
+ };
1125
+ signal?.addEventListener('abort', abort, { once: true });
1126
+ });
1127
+ }
1128
+ function parseRetryAfterMs(headers) {
1129
+ const value = headers.get('retry-after');
1130
+ if (!value)
1131
+ return undefined;
1132
+ const seconds = Number(value);
1133
+ if (Number.isFinite(seconds) && seconds > 0) {
1134
+ return Math.min(15000, Math.max(500, Math.ceil(seconds * 1000)));
1135
+ }
1136
+ const dateMs = Date.parse(value);
1137
+ if (Number.isFinite(dateMs) && dateMs > Date.now()) {
1138
+ return Math.min(15000, Math.max(500, dateMs - Date.now()));
1139
+ }
1140
+ return undefined;
1141
+ }
1142
+ // 调用 Kiro API(流式)
1143
+ async function callKiroApiStream(account, payload, onChunk, onComplete, onError, signal, preferredEndpoint) {
1144
+ const endpoints = getSortedEndpoints(preferredEndpoint);
1145
+ let lastError = null;
1146
+ for (const endpoint of endpoints) {
1147
+ try {
1148
+ throwIfAborted(signal);
1149
+ const requestPayload = clonePayload(payload);
1150
+ // Builder ID streaming behavior differs across Kiro backends. Current
1151
+ // endpoints require the placeholder ARN, while some older deployments
1152
+ // reject it with 403. Send it first, then retry once without it on 403.
1153
+ const resolvedArn = resolveProfileArn(account);
1154
+ if (resolvedArn) {
1155
+ requestPayload.profileArn = resolvedArn;
1156
+ }
1157
+ else {
1158
+ delete requestPayload.profileArn;
1159
+ }
1160
+ const requestedModelId = getPayloadModelId(requestPayload);
1161
+ if (endpoint.name === 'CodeWhisperer') {
1162
+ applyPayloadModelId(requestPayload, await resolveCodeWhispererModelId(account, requestedModelId, signal));
1163
+ }
1164
+ applyPayloadOrigin(requestPayload, endpoint.origin);
1165
+ // AmazonQCLI 端点不支持 agentContinuationId/agentTaskType
1166
+ if (endpoint.name === 'AmazonQCLI') {
1167
+ delete requestPayload.conversationState.agentContinuationId;
1168
+ delete requestPayload.conversationState.agentTaskType;
1169
+ }
1170
+ let payloadStr = JSON.stringify(requestPayload);
1171
+ const headers = getAuthHeaders(account, endpoint, requestPayload.conversationState.agentTaskType || 'vibe');
1172
+ const currentUserInput = requestPayload.conversationState.currentMessage.userInputMessage;
1173
+ const historyMessages = requestPayload.conversationState.history ?? [];
1174
+ const historyToolUseCount = historyMessages.reduce((count, message) => count + (message.assistantResponseMessage?.toolUses?.length ?? 0), 0);
1175
+ const historyToolResultCount = historyMessages.reduce((count, message) => count + (message.userInputMessage?.userInputMessageContext?.toolResults?.length ?? 0), 0);
1176
+ console.log(`[KiroAPI] Request to ${endpoint.name}:`);
1177
+ console.log(`[KiroAPI] - Content length: ${currentUserInput?.content?.length || 0}`);
1178
+ console.log(`[KiroAPI] - Tools count: ${currentUserInput?.userInputMessageContext?.tools?.length || 0}`);
1179
+ console.log(`[KiroAPI] - Current tool results: ${currentUserInput?.userInputMessageContext?.toolResults?.length || 0}`);
1180
+ console.log(`[KiroAPI] - History messages: ${historyMessages.length}`);
1181
+ console.log(`[KiroAPI] - History tool uses/results: ${historyToolUseCount}/${historyToolResultCount}`);
1182
+ console.log(`[KiroAPI] - Model ID: ${currentUserInput?.modelId || 'default'}`);
1183
+ console.log(`[KiroAPI] - Has profileArn: ${requestPayload.profileArn !== undefined}`);
1184
+ console.log(`[KiroAPI] - Agent mode: ${headers['x-amzn-kiro-agent-mode']}`);
1185
+ console.log(`[KiroAPI] - Payload size: ${payloadStr.length} bytes`);
1186
+ const agent = getNetworkAgent(account);
1187
+ if (agent)
1188
+ logger_1.proxyLogger.debug('KiroAPI', `Stream request via proxy to ${endpoint.name}`);
1189
+ const sendRequest = async () => agent
1190
+ ? await (0, undici_1.fetch)(endpoint.url, { method: 'POST', headers, body: payloadStr, signal, dispatcher: agent })
1191
+ : await fetch(endpoint.url, { method: 'POST', headers, body: payloadStr, signal });
1192
+ let response = await sendRequest();
1193
+ if (response.status === 403 && (0, exports.isPlaceholderProfileArn)(requestPayload.profileArn)) {
1194
+ const originalStatus = response.status;
1195
+ const originalBody = await response.text().catch(() => '');
1196
+ throwIfAborted(signal);
1197
+ console.log(`[KiroAPI] Endpoint ${endpoint.name} rejected Builder ID placeholder profileArn, retrying without it...`);
1198
+ delete requestPayload.profileArn;
1199
+ payloadStr = JSON.stringify(requestPayload);
1200
+ response = await sendRequest();
1201
+ if (!response.ok) {
1202
+ // Current Kiro endpoints require profileArn. Preserve the original
1203
+ // authorization failure instead of masking it as "profileArn is required".
1204
+ await response.text().catch(() => '');
1205
+ throw new Error(`Auth error ${originalStatus}: ${originalBody}`);
1206
+ }
1207
+ }
1208
+ if (response.status === 429) {
1209
+ const retryDelayMs = parseRetryAfterMs(response.headers) ?? 2500;
1210
+ const firstBody = await response.text().catch(() => '');
1211
+ console.log(`[KiroAPI] Endpoint ${endpoint.name} rate limited, retrying once after ${retryDelayMs}ms...`);
1212
+ await waitForRetry(retryDelayMs, signal);
1213
+ response = await sendRequest();
1214
+ if (response.status === 429) {
1215
+ const body = await response.text().catch(() => firstBody);
1216
+ console.log(`[KiroAPI] Endpoint ${endpoint.name} rate limited, trying next...`);
1217
+ lastError = new Error(`Endpoint rate limited on ${endpoint.name} (429)${body ? `: ${body}` : ''}`);
1218
+ continue;
1219
+ }
1220
+ }
1221
+ if (response.status === 401 || response.status === 403) {
1222
+ throwIfAborted(signal);
1223
+ const body = await response.text();
1224
+ throwIfAborted(signal);
1225
+ throw new Error(`Auth error ${response.status}: ${body}`);
1226
+ }
1227
+ if (!response.ok) {
1228
+ throwIfAborted(signal);
1229
+ const body = await response.text();
1230
+ throwIfAborted(signal);
1231
+ throw new Error(`API error ${response.status}: ${body}`);
1232
+ }
1233
+ // 解析 Event Stream
1234
+ // 传入 modelId + payloadStr 用于精确 token 计算(contextUsage 反推 + tiktoken)
1235
+ const inputChars = payloadStr.length;
1236
+ await parseEventStream(response.body, onChunk, onComplete, onError, inputChars, signal, requestedModelId, payloadStr);
1237
+ return;
1238
+ }
1239
+ catch (error) {
1240
+ if (signal?.aborted) {
1241
+ onError(getAbortError(signal));
1242
+ return;
1243
+ }
1244
+ lastError = error;
1245
+ console.error(`[KiroAPI] Endpoint ${endpoint.name} failed:`, error instanceof Error ? error.message : String(error));
1246
+ // 如果是认证错误,不继续尝试其他端点
1247
+ if (error.message.includes('Auth error')) {
1248
+ onError(error);
1249
+ return;
1250
+ }
1251
+ }
1252
+ }
1253
+ if (lastError) {
1254
+ onError(lastError);
1255
+ }
1256
+ }
1257
+ // 从 headers 中提取 event type
1258
+ function extractEventType(headers) {
1259
+ let offset = 0;
1260
+ while (offset < headers.length) {
1261
+ if (offset >= headers.length)
1262
+ break;
1263
+ const nameLen = headers[offset];
1264
+ offset++;
1265
+ if (offset + nameLen > headers.length)
1266
+ break;
1267
+ const name = new TextDecoder().decode(headers.slice(offset, offset + nameLen));
1268
+ offset += nameLen;
1269
+ if (offset >= headers.length)
1270
+ break;
1271
+ const valueType = headers[offset];
1272
+ offset++;
1273
+ if (valueType === 7) { // String type
1274
+ if (offset + 2 > headers.length)
1275
+ break;
1276
+ const valueLen = (headers[offset] << 8) | headers[offset + 1];
1277
+ offset += 2;
1278
+ if (offset + valueLen > headers.length)
1279
+ break;
1280
+ const value = new TextDecoder().decode(headers.slice(offset, offset + valueLen));
1281
+ offset += valueLen;
1282
+ if (name === ':event-type') {
1283
+ return value;
1284
+ }
1285
+ continue;
1286
+ }
1287
+ // Skip other value types
1288
+ const skipSizes = { 0: 0, 1: 0, 2: 1, 3: 2, 4: 4, 5: 8, 8: 8, 9: 16 };
1289
+ if (valueType === 6) {
1290
+ if (offset + 2 > headers.length)
1291
+ break;
1292
+ const len = (headers[offset] << 8) | headers[offset + 1];
1293
+ offset += 2 + len;
1294
+ }
1295
+ else if (skipSizes[valueType] !== undefined) {
1296
+ offset += skipSizes[valueType];
1297
+ }
1298
+ else {
1299
+ break;
1300
+ }
1301
+ }
1302
+ return '';
1303
+ }
1304
+ // Token 估算(被 promptCacheTracker 等模块使用,用于 cache 块大小判定)
1305
+ // 优先使用 tiktoken cl100k_base 精确计算(±5%),失败时自动降级到字符系数(±15%)
1306
+ function estimateTokens(text) {
1307
+ return (0, tokenCounter_1.countTokens)(text);
1308
+ }
1309
+ // 解析 AWS Event Stream 二进制格式
1310
+ async function parseEventStream(body, onChunk, onComplete, onError, inputChars = 0, // 输入字符长度(兜底估算用)
1311
+ signal, modelId, // 模型 ID,用于 contextUsagePercentage 反推 inputTokens
1312
+ payloadStr // 请求 payload JSON 字符串,用于 tiktoken 精确计算
1313
+ ) {
1314
+ const reader = body.getReader();
1315
+ const abort = () => {
1316
+ reader.cancel(getAbortError(signal)).catch(() => undefined);
1317
+ };
1318
+ let buffer = new Uint8Array(0);
1319
+ let usage = {
1320
+ inputTokens: 0,
1321
+ outputTokens: 0,
1322
+ credits: 0,
1323
+ cacheReadTokens: 0,
1324
+ cacheWriteTokens: 0,
1325
+ reasoningTokens: 0
1326
+ };
1327
+ // 累积输出文本长度,用于估算 tokens
1328
+ let totalOutputChars = 0;
1329
+ // 累积输出文本内容,用于 tiktoken 精确计算 output tokens
1330
+ let collectedOutputText = '';
1331
+ // 是否已拿到 Kiro 真实 tokenUsage(最高优先级,锁定后不再被 contextUsage/tiktoken 覆盖)
1332
+ let hasRealTokenUsage = false;
1333
+ // 流式事件聚合计数(logStreamEvents 开启时,结束后输出摘要而非逐条输出)
1334
+ const streamEventCounts = {};
1335
+ // 初始化 input tokens 估算(优先级链路:tokenUsage > contextUsage 反推 > tiktoken > 字符系数)
1336
+ // 这里只是兜底初值,后续真实事件会覆盖
1337
+ if (payloadStr) {
1338
+ // 用 tiktoken cl100k_base 精确计算(±5%)
1339
+ usage.inputTokens = (0, tokenCounter_1.countTokens)(payloadStr);
1340
+ }
1341
+ else if (inputChars > 0) {
1342
+ // 字符系数兜底(针对 payload JSON 经验值 0.42)
1343
+ usage.inputTokens = Math.max(1, Math.round(inputChars * 0.42));
1344
+ }
1345
+ // Tool use 状态跟踪 - 用于累积输入片段
1346
+ let currentToolUse = null;
1347
+ const processedIds = new Set();
1348
+ try {
1349
+ throwIfAborted(signal);
1350
+ signal?.addEventListener('abort', abort, { once: true });
1351
+ while (true) {
1352
+ throwIfAborted(signal);
1353
+ const { done, value } = await reader.read();
1354
+ throwIfAborted(signal);
1355
+ if (done) {
1356
+ break;
1357
+ }
1358
+ // 合并缓冲区
1359
+ const newBuffer = new Uint8Array(buffer.length + value.length);
1360
+ newBuffer.set(buffer);
1361
+ newBuffer.set(value, buffer.length);
1362
+ buffer = newBuffer;
1363
+ // 尝试解析消息
1364
+ while (buffer.length >= 16) {
1365
+ // AWS Event Stream 格式:
1366
+ // - 4 bytes: total length
1367
+ // - 4 bytes: headers length
1368
+ // - 4 bytes: prelude CRC
1369
+ // - headers
1370
+ // - payload
1371
+ // - 4 bytes: message CRC
1372
+ const totalLength = new DataView(buffer.buffer, buffer.byteOffset).getUint32(0, false);
1373
+ if (buffer.length < totalLength) {
1374
+ break; // 等待更多数据
1375
+ }
1376
+ const headersLength = new DataView(buffer.buffer, buffer.byteOffset).getUint32(4, false);
1377
+ // 从 headers 中提取 event type
1378
+ const headersStart = 12;
1379
+ const headersEnd = 12 + headersLength;
1380
+ const eventType = extractEventType(buffer.slice(headersStart, headersEnd));
1381
+ // 提取 payload
1382
+ const payloadStart = 12 + headersLength;
1383
+ const payloadEnd = totalLength - 4; // 减去 message CRC
1384
+ if (payloadStart < payloadEnd) {
1385
+ const payloadBytes = buffer.slice(payloadStart, payloadEnd);
1386
+ try {
1387
+ const payloadText = new TextDecoder().decode(payloadBytes);
1388
+ const event = JSON.parse(payloadText);
1389
+ // 根据 event type 处理不同类型的事件
1390
+ if (eventType === 'assistantResponseEvent' || event.assistantResponseEvent) {
1391
+ const assistantResp = event.assistantResponseEvent || event;
1392
+ const content = assistantResp.content;
1393
+ if (content) {
1394
+ onChunk(content);
1395
+ // 累积输出字符长度(兜底估算用)
1396
+ totalOutputChars += content.length;
1397
+ // 累积输出文本(tiktoken 精确计算用)
1398
+ collectedOutputText += content;
1399
+ }
1400
+ }
1401
+ // AmazonQ CLI 协议特有:CodeEvent (代码片段流式输出)
1402
+ // 来自 amzn_qdeveloper_streaming_client 的 ChatResponseStream::CodeEvent { content: String }
1403
+ // CodeWhisperer/AmazonQ 端点用 AssistantResponseEvent 包代码,CLI 端点单独用 CodeEvent
1404
+ if (eventType === 'codeEvent' || event.codeEvent) {
1405
+ const codeResp = event.codeEvent || event;
1406
+ const content = codeResp.content;
1407
+ if (content) {
1408
+ onChunk(content);
1409
+ totalOutputChars += content.length;
1410
+ collectedOutputText += content;
1411
+ }
1412
+ }
1413
+ if (eventType === 'toolUseEvent' || event.toolUseEvent) {
1414
+ const toolUseData = event.toolUseEvent || event;
1415
+ const toolUseId = toolUseData.toolUseId;
1416
+ const toolName = toolUseData.name;
1417
+ const isStop = toolUseData.stop === true;
1418
+ // 获取输入 - 可能是字符串片段或完整对象
1419
+ let inputFragment = '';
1420
+ let inputObj = null;
1421
+ if (typeof toolUseData.input === 'string') {
1422
+ inputFragment = toolUseData.input;
1423
+ }
1424
+ else if (typeof toolUseData.input === 'object' && toolUseData.input !== null) {
1425
+ inputObj = toolUseData.input;
1426
+ }
1427
+ // 新的 tool use 开始
1428
+ if (toolUseId && toolName) {
1429
+ if (currentToolUse && currentToolUse.toolUseId !== toolUseId) {
1430
+ // 前一个 tool use 被中断,完成它
1431
+ if (!processedIds.has(currentToolUse.toolUseId)) {
1432
+ let finalInput = {};
1433
+ try {
1434
+ if (currentToolUse.inputBuffer) {
1435
+ finalInput = JSON.parse(currentToolUse.inputBuffer);
1436
+ }
1437
+ }
1438
+ catch { /* 忽略解析错误 */ }
1439
+ onChunk('', {
1440
+ toolUseId: currentToolUse.toolUseId,
1441
+ name: currentToolUse.name,
1442
+ input: finalInput
1443
+ });
1444
+ totalOutputChars += currentToolUse.name.length + currentToolUse.inputBuffer.length;
1445
+ processedIds.add(currentToolUse.toolUseId);
1446
+ }
1447
+ currentToolUse = null;
1448
+ }
1449
+ if (!currentToolUse) {
1450
+ if (processedIds.has(toolUseId)) {
1451
+ // 跳过重复的 tool use
1452
+ }
1453
+ else {
1454
+ currentToolUse = {
1455
+ toolUseId,
1456
+ name: toolName,
1457
+ inputBuffer: ''
1458
+ };
1459
+ }
1460
+ }
1461
+ }
1462
+ // 累积输入片段
1463
+ if (currentToolUse && inputFragment) {
1464
+ currentToolUse.inputBuffer += inputFragment;
1465
+ }
1466
+ // 如果直接提供了完整输入对象
1467
+ if (currentToolUse && inputObj) {
1468
+ currentToolUse.inputBuffer = JSON.stringify(inputObj);
1469
+ }
1470
+ // Tool use 完成
1471
+ if (isStop && currentToolUse) {
1472
+ let finalInput = {};
1473
+ let parseError = false;
1474
+ try {
1475
+ if (currentToolUse.inputBuffer) {
1476
+ if (logStreamEvents)
1477
+ logger_1.proxyLogger.debug('Kiro', 'Tool input buffer: ' + currentToolUse.inputBuffer.substring(0, 200));
1478
+ finalInput = JSON.parse(currentToolUse.inputBuffer);
1479
+ if (logStreamEvents)
1480
+ logger_1.proxyLogger.debug('Kiro', 'Parsed tool input: ' + JSON.stringify(finalInput).substring(0, 200));
1481
+ }
1482
+ }
1483
+ catch (e) {
1484
+ parseError = true;
1485
+ console.error('[Kiro] Failed to parse tool input:', e, 'Buffer:', currentToolUse.inputBuffer?.substring(0, 100));
1486
+ // 当 JSON 解析失败时,创建一个包含错误信息的 input
1487
+ // 这样客户端可以看到工具调用失败的原因
1488
+ finalInput = {
1489
+ _error: 'Tool input truncated by Kiro API (output token limit exceeded)',
1490
+ _partialInput: currentToolUse.inputBuffer?.substring(0, 500) || ''
1491
+ };
1492
+ }
1493
+ // 只有在成功解析或有错误信息时才发送
1494
+ onChunk('', {
1495
+ toolUseId: currentToolUse.toolUseId,
1496
+ name: currentToolUse.name,
1497
+ input: finalInput
1498
+ });
1499
+ totalOutputChars += currentToolUse.name.length + currentToolUse.inputBuffer.length;
1500
+ // 如果解析失败,额外发送一条文本消息告知用户
1501
+ if (parseError) {
1502
+ onChunk(`\n\n⚠️ Tool "${currentToolUse.name}" input was truncated by Kiro API. The output may be incomplete due to token limits.`);
1503
+ }
1504
+ processedIds.add(currentToolUse.toolUseId);
1505
+ currentToolUse = null;
1506
+ }
1507
+ }
1508
+ // 处理 messageMetadataEvent - 包含 token 使用量
1509
+ if (eventType === 'messageMetadataEvent' || eventType === 'metadataEvent' || event.messageMetadataEvent || event.metadataEvent) {
1510
+ const metadata = event.messageMetadataEvent || event.metadataEvent || event;
1511
+ logger_1.proxyLogger.info('Kiro', 'messageMetadataEvent', metadata);
1512
+ // 检查 tokenUsage 对象
1513
+ if (metadata.tokenUsage) {
1514
+ const tokenUsage = metadata.tokenUsage;
1515
+ logger_1.proxyLogger.info('Kiro', 'tokenUsage', tokenUsage);
1516
+ // 计算 inputTokens = uncachedInputTokens + cacheReadInputTokens + cacheWriteInputTokens
1517
+ const uncached = tokenUsage.uncachedInputTokens || 0;
1518
+ const cacheRead = tokenUsage.cacheReadInputTokens || 0;
1519
+ const cacheWrite = tokenUsage.cacheWriteInputTokens || 0;
1520
+ const calculatedInput = uncached + cacheRead + cacheWrite;
1521
+ if (calculatedInput > 0) {
1522
+ usage.inputTokens = calculatedInput;
1523
+ hasRealTokenUsage = true; // 真实值,锁定不再被 contextUsage/tiktoken 覆盖
1524
+ }
1525
+ if (tokenUsage.outputTokens)
1526
+ usage.outputTokens = tokenUsage.outputTokens;
1527
+ if (tokenUsage.totalTokens) {
1528
+ // 如果有 totalTokens,用它来推算
1529
+ if (usage.inputTokens === 0 && usage.outputTokens > 0) {
1530
+ usage.inputTokens = tokenUsage.totalTokens - usage.outputTokens;
1531
+ hasRealTokenUsage = true;
1532
+ }
1533
+ }
1534
+ // 保存 cache tokens
1535
+ usage.cacheReadTokens = cacheRead;
1536
+ usage.cacheWriteTokens = cacheWrite;
1537
+ // 记录上下文使用百分比
1538
+ if (tokenUsage.contextUsagePercentage !== undefined) {
1539
+ logger_1.proxyLogger.info('Kiro', 'Context usage: ' + tokenUsage.contextUsagePercentage.toFixed(2) + '%');
1540
+ }
1541
+ // 详细的 token 分解日志
1542
+ logger_1.proxyLogger.info('Kiro', 'Token breakdown', {
1543
+ uncached,
1544
+ cacheRead,
1545
+ cacheWrite,
1546
+ inputTotal: calculatedInput,
1547
+ output: tokenUsage.outputTokens || 0,
1548
+ total: tokenUsage.totalTokens || 0,
1549
+ contextUsage: tokenUsage.contextUsagePercentage ? `${tokenUsage.contextUsagePercentage.toFixed(2)}%` : 'N/A'
1550
+ });
1551
+ }
1552
+ // 直接在 metadata 中的 tokens
1553
+ if (metadata.inputTokens) {
1554
+ usage.inputTokens = metadata.inputTokens;
1555
+ hasRealTokenUsage = true;
1556
+ }
1557
+ if (metadata.outputTokens)
1558
+ usage.outputTokens = metadata.outputTokens;
1559
+ }
1560
+ if (logStreamEvents) {
1561
+ // 聚合流式事件(不逐条输出,在 onComplete 时输出摘要)
1562
+ streamEventCounts[eventType || 'unknown'] = (streamEventCounts[eventType || 'unknown'] || 0) + 1;
1563
+ }
1564
+ // 处理 usageEvent
1565
+ if (eventType === 'usageEvent' || eventType === 'usage' || event.usageEvent || event.usage) {
1566
+ const usageData = event.usageEvent || event.usage || event;
1567
+ if (usageData.inputTokens) {
1568
+ usage.inputTokens = usageData.inputTokens;
1569
+ hasRealTokenUsage = true;
1570
+ }
1571
+ if (usageData.outputTokens)
1572
+ usage.outputTokens = usageData.outputTokens;
1573
+ }
1574
+ // 处理 meteringEvent - Kiro API 返回 credit 使用量
1575
+ if (eventType === 'meteringEvent' || event.meteringEvent) {
1576
+ const metering = event.meteringEvent || event;
1577
+ if (metering.usage && typeof metering.usage === 'number') {
1578
+ // 累加 credit 使用量
1579
+ usage.credits += metering.usage;
1580
+ logger_1.proxyLogger.info('Kiro', `meteringEvent - credit: ${metering.usage}, total: ${usage.credits}`);
1581
+ }
1582
+ }
1583
+ // 处理 supplementaryWebLinksEvent - 网页链接引用
1584
+ if (eventType === 'supplementaryWebLinksEvent' || event.supplementaryWebLinksEvent) {
1585
+ const webLinksEvent = event.supplementaryWebLinksEvent || event;
1586
+ if (webLinksEvent.supplementaryWebLinks && Array.isArray(webLinksEvent.supplementaryWebLinks)) {
1587
+ // 格式化网页链接引用
1588
+ const links = webLinksEvent.supplementaryWebLinks
1589
+ .filter((link) => link.url)
1590
+ .map((link) => {
1591
+ const title = link.title || link.url;
1592
+ return `- [${title}](${link.url})`;
1593
+ });
1594
+ if (links.length > 0) {
1595
+ onChunk(`\n\n🔗 **Web References:**\n${links.join('\n')}`);
1596
+ }
1597
+ }
1598
+ logger_1.proxyLogger.debug('Kiro', 'supplementaryWebLinksEvent', JSON.stringify(webLinksEvent).slice(0, 300));
1599
+ }
1600
+ // 处理 contextUsageEvent - 上下文使用百分比(反推真实 inputTokens)
1601
+ if (eventType === 'contextUsageEvent' || event.contextUsageEvent) {
1602
+ const contextEvent = event.contextUsageEvent || event;
1603
+ if (contextEvent.contextUsagePercentage !== undefined) {
1604
+ const percentage = contextEvent.contextUsagePercentage;
1605
+ // 若已拿到真实 tokenUsage,仅记录百分比,不覆盖 inputTokens
1606
+ if (hasRealTokenUsage) {
1607
+ logger_1.proxyLogger.info('Kiro', `contextUsageEvent - Context usage: ${percentage.toFixed(2)}% (real tokenUsage already received)`);
1608
+ }
1609
+ else {
1610
+ // 反推真实 inputTokens:modelContext × percentage / 100
1611
+ const contextLen = (0, tokenCounter_1.getModelContextLength)(modelId);
1612
+ const reverseInput = Math.round(contextLen * percentage / 100);
1613
+ if (reverseInput > 0) {
1614
+ usage.inputTokens = reverseInput;
1615
+ logger_1.proxyLogger.info('Kiro', `contextUsageEvent ${percentage.toFixed(2)}% → inputTokens=${reverseInput} (modelContext=${contextLen}, model=${modelId || 'unknown'})`);
1616
+ }
1617
+ else {
1618
+ logger_1.proxyLogger.info('Kiro', `contextUsageEvent - Context usage: ${percentage.toFixed(2)}%`);
1619
+ }
1620
+ }
1621
+ // 如果上下文使用率超过 80%,发送警告
1622
+ if (percentage > 80) {
1623
+ console.warn('[Kiro] Warning: Context usage is high:', percentage.toFixed(2) + '%');
1624
+ }
1625
+ }
1626
+ }
1627
+ // 处理 reasoningContentEvent - Thinking 模式的推理内容
1628
+ // Kiro ReasoningContentEvent 字段:[text, redactedContent, signature]
1629
+ if (eventType === 'reasoningContentEvent' || event.reasoningContentEvent) {
1630
+ const reasoning = event.reasoningContentEvent || event;
1631
+ if (reasoning.text) {
1632
+ logger_1.proxyLogger.info('Kiro', `Received reasoning content (isThinking=true): ${reasoning.text.slice(0, 50)}...`);
1633
+ onChunk(reasoning.text, undefined, true, reasoning.signature, undefined);
1634
+ totalOutputChars += reasoning.text.length;
1635
+ usage.reasoningTokens += Math.max(1, Math.round(reasoning.text.length * 0.4));
1636
+ }
1637
+ else if (reasoning.signature && !reasoning.redactedContent) {
1638
+ onChunk('', undefined, true, reasoning.signature, undefined);
1639
+ }
1640
+ // 处理 redactedContent(重编辑的加密 thinking 内容)
1641
+ if (reasoning.redactedContent) {
1642
+ logger_1.proxyLogger.info('Kiro', `Received redacted thinking content (len=${reasoning.redactedContent.length})`);
1643
+ onChunk('', undefined, true, undefined, reasoning.redactedContent);
1644
+ }
1645
+ logger_1.proxyLogger.debug('Kiro', 'reasoningContentEvent', JSON.stringify(reasoning).slice(0, 200));
1646
+ }
1647
+ // 处理 codeReferenceEvent - 代码引用/许可证信息
1648
+ if (eventType === 'codeReferenceEvent' || event.codeReferenceEvent) {
1649
+ const codeRef = event.codeReferenceEvent || event;
1650
+ if (codeRef.references && Array.isArray(codeRef.references)) {
1651
+ // 格式化代码引用信息
1652
+ const refTexts = codeRef.references
1653
+ .filter((ref) => ref.licenseName || ref.repository)
1654
+ .map((ref) => {
1655
+ const parts = [];
1656
+ if (ref.licenseName)
1657
+ parts.push(`License: ${ref.licenseName}`);
1658
+ if (ref.repository)
1659
+ parts.push(`Repo: ${ref.repository}`);
1660
+ if (ref.url)
1661
+ parts.push(`URL: ${ref.url}`);
1662
+ return parts.join(', ');
1663
+ });
1664
+ if (refTexts.length > 0) {
1665
+ onChunk(`\n\n📚 **Code References:**\n${refTexts.join('\n')}`);
1666
+ }
1667
+ }
1668
+ logger_1.proxyLogger.debug('Kiro', 'codeReferenceEvent', JSON.stringify(codeRef).slice(0, 300));
1669
+ }
1670
+ // 处理 followupPromptEvent - 后续提示建议
1671
+ if (eventType === 'followupPromptEvent' || event.followupPromptEvent) {
1672
+ const followup = event.followupPromptEvent || event;
1673
+ if (followup.followupPrompt) {
1674
+ const prompt = followup.followupPrompt;
1675
+ if (prompt.content || prompt.userIntent) {
1676
+ // 将后续提示作为建议输出
1677
+ const suggestion = prompt.content || prompt.userIntent;
1678
+ onChunk(`\n\n💡 **Suggested follow-up:** ${suggestion}`);
1679
+ }
1680
+ }
1681
+ logger_1.proxyLogger.debug('Kiro', 'followupPromptEvent', JSON.stringify(followup).slice(0, 200));
1682
+ }
1683
+ // 处理 intentsEvent - 意图事件(artifact、deeplinks 等)
1684
+ if (eventType === 'intentsEvent' || event.intentsEvent) {
1685
+ const intents = event.intentsEvent || event;
1686
+ // 意图事件主要用于 UI 渲染,记录日志即可
1687
+ logger_1.proxyLogger.debug('Kiro', 'intentsEvent', JSON.stringify(intents).slice(0, 300));
1688
+ }
1689
+ // 处理 interactionComponentsEvent - 交互组件事件
1690
+ if (eventType === 'interactionComponentsEvent' || event.interactionComponentsEvent) {
1691
+ const components = event.interactionComponentsEvent || event;
1692
+ // 交互组件主要用于 UI 渲染,记录日志即可
1693
+ logger_1.proxyLogger.debug('Kiro', 'interactionComponentsEvent', JSON.stringify(components).slice(0, 300));
1694
+ }
1695
+ // 处理 invalidStateEvent - 无效状态事件(错误处理)
1696
+ if (eventType === 'invalidStateEvent' || event.invalidStateEvent) {
1697
+ const invalid = event.invalidStateEvent || event;
1698
+ const reason = invalid.reason || 'UNKNOWN';
1699
+ const message = invalid.message || 'Invalid state detected';
1700
+ console.error('[Kiro] invalidStateEvent:', reason, message);
1701
+ // 将无效状态作为错误消息输出
1702
+ onChunk(`\n\n⚠️ **Warning:** ${message} (reason: ${reason})`);
1703
+ }
1704
+ // 处理 citationEvent - 引用事件
1705
+ if (eventType === 'citationEvent' || event.citationEvent) {
1706
+ const citation = event.citationEvent || event;
1707
+ if (citation.citations && Array.isArray(citation.citations)) {
1708
+ // 格式化引用信息
1709
+ const citationTexts = citation.citations
1710
+ .filter((c) => c.title || c.url)
1711
+ .map((c, i) => {
1712
+ const parts = [`[${i + 1}]`];
1713
+ if (c.title)
1714
+ parts.push(c.title);
1715
+ if (c.url)
1716
+ parts.push(`(${c.url})`);
1717
+ return parts.join(' ');
1718
+ });
1719
+ if (citationTexts.length > 0) {
1720
+ onChunk(`\n\n📖 **Citations:**\n${citationTexts.join('\n')}`);
1721
+ }
1722
+ }
1723
+ logger_1.proxyLogger.debug('Kiro', 'citationEvent', JSON.stringify(citation).slice(0, 300));
1724
+ }
1725
+ // 检查错误
1726
+ if (event._type || event.error) {
1727
+ const errMsg = event.message || event.error?.message || 'Unknown stream error';
1728
+ throw new Error(errMsg);
1729
+ }
1730
+ }
1731
+ catch (parseError) {
1732
+ if (parseError instanceof SyntaxError) {
1733
+ // JSON 解析错误,忽略
1734
+ console.debug('[EventStream] JSON parse error:', parseError);
1735
+ }
1736
+ else {
1737
+ throw parseError;
1738
+ }
1739
+ }
1740
+ }
1741
+ // 移动到下一条消息
1742
+ buffer = buffer.slice(totalLength);
1743
+ }
1744
+ }
1745
+ // 完成任何未完成的 tool use
1746
+ if (currentToolUse && !processedIds.has(currentToolUse.toolUseId)) {
1747
+ let finalInput = {};
1748
+ try {
1749
+ if (currentToolUse.inputBuffer) {
1750
+ finalInput = JSON.parse(currentToolUse.inputBuffer);
1751
+ }
1752
+ }
1753
+ catch { /* 忽略解析错误 */ }
1754
+ onChunk('', {
1755
+ toolUseId: currentToolUse.toolUseId,
1756
+ name: currentToolUse.name,
1757
+ input: finalInput
1758
+ });
1759
+ totalOutputChars += currentToolUse.name.length + currentToolUse.inputBuffer.length;
1760
+ }
1761
+ // 如果 API 没有返回 token 信息,优先用 tiktoken 精确计算,兜底字符系数
1762
+ if (usage.outputTokens === 0 && totalOutputChars > 0) {
1763
+ if (collectedOutputText) {
1764
+ // tiktoken cl100k_base 精确计算(±5%)
1765
+ usage.outputTokens = Math.max(1, (0, tokenCounter_1.countTokens)(collectedOutputText));
1766
+ logger_1.proxyLogger.info('Kiro', `Estimated output tokens (tiktoken): ${totalOutputChars} chars -> ${usage.outputTokens} tokens`);
1767
+ }
1768
+ else {
1769
+ // 字符系数兜底(自然语言中英混合约 0.4 token/字符)
1770
+ usage.outputTokens = Math.max(1, Math.round(totalOutputChars * 0.4));
1771
+ logger_1.proxyLogger.info('Kiro', `Estimated output tokens (fallback): ${totalOutputChars} chars -> ${usage.outputTokens} tokens`);
1772
+ }
1773
+ }
1774
+ // 流式事件聚合摘要
1775
+ if (logStreamEvents && Object.keys(streamEventCounts).length > 0) {
1776
+ const total = Object.values(streamEventCounts).reduce((a, b) => a + b, 0);
1777
+ logger_1.proxyLogger.debug('Kiro', `Stream events summary (${total} total)`, streamEventCounts);
1778
+ }
1779
+ throwIfAborted(signal);
1780
+ logger_1.proxyLogger.info('Kiro', 'Stream complete, final usage', usage);
1781
+ onComplete(usage);
1782
+ }
1783
+ catch (error) {
1784
+ onError(signal?.aborted ? getAbortError(signal) : error);
1785
+ }
1786
+ finally {
1787
+ signal?.removeEventListener('abort', abort);
1788
+ reader.releaseLock();
1789
+ }
1790
+ }
1791
+ // 非流式调用(等待完整响应)
1792
+ async function callKiroApi(account, payload, signal) {
1793
+ return new Promise((resolve, reject) => {
1794
+ let content = '';
1795
+ let reasoningText = '';
1796
+ let reasoningSignature;
1797
+ let redactedContent = '';
1798
+ const toolUses = [];
1799
+ let usage = { inputTokens: 0, outputTokens: 0, credits: 0 };
1800
+ callKiroApiStream(account, payload, (text, toolUse, isThinking, signature, redacted) => {
1801
+ if (isThinking) {
1802
+ if (text)
1803
+ reasoningText += text;
1804
+ if (signature)
1805
+ reasoningSignature = signature;
1806
+ if (redacted)
1807
+ redactedContent += redacted;
1808
+ }
1809
+ else {
1810
+ content += text;
1811
+ }
1812
+ if (toolUse) {
1813
+ toolUses.push(toolUse);
1814
+ }
1815
+ }, (u) => {
1816
+ usage = u;
1817
+ if (reasoningText || redactedContent) {
1818
+ const rc = {};
1819
+ if (reasoningText)
1820
+ rc.text = reasoningText;
1821
+ if (reasoningSignature)
1822
+ rc.signature = reasoningSignature;
1823
+ if (redactedContent)
1824
+ rc.redactedContent = redactedContent;
1825
+ resolve({ content, toolUses, usage, reasoningContent: rc });
1826
+ return;
1827
+ }
1828
+ resolve({ content, toolUses, usage });
1829
+ }, reject, signal).catch(reject);
1830
+ });
1831
+ }
1832
+ // 根据账号区域获取 Q Service 端点(官方插件使用 q.{region}.amazonaws.com)
1833
+ function getQServiceEndpoint(region) {
1834
+ if (region?.startsWith('eu-'))
1835
+ return 'https://q.eu-central-1.amazonaws.com';
1836
+ return 'https://q.us-east-1.amazonaws.com';
1837
+ }
1838
+ // 获取 Kiro 官方模型列表(支持分页,与官方插件一致传递 profileArn)
1839
+ async function fetchKiroModels(account, signal) {
1840
+ const baseUrl = getQServiceEndpoint(account.region);
1841
+ const machineId = getAccountMachineId(account.id, account.machineId);
1842
+ const headers = {
1843
+ 'Authorization': `Bearer ${account.accessToken}`,
1844
+ 'Content-Type': 'application/json',
1845
+ 'Accept': 'application/json',
1846
+ 'User-Agent': getKiroUserAgent(machineId),
1847
+ 'x-amz-user-agent': getKiroAmzUserAgent(machineId),
1848
+ 'x-amzn-codewhisperer-optout': 'true'
1849
+ };
1850
+ const allModels = [];
1851
+ let nextToken;
1852
+ try {
1853
+ do {
1854
+ const params = new URLSearchParams({ origin: 'AI_EDITOR', maxResults: '50' });
1855
+ const arnForModels = resolveProfileArn(account);
1856
+ if (arnForModels)
1857
+ params.set('profileArn', arnForModels);
1858
+ if (nextToken)
1859
+ params.set('nextToken', nextToken);
1860
+ const url = `${baseUrl}/ListAvailableModels?${params.toString()}`;
1861
+ throwIfAborted(signal);
1862
+ const response = await fetchWithProxy(url, { method: 'GET', headers, signal }, account);
1863
+ throwIfAborted(signal);
1864
+ if (!response.ok) {
1865
+ console.error('[KiroAPI] ListAvailableModels failed:', response.status);
1866
+ break;
1867
+ }
1868
+ const data = await response.json();
1869
+ allModels.push(...(data.models || []));
1870
+ nextToken = data.nextToken;
1871
+ } while (nextToken);
1872
+ return allModels;
1873
+ }
1874
+ catch (error) {
1875
+ if (signal?.aborted)
1876
+ throw getAbortError(signal);
1877
+ console.error('[KiroAPI] ListAvailableModels error:', error);
1878
+ return allModels.length > 0 ? allModels : [];
1879
+ }
1880
+ }
1881
+ // 订阅请求专用 User-Agent(匹配 Kiro IDE 实际报文格式)
1882
+ const KIRO_SUBSCRIPTION_VERSION = '0.12.155';
1883
+ function getSubscriptionUserAgent(machineId) {
1884
+ const suffix = machineId ? `KiroIDE-${KIRO_SUBSCRIPTION_VERSION}-${machineId}` : `KiroIDE-${KIRO_SUBSCRIPTION_VERSION}`;
1885
+ return `aws-sdk-js/1.0.0 ua/2.1 os/win32#10.0.19043 lang/js md/nodejs#22.22.0 api/codewhispererruntime#1.0.0 m/N,E ${suffix}`;
1886
+ }
1887
+ function getSubscriptionAmzUserAgent(machineId) {
1888
+ const suffix = machineId ? `KiroIDE-${KIRO_SUBSCRIPTION_VERSION}-${machineId}` : `KiroIDE-${KIRO_SUBSCRIPTION_VERSION}`;
1889
+ return `aws-sdk-js/1.0.0 ${suffix}`;
1890
+ }
1891
+ // 获取可用订阅列表
1892
+ async function fetchAvailableSubscriptions(account) {
1893
+ const baseUrl = getQServiceEndpoint(account.region);
1894
+ const url = `${baseUrl}/listAvailableSubscriptions`;
1895
+ const machineId = getAccountMachineId(account.id, account.machineId);
1896
+ const headers = {
1897
+ 'Authorization': `Bearer ${account.accessToken}`,
1898
+ 'content-type': 'application/json',
1899
+ 'user-agent': getSubscriptionUserAgent(machineId),
1900
+ 'x-amz-user-agent': getSubscriptionAmzUserAgent(machineId),
1901
+ 'amz-sdk-invocation-id': (0, uuid_1.v4)(),
1902
+ 'amz-sdk-request': 'attempt=1; max=1'
1903
+ };
1904
+ const profileArn = resolveProfileArn(account);
1905
+ const body = JSON.stringify(profileArn ? { profileArn } : {});
1906
+ console.log(`[KiroAPI] ListAvailableSubscriptions [${account.email || account.id.slice(0, 8)}]`, {
1907
+ url,
1908
+ hasProfileArn: profileArn !== undefined
1909
+ });
1910
+ try {
1911
+ const response = await fetchWithProxy(url, { method: 'POST', headers, body }, account);
1912
+ const responseText = await response.text();
1913
+ console.log(`[KiroAPI] ListAvailableSubscriptions → ${response.status}`, JSON.parse(responseText));
1914
+ if (!response.ok) {
1915
+ return {};
1916
+ }
1917
+ return JSON.parse(responseText);
1918
+ }
1919
+ catch (error) {
1920
+ console.error('[KiroAPI] ListAvailableSubscriptions error:', error);
1921
+ return {};
1922
+ }
1923
+ }
1924
+ // 获取订阅管理/支付链接
1925
+ async function fetchSubscriptionToken(account, subscriptionType) {
1926
+ const baseUrl = getQServiceEndpoint(account.region);
1927
+ const url = `${baseUrl}/CreateSubscriptionToken`;
1928
+ const machineId = getAccountMachineId(account.id, account.machineId);
1929
+ const headers = {
1930
+ 'Authorization': `Bearer ${account.accessToken}`,
1931
+ 'content-type': 'application/json',
1932
+ 'user-agent': getSubscriptionUserAgent(machineId),
1933
+ 'x-amz-user-agent': getSubscriptionAmzUserAgent(machineId),
1934
+ 'amz-sdk-invocation-id': (0, uuid_1.v4)(),
1935
+ 'amz-sdk-request': 'attempt=1; max=1'
1936
+ };
1937
+ const profileArn = resolveProfileArn(account);
1938
+ // clientToken 是必需参数;profileArn 仅在解析出有效值时附带
1939
+ const payload = {
1940
+ clientToken: (0, uuid_1.v4)(),
1941
+ provider: 'STRIPE'
1942
+ };
1943
+ if (profileArn) {
1944
+ payload.profileArn = profileArn;
1945
+ }
1946
+ if (subscriptionType) {
1947
+ payload.subscriptionType = subscriptionType;
1948
+ }
1949
+ try {
1950
+ const response = await fetchWithProxy(url, { method: 'POST', headers, body: JSON.stringify(payload) }, account);
1951
+ if (!response.ok) {
1952
+ const errorData = await response.json().catch(() => ({}));
1953
+ console.error('[KiroAPI] CreateSubscriptionToken failed:', response.status, errorData);
1954
+ return { message: errorData.message || `Request failed with status ${response.status}` };
1955
+ }
1956
+ const data = await response.json();
1957
+ return data;
1958
+ }
1959
+ catch (error) {
1960
+ console.error('[KiroAPI] CreateSubscriptionToken error:', error);
1961
+ return { message: error instanceof Error ? error.message : 'Unknown error' };
1962
+ }
1963
+ }
1964
+ // 设置用户偏好(超额开启/关闭)
1965
+ async function setUserPreference(account, overageStatus) {
1966
+ const baseUrl = getQServiceEndpoint(account.region);
1967
+ const url = `${baseUrl}/setUserPreference`;
1968
+ const machineId = getAccountMachineId(account.id, account.machineId);
1969
+ const headers = {
1970
+ 'Authorization': `Bearer ${account.accessToken}`,
1971
+ 'content-type': 'application/json',
1972
+ 'user-agent': getSubscriptionUserAgent(machineId),
1973
+ 'x-amz-user-agent': getSubscriptionAmzUserAgent(machineId),
1974
+ 'amz-sdk-invocation-id': (0, uuid_1.v4)(),
1975
+ 'amz-sdk-request': 'attempt=1; max=1'
1976
+ };
1977
+ const profileArn = resolveProfileArn(account);
1978
+ const bodyPayload = {
1979
+ overageConfiguration: { overageStatus }
1980
+ };
1981
+ if (profileArn) {
1982
+ bodyPayload.profileArn = profileArn;
1983
+ }
1984
+ const body = JSON.stringify(bodyPayload);
1985
+ try {
1986
+ const response = await fetchWithProxy(url, { method: 'POST', headers, body }, account);
1987
+ if (!response.ok) {
1988
+ const errorText = await response.text().catch(() => '');
1989
+ return { success: false, error: `HTTP ${response.status}: ${errorText.substring(0, 200)}` };
1990
+ }
1991
+ return { success: true };
1992
+ }
1993
+ catch (error) {
1994
+ return { success: false, error: error instanceof Error ? error.message : 'Unknown error' };
1995
+ }
1996
+ }