@stage-labs/metro 0.1.0-beta.7 → 0.1.0-beta.71
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 +14 -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 +37 -0
- package/dist/claude-settings.js +40 -0
- package/dist/claude.js +107 -0
- package/dist/cli.js +101 -0
- package/dist/control.js +74 -0
- package/dist/hold.js +211 -0
- package/dist/local.js +93 -0
- package/dist/node-name.js +51 -0
- package/dist/plugin.js +80 -0
- package/dist/provider-flags.js +9 -0
- package/dist/runtime-install.js +84 -0
- package/dist/runtime.js +70 -0
- package/dist/serve.js +147 -0
- package/dist/service.js +224 -0
- package/dist/tail.js +119 -0
- package/dist/update.js +45 -0
- package/dist/version.js +97 -0
- package/package.json +14 -25
- 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 +220 -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 +64 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/addressed.ts +25 -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 +377 -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 +302 -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 +95 -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 +128 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-session.ts +280 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +204 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/bundle-api.ts +111 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/claude-api.ts +97 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/claude-files.ts +329 -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 +307 -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/control-api.ts +64 -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 +120 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/http.ts +390 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity-key.ts +33 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity-registry.ts +13 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity-routes.ts +54 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity.ts +7 -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 +63 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-mode.ts +187 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-owner.ts +14 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/log.ts +42 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/machine-api.ts +61 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/mode-api.ts +32 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/model-api.ts +169 -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/paths.ts +109 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/protocol.ts +226 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/relay.ts +295 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/return-to.ts +21 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/runtime-deps.ts +82 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/secure-fs.ts +57 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/server-types.ts +21 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/servers-api.ts +80 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-api.ts +29 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-apis.ts +66 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/signed-identity.ts +36 -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/terminal-api.ts +71 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/terminal-tickets.ts +40 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/terminal-ws.ts +114 -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 +352 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/update-api.ts +116 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-api.ts +294 -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/vault-types.ts +12 -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 +19 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/address.ts +6 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-admin.ts +43 -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 +32 -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 +39 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/connectors.ts +69 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/file-admin.ts +329 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/file-source.ts +155 -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 +277 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +253 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +33 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/servers.ts +83 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/stations.ts +12 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/vault.ts +109 -0
- package/runtime/node_modules/@metro-labs/mcp/src/gateway/bedrock.ts +252 -0
- package/runtime/node_modules/@metro-labs/mcp/src/gateway/codex-auth.ts +231 -0
- package/runtime/node_modules/@metro-labs/mcp/src/gateway/codex-device.ts +91 -0
- package/runtime/node_modules/@metro-labs/mcp/src/gateway/codex-stream.ts +288 -0
- package/runtime/node_modules/@metro-labs/mcp/src/gateway/codex-translate.ts +203 -0
- package/runtime/node_modules/@metro-labs/mcp/src/gateway/codex.ts +211 -0
- package/runtime/node_modules/@metro-labs/mcp/src/gateway/eventstream.ts +123 -0
- package/runtime/node_modules/@metro-labs/mcp/src/gateway/forward.ts +169 -0
- package/runtime/node_modules/@metro-labs/mcp/src/gateway/gateway.ts +193 -0
- package/runtime/node_modules/@metro-labs/mcp/src/gateway/model-config.ts +180 -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 +216 -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 +191 -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 +47 -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 +215 -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 +215 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-schemas.ts +400 -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 +237 -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 +184 -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 +156 -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 +120 -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 +201 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/index.ts +109 -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 +58 -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 +27 -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 +327 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/delivery.ts +49 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +138 -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 +185 -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 +335 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +80 -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 +53 -0
- package/runtime/runtime.json +3 -0
- package/runtime/stations.json +36 -0
- package/runtime/trains/.keep +0 -0
- package/LICENSE +0 -21
- package/README.md +0 -193
- package/dist/cache.js +0 -75
- package/dist/cli/actions.js +0 -139
- package/dist/cli/config.js +0 -185
- package/dist/cli/index.js +0 -159
- package/dist/cli/skill.js +0 -62
- package/dist/cli/util.js +0 -72
- package/dist/codex-rc.js +0 -236
- package/dist/dispatcher.js +0 -95
- package/dist/history.js +0 -84
- package/dist/ipc.js +0 -72
- package/dist/log.js +0 -15
- package/dist/paths.js +0 -68
- package/dist/stations/discord.js +0 -200
- package/dist/stations/index.js +0 -73
- package/dist/stations/telegram-md.js +0 -34
- package/dist/stations/telegram-upload.js +0 -113
- package/dist/stations/telegram.js +0 -213
- package/docs/agents.md +0 -185
- package/docs/uri-scheme.md +0 -71
- package/skills/metro/SKILL.md +0 -241
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
3
|
+
import { arch, platform, release } from 'node:os';
|
|
4
|
+
import { isRecord } from '../daemon/is-record.js';
|
|
5
|
+
import { errMsg } from '../daemon/log.js';
|
|
6
|
+
import { refreshTokens, tokensStale, type CodexTokens } from './codex-auth.js';
|
|
7
|
+
import { assembleMessage, CodexEventTranslator, parseEvent, SseParser } from './codex-stream.js';
|
|
8
|
+
import { ToolNames, toResponsesRequest } from './codex-translate.js';
|
|
9
|
+
import { GatewayError, idleMessage, providerStatus, sendError, upstreamMessage, type Watch } from './forward.js';
|
|
10
|
+
import type { ModelConfig } from './model-config.js';
|
|
11
|
+
|
|
12
|
+
export const CODEX_BASE = 'https://chatgpt.com/backend-api/codex';
|
|
13
|
+
const CODEX_VERSION = '0.104.0';
|
|
14
|
+
const PING_MS = 25_000;
|
|
15
|
+
const STATUS_OF: Record<string, number> = { rate_limit_error: 429, invalid_request_error: 400, permission_error: 403, overloaded_error: 529 };
|
|
16
|
+
|
|
17
|
+
export interface CodexDeps {
|
|
18
|
+
base?: string;
|
|
19
|
+
issuer?: string;
|
|
20
|
+
fetchImpl?: typeof fetch;
|
|
21
|
+
save: (tokens: CodexTokens) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CodexState {
|
|
25
|
+
refreshing: Promise<CodexTokens> | null;
|
|
26
|
+
latest: CodexTokens | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const freshCodexState = (): CodexState => ({ refreshing: null, latest: null });
|
|
30
|
+
|
|
31
|
+
const OS_NAMES: Record<string, string> = { darwin: 'Mac OS', linux: 'Linux', win32: 'Windows' };
|
|
32
|
+
|
|
33
|
+
export const userAgent = (): string => `codex_cli_rs/${CODEX_VERSION} (${OS_NAMES[platform()] ?? platform()} ${release()}; ${arch()}) metro`;
|
|
34
|
+
|
|
35
|
+
function headersFor(tokens: CodexTokens, sessionId: string): Record<string, string> {
|
|
36
|
+
return {
|
|
37
|
+
authorization: `Bearer ${tokens.accessToken}`,
|
|
38
|
+
'chatgpt-account-id': tokens.accountId,
|
|
39
|
+
'content-type': 'application/json',
|
|
40
|
+
accept: 'text/event-stream',
|
|
41
|
+
originator: 'codex_cli_rs',
|
|
42
|
+
'user-agent': userAgent(),
|
|
43
|
+
'openai-beta': 'responses=experimental',
|
|
44
|
+
'session-id': sessionId,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const newerThan = (a: CodexTokens, b: CodexTokens): boolean => Date.parse(a.savedAt) > Date.parse(b.savedAt);
|
|
49
|
+
|
|
50
|
+
function refreshed(tokens: CodexTokens, deps: CodexDeps, state: CodexState): Promise<CodexTokens> {
|
|
51
|
+
const latest = state.latest;
|
|
52
|
+
if (latest !== null && newerThan(latest, tokens) && !tokensStale(latest)) return Promise.resolve(latest);
|
|
53
|
+
if (state.refreshing !== null) return state.refreshing;
|
|
54
|
+
const run = refreshTokens(tokens, deps.issuer, deps.fetchImpl)
|
|
55
|
+
.then((fresh) => {
|
|
56
|
+
deps.save(fresh);
|
|
57
|
+
state.latest = fresh;
|
|
58
|
+
return fresh;
|
|
59
|
+
})
|
|
60
|
+
.catch((err: unknown) => {
|
|
61
|
+
throw new GatewayError(403, 'permission_error', `Codex sign-in expired (${errMsg(err)}): connect again on the Model page`);
|
|
62
|
+
})
|
|
63
|
+
.finally(() => {
|
|
64
|
+
state.refreshing = null;
|
|
65
|
+
});
|
|
66
|
+
state.refreshing = run;
|
|
67
|
+
return run;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function currentTokens(cfg: ModelConfig, deps: CodexDeps, state: CodexState): Promise<CodexTokens> {
|
|
71
|
+
const tokens = cfg.codex.auth;
|
|
72
|
+
if (tokens === null) throw new GatewayError(400, 'invalid_request_error', 'Codex is not connected: sign in on the Model page.');
|
|
73
|
+
return tokensStale(tokens) ? refreshed(tokens, deps, state) : Promise.resolve(tokens);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function errorKind(status: number): string {
|
|
77
|
+
if (status === 401 || status === 403) return 'permission_error';
|
|
78
|
+
if (status === 429) return 'rate_limit_error';
|
|
79
|
+
if (status === 400 || status === 404 || status === 422) return 'invalid_request_error';
|
|
80
|
+
return 'api_error';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const sessionIdOf = (req: IncomingMessage): string => {
|
|
84
|
+
const raw = req.headers['x-claude-code-session-id'];
|
|
85
|
+
const given = (Array.isArray(raw) ? raw[0] : raw)?.trim() ?? '';
|
|
86
|
+
return given === '' ? randomUUID() : given;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
interface Call {
|
|
90
|
+
body: Record<string, unknown>;
|
|
91
|
+
model: string;
|
|
92
|
+
sessionId: string;
|
|
93
|
+
watch: Watch;
|
|
94
|
+
deps: CodexDeps;
|
|
95
|
+
names: ToolNames;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function send(call: Call, tokens: CodexTokens): Promise<Response> {
|
|
99
|
+
const request = toResponsesRequest(call.body, call.model, { promptCacheKey: call.sessionId, names: call.names });
|
|
100
|
+
return (call.deps.fetchImpl ?? fetch)(`${call.deps.base ?? CODEX_BASE}/responses`, {
|
|
101
|
+
method: 'POST',
|
|
102
|
+
headers: headersFor(tokens, call.sessionId),
|
|
103
|
+
body: JSON.stringify(request),
|
|
104
|
+
signal: call.watch.signal,
|
|
105
|
+
redirect: 'manual',
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function reach(call: Call, cfg: ModelConfig, state: CodexState): Promise<Response> {
|
|
110
|
+
let tokens = await currentTokens(cfg, call.deps, state);
|
|
111
|
+
let upstream = await send(call, tokens);
|
|
112
|
+
if (upstream.status === 401) {
|
|
113
|
+
await upstream.body?.cancel();
|
|
114
|
+
tokens = await refreshed(tokens, call.deps, state);
|
|
115
|
+
upstream = await send(call, tokens);
|
|
116
|
+
}
|
|
117
|
+
return upstream;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function relayStream(upstream: Response, res: ServerResponse, model: string, names: ToolNames, watch: Watch): Promise<void> {
|
|
121
|
+
res.writeHead(200, { 'content-type': 'text/event-stream', 'cache-control': 'no-store', connection: 'keep-alive' });
|
|
122
|
+
const translator = new CodexEventTranslator(model, (name) => names.restore(name));
|
|
123
|
+
const body = upstream.body;
|
|
124
|
+
if (body === null) {
|
|
125
|
+
res.end(translator.close());
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const ping = setInterval(() => res.write('event: ping\ndata: {"type":"ping"}\n\n'), PING_MS);
|
|
129
|
+
try {
|
|
130
|
+
const parser = new SseParser();
|
|
131
|
+
const decoder = new TextDecoder();
|
|
132
|
+
const reader = body.getReader();
|
|
133
|
+
for (;;) {
|
|
134
|
+
const { done, value } = await reader.read();
|
|
135
|
+
if (done) break;
|
|
136
|
+
watch.touch();
|
|
137
|
+
for (const raw of parser.push(decoder.decode(value, { stream: true }))) {
|
|
138
|
+
const parsed = parseEvent(raw);
|
|
139
|
+
if (parsed !== null) res.write(translator.push(parsed.event, parsed.data));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
res.write(translator.close());
|
|
143
|
+
} catch (err) {
|
|
144
|
+
if (!watch.idle()) throw err;
|
|
145
|
+
res.write(translator.finished ? '' : translator.close(idleMessage(watch.ms)));
|
|
146
|
+
} finally {
|
|
147
|
+
clearInterval(ping);
|
|
148
|
+
watch.stop();
|
|
149
|
+
res.end();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function relayWhole(upstream: Response, res: ServerResponse, model: string, names: ToolNames): Promise<void> {
|
|
154
|
+
const translator = new CodexEventTranslator(model, (name) => names.restore(name));
|
|
155
|
+
let frames = '';
|
|
156
|
+
const parser = new SseParser();
|
|
157
|
+
for (const raw of parser.push(await upstream.text())) {
|
|
158
|
+
const parsed = parseEvent(raw);
|
|
159
|
+
if (parsed !== null) frames += translator.push(parsed.event, parsed.data);
|
|
160
|
+
}
|
|
161
|
+
frames += translator.close();
|
|
162
|
+
const message = assembleMessage(frames);
|
|
163
|
+
const kind = isRecord(message.error) ? String(message.error.type) : '';
|
|
164
|
+
res.writeHead(message.type === 'error' ? (STATUS_OF[kind] ?? 502) : 200, { 'content-type': 'application/json' });
|
|
165
|
+
res.end(JSON.stringify(message));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export async function codexMessages(
|
|
169
|
+
req: IncomingMessage,
|
|
170
|
+
res: ServerResponse,
|
|
171
|
+
body: Record<string, unknown>,
|
|
172
|
+
model: string,
|
|
173
|
+
cfg: ModelConfig,
|
|
174
|
+
deps: CodexDeps,
|
|
175
|
+
state: CodexState,
|
|
176
|
+
watch: Watch,
|
|
177
|
+
): Promise<void> {
|
|
178
|
+
const call: Call = { body, model, sessionId: sessionIdOf(req), watch, deps, names: new ToolNames() };
|
|
179
|
+
const upstream = await reach(call, cfg, state);
|
|
180
|
+
if (!upstream.ok) {
|
|
181
|
+
const text = await upstream.text();
|
|
182
|
+
sendError(res, providerStatus(upstream.status), errorKind(upstream.status), upstreamMessage(text, `Codex answered ${String(upstream.status)}`));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (body.stream === true) await relayStream(upstream, res, model, call.names, watch);
|
|
186
|
+
else await relayWhole(upstream, res, model, call.names);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function codexCount(res: ServerResponse, body: Record<string, unknown>): void {
|
|
190
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
191
|
+
res.end(JSON.stringify({ input_tokens: Math.ceil(JSON.stringify(body).length / 4) }));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export async function codexModels(tokens: CodexTokens, deps: Omit<CodexDeps, 'save'>): Promise<string[]> {
|
|
195
|
+
const res = await (deps.fetchImpl ?? fetch)(`${deps.base ?? CODEX_BASE}/models?client_version=${CODEX_VERSION}`, {
|
|
196
|
+
headers: { ...headersFor(tokens, randomUUID()), accept: 'application/json' },
|
|
197
|
+
redirect: 'manual',
|
|
198
|
+
});
|
|
199
|
+
if (!res.ok) throw new GatewayError(res.status, errorKind(res.status), `Codex would not list models (${String(res.status)})`);
|
|
200
|
+
const slugs = new Set<string>();
|
|
201
|
+
const walk = (value: unknown): void => {
|
|
202
|
+
if (Array.isArray(value)) value.forEach(walk);
|
|
203
|
+
else if (typeof value === 'object' && value !== null) {
|
|
204
|
+
const record = value as Record<string, unknown>;
|
|
205
|
+
if (typeof record.slug === 'string' && record.slug !== '') slugs.add(record.slug);
|
|
206
|
+
Object.values(record).forEach(walk);
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
walk(await res.json());
|
|
210
|
+
return [...slugs];
|
|
211
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
export interface EventStreamMessage {
|
|
2
|
+
headers: Record<string, string>;
|
|
3
|
+
payload: Buffer;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const PRELUDE_BYTES = 12;
|
|
7
|
+
const CRC_BYTES = 4;
|
|
8
|
+
|
|
9
|
+
const HEADER_TRUE = 0;
|
|
10
|
+
const HEADER_FALSE = 1;
|
|
11
|
+
const HEADER_BYTE = 2;
|
|
12
|
+
const HEADER_SHORT = 3;
|
|
13
|
+
const HEADER_INT = 4;
|
|
14
|
+
const HEADER_LONG = 5;
|
|
15
|
+
const HEADER_BYTES = 6;
|
|
16
|
+
const HEADER_STRING = 7;
|
|
17
|
+
const HEADER_TIMESTAMP = 8;
|
|
18
|
+
const HEADER_UUID = 9;
|
|
19
|
+
|
|
20
|
+
const FIXED_WIDTH: Record<number, number> = {
|
|
21
|
+
[HEADER_TRUE]: 0,
|
|
22
|
+
[HEADER_FALSE]: 0,
|
|
23
|
+
[HEADER_BYTE]: 1,
|
|
24
|
+
[HEADER_SHORT]: 2,
|
|
25
|
+
[HEADER_INT]: 4,
|
|
26
|
+
[HEADER_LONG]: 8,
|
|
27
|
+
[HEADER_TIMESTAMP]: 8,
|
|
28
|
+
[HEADER_UUID]: 16,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function readHeaderValue(buf: Buffer, at: number, type: number): { value: string; next: number } {
|
|
32
|
+
if (type === HEADER_BYTES || type === HEADER_STRING) {
|
|
33
|
+
const len = buf.readUInt16BE(at);
|
|
34
|
+
const raw = buf.subarray(at + 2, at + 2 + len);
|
|
35
|
+
const value = type === HEADER_STRING ? raw.toString('utf8') : raw.toString('base64');
|
|
36
|
+
return { value, next: at + 2 + len };
|
|
37
|
+
}
|
|
38
|
+
const width = FIXED_WIDTH[type];
|
|
39
|
+
if (width === undefined) throw new Error(`eventstream: unknown header type ${String(type)}`);
|
|
40
|
+
if (type === HEADER_TRUE) return { value: 'true', next: at };
|
|
41
|
+
if (type === HEADER_FALSE) return { value: 'false', next: at };
|
|
42
|
+
return { value: buf.subarray(at, at + width).toString('hex'), next: at + width };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function readHeaders(buf: Buffer): Record<string, string> {
|
|
46
|
+
const out: Record<string, string> = {};
|
|
47
|
+
let at = 0;
|
|
48
|
+
while (at < buf.length) {
|
|
49
|
+
const nameLen = buf.readUInt8(at);
|
|
50
|
+
const name = buf.subarray(at + 1, at + 1 + nameLen).toString('utf8');
|
|
51
|
+
const type = buf.readUInt8(at + 1 + nameLen);
|
|
52
|
+
const parsed = readHeaderValue(buf, at + 2 + nameLen, type);
|
|
53
|
+
out[name] = parsed.value;
|
|
54
|
+
at = parsed.next;
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function decodeFrame(buf: Buffer, offset: number): { message: EventStreamMessage; end: number } | null {
|
|
60
|
+
if (buf.length - offset < PRELUDE_BYTES) return null;
|
|
61
|
+
const total = buf.readUInt32BE(offset);
|
|
62
|
+
const headersLen = buf.readUInt32BE(offset + 4);
|
|
63
|
+
if (total < PRELUDE_BYTES + CRC_BYTES || headersLen > total) throw new Error('eventstream: malformed frame prelude');
|
|
64
|
+
if (buf.length - offset < total) return null;
|
|
65
|
+
const headersStart = offset + PRELUDE_BYTES;
|
|
66
|
+
const payloadStart = headersStart + headersLen;
|
|
67
|
+
const payloadEnd = offset + total - CRC_BYTES;
|
|
68
|
+
return {
|
|
69
|
+
message: {
|
|
70
|
+
headers: readHeaders(buf.subarray(headersStart, payloadStart)),
|
|
71
|
+
payload: Buffer.from(buf.subarray(payloadStart, payloadEnd)),
|
|
72
|
+
},
|
|
73
|
+
end: offset + total,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export class EventStreamDecoder {
|
|
78
|
+
private pending: Buffer = Buffer.alloc(0);
|
|
79
|
+
|
|
80
|
+
push(chunk: Buffer): EventStreamMessage[] {
|
|
81
|
+
this.pending = this.pending.length === 0 ? chunk : Buffer.concat([this.pending, chunk]);
|
|
82
|
+
const out: EventStreamMessage[] = [];
|
|
83
|
+
let offset = 0;
|
|
84
|
+
for (;;) {
|
|
85
|
+
const frame = decodeFrame(this.pending, offset);
|
|
86
|
+
if (frame === null) break;
|
|
87
|
+
out.push(frame.message);
|
|
88
|
+
offset = frame.end;
|
|
89
|
+
}
|
|
90
|
+
this.pending = this.pending.subarray(offset);
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function encodeHeader(name: string, value: string): Buffer {
|
|
96
|
+
const nameBuf = Buffer.from(name, 'utf8');
|
|
97
|
+
const valueBuf = Buffer.from(value, 'utf8');
|
|
98
|
+
const out = Buffer.alloc(1 + nameBuf.length + 1 + 2 + valueBuf.length);
|
|
99
|
+
let at = 0;
|
|
100
|
+
out.writeUInt8(nameBuf.length, at);
|
|
101
|
+
at += 1;
|
|
102
|
+
nameBuf.copy(out, at);
|
|
103
|
+
at += nameBuf.length;
|
|
104
|
+
out.writeUInt8(HEADER_STRING, at);
|
|
105
|
+
at += 1;
|
|
106
|
+
out.writeUInt16BE(valueBuf.length, at);
|
|
107
|
+
at += 2;
|
|
108
|
+
valueBuf.copy(out, at);
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function encodeFrame(headers: Record<string, string>, payload: Buffer): Buffer {
|
|
113
|
+
const headerBuf = Buffer.concat(Object.entries(headers).map(([name, value]) => encodeHeader(name, value)));
|
|
114
|
+
const total = PRELUDE_BYTES + headerBuf.length + payload.length + CRC_BYTES;
|
|
115
|
+
const out = Buffer.alloc(total);
|
|
116
|
+
out.writeUInt32BE(total, 0);
|
|
117
|
+
out.writeUInt32BE(headerBuf.length, 4);
|
|
118
|
+
out.writeUInt32BE(0, 8);
|
|
119
|
+
headerBuf.copy(out, PRELUDE_BYTES);
|
|
120
|
+
payload.copy(out, PRELUDE_BYTES + headerBuf.length);
|
|
121
|
+
out.writeUInt32BE(0, total - CRC_BYTES);
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
|
|
3
|
+
export class GatewayError extends Error {
|
|
4
|
+
constructor(
|
|
5
|
+
readonly status: number,
|
|
6
|
+
readonly kind: string,
|
|
7
|
+
message: string,
|
|
8
|
+
) {
|
|
9
|
+
super(message);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const BODY_MAX = 64 * 1024 * 1024;
|
|
14
|
+
const DRAIN_FACTOR = 2;
|
|
15
|
+
const PING_MS = 25_000;
|
|
16
|
+
const IDLE_MS = 300_000;
|
|
17
|
+
const HOP_BY_HOP = new Set([
|
|
18
|
+
'host',
|
|
19
|
+
'connection',
|
|
20
|
+
'content-length',
|
|
21
|
+
'transfer-encoding',
|
|
22
|
+
'keep-alive',
|
|
23
|
+
'proxy-authorization',
|
|
24
|
+
'te',
|
|
25
|
+
'trailer',
|
|
26
|
+
'upgrade',
|
|
27
|
+
'accept-encoding',
|
|
28
|
+
'x-metro-key',
|
|
29
|
+
]);
|
|
30
|
+
const RESPONSE_DROP = new Set(['content-length', 'content-encoding', 'transfer-encoding', 'connection', 'keep-alive']);
|
|
31
|
+
|
|
32
|
+
export function sendError(res: ServerResponse, status: number, kind: string, message: string): void {
|
|
33
|
+
res.writeHead(status, { 'content-type': 'application/json' });
|
|
34
|
+
res.end(JSON.stringify({ type: 'error', error: { type: kind, message } }));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const errorFrame = (kind: string, message: string): string => `event: error\ndata: ${JSON.stringify({ type: 'error', error: { type: kind, message } })}\n\n`;
|
|
38
|
+
|
|
39
|
+
export const providerStatus = (status: number): number => (status === 401 ? 403 : status);
|
|
40
|
+
|
|
41
|
+
export const idleMs = (): number => Number(process.env.METRO_GATEWAY_IDLE_MS) || IDLE_MS;
|
|
42
|
+
|
|
43
|
+
export const idleMessage = (ms: number): string => `metro gateway: the provider sent nothing for ${String(Math.round(ms / 1000))}s; giving up on this request`;
|
|
44
|
+
|
|
45
|
+
export interface Watch {
|
|
46
|
+
signal: AbortSignal;
|
|
47
|
+
ms: number;
|
|
48
|
+
touch: () => void;
|
|
49
|
+
idle: () => boolean;
|
|
50
|
+
stop: () => void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function watchUpstream(res: ServerResponse, ms = idleMs()): Watch {
|
|
54
|
+
const control = new AbortController();
|
|
55
|
+
let expired = false;
|
|
56
|
+
let timer: NodeJS.Timeout | null = null;
|
|
57
|
+
const stop = (): void => {
|
|
58
|
+
if (timer !== null) clearTimeout(timer);
|
|
59
|
+
timer = null;
|
|
60
|
+
};
|
|
61
|
+
const arm = (): void => {
|
|
62
|
+
stop();
|
|
63
|
+
timer = setTimeout(() => {
|
|
64
|
+
expired = true;
|
|
65
|
+
control.abort();
|
|
66
|
+
}, ms);
|
|
67
|
+
timer.unref();
|
|
68
|
+
};
|
|
69
|
+
res.on('close', () => {
|
|
70
|
+
stop();
|
|
71
|
+
if (!res.writableEnded) control.abort();
|
|
72
|
+
});
|
|
73
|
+
arm();
|
|
74
|
+
return { signal: control.signal, ms, touch: arm, idle: () => expired, stop };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function readBody(req: IncomingMessage): Promise<Buffer> {
|
|
78
|
+
const chunks: Buffer[] = [];
|
|
79
|
+
let total = 0;
|
|
80
|
+
let over = false;
|
|
81
|
+
for await (const chunk of req) {
|
|
82
|
+
const buf = chunk as Buffer;
|
|
83
|
+
total += buf.length;
|
|
84
|
+
if (total > BODY_MAX) {
|
|
85
|
+
over = true;
|
|
86
|
+
if (total > BODY_MAX * DRAIN_FACTOR) break;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
chunks.push(buf);
|
|
90
|
+
}
|
|
91
|
+
if (over) throw new GatewayError(413, 'invalid_request_error', 'request too large');
|
|
92
|
+
return Buffer.concat(chunks);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function parseJson(raw: Buffer): Record<string, unknown> {
|
|
96
|
+
let parsed: unknown;
|
|
97
|
+
try {
|
|
98
|
+
parsed = JSON.parse(raw.toString('utf8'));
|
|
99
|
+
} catch {
|
|
100
|
+
throw new GatewayError(400, 'invalid_request_error', 'body must be JSON');
|
|
101
|
+
}
|
|
102
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
|
|
103
|
+
throw new GatewayError(400, 'invalid_request_error', 'body must be a JSON object');
|
|
104
|
+
return parsed as Record<string, unknown>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function forwardedHeaders(req: IncomingMessage): Record<string, string> {
|
|
108
|
+
const out: Record<string, string> = {};
|
|
109
|
+
for (const [name, value] of Object.entries(req.headers)) {
|
|
110
|
+
if (HOP_BY_HOP.has(name) || value === undefined) continue;
|
|
111
|
+
out[name] = Array.isArray(value) ? value.join(', ') : value;
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface PipeOptions {
|
|
117
|
+
keepalive?: boolean;
|
|
118
|
+
ownCredential?: boolean;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function openResponse(upstream: Response, res: ServerResponse, opts: PipeOptions): boolean {
|
|
122
|
+
const headers: Record<string, string> = {};
|
|
123
|
+
upstream.headers.forEach((value, name) => {
|
|
124
|
+
if (!RESPONSE_DROP.has(name)) headers[name] = value;
|
|
125
|
+
});
|
|
126
|
+
res.writeHead(opts.ownCredential === true ? providerStatus(upstream.status) : upstream.status, headers);
|
|
127
|
+
return (upstream.headers.get('content-type') ?? '').startsWith('text/event-stream');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function pump(body: ReadableStream<Uint8Array>, res: ServerResponse, watch: Watch): Promise<void> {
|
|
131
|
+
const reader = body.getReader();
|
|
132
|
+
for (;;) {
|
|
133
|
+
const { done, value } = await reader.read();
|
|
134
|
+
if (done) return;
|
|
135
|
+
watch.touch();
|
|
136
|
+
res.write(value);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function pipeResponse(upstream: Response, res: ServerResponse, watch: Watch, opts: PipeOptions = {}): Promise<void> {
|
|
141
|
+
const streaming = openResponse(upstream, res, opts);
|
|
142
|
+
const body = upstream.body;
|
|
143
|
+
if (body === null) {
|
|
144
|
+
res.end();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const ping = opts.keepalive === true && streaming ? setInterval(() => res.write('event: ping\ndata: {"type":"ping"}\n\n'), PING_MS) : null;
|
|
148
|
+
try {
|
|
149
|
+
await pump(body, res, watch);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
if (!watch.idle()) throw err;
|
|
152
|
+
if (streaming) res.write(errorFrame('api_error', idleMessage(watch.ms)));
|
|
153
|
+
} finally {
|
|
154
|
+
if (ping !== null) clearInterval(ping);
|
|
155
|
+
watch.stop();
|
|
156
|
+
res.end();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function upstreamMessage(text: string, fallback: string): string {
|
|
161
|
+
if (text === '') return fallback;
|
|
162
|
+
try {
|
|
163
|
+
const parsed = JSON.parse(text) as { message?: unknown; Message?: unknown; error?: { message?: unknown } };
|
|
164
|
+
const message = parsed.message ?? parsed.Message ?? parsed.error?.message;
|
|
165
|
+
return typeof message === 'string' && message !== '' ? message : text;
|
|
166
|
+
} catch {
|
|
167
|
+
return text;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import { errMsg, log } from '../daemon/log.js';
|
|
3
|
+
import { agentIdForKey } from '../db/key-map.js';
|
|
4
|
+
import {
|
|
5
|
+
assertBedrockReady,
|
|
6
|
+
bedrockBase,
|
|
7
|
+
bedrockCount,
|
|
8
|
+
bedrockMessages,
|
|
9
|
+
freshAdaptations,
|
|
10
|
+
type Adaptations,
|
|
11
|
+
} from './bedrock.js';
|
|
12
|
+
import { forwardedHeaders, GatewayError, parseJson, pipeResponse, readBody, sendError, watchUpstream } from './forward.js';
|
|
13
|
+
import { notReady, readModelConfig, resolveRoute, routeLabel, setCodexAuth, writeModelConfig, type ModelConfig, type Route } from './model-config.js';
|
|
14
|
+
import { codexCount, codexMessages, freshCodexState, type CodexDeps } from './codex.js';
|
|
15
|
+
import type { CodexTokens } from './codex-auth.js';
|
|
16
|
+
|
|
17
|
+
export const GATEWAY_PREFIX = '/gateway';
|
|
18
|
+
export const ANTHROPIC_BASE = 'https://api.anthropic.com';
|
|
19
|
+
export const OPENROUTER_BASE = 'https://openrouter.ai/api';
|
|
20
|
+
const MESSAGES = '/v1/messages';
|
|
21
|
+
const COUNT = '/v1/messages/count_tokens';
|
|
22
|
+
const MODELS = '/v1/models';
|
|
23
|
+
const HELLO = '/api/hello';
|
|
24
|
+
|
|
25
|
+
export interface GatewayDeps {
|
|
26
|
+
config: () => ModelConfig;
|
|
27
|
+
identify?: (key: string) => boolean;
|
|
28
|
+
anthropicBase?: string;
|
|
29
|
+
bedrockBase?: string;
|
|
30
|
+
openrouterBase?: string;
|
|
31
|
+
codex?: Partial<CodexDeps>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const learned: Adaptations = freshAdaptations();
|
|
35
|
+
const codexState = freshCodexState();
|
|
36
|
+
|
|
37
|
+
export function resetGatewayState(): void {
|
|
38
|
+
learned.fields.clear();
|
|
39
|
+
learned.dropBetas = false;
|
|
40
|
+
Object.assign(codexState, freshCodexState());
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const saveCodexTokens = (tokens: CodexTokens): void => {
|
|
44
|
+
writeModelConfig(setCodexAuth(readModelConfig(), tokens));
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const keyOf = (req: IncomingMessage): string => {
|
|
48
|
+
const raw = req.headers['x-metro-key'];
|
|
49
|
+
return (Array.isArray(raw) ? raw[0] : raw)?.trim() ?? '';
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const defaultIdentify = (key: string): boolean => key !== '' && agentIdForKey(key) !== undefined;
|
|
53
|
+
|
|
54
|
+
function modelsBody(cfg: ModelConfig): Record<string, unknown> {
|
|
55
|
+
const data: Record<string, string>[] = [];
|
|
56
|
+
if (cfg.bedrock.model !== '')
|
|
57
|
+
data.push({ id: `bedrock:${cfg.bedrock.model}`, display_name: `Bedrock · ${cfg.bedrock.model}`, description: 'Through metro, billed to Amazon Bedrock' });
|
|
58
|
+
if (cfg.openrouter.model !== '')
|
|
59
|
+
data.push({ id: `openrouter:${cfg.openrouter.model}`, display_name: `OpenRouter · ${cfg.openrouter.model}`, description: 'Through metro, billed to OpenRouter' });
|
|
60
|
+
if (cfg.codex.model !== '')
|
|
61
|
+
data.push({ id: `codex:${cfg.codex.model}`, display_name: `Codex · ${cfg.codex.model}`, description: 'Through metro, on your ChatGPT subscription' });
|
|
62
|
+
return { data, has_more: false, first_id: data[0]?.id ?? null, last_id: data.at(-1)?.id ?? null };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function requestedModel(body: Record<string, unknown>): string {
|
|
66
|
+
const model = body.model;
|
|
67
|
+
if (typeof model !== 'string' || model === '') throw new GatewayError(400, 'invalid_request_error', 'model is required');
|
|
68
|
+
return model;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function toAnthropic(
|
|
72
|
+
req: IncomingMessage,
|
|
73
|
+
res: ServerResponse,
|
|
74
|
+
raw: Buffer,
|
|
75
|
+
body: Record<string, unknown>,
|
|
76
|
+
route: Route,
|
|
77
|
+
deps: GatewayDeps,
|
|
78
|
+
): Promise<void> {
|
|
79
|
+
const explicit = typeof body.model === 'string' && body.model !== route.model;
|
|
80
|
+
const payload = explicit ? Buffer.from(JSON.stringify({ ...body, model: route.model })) : raw;
|
|
81
|
+
const url = `${deps.anthropicBase ?? ANTHROPIC_BASE}${(req.url ?? '').slice(GATEWAY_PREFIX.length)}`;
|
|
82
|
+
const watch = watchUpstream(res);
|
|
83
|
+
const upstream = await fetch(url, {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
headers: forwardedHeaders(req),
|
|
86
|
+
body: new Uint8Array(payload),
|
|
87
|
+
signal: watch.signal,
|
|
88
|
+
redirect: 'manual',
|
|
89
|
+
});
|
|
90
|
+
await pipeResponse(upstream, res, watch);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function toOpenRouter(
|
|
94
|
+
req: IncomingMessage,
|
|
95
|
+
res: ServerResponse,
|
|
96
|
+
body: Record<string, unknown>,
|
|
97
|
+
route: Route,
|
|
98
|
+
cfg: ModelConfig,
|
|
99
|
+
deps: GatewayDeps,
|
|
100
|
+
): Promise<void> {
|
|
101
|
+
const reason = notReady(cfg, 'openrouter');
|
|
102
|
+
if (reason !== null) throw new GatewayError(400, 'invalid_request_error', reason);
|
|
103
|
+
const headers: Record<string, string> = {
|
|
104
|
+
'content-type': 'application/json',
|
|
105
|
+
accept: req.headers.accept ?? 'application/json',
|
|
106
|
+
authorization: `Bearer ${cfg.openrouter.apiKey}`,
|
|
107
|
+
'anthropic-version': typeof req.headers['anthropic-version'] === 'string' ? req.headers['anthropic-version'] : '2023-06-01',
|
|
108
|
+
'http-referer': 'https://metro.box',
|
|
109
|
+
'x-title': 'metro',
|
|
110
|
+
};
|
|
111
|
+
const beta = req.headers['anthropic-beta'];
|
|
112
|
+
if (typeof beta === 'string' && beta !== '') headers['anthropic-beta'] = beta;
|
|
113
|
+
const watch = watchUpstream(res);
|
|
114
|
+
const upstream = await fetch(`${deps.openrouterBase ?? OPENROUTER_BASE}${MESSAGES}`, {
|
|
115
|
+
method: 'POST',
|
|
116
|
+
headers,
|
|
117
|
+
body: JSON.stringify({ ...body, model: route.model }),
|
|
118
|
+
signal: watch.signal,
|
|
119
|
+
redirect: 'manual',
|
|
120
|
+
});
|
|
121
|
+
await pipeResponse(upstream, res, watch, { keepalive: true, ownCredential: true });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function dispatch(req: IncomingMessage, res: ServerResponse, path: string, deps: GatewayDeps): Promise<void> {
|
|
125
|
+
const cfg = deps.config();
|
|
126
|
+
const raw = await readBody(req);
|
|
127
|
+
const body = parseJson(raw);
|
|
128
|
+
const route = resolveRoute(requestedModel(body), cfg);
|
|
129
|
+
log.info({ route: routeLabel(route), path }, 'gateway: routing');
|
|
130
|
+
if (route.provider === 'bedrock') {
|
|
131
|
+
assertBedrockReady(cfg.bedrock);
|
|
132
|
+
const up = { settings: cfg.bedrock, base: deps.bedrockBase ?? bedrockBase(cfg.bedrock.region), learned, watch: watchUpstream(res) };
|
|
133
|
+
if (path === COUNT) await bedrockCount(req, res, body, route.model, up);
|
|
134
|
+
else await bedrockMessages(req, res, body, route.model, up);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (route.provider === 'openrouter') {
|
|
138
|
+
if (path === COUNT) throw new GatewayError(404, 'not_found_error', 'OpenRouter does not count tokens');
|
|
139
|
+
await toOpenRouter(req, res, body, route, cfg, deps);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (route.provider === 'codex') {
|
|
143
|
+
if (path === COUNT) {
|
|
144
|
+
codexCount(res, body);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const codexDeps: CodexDeps = { save: saveCodexTokens, ...deps.codex };
|
|
148
|
+
await codexMessages(req, res, body, route.model, cfg, codexDeps, codexState, watchUpstream(res));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
await toAnthropic(req, res, raw, body, route, deps);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function failed(res: ServerResponse, err: unknown): void {
|
|
155
|
+
if (res.headersSent) {
|
|
156
|
+
res.end();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (err instanceof GatewayError) {
|
|
160
|
+
sendError(res, err.status, err.kind, err.message);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
log.warn({ err: errMsg(err) }, 'gateway: request failed');
|
|
164
|
+
sendError(res, 502, 'api_error', `metro gateway: ${errMsg(err)}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function answer(req: IncomingMessage, res: ServerResponse, path: string, deps: GatewayDeps): void {
|
|
168
|
+
if (req.method === 'GET' && path === MODELS) {
|
|
169
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
170
|
+
res.end(JSON.stringify(modelsBody(deps.config())));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (req.method === 'POST' && (path === MESSAGES || path === COUNT)) {
|
|
174
|
+
dispatch(req, res, path, deps).catch((err: unknown) => {
|
|
175
|
+
failed(res, err);
|
|
176
|
+
});
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
sendError(res, 404, 'not_found_error', 'not found');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function handleGatewayRequest(req: IncomingMessage, res: ServerResponse, deps: GatewayDeps): boolean {
|
|
183
|
+
const full = req.url ?? '';
|
|
184
|
+
if (full !== GATEWAY_PREFIX && !full.startsWith(`${GATEWAY_PREFIX}/`)) return false;
|
|
185
|
+
const path = (full.split('?')[0] ?? '').slice(GATEWAY_PREFIX.length);
|
|
186
|
+
if (req.method === 'HEAD' && path === HELLO) {
|
|
187
|
+
res.writeHead(200).end();
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
if ((deps.identify ?? defaultIdentify)(keyOf(req))) answer(req, res, path, deps);
|
|
191
|
+
else sendError(res, 401, 'authentication_error', 'metro gateway: no agent key (x-metro-key); start Claude Code with metro claude');
|
|
192
|
+
return true;
|
|
193
|
+
}
|