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
package/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # responses-proxy
2
+
3
+ AI routing proxy with multi-provider fallback, RTK token saver, and web dashboard.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ npm install -g responses-proxy
9
+ responses-proxy
10
+ ```
11
+
12
+ Dashboard opens at `http://localhost:8318`
13
+
14
+ ## Usage
15
+
16
+ ```bash
17
+ responses-proxy # Start with defaults
18
+ responses-proxy --port 9000 # Custom port
19
+ responses-proxy --no-browser # Don't open browser
20
+ responses-proxy --help # Show options
21
+ ```
22
+
23
+ ## Features
24
+
25
+ - **Multi-provider routing** — Route through Kiro, OpenAI, Anthropic, DeepSeek, etc.
26
+ - **RTK Token Saver** — Compress tool outputs, save 20-40% tokens per request
27
+ - **Model Combos** — Named fallback chains (9Router-style)
28
+ - **Prompt Cache** — Maximize cache hit rates across sessions
29
+ - **CLI Tools** — Auto-configure Claude Code, Codex, Cursor, Cline
30
+ - **Web Dashboard** — Full management UI at localhost
31
+ - **Docker support** — Deploy anywhere with Docker Compose
32
+
33
+ ## Configure CLI Tools
34
+
35
+ ```
36
+ Claude Code / Codex / Cursor / Cline:
37
+ Endpoint: http://localhost:8318/v1
38
+ API Key: [copy from dashboard]
39
+ Model: kr/claude-sonnet-4.5
40
+ ```
41
+
42
+ ## Docker
43
+
44
+ ```bash
45
+ docker run -d --name responses-proxy -p 8318:8318 \
46
+ -v "$HOME/.responses-proxy:/app/logs" \
47
+ ghcr.io/phamtuandat/responses-proxy:latest
48
+ ```
49
+
50
+ ## Documentation
51
+
52
+ - GitHub: https://github.com/phamtuandat/responses-proxy
53
+
54
+ ## License
55
+
56
+ MIT
package/cli.js ADDED
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * responses-proxy CLI — starts the proxy server and opens the dashboard.
4
+ *
5
+ * Usage:
6
+ * npx responses-proxy
7
+ * responses-proxy --port 8318
8
+ * responses-proxy --no-browser
9
+ */
10
+
11
+ const { spawn, exec } = require("child_process");
12
+ const path = require("path");
13
+ const fs = require("fs");
14
+ const os = require("os");
15
+
16
+ const pkg = require("./package.json");
17
+ const args = process.argv.slice(2);
18
+
19
+ // Defaults
20
+ const DEFAULT_PORT = 8318;
21
+ let port = DEFAULT_PORT;
22
+ let host = "0.0.0.0";
23
+ let noBrowser = false;
24
+
25
+ // Parse args
26
+ for (let i = 0; i < args.length; i++) {
27
+ if (args[i] === "--port" || args[i] === "-p") {
28
+ port = parseInt(args[i + 1], 10) || DEFAULT_PORT;
29
+ i++;
30
+ } else if (args[i] === "--host" || args[i] === "-H") {
31
+ host = args[i + 1] || "0.0.0.0";
32
+ i++;
33
+ } else if (args[i] === "--no-browser" || args[i] === "-n") {
34
+ noBrowser = true;
35
+ } else if (args[i] === "--help" || args[i] === "-h") {
36
+ console.log(`
37
+ responses-proxy v${pkg.version}
38
+
39
+ Usage: responses-proxy [options]
40
+
41
+ Options:
42
+ -p, --port <port> Port to run the server (default: ${DEFAULT_PORT})
43
+ -H, --host <host> Host to bind (default: 0.0.0.0)
44
+ -n, --no-browser Don't open browser automatically
45
+ -h, --help Show this help message
46
+ -v, --version Show version
47
+ `);
48
+ process.exit(0);
49
+ } else if (args[i] === "--version" || args[i] === "-v") {
50
+ console.log(pkg.version);
51
+ process.exit(0);
52
+ }
53
+ }
54
+
55
+ // Resolve paths
56
+ const serverPath = path.join(__dirname, "dist", "server.js");
57
+
58
+ if (!fs.existsSync(serverPath)) {
59
+ console.error("Error: Built server not found at", serverPath);
60
+ console.error("Run 'npm run build' first, or reinstall the package.");
61
+ process.exit(1);
62
+ }
63
+
64
+ // Data directory
65
+ const dataDir = path.join(os.homedir(), ".responses-proxy");
66
+ fs.mkdirSync(path.join(dataDir, "sessions"), { recursive: true });
67
+
68
+ const displayHost = host === "0.0.0.0" ? "localhost" : host;
69
+ const url = `http://${displayHost}:${port}`;
70
+
71
+ console.log(`
72
+ ┌─────────────────────────────────────────┐
73
+ │ responses-proxy v${pkg.version.padEnd(25)}│
74
+ │ ${url.padEnd(39)}│
75
+ │ Dashboard: ${(url + "/").padEnd(27)}│
76
+ │ API: ${(url + "/v1").padEnd(33)}│
77
+ └─────────────────────────────────────────┘
78
+ `);
79
+
80
+ // Spawn server
81
+ const server = spawn(process.execPath, [serverPath], {
82
+ cwd: __dirname,
83
+ stdio: "inherit",
84
+ env: {
85
+ ...process.env,
86
+ PORT: String(port),
87
+ HOST: host,
88
+ APP_DB_PATH: path.join(dataDir, "app.sqlite"),
89
+ SESSION_LOG_DIR: path.join(dataDir, "sessions"),
90
+ CUSTOMER_KEY_DB_PATH: path.join(dataDir, "telegram-bot.sqlite"),
91
+ KIRO_DB_PATH: path.join(dataDir, "kiro.sqlite"),
92
+ },
93
+ });
94
+
95
+ // Open browser after short delay
96
+ if (!noBrowser) {
97
+ setTimeout(() => {
98
+ const openCmd =
99
+ process.platform === "darwin" ? `open "${url}"` :
100
+ process.platform === "win32" ? `start "" "${url}"` :
101
+ `xdg-open "${url}"`;
102
+ exec(openCmd, { windowsHide: true }, () => {});
103
+ }, 2000);
104
+ }
105
+
106
+ // Handle exit
107
+ function cleanup() {
108
+ if (server.pid) {
109
+ try { process.kill(server.pid, "SIGTERM"); } catch {}
110
+ }
111
+ }
112
+
113
+ process.on("SIGINT", () => { cleanup(); process.exit(0); });
114
+ process.on("SIGTERM", () => { cleanup(); process.exit(0); });
115
+
116
+ server.on("close", (code) => {
117
+ process.exit(code || 0);
118
+ });
@@ -0,0 +1,383 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { estimateTokens, } from "./kiro-codewhisperer.js";
3
+ /**
4
+ * Translates between the Anthropic Messages API (what Claude Code speaks) and the
5
+ * structured turn/tool model the Kiro/CodeWhisperer forwarder consumes.
6
+ *
7
+ * Anthropic request → { turns, tools } for buildCodeWhispererRequestFromTurns.
8
+ * CodeWhisperer response → Anthropic `message` JSON or the Anthropic SSE event
9
+ * sequence (message_start → content_block_* → message_delta → message_stop).
10
+ */
11
+ const ANTHROPIC_MAX_TOKENS_DEFAULT = 32000;
12
+ function readString(value) {
13
+ return typeof value === "string" ? value : "";
14
+ }
15
+ function readNumber(value) {
16
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
17
+ }
18
+ /** Extract plain text from an Anthropic system field (string or text-block array). */
19
+ function extractSystemText(system) {
20
+ if (typeof system === "string") {
21
+ return system.trim();
22
+ }
23
+ if (!Array.isArray(system)) {
24
+ return "";
25
+ }
26
+ const parts = [];
27
+ for (const block of system) {
28
+ if (typeof block === "object" && block !== null) {
29
+ const record = block;
30
+ if (record.type === "text" && typeof record.text === "string") {
31
+ parts.push(record.text);
32
+ }
33
+ }
34
+ }
35
+ return parts.join("\n\n").trim();
36
+ }
37
+ /** Extract text from a tool_result `content` (string or array of text blocks). */
38
+ function extractToolResultText(content) {
39
+ if (typeof content === "string") {
40
+ return content;
41
+ }
42
+ if (!Array.isArray(content)) {
43
+ return "";
44
+ }
45
+ const parts = [];
46
+ for (const block of content) {
47
+ if (typeof block === "object" && block !== null) {
48
+ const record = block;
49
+ if (typeof record.text === "string") {
50
+ parts.push(record.text);
51
+ }
52
+ }
53
+ }
54
+ return parts.join("");
55
+ }
56
+ /** Parse an Anthropic message `content` (string or block array) into its parts. */
57
+ function parseMessageContent(content) {
58
+ const result = { text: "", toolUses: [], toolResults: [] };
59
+ if (typeof content === "string") {
60
+ result.text = content;
61
+ return result;
62
+ }
63
+ if (!Array.isArray(content)) {
64
+ return result;
65
+ }
66
+ const textParts = [];
67
+ for (const block of content) {
68
+ if (typeof block !== "object" || block === null) {
69
+ continue;
70
+ }
71
+ const record = block;
72
+ switch (record.type) {
73
+ case "text":
74
+ if (typeof record.text === "string") {
75
+ textParts.push(record.text);
76
+ }
77
+ break;
78
+ case "tool_use":
79
+ result.toolUses.push({
80
+ toolUseId: readString(record.id),
81
+ name: readString(record.name),
82
+ input: typeof record.input === "object" && record.input !== null
83
+ ? record.input
84
+ : {},
85
+ });
86
+ break;
87
+ case "tool_result":
88
+ result.toolResults.push({
89
+ toolUseId: readString(record.tool_use_id),
90
+ content: extractToolResultText(record.content),
91
+ status: record.is_error === true ? "error" : "success",
92
+ });
93
+ break;
94
+ default:
95
+ // image / other blocks are not translated in v1.
96
+ break;
97
+ }
98
+ }
99
+ result.text = textParts.join("");
100
+ return result;
101
+ }
102
+ /** Parse Anthropic `tools` into CodeWhisperer tool specs (input_schema → inputSchema). */
103
+ function parseTools(tools) {
104
+ if (!Array.isArray(tools)) {
105
+ return [];
106
+ }
107
+ const specs = [];
108
+ for (const tool of tools) {
109
+ if (typeof tool !== "object" || tool === null) {
110
+ continue;
111
+ }
112
+ const record = tool;
113
+ const name = readString(record.name);
114
+ if (!name) {
115
+ continue;
116
+ }
117
+ const schema = typeof record.input_schema === "object" && record.input_schema !== null
118
+ ? record.input_schema
119
+ : {};
120
+ specs.push({
121
+ name,
122
+ description: readString(record.description) || undefined,
123
+ inputSchema: schema,
124
+ });
125
+ }
126
+ return specs;
127
+ }
128
+ /**
129
+ * Parse an Anthropic Messages request body into structured turns + tools. The
130
+ * system prompt is folded into the first user turn (CodeWhisperer has no system
131
+ * slot), matching how the Responses path handles instructions.
132
+ */
133
+ export function parseAnthropicRequest(body) {
134
+ const systemText = extractSystemText(body.system);
135
+ const rawMessages = Array.isArray(body.messages) ? body.messages : [];
136
+ const turns = [];
137
+ for (const message of rawMessages) {
138
+ if (typeof message !== "object" || message === null) {
139
+ continue;
140
+ }
141
+ const record = message;
142
+ const role = record.role === "assistant" ? "assistant" : "user";
143
+ const parsed = parseMessageContent(record.content);
144
+ if (role === "assistant") {
145
+ turns.push({
146
+ role: "assistant",
147
+ content: parsed.text,
148
+ ...(parsed.toolUses.length > 0 ? { toolUses: parsed.toolUses } : {}),
149
+ });
150
+ }
151
+ else {
152
+ turns.push({
153
+ role: "user",
154
+ content: parsed.text,
155
+ ...(parsed.toolResults.length > 0 ? { toolResults: parsed.toolResults } : {}),
156
+ });
157
+ }
158
+ }
159
+ if (systemText) {
160
+ const firstUserIndex = turns.findIndex((turn) => turn.role === "user");
161
+ if (firstUserIndex >= 0) {
162
+ const existing = turns[firstUserIndex];
163
+ turns[firstUserIndex] = {
164
+ ...existing,
165
+ content: existing.content ? `${systemText}\n\n${existing.content}` : systemText,
166
+ };
167
+ }
168
+ else {
169
+ turns.unshift({ role: "user", content: systemText });
170
+ }
171
+ }
172
+ const inputText = [systemText, ...turns.map((turn) => turn.content)].filter(Boolean).join("\n");
173
+ return {
174
+ model: readString(body.model),
175
+ turns,
176
+ tools: parseTools(body.tools),
177
+ maxTokens: readNumber(body.max_tokens) ?? ANTHROPIC_MAX_TOKENS_DEFAULT,
178
+ temperature: readNumber(body.temperature),
179
+ topP: readNumber(body.top_p),
180
+ stream: body.stream === true,
181
+ inputText,
182
+ };
183
+ }
184
+ function newMessageId() {
185
+ return `msg_${randomUUID().replace(/-/g, "")}`;
186
+ }
187
+ /** Build a non-streaming Anthropic `message` response from collected output. */
188
+ export function buildAnthropicMessage(args) {
189
+ const content = [];
190
+ if (args.text) {
191
+ content.push({ type: "text", text: args.text });
192
+ }
193
+ for (const toolUse of args.toolUses) {
194
+ content.push({
195
+ type: "tool_use",
196
+ id: toolUse.toolUseId,
197
+ name: toolUse.name,
198
+ input: toolUse.input,
199
+ });
200
+ }
201
+ // Anthropic requires at least one content block.
202
+ if (content.length === 0) {
203
+ content.push({ type: "text", text: "" });
204
+ }
205
+ return {
206
+ id: args.messageId ?? newMessageId(),
207
+ type: "message",
208
+ role: "assistant",
209
+ model: args.model,
210
+ content,
211
+ stop_reason: args.toolUses.length > 0 ? "tool_use" : "end_turn",
212
+ stop_sequence: null,
213
+ usage: { input_tokens: args.inputTokens, output_tokens: args.outputTokens },
214
+ };
215
+ }
216
+ /** Build the `/v1/messages/count_tokens` response body (estimated). */
217
+ export function buildCountTokensResponse(inputText) {
218
+ return { input_tokens: estimateTokens(inputText) };
219
+ }
220
+ function sseFrame(event, data) {
221
+ return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
222
+ }
223
+ /**
224
+ * Stateful emitter for the Anthropic Messages SSE sequence. Drives content blocks
225
+ * as text and tool-use deltas arrive from the CodeWhisperer stream, opening/closing
226
+ * indexed blocks per the Anthropic protocol:
227
+ * message_start → (content_block_start → content_block_delta* → content_block_stop)*
228
+ * → message_delta → message_stop
229
+ */
230
+ export class AnthropicSseEmitter {
231
+ messageId;
232
+ model;
233
+ inputTokens;
234
+ nextIndex = 0;
235
+ current = null;
236
+ sawToolUse = false;
237
+ startedToolIds = new Set();
238
+ constructor(args) {
239
+ this.messageId = args.messageId ?? newMessageId();
240
+ this.model = args.model;
241
+ this.inputTokens = args.inputTokens;
242
+ }
243
+ /** Opening frames: message_start + an initial ping. */
244
+ start() {
245
+ return [
246
+ sseFrame("message_start", {
247
+ type: "message_start",
248
+ message: {
249
+ id: this.messageId,
250
+ type: "message",
251
+ role: "assistant",
252
+ model: this.model,
253
+ content: [],
254
+ stop_reason: null,
255
+ stop_sequence: null,
256
+ usage: { input_tokens: this.inputTokens, output_tokens: 0 },
257
+ },
258
+ }),
259
+ sseFrame("ping", { type: "ping" }),
260
+ ];
261
+ }
262
+ closeCurrent() {
263
+ if (!this.current) {
264
+ return [];
265
+ }
266
+ const frame = sseFrame("content_block_stop", {
267
+ type: "content_block_stop",
268
+ index: this.current.index,
269
+ });
270
+ this.current = null;
271
+ return [frame];
272
+ }
273
+ /** Emit a text delta, opening a text content block if one is not already open. */
274
+ textDelta(text) {
275
+ if (!text) {
276
+ return [];
277
+ }
278
+ const frames = [];
279
+ if (this.current && this.current.type !== "text") {
280
+ frames.push(...this.closeCurrent());
281
+ }
282
+ if (!this.current) {
283
+ const index = this.nextIndex++;
284
+ this.current = { index, type: "text" };
285
+ frames.push(sseFrame("content_block_start", {
286
+ type: "content_block_start",
287
+ index,
288
+ content_block: { type: "text", text: "" },
289
+ }));
290
+ }
291
+ frames.push(sseFrame("content_block_delta", {
292
+ type: "content_block_delta",
293
+ index: this.current.index,
294
+ delta: { type: "text_delta", text },
295
+ }));
296
+ return frames;
297
+ }
298
+ /** Emit a tool-use delta, managing tool_use content blocks (one per toolUseId). */
299
+ toolUseDelta(delta) {
300
+ this.sawToolUse = true;
301
+ const frames = [];
302
+ const isNewBlock = !this.current ||
303
+ this.current.type !== "tool" ||
304
+ this.current.toolUseId !== delta.toolUseId;
305
+ if (isNewBlock) {
306
+ frames.push(...this.closeCurrent());
307
+ const index = this.nextIndex++;
308
+ this.current = { index, type: "tool", toolUseId: delta.toolUseId };
309
+ this.startedToolIds.add(delta.toolUseId);
310
+ frames.push(sseFrame("content_block_start", {
311
+ type: "content_block_start",
312
+ index,
313
+ content_block: {
314
+ type: "tool_use",
315
+ id: delta.toolUseId,
316
+ name: delta.name ?? "",
317
+ input: {},
318
+ },
319
+ }));
320
+ }
321
+ if (delta.inputDelta) {
322
+ frames.push(sseFrame("content_block_delta", {
323
+ type: "content_block_delta",
324
+ index: this.current.index,
325
+ delta: { type: "input_json_delta", partial_json: delta.inputDelta },
326
+ }));
327
+ }
328
+ return frames;
329
+ }
330
+ /** Closing frames: close any open block, message_delta (stop_reason + usage), message_stop. */
331
+ finish(outputTokens) {
332
+ const frames = [];
333
+ frames.push(...this.closeCurrent());
334
+ frames.push(sseFrame("message_delta", {
335
+ type: "message_delta",
336
+ delta: {
337
+ stop_reason: this.sawToolUse ? "tool_use" : "end_turn",
338
+ stop_sequence: null,
339
+ },
340
+ usage: { output_tokens: outputTokens },
341
+ }));
342
+ frames.push(sseFrame("message_stop", { type: "message_stop" }));
343
+ return frames;
344
+ }
345
+ }
346
+ /** Build an Anthropic-shaped error envelope. */
347
+ export function buildAnthropicError(type, message) {
348
+ return { type: "error", error: { type, message } };
349
+ }
350
+ function humanizeModelId(id) {
351
+ return id
352
+ .split(/[-.]/)
353
+ .map((part) => (part ? part[0].toUpperCase() + part.slice(1) : part))
354
+ .join(" ");
355
+ }
356
+ /**
357
+ * Build an Anthropic-format model listing (`GET /v1/models`). Claude Code performs
358
+ * a preflight model lookup against this endpoint and refuses to start if the
359
+ * configured model is absent, so we surface every Kiro model id/alias here.
360
+ */
361
+ export function buildAnthropicModelsList(modelIds) {
362
+ const seen = new Set();
363
+ const data = [];
364
+ for (const id of modelIds) {
365
+ const trimmed = id.trim();
366
+ if (!trimmed || seen.has(trimmed)) {
367
+ continue;
368
+ }
369
+ seen.add(trimmed);
370
+ data.push({
371
+ type: "model",
372
+ id: trimmed,
373
+ display_name: humanizeModelId(trimmed),
374
+ created_at: "2025-01-01T00:00:00Z",
375
+ });
376
+ }
377
+ return {
378
+ data,
379
+ has_more: false,
380
+ first_id: data.length > 0 ? data[0].id : null,
381
+ last_id: data.length > 0 ? data[data.length - 1].id : null,
382
+ };
383
+ }