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.
- package/README.md +56 -0
- package/cli.js +118 -0
- package/dist/anthropic-messages.js +383 -0
- package/dist/anthropic-messages.test.js +209 -0
- package/dist/audit-log.js +138 -0
- package/dist/audit-log.test.js +480 -0
- package/dist/billing-expiration.js +70 -0
- package/dist/billing-expiration.test.js +114 -0
- package/dist/billing.js +716 -0
- package/dist/billing.test.js +228 -0
- package/dist/chatgpt-oauth-store.js +240 -0
- package/dist/chatgpt-oauth-store.test.js +88 -0
- package/dist/chatgpt-oauth.js +118 -0
- package/dist/chatgpt-oauth.test.js +63 -0
- package/dist/chatgpt-provider-auth.js +60 -0
- package/dist/chatgpt-provider-auth.test.js +101 -0
- package/dist/client/app-icon.svg +17 -0
- package/dist/client/assets/index-C7Vvhst8.js +14 -0
- package/dist/client/assets/index-DpqgYK3L.css +1 -0
- package/dist/client/favicon.svg +17 -0
- package/dist/client/index.html +31 -0
- package/dist/client-config-apply.js +345 -0
- package/dist/client-config-apply.test.js +185 -0
- package/dist/client-token-limits.js +111 -0
- package/dist/client-token-limits.test.js +129 -0
- package/dist/codex-config.js +47 -0
- package/dist/codex-setup.js +87 -0
- package/dist/codex-setup.test.js +30 -0
- package/dist/config.js +314 -0
- package/dist/cost-analytics.js +31 -0
- package/dist/cost-analytics.test.js +38 -0
- package/dist/customer-key-access.js +126 -0
- package/dist/customer-key-access.test.js +178 -0
- package/dist/customer-keys.js +209 -0
- package/dist/customer-keys.test.js +68 -0
- package/dist/customer-usage.js +18 -0
- package/dist/customer-usage.test.js +55 -0
- package/dist/dashboard-auth.js +318 -0
- package/dist/dashboard-auth.test.js +133 -0
- package/dist/dashboard-serving.test.js +235 -0
- package/dist/error-response.js +174 -0
- package/dist/error-response.test.js +88 -0
- package/dist/forward.js +357 -0
- package/dist/health-websocket-manager.js +174 -0
- package/dist/http-rate-limit.js +36 -0
- package/dist/http-rate-limit.test.js +62 -0
- package/dist/kiro-auth.js +136 -0
- package/dist/kiro-auth.test.js +234 -0
- package/dist/kiro-codewhisperer.js +646 -0
- package/dist/kiro-codewhisperer.test.js +219 -0
- package/dist/kiro-device-login.js +338 -0
- package/dist/kiro-eventstream.js +219 -0
- package/dist/kiro-eventstream.test.js +79 -0
- package/dist/kiro-forward.js +401 -0
- package/dist/kiro-import-cli.js +69 -0
- package/dist/kiro-import.js +94 -0
- package/dist/kiro-import.test.js +125 -0
- package/dist/kiro-token-store.js +196 -0
- package/dist/kiro-token-store.test.js +207 -0
- package/dist/krouter-usage.js +243 -0
- package/dist/model-combo-repository.js +147 -0
- package/dist/model-routing.js +69 -0
- package/dist/model-routing.test.js +41 -0
- package/dist/normalize-request.js +531 -0
- package/dist/normalize-request.test.js +277 -0
- package/dist/omv-public-firewall.test.js +11 -0
- package/dist/package.json +17 -0
- package/dist/prompt-cache-state.js +146 -0
- package/dist/prompt-cache-state.test.js +71 -0
- package/dist/prompt-cache.js +229 -0
- package/dist/provider-health-service.js +404 -0
- package/dist/provider-request-parameters.js +107 -0
- package/dist/provider-request-parameters.test.js +26 -0
- package/dist/provider-routing.js +114 -0
- package/dist/provider-routing.test.js +64 -0
- package/dist/provider-usage.js +314 -0
- package/dist/request-timeout-policy.js +61 -0
- package/dist/request-timeout-policy.test.js +40 -0
- package/dist/response-cache.js +69 -0
- package/dist/response-cache.test.js +28 -0
- package/dist/routing-combo-repository.js +300 -0
- package/dist/routing-engine.js +377 -0
- package/dist/routing-integration.js +155 -0
- package/dist/routing-simulation-engine.js +326 -0
- package/dist/rtk-layer.js +483 -0
- package/dist/rtk-layer.test.js +198 -0
- package/dist/runtime-provider-repository.js +1742 -0
- package/dist/runtime-provider-repository.test.js +1177 -0
- package/dist/schema.js +118 -0
- package/dist/schema.test.js +16 -0
- package/dist/sepay-webhook.js +87 -0
- package/dist/sepay-webhook.test.js +142 -0
- package/dist/server-body-limit.test.js +35 -0
- package/dist/server-client-token-limits.test.js +161 -0
- package/dist/server-codex-config-setup.test.js +76 -0
- package/dist/server-http-rate-limit.test.js +80 -0
- package/dist/server-response-cache.test.js +105 -0
- package/dist/server-routes-alias.test.js +39 -0
- package/dist/server-sepay-webhook-security.test.js +59 -0
- package/dist/server.js +5906 -0
- package/dist/session-log.js +178 -0
- package/dist/tailnet-funnel-script.test.js +33 -0
- package/dist/telegram-bot/actions.js +118 -0
- package/dist/telegram-bot/admin-actions.js +103 -0
- package/dist/telegram-bot/auth.js +46 -0
- package/dist/telegram-bot/auth.test.js +1 -0
- package/dist/telegram-bot/bot-identity-repository.js +189 -0
- package/dist/telegram-bot/bot-identity-repository.test.js +78 -0
- package/dist/telegram-bot/callbacks.js +30 -0
- package/dist/telegram-bot/codex-config-delivery.js +38 -0
- package/dist/telegram-bot/codex-config-delivery.test.js +75 -0
- package/dist/telegram-bot/commands/accounts.js +140 -0
- package/dist/telegram-bot/commands/apikey.js +737 -0
- package/dist/telegram-bot/commands/apply.js +265 -0
- package/dist/telegram-bot/commands/clients.js +13 -0
- package/dist/telegram-bot/commands/customer-billing.test.js +271 -0
- package/dist/telegram-bot/commands/grant.js +138 -0
- package/dist/telegram-bot/commands/grant.test.js +217 -0
- package/dist/telegram-bot/commands/help.js +52 -0
- package/dist/telegram-bot/commands/me.js +53 -0
- package/dist/telegram-bot/commands/models.js +6 -0
- package/dist/telegram-bot/commands/oauth.js +64 -0
- package/dist/telegram-bot/commands/plans.js +96 -0
- package/dist/telegram-bot/commands/providers.js +27 -0
- package/dist/telegram-bot/commands/quota.js +10 -0
- package/dist/telegram-bot/commands/renew-user.js +139 -0
- package/dist/telegram-bot/commands/renew-user.test.js +184 -0
- package/dist/telegram-bot/commands/renew.js +1369 -0
- package/dist/telegram-bot/commands/renew.test.js +1633 -0
- package/dist/telegram-bot/commands/start.js +212 -0
- package/dist/telegram-bot/commands/start.test.js +280 -0
- package/dist/telegram-bot/commands/status.js +6 -0
- package/dist/telegram-bot/commands/tailscale.js +15 -0
- package/dist/telegram-bot/commands/tailscale.test.js +76 -0
- package/dist/telegram-bot/commands/test.js +51 -0
- package/dist/telegram-bot/commands/test.test.js +14 -0
- package/dist/telegram-bot/commands/usage.js +10 -0
- package/dist/telegram-bot/config.js +98 -0
- package/dist/telegram-bot/config.test.js +42 -0
- package/dist/telegram-bot/customer-actions.js +160 -0
- package/dist/telegram-bot/customer-api-keys.js +68 -0
- package/dist/telegram-bot/customer-billing.js +72 -0
- package/dist/telegram-bot/customer-workspace-repository.js +134 -0
- package/dist/telegram-bot/customer-workspace-repository.test.js +47 -0
- package/dist/telegram-bot/dashboard-login.js +39 -0
- package/dist/telegram-bot/format.js +140 -0
- package/dist/telegram-bot/grants.js +370 -0
- package/dist/telegram-bot/grants.test.js +290 -0
- package/dist/telegram-bot/index.js +85 -0
- package/dist/telegram-bot/message-cleanup.js +55 -0
- package/dist/telegram-bot/message-cleanup.test.js +77 -0
- package/dist/telegram-bot/message-format.js +45 -0
- package/dist/telegram-bot/message-format.test.js +10 -0
- package/dist/telegram-bot/proxy-client.js +174 -0
- package/dist/telegram-bot/rate-limit.js +95 -0
- package/dist/telegram-bot/rate-limit.test.js +58 -0
- package/dist/telegram-bot/sessions.js +171 -0
- package/dist/telegram-bot/sessions.test.js +107 -0
- package/dist/telegram-bot/telegram-adapter.js +126 -0
- package/dist/telegram-bot/worker.js +63 -0
- 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
|
+
}
|