@stage-labs/metro 0.1.0-beta.5 → 0.1.0-beta.50
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/dist/api.js +148 -0
- package/dist/bedrock-eventstream.js +120 -0
- package/dist/bedrock-proxy.js +260 -0
- package/dist/bedrock-upstream.js +72 -0
- package/dist/bedrock.js +106 -0
- package/dist/claude.js +15 -0
- package/dist/cli.js +198 -0
- package/dist/control.js +173 -0
- package/dist/local.js +67 -0
- package/dist/plugin.js +82 -0
- package/dist/prompt.js +61 -0
- package/dist/runtime.js +117 -0
- package/dist/serve.js +109 -0
- package/dist/store.js +93 -0
- package/dist/tail.js +120 -0
- package/dist/update.js +33 -0
- package/dist/version.js +97 -0
- package/package.json +34 -23
- package/runtime/node_modules/@metro-labs/discord-bot/package.json +28 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/accounts.ts +118 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +388 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/api-base.ts +1 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/attachments.ts +29 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/format.ts +210 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/group-actions.ts +50 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/group.ts +115 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/index.ts +127 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/members.ts +215 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/send-files.ts +43 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/station.ts +19 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/verify.ts +82 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/voice-speak.ts +195 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/voice-transcribe.ts +292 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/voice.ts +197 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/wire.ts +5 -0
- package/runtime/node_modules/@metro-labs/mcp/package.json +63 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/dedupe.ts +24 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/inbound.ts +367 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/media-note.ts +85 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/pending.ts +75 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/relay.ts +129 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/webhook-note.ts +51 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/account-api.ts +311 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-api.ts +334 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-connector-api.ts +150 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-import.ts +76 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-pair.ts +20 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-error.ts +8 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-http.ts +152 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-grant.ts +99 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-owner.ts +32 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-serve.ts +130 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-session.ts +278 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +374 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/bundle-api.ts +112 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/claude-api.ts +101 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/claude-files.ts +329 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair-api.ts +141 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connect-hint.ts +39 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-api.ts +315 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-json.ts +21 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth-routes.ts +157 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth.ts +127 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-tools.ts +98 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-url.ts +70 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-verify.ts +330 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/cors.ts +29 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/crash-guard.ts +49 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/events.ts +117 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/http.ts +368 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity.ts +7 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/import-api.ts +68 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/is-record.ts +2 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-cli-api.ts +70 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-mode.ts +232 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-owner.ts +14 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-secret.ts +21 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/log.ts +42 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/mode-api.ts +32 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-client.ts +159 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-discovery.ts +117 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-pending.ts +65 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/pair-codes.ts +58 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/paths.ts +112 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/project-api.ts +214 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/protocol.ts +224 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/relay.ts +304 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-api.ts +139 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/runtime-source.ts +105 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/secure-fs.ts +57 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-api.ts +27 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-apis.ts +66 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-config.ts +22 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session.ts +177 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/siwe-auth.ts +73 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/siwe-nonces.ts +43 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/siwe-routes.ts +89 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor-io.ts +97 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor.ts +320 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-call.ts +22 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-error.ts +53 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/tunnel.ts +175 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-api.ts +295 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-store.ts +181 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/vault-api.ts +78 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/version.ts +2 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/account-attach.ts +91 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-admin.ts +214 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-connectors.ts +143 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-map.ts +84 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-scope.ts +64 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/client.ts +37 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/connector-config.ts +144 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/connector-relay.ts +126 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/connectors.ts +336 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/file-admin.ts +375 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/file-source.ts +161 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/ids.ts +21 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/key-map.ts +45 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/local-connectors.ts +322 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +391 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/projects.ts +309 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/runtime-admin.ts +24 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/runtimes.ts +105 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +123 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/users.ts +72 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/vault.ts +119 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/whatsapp-login.ts +18 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/accounts.ts +141 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/call-tools.ts +203 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/channel-owner.ts +29 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/ctx.ts +62 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/event-store.ts +91 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/frame-scope.ts +21 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/group-tools.ts +129 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/index.ts +192 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/member-tools.ts +72 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/permission-relay.ts +66 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/raw-get-stream.ts +207 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/request-identity.ts +73 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-registry.ts +172 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-route.ts +43 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/session.ts +211 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/str.ts +5 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-dispatch.ts +212 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-schemas.ts +397 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/upload-tool.ts +66 -0
- package/runtime/node_modules/@metro-labs/mcp/src/monitor/api.ts +240 -0
- package/runtime/node_modules/@metro-labs/mcp/src/server.ts +2 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/account-store.ts +60 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-inline.ts +100 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-interactive.ts +161 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-resolve.ts +200 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-xmtp.ts +111 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach.ts +182 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attachments.ts +196 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/lines.ts +82 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/messaging-normalize.ts +88 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/registry.ts +35 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/station-runtime.ts +64 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/types.ts +107 -0
- package/runtime/node_modules/@metro-labs/telegram/package.json +26 -0
- package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +80 -0
- package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +254 -0
- package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +87 -0
- package/runtime/node_modules/@metro-labs/telegram/src/client.ts +77 -0
- package/runtime/node_modules/@metro-labs/telegram/src/format.ts +141 -0
- package/runtime/node_modules/@metro-labs/telegram/src/group.ts +261 -0
- package/runtime/node_modules/@metro-labs/telegram/src/history.ts +52 -0
- package/runtime/node_modules/@metro-labs/telegram/src/inbound.ts +78 -0
- package/runtime/node_modules/@metro-labs/telegram/src/index.ts +46 -0
- package/runtime/node_modules/@metro-labs/telegram/src/login.ts +166 -0
- package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +80 -0
- package/runtime/node_modules/@metro-labs/telegram/src/members.ts +75 -0
- package/runtime/node_modules/@metro-labs/telegram/src/normalize.ts +22 -0
- package/runtime/node_modules/@metro-labs/telegram/src/reactions.ts +167 -0
- package/runtime/node_modules/@metro-labs/telegram/src/station.ts +24 -0
- package/runtime/node_modules/@metro-labs/telegram/src/types.ts +7 -0
- package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +1 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/package.json +25 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +119 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +295 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/attachments.ts +128 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +172 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/index.ts +103 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +166 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/members.ts +47 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/station.ts +17 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/types.ts +48 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/verify.ts +38 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/wire.ts +3 -0
- package/runtime/node_modules/@metro-labs/webhook/package.json +23 -0
- package/runtime/node_modules/@metro-labs/webhook/src/station.ts +37 -0
- package/runtime/node_modules/@metro-labs/whatsapp/package.json +29 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +66 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/ack.ts +101 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +178 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/attachments.ts +72 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/auth-state.ts +129 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/client.ts +318 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/delivery.ts +49 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +132 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/inbound.ts +57 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/index.ts +40 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/keys.ts +59 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/logger.ts +60 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/login.ts +176 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/media.ts +84 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/normalize.ts +22 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/outbox.ts +38 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/parse.ts +141 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/station.ts +17 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/token-store.ts +98 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/types.ts +6 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/wire.ts +5 -0
- package/runtime/node_modules/@metro-labs/xmtp/package.json +33 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +112 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-close.ts +110 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +299 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +157 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-push.ts +87 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +375 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/attachments.ts +107 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/codecs.ts +171 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/conv-helpers.ts +239 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/emit-core.ts +55 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/emit-payloads.ts +333 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +78 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/group-members.ts +34 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/group.ts +56 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/identity.ts +33 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +125 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/labels.ts +157 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/member-args.ts +40 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/push.ts +349 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/station.ts +13 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/tools-handlers.ts +126 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/tools.ts +212 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +90 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/verify.ts +84 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +39 -0
- package/runtime/runtime.json +3 -0
- package/runtime/trains/.keep +0 -0
- package/LICENSE +0 -21
- package/README.md +0 -277
- package/dist/cli/index.js +0 -236
- package/dist/cli/lines.js +0 -40
- package/dist/cli/update.js +0 -27
- package/dist/dispatcher.js +0 -203
- package/dist/helpers/async-queue.js +0 -41
- package/dist/helpers/scope-cache.js +0 -67
- package/dist/helpers/streaming.js +0 -219
- package/dist/helpers/turn.js +0 -62
- package/dist/log.js +0 -15
- package/dist/paths.js +0 -68
- package/dist/stations/claude/index.js +0 -224
- package/dist/stations/codex/index.js +0 -226
- package/dist/stations/discord/index.js +0 -160
- package/dist/stations/github/index.js +0 -135
- package/dist/stations/line.js +0 -54
- package/dist/stations/listing.js +0 -16
- package/dist/stations/send.js +0 -19
- package/dist/stations/telegram/files.js +0 -31
- package/dist/stations/telegram/format.js +0 -39
- package/dist/stations/telegram/index.js +0 -209
- package/dist/stations/types.js +0 -2
- package/docs/agents.md +0 -92
- package/docs/uri-scheme.md +0 -52
package/dist/api.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { metroUrl, readRunTokens, readToken } from './store.js';
|
|
2
|
+
import { localAgents, localDaemonUp, localMcpServers, pickLocalAgent } from './local.js';
|
|
3
|
+
import { localUrl } from './runtime.js';
|
|
4
|
+
export class NotSignedIn extends Error {
|
|
5
|
+
}
|
|
6
|
+
const TIMEOUT_MS = 20_000;
|
|
7
|
+
const LOOPBACK = new Set(['localhost', '127.0.0.1', '::1', '[::1]']);
|
|
8
|
+
export function carriesSecretsSafely(base) {
|
|
9
|
+
let url;
|
|
10
|
+
try {
|
|
11
|
+
url = new URL(base);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
if (url.protocol === 'https:')
|
|
17
|
+
return true;
|
|
18
|
+
return url.protocol === 'http:' && LOOPBACK.has(url.hostname);
|
|
19
|
+
}
|
|
20
|
+
function tokenOrThrow() {
|
|
21
|
+
if (!carriesSecretsSafely(metroUrl()))
|
|
22
|
+
throw new Error(`refusing to send your session to ${metroUrl()} in the clear — use https, or a loopback address`);
|
|
23
|
+
const token = readToken() ?? soleRunToken();
|
|
24
|
+
if (token === null)
|
|
25
|
+
throw new NotSignedIn('not signed in — run `metro login`, or `metro start <agent-id>` on this machine');
|
|
26
|
+
return token;
|
|
27
|
+
}
|
|
28
|
+
function soleRunToken() {
|
|
29
|
+
const tokens = readRunTokens();
|
|
30
|
+
return tokens.length === 1 ? (tokens[0] ?? null) : null;
|
|
31
|
+
}
|
|
32
|
+
async function get(path) {
|
|
33
|
+
const auth = tokenOrThrow();
|
|
34
|
+
let res;
|
|
35
|
+
try {
|
|
36
|
+
res = await fetch(`${metroUrl()}${path}`, {
|
|
37
|
+
headers: { authorization: `Bearer ${auth}` },
|
|
38
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
throw new Error(`could not reach ${metroUrl()}`);
|
|
43
|
+
}
|
|
44
|
+
if (res.status === 401)
|
|
45
|
+
throw new NotSignedIn("metro refused this machine's sign-in — run `metro login` again");
|
|
46
|
+
if (!res.ok)
|
|
47
|
+
throw new Error(`metro answered ${String(res.status)}`);
|
|
48
|
+
return res.json();
|
|
49
|
+
}
|
|
50
|
+
function isRecord(value) {
|
|
51
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
52
|
+
}
|
|
53
|
+
async function post(path, body) {
|
|
54
|
+
if (!carriesSecretsSafely(metroUrl()))
|
|
55
|
+
throw new Error(`refusing to send your code to ${metroUrl()} in the clear — use https, or a loopback address`);
|
|
56
|
+
let res;
|
|
57
|
+
try {
|
|
58
|
+
res = await fetch(`${metroUrl()}${path}`, {
|
|
59
|
+
method: 'POST',
|
|
60
|
+
headers: { 'content-type': 'application/json' },
|
|
61
|
+
body: JSON.stringify(body),
|
|
62
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
throw new Error(`could not reach ${metroUrl()}`);
|
|
67
|
+
}
|
|
68
|
+
const parsed = await res.json().catch(() => null);
|
|
69
|
+
if (res.ok)
|
|
70
|
+
return parsed;
|
|
71
|
+
const detail = isRecord(parsed) ? parsed.error : undefined;
|
|
72
|
+
throw new Error(typeof detail === 'string' ? detail : `metro answered ${String(res.status)}`);
|
|
73
|
+
}
|
|
74
|
+
export class RuntimeRevoked extends Error {
|
|
75
|
+
}
|
|
76
|
+
export function keyOfRunConfig(body) {
|
|
77
|
+
const agent = isRecord(body) ? body.agent : undefined;
|
|
78
|
+
const key = isRecord(agent) ? agent.key : undefined;
|
|
79
|
+
if (typeof key !== 'string' || key === '')
|
|
80
|
+
throw new Error('this agent has no key — reset it on its page in the web UI');
|
|
81
|
+
return key;
|
|
82
|
+
}
|
|
83
|
+
export async function runConfigKey(token) {
|
|
84
|
+
if (!carriesSecretsSafely(metroUrl()))
|
|
85
|
+
throw new Error(`refusing to send this machine's authorization to ${metroUrl()} in the clear — use https, or a loopback address`);
|
|
86
|
+
let res;
|
|
87
|
+
try {
|
|
88
|
+
res = await fetch(`${metroUrl()}/api/run/config`, {
|
|
89
|
+
headers: { authorization: `Bearer ${token}` },
|
|
90
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
throw new Error(`could not reach ${metroUrl()}`);
|
|
95
|
+
}
|
|
96
|
+
if (res.status === 401 || res.status === 403 || res.status === 409)
|
|
97
|
+
throw new RuntimeRevoked('this machine no longer holds the agent — authorize it again from the agent page');
|
|
98
|
+
if (!res.ok)
|
|
99
|
+
throw new Error(`metro answered ${String(res.status)}`);
|
|
100
|
+
return keyOfRunConfig(await res.json());
|
|
101
|
+
}
|
|
102
|
+
export async function claimCode(code) {
|
|
103
|
+
const body = await post('/api/cli/claim', { code });
|
|
104
|
+
if (!isRecord(body) ||
|
|
105
|
+
typeof body.token !== 'string' ||
|
|
106
|
+
typeof body.subject !== 'string' ||
|
|
107
|
+
typeof body.agent !== 'string')
|
|
108
|
+
throw new Error('metro returned an unexpected response');
|
|
109
|
+
return { token: body.token, subject: body.subject, agent: body.agent };
|
|
110
|
+
}
|
|
111
|
+
export async function claimRuntime(code, label) {
|
|
112
|
+
const body = await post('/api/run/claim', { code, label });
|
|
113
|
+
if (!isRecord(body) ||
|
|
114
|
+
typeof body.token !== 'string' ||
|
|
115
|
+
typeof body.agent !== 'string' ||
|
|
116
|
+
typeof body.label !== 'string')
|
|
117
|
+
throw new Error('metro returned an unexpected response');
|
|
118
|
+
return { token: body.token, agent: body.agent, label: body.label };
|
|
119
|
+
}
|
|
120
|
+
export async function mcpServers(wanted) {
|
|
121
|
+
if (await localDaemonUp()) {
|
|
122
|
+
const agents = localAgents();
|
|
123
|
+
if (agents.length > 0)
|
|
124
|
+
return localMcpServers(pickLocalAgent(agents, wanted));
|
|
125
|
+
}
|
|
126
|
+
const body = await get('/api/cli/mcp');
|
|
127
|
+
if (!isRecord(body) || typeof body.json !== 'string')
|
|
128
|
+
throw new Error('metro returned an unexpected response');
|
|
129
|
+
return body.json;
|
|
130
|
+
}
|
|
131
|
+
export async function whoisAuthorized(wanted) {
|
|
132
|
+
if (await localDaemonUp()) {
|
|
133
|
+
const agents = localAgents();
|
|
134
|
+
if (agents.length > 0) {
|
|
135
|
+
const agent = pickLocalAgent(agents, wanted);
|
|
136
|
+
return { subject: 'this machine', agent: agent.name, where: `${localUrl()} (local)` };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return { ...(await hostedIdentity()), where: metroUrl() };
|
|
140
|
+
}
|
|
141
|
+
async function hostedIdentity() {
|
|
142
|
+
const body = await get('/api/cli/session');
|
|
143
|
+
if (!isRecord(body) ||
|
|
144
|
+
typeof body.subject !== 'string' ||
|
|
145
|
+
typeof body.agent !== 'string')
|
|
146
|
+
throw new Error('metro returned an unexpected response');
|
|
147
|
+
return { subject: body.subject, agent: body.agent };
|
|
148
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
const PRELUDE_BYTES = 12;
|
|
2
|
+
const CRC_BYTES = 4;
|
|
3
|
+
const HEADER_TRUE = 0;
|
|
4
|
+
const HEADER_FALSE = 1;
|
|
5
|
+
const HEADER_BYTE = 2;
|
|
6
|
+
const HEADER_SHORT = 3;
|
|
7
|
+
const HEADER_INT = 4;
|
|
8
|
+
const HEADER_LONG = 5;
|
|
9
|
+
const HEADER_BYTES = 6;
|
|
10
|
+
const HEADER_STRING = 7;
|
|
11
|
+
const HEADER_TIMESTAMP = 8;
|
|
12
|
+
const HEADER_UUID = 9;
|
|
13
|
+
const FIXED_WIDTH = {
|
|
14
|
+
[HEADER_TRUE]: 0,
|
|
15
|
+
[HEADER_FALSE]: 0,
|
|
16
|
+
[HEADER_BYTE]: 1,
|
|
17
|
+
[HEADER_SHORT]: 2,
|
|
18
|
+
[HEADER_INT]: 4,
|
|
19
|
+
[HEADER_LONG]: 8,
|
|
20
|
+
[HEADER_TIMESTAMP]: 8,
|
|
21
|
+
[HEADER_UUID]: 16,
|
|
22
|
+
};
|
|
23
|
+
function readHeaderValue(buf, at, type) {
|
|
24
|
+
if (type === HEADER_BYTES || type === HEADER_STRING) {
|
|
25
|
+
const len = buf.readUInt16BE(at);
|
|
26
|
+
const raw = buf.subarray(at + 2, at + 2 + len);
|
|
27
|
+
const value = type === HEADER_STRING ? raw.toString('utf8') : raw.toString('base64');
|
|
28
|
+
return { value, next: at + 2 + len };
|
|
29
|
+
}
|
|
30
|
+
const width = FIXED_WIDTH[type];
|
|
31
|
+
if (width === undefined)
|
|
32
|
+
throw new Error(`eventstream: unknown header type ${String(type)}`);
|
|
33
|
+
if (type === HEADER_TRUE)
|
|
34
|
+
return { value: 'true', next: at };
|
|
35
|
+
if (type === HEADER_FALSE)
|
|
36
|
+
return { value: 'false', next: at };
|
|
37
|
+
return { value: buf.subarray(at, at + width).toString('hex'), next: at + width };
|
|
38
|
+
}
|
|
39
|
+
function readHeaders(buf) {
|
|
40
|
+
const out = {};
|
|
41
|
+
let at = 0;
|
|
42
|
+
while (at < buf.length) {
|
|
43
|
+
const nameLen = buf.readUInt8(at);
|
|
44
|
+
const name = buf.subarray(at + 1, at + 1 + nameLen).toString('utf8');
|
|
45
|
+
const type = buf.readUInt8(at + 1 + nameLen);
|
|
46
|
+
const parsed = readHeaderValue(buf, at + 2 + nameLen, type);
|
|
47
|
+
out[name] = parsed.value;
|
|
48
|
+
at = parsed.next;
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
export function decodeFrame(buf, offset) {
|
|
53
|
+
if (buf.length - offset < PRELUDE_BYTES)
|
|
54
|
+
return null;
|
|
55
|
+
const total = buf.readUInt32BE(offset);
|
|
56
|
+
const headersLen = buf.readUInt32BE(offset + 4);
|
|
57
|
+
if (total < PRELUDE_BYTES + CRC_BYTES || headersLen > total)
|
|
58
|
+
throw new Error('eventstream: malformed frame prelude');
|
|
59
|
+
if (buf.length - offset < total)
|
|
60
|
+
return null;
|
|
61
|
+
const headersStart = offset + PRELUDE_BYTES;
|
|
62
|
+
const payloadStart = headersStart + headersLen;
|
|
63
|
+
const payloadEnd = offset + total - CRC_BYTES;
|
|
64
|
+
return {
|
|
65
|
+
message: {
|
|
66
|
+
headers: readHeaders(buf.subarray(headersStart, payloadStart)),
|
|
67
|
+
payload: Buffer.from(buf.subarray(payloadStart, payloadEnd)),
|
|
68
|
+
},
|
|
69
|
+
end: offset + total,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
export class EventStreamDecoder {
|
|
73
|
+
pending = Buffer.alloc(0);
|
|
74
|
+
push(chunk) {
|
|
75
|
+
this.pending =
|
|
76
|
+
this.pending.length === 0 ? chunk : Buffer.concat([this.pending, chunk]);
|
|
77
|
+
const out = [];
|
|
78
|
+
let offset = 0;
|
|
79
|
+
for (;;) {
|
|
80
|
+
const frame = decodeFrame(this.pending, offset);
|
|
81
|
+
if (frame === null)
|
|
82
|
+
break;
|
|
83
|
+
out.push(frame.message);
|
|
84
|
+
offset = frame.end;
|
|
85
|
+
}
|
|
86
|
+
this.pending = this.pending.subarray(offset);
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
leftover() {
|
|
90
|
+
return this.pending.length;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function encodeHeader(name, value) {
|
|
94
|
+
const nameBuf = Buffer.from(name, 'utf8');
|
|
95
|
+
const valueBuf = Buffer.from(value, 'utf8');
|
|
96
|
+
const out = Buffer.alloc(1 + nameBuf.length + 1 + 2 + valueBuf.length);
|
|
97
|
+
let at = 0;
|
|
98
|
+
out.writeUInt8(nameBuf.length, at);
|
|
99
|
+
at += 1;
|
|
100
|
+
nameBuf.copy(out, at);
|
|
101
|
+
at += nameBuf.length;
|
|
102
|
+
out.writeUInt8(HEADER_STRING, at);
|
|
103
|
+
at += 1;
|
|
104
|
+
out.writeUInt16BE(valueBuf.length, at);
|
|
105
|
+
at += 2;
|
|
106
|
+
valueBuf.copy(out, at);
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
export function encodeFrame(headers, payload) {
|
|
110
|
+
const headerBuf = Buffer.concat(Object.entries(headers).map(([name, value]) => encodeHeader(name, value)));
|
|
111
|
+
const total = PRELUDE_BYTES + headerBuf.length + payload.length + CRC_BYTES;
|
|
112
|
+
const out = Buffer.alloc(total);
|
|
113
|
+
out.writeUInt32BE(total, 0);
|
|
114
|
+
out.writeUInt32BE(headerBuf.length, 4);
|
|
115
|
+
out.writeUInt32BE(0, 8);
|
|
116
|
+
headerBuf.copy(out, PRELUDE_BYTES);
|
|
117
|
+
payload.copy(out, PRELUDE_BYTES + headerBuf.length);
|
|
118
|
+
out.writeUInt32BE(0, total - CRC_BYTES);
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { createServer, } from 'node:http';
|
|
2
|
+
import { EventStreamDecoder } from './bedrock-eventstream.js';
|
|
3
|
+
import { callBedrock, freshAdaptations, invoke, invokeUrl, upstreamMessage, } from './bedrock-upstream.js';
|
|
4
|
+
export class BedrockConfigError extends Error {
|
|
5
|
+
}
|
|
6
|
+
class ProxyError extends Error {
|
|
7
|
+
status;
|
|
8
|
+
kind;
|
|
9
|
+
constructor(status, kind, message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.status = status;
|
|
12
|
+
this.kind = kind;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const ANTHROPIC_VERSION = 'bedrock-2023-05-31';
|
|
16
|
+
const BODY_MAX = 64 * 1024 * 1024;
|
|
17
|
+
const MESSAGES_PATH = '/v1/messages';
|
|
18
|
+
const COUNT_PATH = '/v1/messages/count_tokens';
|
|
19
|
+
function firstEnv(env, names) {
|
|
20
|
+
for (const name of names) {
|
|
21
|
+
const value = env[name]?.trim() ?? '';
|
|
22
|
+
if (value !== '')
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
return '';
|
|
26
|
+
}
|
|
27
|
+
export function bedrockConfigFromEnv(env = process.env) {
|
|
28
|
+
const bearerToken = firstEnv(env, ['AWS_BEARER_TOKEN_BEDROCK']);
|
|
29
|
+
if (bearerToken === '')
|
|
30
|
+
throw new BedrockConfigError('AWS_BEARER_TOKEN_BEDROCK is not set. Create an Amazon Bedrock API key in the AWS console (Bedrock → API keys) and export it.');
|
|
31
|
+
const region = firstEnv(env, ['METRO_BEDROCK_REGION', 'AWS_REGION', 'AWS_DEFAULT_REGION']);
|
|
32
|
+
if (region === '')
|
|
33
|
+
throw new BedrockConfigError('no AWS region: set AWS_REGION (or METRO_BEDROCK_REGION) to the region your Bedrock API key belongs to');
|
|
34
|
+
const model = firstEnv(env, ['METRO_BEDROCK_MODEL']);
|
|
35
|
+
return { region, bearerToken, model: model === '' ? null : model };
|
|
36
|
+
}
|
|
37
|
+
const PREFIXES = [
|
|
38
|
+
[/^us-gov-/, 'us-gov'],
|
|
39
|
+
[/^us-/, 'us'],
|
|
40
|
+
[/^eu-/, 'eu'],
|
|
41
|
+
[/^ap-/, 'apac'],
|
|
42
|
+
];
|
|
43
|
+
export function regionPrefix(region) {
|
|
44
|
+
for (const [pattern, prefix] of PREFIXES)
|
|
45
|
+
if (pattern.test(region))
|
|
46
|
+
return prefix;
|
|
47
|
+
return 'global';
|
|
48
|
+
}
|
|
49
|
+
export function mapModel(requested, cfg) {
|
|
50
|
+
if (cfg.model !== null)
|
|
51
|
+
return cfg.model;
|
|
52
|
+
if (requested.includes('anthropic.'))
|
|
53
|
+
return requested;
|
|
54
|
+
return `${regionPrefix(cfg.region)}.anthropic.${requested}`;
|
|
55
|
+
}
|
|
56
|
+
export const upstreamBase = (cfg, override) => override ?? `https://bedrock-runtime.${cfg.region}.amazonaws.com`;
|
|
57
|
+
function splitBetas(header) {
|
|
58
|
+
const raw = Array.isArray(header) ? header.join(',') : (header ?? '');
|
|
59
|
+
return raw
|
|
60
|
+
.split(',')
|
|
61
|
+
.map((b) => b.trim())
|
|
62
|
+
.filter((b) => b !== '');
|
|
63
|
+
}
|
|
64
|
+
export function rewriteBody(raw, betaHeader, cfg) {
|
|
65
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw))
|
|
66
|
+
throw new ProxyError(400, 'invalid_request_error', 'body must be a JSON object');
|
|
67
|
+
const { model, stream, ...rest } = raw;
|
|
68
|
+
if (typeof model !== 'string' || model === '')
|
|
69
|
+
throw new ProxyError(400, 'invalid_request_error', 'model is required');
|
|
70
|
+
const betas = splitBetas(betaHeader);
|
|
71
|
+
const body = { ...rest, anthropic_version: ANTHROPIC_VERSION };
|
|
72
|
+
if (betas.length > 0)
|
|
73
|
+
body.anthropic_beta = betas;
|
|
74
|
+
return { modelId: mapModel(model, cfg), stream: stream === true, body, betas };
|
|
75
|
+
}
|
|
76
|
+
export function errorKind(status, errorType) {
|
|
77
|
+
if (status === 429 || errorType === 'ThrottlingException')
|
|
78
|
+
return 'rate_limit_error';
|
|
79
|
+
if (status === 400)
|
|
80
|
+
return 'invalid_request_error';
|
|
81
|
+
if (status === 401)
|
|
82
|
+
return 'authentication_error';
|
|
83
|
+
if (status === 403)
|
|
84
|
+
return 'permission_error';
|
|
85
|
+
if (status === 404)
|
|
86
|
+
return 'not_found_error';
|
|
87
|
+
if (status === 503)
|
|
88
|
+
return 'overloaded_error';
|
|
89
|
+
return 'api_error';
|
|
90
|
+
}
|
|
91
|
+
function sendError(res, status, kind, message) {
|
|
92
|
+
res.writeHead(status, { 'content-type': 'application/json' });
|
|
93
|
+
res.end(JSON.stringify({ type: 'error', error: { type: kind, message } }));
|
|
94
|
+
}
|
|
95
|
+
async function readBody(req) {
|
|
96
|
+
const chunks = [];
|
|
97
|
+
let total = 0;
|
|
98
|
+
for await (const chunk of req) {
|
|
99
|
+
const buf = chunk;
|
|
100
|
+
total += buf.length;
|
|
101
|
+
if (total > BODY_MAX)
|
|
102
|
+
throw new ProxyError(413, 'invalid_request_error', 'request too large');
|
|
103
|
+
chunks.push(buf);
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
throw new ProxyError(400, 'invalid_request_error', 'body must be JSON');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function relayError(upstream, res) {
|
|
113
|
+
const text = await upstream.text();
|
|
114
|
+
const kind = errorKind(upstream.status, upstream.headers.get('x-amzn-errortype'));
|
|
115
|
+
sendError(res, upstream.status, kind, upstreamMessage(text));
|
|
116
|
+
}
|
|
117
|
+
function eventTypeOf(event) {
|
|
118
|
+
try {
|
|
119
|
+
const parsed = JSON.parse(event);
|
|
120
|
+
return typeof parsed.type === 'string' ? parsed.type : 'message';
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return 'message';
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function writeEvent(res, message) {
|
|
127
|
+
if (message.headers[':message-type'] === 'event') {
|
|
128
|
+
const parsed = JSON.parse(message.payload.toString('utf8'));
|
|
129
|
+
if (typeof parsed.bytes !== 'string')
|
|
130
|
+
return;
|
|
131
|
+
const event = Buffer.from(parsed.bytes, 'base64').toString('utf8');
|
|
132
|
+
res.write(`event: ${eventTypeOf(event)}\ndata: ${event}\n\n`);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const errorType = message.headers[':exception-type'] ?? message.headers[':error-code'] ?? null;
|
|
136
|
+
const body = {
|
|
137
|
+
type: 'error',
|
|
138
|
+
error: {
|
|
139
|
+
type: errorKind(500, errorType),
|
|
140
|
+
message: upstreamMessage(message.payload.toString('utf8')),
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
res.write(`event: error\ndata: ${JSON.stringify(body)}\n\n`);
|
|
144
|
+
}
|
|
145
|
+
async function relayStream(upstream, res) {
|
|
146
|
+
res.writeHead(200, {
|
|
147
|
+
'content-type': 'text/event-stream',
|
|
148
|
+
'cache-control': 'no-store',
|
|
149
|
+
connection: 'keep-alive',
|
|
150
|
+
});
|
|
151
|
+
const body = upstream.body;
|
|
152
|
+
if (body === null) {
|
|
153
|
+
res.end();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const decoder = new EventStreamDecoder();
|
|
157
|
+
const reader = body.getReader();
|
|
158
|
+
for (;;) {
|
|
159
|
+
const { done, value } = await reader.read();
|
|
160
|
+
if (done)
|
|
161
|
+
break;
|
|
162
|
+
for (const message of decoder.push(Buffer.from(value)))
|
|
163
|
+
writeEvent(res, message);
|
|
164
|
+
}
|
|
165
|
+
res.end();
|
|
166
|
+
}
|
|
167
|
+
async function relayJson(upstream, res) {
|
|
168
|
+
const text = await upstream.text();
|
|
169
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
170
|
+
res.end(text);
|
|
171
|
+
}
|
|
172
|
+
async function handleMessages(req, res, up) {
|
|
173
|
+
const rewritten = rewriteBody(await readBody(req), req.headers['anthropic-beta'], up.cfg);
|
|
174
|
+
const controller = new AbortController();
|
|
175
|
+
res.on('close', () => {
|
|
176
|
+
if (!res.writableEnded)
|
|
177
|
+
controller.abort();
|
|
178
|
+
});
|
|
179
|
+
const upstream = await invoke(up, rewritten, controller.signal);
|
|
180
|
+
if (!upstream.ok) {
|
|
181
|
+
await relayError(upstream, res);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (rewritten.stream)
|
|
185
|
+
await relayStream(upstream, res);
|
|
186
|
+
else
|
|
187
|
+
await relayJson(upstream, res);
|
|
188
|
+
}
|
|
189
|
+
function estimateTokens(body) {
|
|
190
|
+
return Math.ceil(JSON.stringify(body).length / 4);
|
|
191
|
+
}
|
|
192
|
+
async function handleCount(req, res, up) {
|
|
193
|
+
const rewritten = rewriteBody(await readBody(req), req.headers['anthropic-beta'], up.cfg);
|
|
194
|
+
const controller = new AbortController();
|
|
195
|
+
const upstream = await callBedrock(up, invokeUrl(up, rewritten.modelId, 'count-tokens'), { input: { invokeModel: { body: JSON.stringify(rewritten.body) } } }, 'application/json', controller.signal);
|
|
196
|
+
const text = await upstream.text();
|
|
197
|
+
let counted = null;
|
|
198
|
+
if (upstream.ok) {
|
|
199
|
+
const parsed = JSON.parse(text);
|
|
200
|
+
if (typeof parsed.inputTokens === 'number')
|
|
201
|
+
counted = parsed.inputTokens;
|
|
202
|
+
}
|
|
203
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
204
|
+
res.end(JSON.stringify({ input_tokens: counted ?? estimateTokens(rewritten.body) }));
|
|
205
|
+
}
|
|
206
|
+
function presentedToken(req) {
|
|
207
|
+
const auth = req.headers.authorization ?? '';
|
|
208
|
+
if (auth.toLowerCase().startsWith('bearer '))
|
|
209
|
+
return auth.slice(7).trim();
|
|
210
|
+
const key = req.headers['x-api-key'];
|
|
211
|
+
return Array.isArray(key) ? (key[0] ?? '') : (key ?? '');
|
|
212
|
+
}
|
|
213
|
+
async function route(req, res, cfg, opts, learned) {
|
|
214
|
+
if (presentedToken(req) !== opts.token)
|
|
215
|
+
throw new ProxyError(401, 'authentication_error', 'unauthorized');
|
|
216
|
+
const path = (req.url ?? '').split('?')[0] ?? '';
|
|
217
|
+
const up = { cfg, base: upstreamBase(cfg, opts.upstream), learned };
|
|
218
|
+
if (req.method === 'POST' && path === MESSAGES_PATH)
|
|
219
|
+
return handleMessages(req, res, up);
|
|
220
|
+
if (req.method === 'POST' && path === COUNT_PATH)
|
|
221
|
+
return handleCount(req, res, up);
|
|
222
|
+
throw new ProxyError(404, 'not_found_error', 'not found');
|
|
223
|
+
}
|
|
224
|
+
function failed(res, err) {
|
|
225
|
+
if (res.headersSent) {
|
|
226
|
+
res.end();
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (err instanceof ProxyError) {
|
|
230
|
+
sendError(res, err.status, err.kind, err.message);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const message = err instanceof Error ? err.message : 'proxy failure';
|
|
234
|
+
sendError(res, 502, 'api_error', `metro bedrock proxy: ${message}`);
|
|
235
|
+
}
|
|
236
|
+
export function startBedrockProxy(cfg, opts) {
|
|
237
|
+
const learned = freshAdaptations();
|
|
238
|
+
const server = createServer((req, res) => {
|
|
239
|
+
route(req, res, cfg, opts, learned).catch((err) => {
|
|
240
|
+
failed(res, err);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
return new Promise((resolve, reject) => {
|
|
244
|
+
server.once('error', reject);
|
|
245
|
+
server.listen(opts.port ?? 0, '127.0.0.1', () => {
|
|
246
|
+
const address = server.address();
|
|
247
|
+
const port = typeof address === 'object' && address !== null ? address.port : 0;
|
|
248
|
+
resolve({
|
|
249
|
+
port,
|
|
250
|
+
server,
|
|
251
|
+
learned,
|
|
252
|
+
close: () => new Promise((done) => {
|
|
253
|
+
server.close(() => {
|
|
254
|
+
done();
|
|
255
|
+
});
|
|
256
|
+
}),
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export const freshAdaptations = () => ({
|
|
2
|
+
fields: new Set(),
|
|
3
|
+
dropBetas: false,
|
|
4
|
+
});
|
|
5
|
+
const EXTRA_INPUT_RE = /^([A-Za-z0-9_]+)(?:\.[^:]*)?: Extra inputs are not permitted/;
|
|
6
|
+
const MAX_REPAIRS = 4;
|
|
7
|
+
export function invokeUrl(up, modelId, action) {
|
|
8
|
+
return `${up.base}/model/${encodeURIComponent(modelId)}/${action}`;
|
|
9
|
+
}
|
|
10
|
+
export function callBedrock(up, url, body, accept, signal) {
|
|
11
|
+
return fetch(url, {
|
|
12
|
+
method: 'POST',
|
|
13
|
+
headers: {
|
|
14
|
+
authorization: `Bearer ${up.cfg.bearerToken}`,
|
|
15
|
+
'content-type': 'application/json',
|
|
16
|
+
accept,
|
|
17
|
+
},
|
|
18
|
+
body: JSON.stringify(body),
|
|
19
|
+
signal,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export function upstreamMessage(text) {
|
|
23
|
+
const fallback = text === '' ? 'Bedrock returned no body' : text;
|
|
24
|
+
try {
|
|
25
|
+
const parsed = JSON.parse(text);
|
|
26
|
+
const message = parsed.message ?? parsed.Message;
|
|
27
|
+
return typeof message === 'string' && message !== '' ? message : fallback;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return fallback;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const without = (body, keys) => Object.fromEntries(Object.entries(body).filter(([key]) => !keys.has(key)));
|
|
34
|
+
function applyLearned(req, learned) {
|
|
35
|
+
const dropped = new Set(learned.fields);
|
|
36
|
+
if (learned.dropBetas)
|
|
37
|
+
dropped.add('anthropic_beta');
|
|
38
|
+
return {
|
|
39
|
+
body: without(req.body, dropped),
|
|
40
|
+
betas: learned.dropBetas ? [] : req.betas,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function repair(attempt, learned, message) {
|
|
44
|
+
const named = EXTRA_INPUT_RE.exec(message)?.[1];
|
|
45
|
+
if (named !== undefined && named in attempt.body) {
|
|
46
|
+
learned.fields.add(named);
|
|
47
|
+
process.stderr.write(`metro bedrock: Bedrock refused "${named}" (extra input); dropping it from every request\n`);
|
|
48
|
+
return { body: without(attempt.body, new Set([named])), betas: attempt.betas };
|
|
49
|
+
}
|
|
50
|
+
if (attempt.betas.length > 0) {
|
|
51
|
+
learned.dropBetas = true;
|
|
52
|
+
process.stderr.write(`metro bedrock: retrying without anthropic_beta [${attempt.betas.join(', ')}]\n`);
|
|
53
|
+
return { body: without(attempt.body, new Set(['anthropic_beta'])), betas: [] };
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
export async function invoke(up, req, signal) {
|
|
58
|
+
const action = req.stream ? 'invoke-with-response-stream' : 'invoke';
|
|
59
|
+
const accept = req.stream ? 'application/vnd.amazon.eventstream' : 'application/json';
|
|
60
|
+
const url = invokeUrl(up, req.modelId, action);
|
|
61
|
+
let attempt = applyLearned(req, up.learned);
|
|
62
|
+
for (let repairs = 0;; repairs += 1) {
|
|
63
|
+
const res = await callBedrock(up, url, attempt.body, accept, signal);
|
|
64
|
+
if (res.status !== 400 || repairs >= MAX_REPAIRS)
|
|
65
|
+
return res;
|
|
66
|
+
const text = await res.text();
|
|
67
|
+
const next = repair(attempt, up.learned, upstreamMessage(text));
|
|
68
|
+
if (next === null)
|
|
69
|
+
return new Response(text, { status: 400, headers: res.headers });
|
|
70
|
+
attempt = next;
|
|
71
|
+
}
|
|
72
|
+
}
|