responses-proxy 0.1.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 (161) hide show
  1. package/README.md +56 -0
  2. package/cli.js +118 -0
  3. package/dist/anthropic-messages.js +383 -0
  4. package/dist/anthropic-messages.test.js +209 -0
  5. package/dist/audit-log.js +138 -0
  6. package/dist/audit-log.test.js +480 -0
  7. package/dist/billing-expiration.js +70 -0
  8. package/dist/billing-expiration.test.js +114 -0
  9. package/dist/billing.js +716 -0
  10. package/dist/billing.test.js +228 -0
  11. package/dist/chatgpt-oauth-store.js +240 -0
  12. package/dist/chatgpt-oauth-store.test.js +88 -0
  13. package/dist/chatgpt-oauth.js +118 -0
  14. package/dist/chatgpt-oauth.test.js +63 -0
  15. package/dist/chatgpt-provider-auth.js +60 -0
  16. package/dist/chatgpt-provider-auth.test.js +101 -0
  17. package/dist/client/app-icon.svg +17 -0
  18. package/dist/client/assets/index-C7Vvhst8.js +14 -0
  19. package/dist/client/assets/index-DpqgYK3L.css +1 -0
  20. package/dist/client/favicon.svg +17 -0
  21. package/dist/client/index.html +31 -0
  22. package/dist/client-config-apply.js +345 -0
  23. package/dist/client-config-apply.test.js +185 -0
  24. package/dist/client-token-limits.js +111 -0
  25. package/dist/client-token-limits.test.js +129 -0
  26. package/dist/codex-config.js +47 -0
  27. package/dist/codex-setup.js +87 -0
  28. package/dist/codex-setup.test.js +30 -0
  29. package/dist/config.js +314 -0
  30. package/dist/cost-analytics.js +31 -0
  31. package/dist/cost-analytics.test.js +38 -0
  32. package/dist/customer-key-access.js +126 -0
  33. package/dist/customer-key-access.test.js +178 -0
  34. package/dist/customer-keys.js +209 -0
  35. package/dist/customer-keys.test.js +68 -0
  36. package/dist/customer-usage.js +18 -0
  37. package/dist/customer-usage.test.js +55 -0
  38. package/dist/dashboard-auth.js +318 -0
  39. package/dist/dashboard-auth.test.js +133 -0
  40. package/dist/dashboard-serving.test.js +235 -0
  41. package/dist/error-response.js +174 -0
  42. package/dist/error-response.test.js +88 -0
  43. package/dist/forward.js +357 -0
  44. package/dist/health-websocket-manager.js +174 -0
  45. package/dist/http-rate-limit.js +36 -0
  46. package/dist/http-rate-limit.test.js +62 -0
  47. package/dist/kiro-auth.js +136 -0
  48. package/dist/kiro-auth.test.js +234 -0
  49. package/dist/kiro-codewhisperer.js +646 -0
  50. package/dist/kiro-codewhisperer.test.js +219 -0
  51. package/dist/kiro-device-login.js +338 -0
  52. package/dist/kiro-eventstream.js +219 -0
  53. package/dist/kiro-eventstream.test.js +79 -0
  54. package/dist/kiro-forward.js +401 -0
  55. package/dist/kiro-import-cli.js +69 -0
  56. package/dist/kiro-import.js +94 -0
  57. package/dist/kiro-import.test.js +125 -0
  58. package/dist/kiro-token-store.js +196 -0
  59. package/dist/kiro-token-store.test.js +207 -0
  60. package/dist/krouter-usage.js +243 -0
  61. package/dist/model-combo-repository.js +147 -0
  62. package/dist/model-routing.js +69 -0
  63. package/dist/model-routing.test.js +41 -0
  64. package/dist/normalize-request.js +531 -0
  65. package/dist/normalize-request.test.js +277 -0
  66. package/dist/omv-public-firewall.test.js +11 -0
  67. package/dist/package.json +17 -0
  68. package/dist/prompt-cache-state.js +146 -0
  69. package/dist/prompt-cache-state.test.js +71 -0
  70. package/dist/prompt-cache.js +229 -0
  71. package/dist/provider-health-service.js +404 -0
  72. package/dist/provider-request-parameters.js +107 -0
  73. package/dist/provider-request-parameters.test.js +26 -0
  74. package/dist/provider-routing.js +114 -0
  75. package/dist/provider-routing.test.js +64 -0
  76. package/dist/provider-usage.js +314 -0
  77. package/dist/request-timeout-policy.js +61 -0
  78. package/dist/request-timeout-policy.test.js +40 -0
  79. package/dist/response-cache.js +69 -0
  80. package/dist/response-cache.test.js +28 -0
  81. package/dist/routing-combo-repository.js +300 -0
  82. package/dist/routing-engine.js +377 -0
  83. package/dist/routing-integration.js +155 -0
  84. package/dist/routing-simulation-engine.js +326 -0
  85. package/dist/rtk-layer.js +483 -0
  86. package/dist/rtk-layer.test.js +198 -0
  87. package/dist/runtime-provider-repository.js +1742 -0
  88. package/dist/runtime-provider-repository.test.js +1177 -0
  89. package/dist/schema.js +118 -0
  90. package/dist/schema.test.js +16 -0
  91. package/dist/sepay-webhook.js +87 -0
  92. package/dist/sepay-webhook.test.js +142 -0
  93. package/dist/server-body-limit.test.js +35 -0
  94. package/dist/server-client-token-limits.test.js +161 -0
  95. package/dist/server-codex-config-setup.test.js +76 -0
  96. package/dist/server-http-rate-limit.test.js +80 -0
  97. package/dist/server-response-cache.test.js +105 -0
  98. package/dist/server-routes-alias.test.js +39 -0
  99. package/dist/server-sepay-webhook-security.test.js +59 -0
  100. package/dist/server.js +5906 -0
  101. package/dist/session-log.js +178 -0
  102. package/dist/tailnet-funnel-script.test.js +33 -0
  103. package/dist/telegram-bot/actions.js +118 -0
  104. package/dist/telegram-bot/admin-actions.js +103 -0
  105. package/dist/telegram-bot/auth.js +46 -0
  106. package/dist/telegram-bot/auth.test.js +1 -0
  107. package/dist/telegram-bot/bot-identity-repository.js +189 -0
  108. package/dist/telegram-bot/bot-identity-repository.test.js +78 -0
  109. package/dist/telegram-bot/callbacks.js +30 -0
  110. package/dist/telegram-bot/codex-config-delivery.js +38 -0
  111. package/dist/telegram-bot/codex-config-delivery.test.js +75 -0
  112. package/dist/telegram-bot/commands/accounts.js +140 -0
  113. package/dist/telegram-bot/commands/apikey.js +737 -0
  114. package/dist/telegram-bot/commands/apply.js +265 -0
  115. package/dist/telegram-bot/commands/clients.js +13 -0
  116. package/dist/telegram-bot/commands/customer-billing.test.js +271 -0
  117. package/dist/telegram-bot/commands/grant.js +138 -0
  118. package/dist/telegram-bot/commands/grant.test.js +217 -0
  119. package/dist/telegram-bot/commands/help.js +52 -0
  120. package/dist/telegram-bot/commands/me.js +53 -0
  121. package/dist/telegram-bot/commands/models.js +6 -0
  122. package/dist/telegram-bot/commands/oauth.js +64 -0
  123. package/dist/telegram-bot/commands/plans.js +96 -0
  124. package/dist/telegram-bot/commands/providers.js +27 -0
  125. package/dist/telegram-bot/commands/quota.js +10 -0
  126. package/dist/telegram-bot/commands/renew-user.js +139 -0
  127. package/dist/telegram-bot/commands/renew-user.test.js +184 -0
  128. package/dist/telegram-bot/commands/renew.js +1369 -0
  129. package/dist/telegram-bot/commands/renew.test.js +1633 -0
  130. package/dist/telegram-bot/commands/start.js +212 -0
  131. package/dist/telegram-bot/commands/start.test.js +280 -0
  132. package/dist/telegram-bot/commands/status.js +6 -0
  133. package/dist/telegram-bot/commands/tailscale.js +15 -0
  134. package/dist/telegram-bot/commands/tailscale.test.js +76 -0
  135. package/dist/telegram-bot/commands/test.js +51 -0
  136. package/dist/telegram-bot/commands/test.test.js +14 -0
  137. package/dist/telegram-bot/commands/usage.js +10 -0
  138. package/dist/telegram-bot/config.js +98 -0
  139. package/dist/telegram-bot/config.test.js +42 -0
  140. package/dist/telegram-bot/customer-actions.js +160 -0
  141. package/dist/telegram-bot/customer-api-keys.js +68 -0
  142. package/dist/telegram-bot/customer-billing.js +72 -0
  143. package/dist/telegram-bot/customer-workspace-repository.js +134 -0
  144. package/dist/telegram-bot/customer-workspace-repository.test.js +47 -0
  145. package/dist/telegram-bot/dashboard-login.js +39 -0
  146. package/dist/telegram-bot/format.js +140 -0
  147. package/dist/telegram-bot/grants.js +370 -0
  148. package/dist/telegram-bot/grants.test.js +290 -0
  149. package/dist/telegram-bot/index.js +85 -0
  150. package/dist/telegram-bot/message-cleanup.js +55 -0
  151. package/dist/telegram-bot/message-cleanup.test.js +77 -0
  152. package/dist/telegram-bot/message-format.js +45 -0
  153. package/dist/telegram-bot/message-format.test.js +10 -0
  154. package/dist/telegram-bot/proxy-client.js +174 -0
  155. package/dist/telegram-bot/rate-limit.js +95 -0
  156. package/dist/telegram-bot/rate-limit.test.js +58 -0
  157. package/dist/telegram-bot/sessions.js +171 -0
  158. package/dist/telegram-bot/sessions.test.js +107 -0
  159. package/dist/telegram-bot/telegram-adapter.js +126 -0
  160. package/dist/telegram-bot/worker.js +63 -0
  161. package/package.json +39 -0
@@ -0,0 +1,646 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { EventStreamParser, decodeJsonPayload, eventType, } from "./kiro-eventstream.js";
3
+ /**
4
+ * Translates between the OpenAI Responses payloads this proxy speaks and the AWS
5
+ * CodeWhisperer / Amazon Q `generateAssistantResponse` protocol that Kiro tokens
6
+ * authenticate against.
7
+ *
8
+ * The CodeWhisperer wire format is not an officially documented public API; the
9
+ * envelope shape here mirrors the Kiro desktop client and community proxies. The
10
+ * request is plain JSON; the response is an `application/vnd.amazon.eventstream`
11
+ * binary stream of `assistantResponseEvent` frames (see kiro-eventstream.ts).
12
+ */
13
+ export const CODEWHISPERER_GENERATE_PATH = "/generateAssistantResponse";
14
+ /**
15
+ * Default alias → CodeWhisperer modelId map for the Kiro provider. CodeWhisperer
16
+ * for Kiro uses lowercase model ids (e.g. `claude-sonnet-4`, `auto`), NOT the
17
+ * bedrock-style `CLAUDE_SONNET_4_...` ids. Values mirror 9router's catalog.
18
+ */
19
+ export const DEFAULT_KIRO_MODEL_ALIASES = {
20
+ // ─── Auto (router) ───
21
+ auto: "auto",
22
+ "kiro-auto": "auto",
23
+ "kr/auto": "auto",
24
+ // ─── Claude Opus series ───
25
+ "claude-opus-4-8": "claude-opus-4-8",
26
+ "claude-opus-4.8": "claude-opus-4-8",
27
+ "kr/claude-opus-4-8": "claude-opus-4-8",
28
+ "kr/claude-opus-4.8": "claude-opus-4-8",
29
+ "claude-opus-4-7": "claude-opus-4-7",
30
+ "claude-opus-4.7": "claude-opus-4-7",
31
+ "kr/claude-opus-4-7": "claude-opus-4-7",
32
+ "kr/claude-opus-4.7": "claude-opus-4-7",
33
+ "claude-opus-4-6": "claude-opus-4-6",
34
+ "claude-opus-4.6": "claude-opus-4-6",
35
+ "kr/claude-opus-4-6": "claude-opus-4-6",
36
+ "kr/claude-opus-4.6": "claude-opus-4-6",
37
+ "claude-opus-4-5": "claude-opus-4-5",
38
+ "claude-opus-4.5": "claude-opus-4-5",
39
+ "kr/claude-opus-4-5": "claude-opus-4-5",
40
+ "kr/claude-opus-4.5": "claude-opus-4-5",
41
+ // ─── Claude Sonnet series ───
42
+ "claude-sonnet-4-6": "claude-sonnet-4-6",
43
+ "claude-sonnet-4.6": "claude-sonnet-4-6",
44
+ "kr/claude-sonnet-4-6": "claude-sonnet-4-6",
45
+ "kr/claude-sonnet-4.6": "claude-sonnet-4-6",
46
+ "claude-sonnet-4-5": "claude-sonnet-4-5",
47
+ "claude-sonnet-4.5": "claude-sonnet-4-5",
48
+ "kr/claude-sonnet-4-5": "claude-sonnet-4-5",
49
+ "kr/claude-sonnet-4.5": "claude-sonnet-4-5",
50
+ "claude-sonnet-4": "claude-sonnet-4",
51
+ "claude-sonnet-4-0": "claude-sonnet-4",
52
+ "claude-sonnet-4.0": "claude-sonnet-4",
53
+ "kr/claude-sonnet-4": "claude-sonnet-4",
54
+ "kr/claude-sonnet-4-0": "claude-sonnet-4",
55
+ // ─── Claude Haiku ───
56
+ "claude-haiku-4-5": "claude-haiku-4.5",
57
+ "claude-haiku-4.5": "claude-haiku-4.5",
58
+ "kr/claude-haiku-4-5": "claude-haiku-4.5",
59
+ "kr/claude-haiku-4.5": "claude-haiku-4.5",
60
+ // ─── Non-Claude models (open weight) ───
61
+ "deepseek-3.2": "deepseek-3.2",
62
+ "deepseek-3-2": "deepseek-3.2",
63
+ "kr/deepseek-3.2": "deepseek-3.2",
64
+ "kr/deepseek-3-2": "deepseek-3.2",
65
+ "minimax-m2.5": "MiniMax-M2.5",
66
+ "minimax-m2-5": "MiniMax-M2.5",
67
+ "MiniMax-M2.5": "MiniMax-M2.5",
68
+ "kr/minimax-m2.5": "MiniMax-M2.5",
69
+ "kr/minimax-m2-5": "MiniMax-M2.5",
70
+ "minimax-m2.1": "MiniMax-M2.1",
71
+ "minimax-m2-1": "MiniMax-M2.1",
72
+ "MiniMax-M2.1": "MiniMax-M2.1",
73
+ "kr/minimax-m2.1": "MiniMax-M2.1",
74
+ "kr/minimax-m2-1": "MiniMax-M2.1",
75
+ "glm-5": "glm-5",
76
+ "kr/glm-5": "glm-5",
77
+ "qwen3-coder-next": "qwen3-coder-next",
78
+ "kr/qwen3-coder-next": "qwen3-coder-next",
79
+ // ─── Legacy aliases (kiro- prefix) ───
80
+ "kiro-claude-sonnet-4": "claude-sonnet-4",
81
+ "kiro-claude-sonnet-4-5": "claude-sonnet-4-5",
82
+ "kiro-claude-sonnet-4-6": "claude-sonnet-4-6",
83
+ "kiro-claude-haiku-4-5": "claude-haiku-4.5",
84
+ "kiro-claude-opus-4-5": "claude-opus-4-5",
85
+ "kiro-claude-opus-4-6": "claude-opus-4-6",
86
+ "kiro-claude-opus-4-7": "claude-opus-4-7",
87
+ "kiro-claude-opus-4-8": "claude-opus-4-8",
88
+ };
89
+ /** `auto` lets CodeWhisperer pick the model; safest default matching 9router. */
90
+ export const DEFAULT_KIRO_MODEL_ID = "auto";
91
+ const CHAT_TRIGGER_TYPE = "MANUAL";
92
+ const MESSAGE_ORIGIN = "AI_EDITOR";
93
+ const DEFAULT_MAX_TOKENS = 32000;
94
+ function readNumber(value) {
95
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
96
+ }
97
+ /**
98
+ * Resolve a client-facing model name to a CodeWhisperer modelId. Lookups are
99
+ * case-insensitive; unknown names fall through to `defaultModelId` so the proxy
100
+ * still issues a request rather than rejecting unfamiliar aliases.
101
+ */
102
+ export function mapModelToCodeWhisperer(model, aliases = DEFAULT_KIRO_MODEL_ALIASES, defaultModelId = DEFAULT_KIRO_MODEL_ID) {
103
+ const normalized = typeof model === "string" ? model.trim() : "";
104
+ if (!normalized) {
105
+ return defaultModelId;
106
+ }
107
+ // Exact match first, then case-insensitive.
108
+ if (aliases[normalized]) {
109
+ return aliases[normalized];
110
+ }
111
+ const lower = normalized.toLowerCase();
112
+ for (const [alias, modelId] of Object.entries(aliases)) {
113
+ if (alias.toLowerCase() === lower) {
114
+ return modelId;
115
+ }
116
+ }
117
+ // Claude Code / the Anthropic SDK send date-suffixed ids (e.g.
118
+ // `claude-sonnet-4-20250514`). CodeWhisperer expects the bare lowercase id, so
119
+ // strip a trailing `-YYYYMMDD` and re-check aliases before falling through.
120
+ const deDated = lower.replace(/-\d{8}$/, "");
121
+ if (deDated !== lower) {
122
+ if (aliases[deDated]) {
123
+ return aliases[deDated];
124
+ }
125
+ if (deDated === "auto" || deDated.startsWith("claude-")) {
126
+ return deDated;
127
+ }
128
+ }
129
+ // Pass a recognized Kiro model id straight through (lowercase `auto`/`claude-*`).
130
+ if (lower === "auto" || lower.startsWith("claude-")) {
131
+ return lower;
132
+ }
133
+ return defaultModelId;
134
+ }
135
+ /** Pull plain text out of a Responses content value (string or content-part array). */
136
+ function extractContentText(content) {
137
+ if (typeof content === "string") {
138
+ return content;
139
+ }
140
+ if (!Array.isArray(content)) {
141
+ return "";
142
+ }
143
+ const parts = [];
144
+ for (const part of content) {
145
+ if (typeof part === "string") {
146
+ parts.push(part);
147
+ continue;
148
+ }
149
+ if (typeof part === "object" && part !== null) {
150
+ const record = part;
151
+ if (typeof record.text === "string") {
152
+ parts.push(record.text);
153
+ }
154
+ }
155
+ }
156
+ return parts.join("");
157
+ }
158
+ function normalizeRole(role) {
159
+ if (role === "assistant") {
160
+ return "assistant";
161
+ }
162
+ if (role === "system" || role === "developer") {
163
+ return "system";
164
+ }
165
+ return "user";
166
+ }
167
+ /**
168
+ * Flatten a Responses request (instructions + input/messages) into an ordered list
169
+ * of user/assistant turns. System/developer text and top-level `instructions` are
170
+ * folded into the first user turn, since CodeWhisperer has no separate system slot.
171
+ */
172
+ export function flattenResponsesConversation(body) {
173
+ const systemChunks = [];
174
+ if (typeof body.instructions === "string" && body.instructions.trim()) {
175
+ systemChunks.push(body.instructions.trim());
176
+ }
177
+ const turns = [];
178
+ const rawInput = body.input ?? body.messages;
179
+ if (typeof rawInput === "string") {
180
+ turns.push({ role: "user", content: rawInput });
181
+ }
182
+ else if (Array.isArray(rawInput)) {
183
+ for (const item of rawInput) {
184
+ if (typeof item !== "object" || item === null) {
185
+ continue;
186
+ }
187
+ const record = item;
188
+ const role = normalizeRole(record.role);
189
+ const text = extractContentText(record.content);
190
+ if (!text) {
191
+ continue;
192
+ }
193
+ if (role === "system") {
194
+ systemChunks.push(text);
195
+ continue;
196
+ }
197
+ turns.push({ role, content: text });
198
+ }
199
+ }
200
+ if (systemChunks.length > 0) {
201
+ const systemText = systemChunks.join("\n\n");
202
+ const firstUserIndex = turns.findIndex((turn) => turn.role === "user");
203
+ if (firstUserIndex >= 0) {
204
+ turns[firstUserIndex] = {
205
+ role: "user",
206
+ content: `${systemText}\n\n${turns[firstUserIndex].content}`,
207
+ };
208
+ }
209
+ else {
210
+ turns.unshift({ role: "user", content: systemText });
211
+ }
212
+ }
213
+ return turns;
214
+ }
215
+ /**
216
+ * Build the CodeWhisperer `generateAssistantResponse` request body. Mirrors the
217
+ * shape 9router sends: a `[Context: ...]` prefix on the current message, an
218
+ * `inferenceConfig`, and `profileArn` only when non-empty.
219
+ */
220
+ export function buildCodeWhispererRequest(args) {
221
+ const turns = flattenResponsesConversation(args.body).map((turn) => ({ role: turn.role, content: turn.content }));
222
+ return buildCodeWhispererRequestFromTurns({
223
+ turns,
224
+ modelId: args.modelId,
225
+ profileArn: args.profileArn,
226
+ conversationId: args.conversationId,
227
+ now: args.now,
228
+ maxTokens: readNumber(args.body.max_output_tokens),
229
+ temperature: readNumber(args.body.temperature),
230
+ topP: readNumber(args.body.top_p),
231
+ });
232
+ }
233
+ function normalizeToolSchema(schema) {
234
+ if (!schema || Object.keys(schema).length === 0) {
235
+ return { type: "object", properties: {}, required: [] };
236
+ }
237
+ return {
238
+ ...schema,
239
+ required: Array.isArray(schema.required) ? schema.required : [],
240
+ };
241
+ }
242
+ function toolResultContext(results) {
243
+ return {
244
+ toolResults: results.map((result) => ({
245
+ toolUseId: result.toolUseId,
246
+ status: result.status ?? "success",
247
+ content: [{ text: result.content }],
248
+ })),
249
+ };
250
+ }
251
+ /**
252
+ * Lower-level builder shared by the Responses and Anthropic Messages paths. Takes
253
+ * already-structured turns (optionally carrying tool calls / tool results), splits
254
+ * out the final user turn as the current message, and assembles the CodeWhisperer
255
+ * request. Available `tools` and the current turn's `toolResults` are placed in the
256
+ * current message's `userInputMessageContext`, matching 9router's wire format.
257
+ */
258
+ export function buildCodeWhispererRequestFromTurns(args) {
259
+ const { turns } = args;
260
+ // The final user turn is the "current" message; everything before is history.
261
+ let lastUserIndex = -1;
262
+ for (let i = turns.length - 1; i >= 0; i -= 1) {
263
+ if (turns[i].role === "user") {
264
+ lastUserIndex = i;
265
+ break;
266
+ }
267
+ }
268
+ const currentTurn = lastUserIndex >= 0 ? turns[lastUserIndex] : undefined;
269
+ const historyTurns = lastUserIndex >= 0 ? turns.slice(0, lastUserIndex) : turns;
270
+ const rawCurrentContent = currentTurn ? currentTurn.content : "";
271
+ // 9router prepends a context block to the current message; match it so behavior
272
+ // is consistent with the proven client.
273
+ const nowIso = (args.now ?? new Date()).toISOString();
274
+ const currentContent = `[Context: Current time is ${nowIso}]\n\n${rawCurrentContent}`;
275
+ // Current message context: available tool specs + any tool results answering a
276
+ // previous assistant tool call.
277
+ const currentContext = {};
278
+ if (args.tools && args.tools.length > 0) {
279
+ currentContext.tools = args.tools.map((tool) => ({
280
+ toolSpecification: {
281
+ name: tool.name,
282
+ ...(tool.description ? { description: tool.description } : {}),
283
+ inputSchema: { json: normalizeToolSchema(tool.inputSchema) },
284
+ },
285
+ }));
286
+ }
287
+ const currentToolResults = currentTurn && currentTurn.role === "user" ? currentTurn.toolResults : undefined;
288
+ if (currentToolResults && currentToolResults.length > 0) {
289
+ Object.assign(currentContext, toolResultContext(currentToolResults));
290
+ }
291
+ const history = [];
292
+ for (const turn of historyTurns) {
293
+ if (turn.role === "user") {
294
+ const userMessage = {
295
+ content: turn.content,
296
+ modelId: args.modelId,
297
+ origin: MESSAGE_ORIGIN,
298
+ };
299
+ if (turn.toolResults && turn.toolResults.length > 0) {
300
+ userMessage.userInputMessageContext = toolResultContext(turn.toolResults);
301
+ }
302
+ history.push({ userInputMessage: userMessage });
303
+ }
304
+ else {
305
+ const assistantMessage = { content: turn.content };
306
+ if (turn.toolUses && turn.toolUses.length > 0) {
307
+ assistantMessage.toolUses = turn.toolUses.map((toolUse) => ({
308
+ toolUseId: toolUse.toolUseId,
309
+ name: toolUse.name,
310
+ input: toolUse.input,
311
+ }));
312
+ }
313
+ history.push({ assistantResponseMessage: assistantMessage });
314
+ }
315
+ }
316
+ const maxTokens = args.maxTokens ?? DEFAULT_MAX_TOKENS;
317
+ const request = {
318
+ conversationState: {
319
+ chatTriggerType: CHAT_TRIGGER_TYPE,
320
+ conversationId: args.conversationId ?? randomUUID(),
321
+ currentMessage: {
322
+ userInputMessage: {
323
+ content: currentContent,
324
+ modelId: args.modelId,
325
+ origin: MESSAGE_ORIGIN,
326
+ userInputMessageContext: currentContext,
327
+ },
328
+ },
329
+ history,
330
+ },
331
+ inferenceConfig: {
332
+ maxTokens,
333
+ ...(args.temperature !== undefined ? { temperature: args.temperature } : {}),
334
+ ...(args.topP !== undefined ? { topP: args.topP } : {}),
335
+ },
336
+ ...(args.profileArn ? { profileArn: args.profileArn } : {}),
337
+ };
338
+ return request;
339
+ }
340
+ /** Event types that carry assistant-visible text (per 9router's parser). */
341
+ const TEXT_EVENT_TYPES = new Set(["assistantResponseEvent", "codeEvent"]);
342
+ /** Extract the assistant text delta from a single parsed CodeWhisperer event. */
343
+ export function extractAssistantDelta(message) {
344
+ const type = eventType(message);
345
+ // Untyped frames (no `:event-type`) still commonly carry `{ content }`.
346
+ if (type && !TEXT_EVENT_TYPES.has(type)) {
347
+ return "";
348
+ }
349
+ const payload = decodeJsonPayload(message);
350
+ if (!payload) {
351
+ return "";
352
+ }
353
+ // CodeWhisperer assistant/code frames carry `{ content: "..." }`; some variants
354
+ // nest the text under `assistantResponseEvent.content`.
355
+ if (typeof payload.content === "string") {
356
+ return payload.content;
357
+ }
358
+ const nested = payload.assistantResponseEvent;
359
+ if (typeof nested === "object" && nested !== null) {
360
+ const nestedContent = nested.content;
361
+ if (typeof nestedContent === "string") {
362
+ return nestedContent;
363
+ }
364
+ }
365
+ return "";
366
+ }
367
+ /** Check whether a parsed CodeWhisperer event signals an upstream error. */
368
+ export function extractCodeWhispererError(message) {
369
+ const type = eventType(message);
370
+ if (!type || !/error|exception/i.test(type)) {
371
+ return undefined;
372
+ }
373
+ const payload = decodeJsonPayload(message);
374
+ const messageText = payload && typeof payload.message === "string" ? payload.message : `CodeWhisperer ${type}`;
375
+ return messageText;
376
+ }
377
+ /** Concatenate all assistant text from a fully buffered event-stream response. */
378
+ export function collectAssistantText(buffer) {
379
+ const parser = new EventStreamParser();
380
+ const messages = parser.push(buffer);
381
+ let text = "";
382
+ let error;
383
+ for (const message of messages) {
384
+ const errText = extractCodeWhispererError(message);
385
+ if (errText) {
386
+ error = errText;
387
+ continue;
388
+ }
389
+ text += extractAssistantDelta(message);
390
+ }
391
+ return { text, error };
392
+ }
393
+ /** Parse a `toolUseEvent` frame into a partial tool-use delta, if present. */
394
+ export function extractToolUseDelta(message) {
395
+ if (eventType(message) !== "toolUseEvent") {
396
+ return undefined;
397
+ }
398
+ const payload = decodeJsonPayload(message);
399
+ if (!payload) {
400
+ return undefined;
401
+ }
402
+ const toolUseId = typeof payload.toolUseId === "string" ? payload.toolUseId : "";
403
+ if (!toolUseId) {
404
+ return undefined;
405
+ }
406
+ return {
407
+ toolUseId,
408
+ name: typeof payload.name === "string" ? payload.name : undefined,
409
+ inputDelta: typeof payload.input === "string" ? payload.input : undefined,
410
+ stop: payload.stop === true,
411
+ };
412
+ }
413
+ function parseJsonObject(raw) {
414
+ if (!raw.trim()) {
415
+ return {};
416
+ }
417
+ try {
418
+ const parsed = JSON.parse(raw);
419
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
420
+ ? parsed
421
+ : {};
422
+ }
423
+ catch {
424
+ return {};
425
+ }
426
+ }
427
+ /**
428
+ * Accumulates assistant text and tool-use calls across CodeWhisperer event frames.
429
+ * A tool call's `input` arrives as a partial JSON string spread over multiple
430
+ * `toolUseEvent` frames keyed by `toolUseId`, so we concatenate per id and parse
431
+ * once the stream completes. `push` returns the per-frame delta so the streaming
432
+ * path can forward incremental text / tool-input deltas to the client.
433
+ */
434
+ export class KiroResponseAccumulator {
435
+ text = "";
436
+ error;
437
+ toolOrder = [];
438
+ toolNames = new Map();
439
+ toolInputs = new Map();
440
+ push(message) {
441
+ const errText = extractCodeWhispererError(message);
442
+ if (errText) {
443
+ this.error = errText;
444
+ return {};
445
+ }
446
+ const toolDelta = extractToolUseDelta(message);
447
+ if (toolDelta) {
448
+ if (!this.toolNames.has(toolDelta.toolUseId)) {
449
+ this.toolOrder.push(toolDelta.toolUseId);
450
+ this.toolNames.set(toolDelta.toolUseId, toolDelta.name ?? "");
451
+ }
452
+ else if (toolDelta.name) {
453
+ this.toolNames.set(toolDelta.toolUseId, toolDelta.name);
454
+ }
455
+ if (toolDelta.inputDelta) {
456
+ this.toolInputs.set(toolDelta.toolUseId, (this.toolInputs.get(toolDelta.toolUseId) ?? "") + toolDelta.inputDelta);
457
+ }
458
+ return { toolUse: toolDelta };
459
+ }
460
+ const delta = extractAssistantDelta(message);
461
+ if (delta) {
462
+ this.text += delta;
463
+ return { textDelta: delta };
464
+ }
465
+ return {};
466
+ }
467
+ hasToolUses() {
468
+ return this.toolOrder.length > 0;
469
+ }
470
+ toolUses() {
471
+ return this.toolOrder.map((id) => ({
472
+ toolUseId: id,
473
+ name: this.toolNames.get(id) ?? "",
474
+ input: parseJsonObject(this.toolInputs.get(id) ?? ""),
475
+ }));
476
+ }
477
+ }
478
+ /** Rough token estimate (~4 chars/token) used for usage accounting; CW omits usage. */
479
+ export function estimateTokens(text) {
480
+ if (!text) {
481
+ return 0;
482
+ }
483
+ return Math.max(1, Math.ceil(text.length / 4));
484
+ }
485
+ /** Assemble a non-streaming Responses API JSON body from collected assistant text. */
486
+ export function buildResponsesJson(args) {
487
+ const inputTokens = estimateTokens(args.inputText);
488
+ const outputTokens = estimateTokens(args.text);
489
+ const responseId = args.responseId ?? `resp_${randomUUID().replace(/-/g, "")}`;
490
+ const messageId = `msg_${randomUUID().replace(/-/g, "")}`;
491
+ return {
492
+ id: responseId,
493
+ object: "response",
494
+ created_at: args.createdAt ?? Math.floor(Date.now() / 1000),
495
+ status: "completed",
496
+ model: args.model,
497
+ output: [
498
+ {
499
+ type: "message",
500
+ id: messageId,
501
+ status: "completed",
502
+ role: "assistant",
503
+ content: [
504
+ {
505
+ type: "output_text",
506
+ text: args.text,
507
+ annotations: [],
508
+ },
509
+ ],
510
+ },
511
+ ],
512
+ usage: {
513
+ input_tokens: inputTokens,
514
+ output_tokens: outputTokens,
515
+ total_tokens: inputTokens + outputTokens,
516
+ },
517
+ };
518
+ }
519
+ function sseFrame(event, data) {
520
+ return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
521
+ }
522
+ /** Allocate fresh response/message ids for a streaming turn. */
523
+ export function newSseStreamIds(model) {
524
+ return {
525
+ responseId: `resp_${randomUUID().replace(/-/g, "")}`,
526
+ messageId: `msg_${randomUUID().replace(/-/g, "")}`,
527
+ model,
528
+ createdAt: Math.floor(Date.now() / 1000),
529
+ };
530
+ }
531
+ /** Opening SSE frames sent before any assistant text (response/item/part created). */
532
+ export function buildSsePreludeFrames(ids) {
533
+ const baseResponse = {
534
+ id: ids.responseId,
535
+ object: "response",
536
+ created_at: ids.createdAt,
537
+ model: ids.model,
538
+ };
539
+ return [
540
+ sseFrame("response.created", {
541
+ type: "response.created",
542
+ response: { ...baseResponse, status: "in_progress" },
543
+ }),
544
+ sseFrame("response.output_item.added", {
545
+ type: "response.output_item.added",
546
+ output_index: 0,
547
+ item: { type: "message", id: ids.messageId, status: "in_progress", role: "assistant", content: [] },
548
+ }),
549
+ sseFrame("response.content_part.added", {
550
+ type: "response.content_part.added",
551
+ item_id: ids.messageId,
552
+ output_index: 0,
553
+ content_index: 0,
554
+ part: { type: "output_text", text: "", annotations: [] },
555
+ }),
556
+ ];
557
+ }
558
+ /** A single incremental `response.output_text.delta` frame. */
559
+ export function buildSseDeltaFrame(ids, delta) {
560
+ return sseFrame("response.output_text.delta", {
561
+ type: "response.output_text.delta",
562
+ item_id: ids.messageId,
563
+ output_index: 0,
564
+ content_index: 0,
565
+ delta,
566
+ });
567
+ }
568
+ /** Closing SSE frames once all deltas are sent (text/part/item done, completed, [DONE]). */
569
+ export function buildSseFinaleFrames(ids, args) {
570
+ const completedResponse = {
571
+ id: ids.responseId,
572
+ object: "response",
573
+ created_at: ids.createdAt,
574
+ model: ids.model,
575
+ status: "completed",
576
+ output: [
577
+ {
578
+ type: "message",
579
+ id: ids.messageId,
580
+ status: "completed",
581
+ role: "assistant",
582
+ content: [{ type: "output_text", text: args.text, annotations: [] }],
583
+ },
584
+ ],
585
+ usage: {
586
+ input_tokens: args.inputTokens,
587
+ output_tokens: args.outputTokens,
588
+ total_tokens: args.inputTokens + args.outputTokens,
589
+ },
590
+ };
591
+ return [
592
+ sseFrame("response.output_text.done", {
593
+ type: "response.output_text.done",
594
+ item_id: ids.messageId,
595
+ output_index: 0,
596
+ content_index: 0,
597
+ text: args.text,
598
+ }),
599
+ sseFrame("response.content_part.done", {
600
+ type: "response.content_part.done",
601
+ item_id: ids.messageId,
602
+ output_index: 0,
603
+ content_index: 0,
604
+ part: { type: "output_text", text: args.text, annotations: [] },
605
+ }),
606
+ sseFrame("response.output_item.done", {
607
+ type: "response.output_item.done",
608
+ output_index: 0,
609
+ item: {
610
+ type: "message",
611
+ id: ids.messageId,
612
+ status: "completed",
613
+ role: "assistant",
614
+ content: [{ type: "output_text", text: args.text, annotations: [] }],
615
+ },
616
+ }),
617
+ sseFrame("response.completed", { type: "response.completed", response: completedResponse }),
618
+ "data: [DONE]\n\n",
619
+ ];
620
+ }
621
+ /**
622
+ * Build the full ordered list of Responses SSE frames for an already-collected
623
+ * assistant turn (prelude + one delta + finale). Used for tests and as a fallback;
624
+ * the live streaming path emits the same frames incrementally via the helpers above.
625
+ */
626
+ export function buildResponsesSseFrames(args) {
627
+ const ids = newSseStreamIds(args.model);
628
+ if (args.responseId) {
629
+ ids.responseId = args.responseId;
630
+ }
631
+ return [
632
+ ...buildSsePreludeFrames(ids),
633
+ buildSseDeltaFrame(ids, args.text),
634
+ ...buildSseFinaleFrames(ids, {
635
+ text: args.text,
636
+ inputTokens: estimateTokens(args.inputText),
637
+ outputTokens: estimateTokens(args.text),
638
+ }),
639
+ ];
640
+ }
641
+ /** The flattened input text, used for token estimation on the request side. */
642
+ export function collectInputText(body) {
643
+ return flattenResponsesConversation(body)
644
+ .map((turn) => turn.content)
645
+ .join("\n");
646
+ }