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
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
|
+
}
|