@stage-labs/metro 0.1.0-beta.321 → 0.1.0-beta.323
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/claude-env.js +2 -2
- package/dist/cli.js +1 -1
- package/package.json +1 -1
- package/runtime/node_modules/@metro-labs/core/src/runner-activity.ts +12 -12
- package/runtime/node_modules/@metro-labs/daemon/package.json +1 -0
- package/runtime/node_modules/@metro-labs/daemon/src/boot/boot.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/claude/session.ts +4 -3
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/box-gateway.ts +35 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/effective.ts +2 -8
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-api.ts +10 -9
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/org-models.ts +2 -2
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/relay.ts +108 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp-server/config.ts +2 -2
- package/runtime/node_modules/@metro-labs/daemon/src/routes/http.ts +1 -2
- package/runtime/node_modules/@metro-labs/daemon/src/routes/local-mode.ts +2 -2
- package/runtime/node_modules/@metro-labs/daemon/src/routes/session-apis.ts +1 -2
- package/runtime/node_modules/@metro-labs/http/src/api-http.ts +14 -1
- package/runtime/node_modules/@metro-labs/http/src/connector-tools.ts +10 -1
- package/runtime/node_modules/@metro-labs/http/src/mcp-server-tools.ts +21 -7
- package/runtime/node_modules/@metro-labs/inference/package.json +42 -0
- package/runtime/node_modules/@metro-labs/inference/src/attempts.ts +120 -0
- package/runtime/node_modules/@metro-labs/{daemon/src/gateway → inference/src}/codex.ts +1 -1
- package/runtime/node_modules/@metro-labs/{daemon/src/gateway → inference/src}/delivered.ts +13 -2
- package/runtime/node_modules/@metro-labs/{daemon/src/gateway → inference/src}/effort.ts +2 -2
- package/runtime/node_modules/@metro-labs/{daemon/src/gateway → inference/src}/failures.ts +11 -1
- package/runtime/node_modules/@metro-labs/{daemon/src/gateway → inference/src}/fallback.ts +5 -4
- package/runtime/node_modules/@metro-labs/{daemon/src/gateway → inference/src}/forward.ts +1 -1
- package/runtime/node_modules/@metro-labs/{daemon/src/gateway → inference/src}/gateway.ts +47 -37
- package/runtime/node_modules/@metro-labs/inference/src/measure.ts +42 -0
- package/runtime/node_modules/@metro-labs/{daemon/src/gateway → inference/src}/model-config.ts +22 -11
- package/runtime/node_modules/@metro-labs/inference/src/named.ts +36 -0
- package/runtime/node_modules/@metro-labs/{daemon/src/gateway → inference/src}/routeway.ts +2 -1
- package/runtime/node_modules/@metro-labs/inference/src/served.ts +18 -0
- package/runtime/node_modules/@metro-labs/{daemon/src/gateway → inference/src}/usage.ts +34 -9
- package/runtime/runtime.json +1 -1
- package/runtime/sdk-runner/node_modules/@metro-labs/core/src/runner-activity.ts +12 -12
- package/runtime/sdk-runner/runner.json +1 -1
- package/runtime/sdk-runner/src/app.ts +1 -1
- package/runtime/sdk-runner/src/runner.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/served.ts +0 -18
- /package/runtime/node_modules/@metro-labs/{daemon/src/gateway → inference/src}/bedrock.ts +0 -0
- /package/runtime/node_modules/@metro-labs/{daemon/src/gateway → inference/src}/cache-ttl.ts +0 -0
- /package/runtime/node_modules/@metro-labs/{daemon/src/gateway → inference/src}/codex-stream.ts +0 -0
- /package/runtime/node_modules/@metro-labs/{daemon/src/gateway → inference/src}/codex-translate.ts +0 -0
- /package/runtime/node_modules/@metro-labs/{daemon/src/gateway → inference/src}/codex-version.ts +0 -0
- /package/runtime/node_modules/@metro-labs/{daemon/src/gateway → inference/src}/eventstream.ts +0 -0
- /package/runtime/node_modules/@metro-labs/{daemon/src/gateway → inference/src}/frames.ts +0 -0
- /package/runtime/node_modules/@metro-labs/{daemon/src/gateway → inference/src}/openrouter.ts +0 -0
- /package/runtime/node_modules/@metro-labs/{daemon/src/gateway → inference/src}/subscription.ts +0 -0
- /package/runtime/node_modules/@metro-labs/{daemon/src/gateway → inference/src}/text.ts +0 -0
- /package/runtime/node_modules/@metro-labs/{daemon/src/gateway → inference/src}/tool-search.ts +0 -0
- /package/runtime/node_modules/@metro-labs/{daemon/src/gateway → inference/src}/usage-refresh.ts +0 -0
package/dist/claude-env.js
CHANGED
|
@@ -90,7 +90,7 @@ async function verdict() {
|
|
|
90
90
|
return { skip: 'the daemon is not serving here (stopped, or not running), so Claude Code talks to Anthropic directly' };
|
|
91
91
|
}
|
|
92
92
|
function gatewayLaunchEnv(key, port, claude, say) {
|
|
93
|
-
say(`inference goes through the daemon at http://127.0.0.1:${String(port)}/gateway (the
|
|
93
|
+
say(`inference goes through the daemon at http://127.0.0.1:${String(port)}/gateway (the Gateway page decides where)`);
|
|
94
94
|
const pointed = routeModelEnv(gatewayEnv(process.env, key, port), currentRoute());
|
|
95
95
|
const searched = toolSearchEnv(pointed);
|
|
96
96
|
if (searched !== pointed)
|
|
@@ -98,7 +98,7 @@ function gatewayLaunchEnv(key, port, claude, say) {
|
|
|
98
98
|
const routed = cacheTtlEnv(searched);
|
|
99
99
|
const env = credentialEnv(routed, key, claudeSignedIn(claude));
|
|
100
100
|
if (env !== routed)
|
|
101
|
-
say("Claude Code has no login of its own here, so metro's key stands in as its credential; the organization's
|
|
101
|
+
say("Claude Code has no login of its own here, so metro's key stands in as its credential; the organization's Gateway page decides where requests go");
|
|
102
102
|
return env;
|
|
103
103
|
}
|
|
104
104
|
export async function headlessEnv(claude, command) {
|
package/dist/cli.js
CHANGED
|
@@ -30,7 +30,7 @@ const USAGE = `metro: run your agent on this machine
|
|
|
30
30
|
follow this machine's inbound events, one JSON line each
|
|
31
31
|
metro whoami print the agent this machine runs
|
|
32
32
|
metro agent run chat, calls and background workers in one Agent SDK conversation,
|
|
33
|
-
using the
|
|
33
|
+
using the Gateway page's model through the daemon's model gateway;
|
|
34
34
|
resume it after a restart; call audio runs independently
|
|
35
35
|
metro agent --prepare
|
|
36
36
|
install the Agent SDK runtime and the Claude Code it bundles if needed,
|
package/package.json
CHANGED
|
@@ -8,17 +8,17 @@ export type RunnerTaskState = (typeof TASK_STATES)[number];
|
|
|
8
8
|
export type RunnerEventKind = (typeof EVENTS)[number];
|
|
9
9
|
|
|
10
10
|
const FAILURE = {
|
|
11
|
-
authentication: 'The provider refused authentication. Check the
|
|
12
|
-
billing: 'The provider refused billing. Check the
|
|
13
|
-
rate_limit: 'The provider rate limit was reached. Retry later or check the
|
|
14
|
-
provider_unavailable: 'The provider is unavailable. Retry later or check the
|
|
15
|
-
invalid_request: 'The provider rejected the request. Check Conversations and the
|
|
11
|
+
authentication: 'The provider refused authentication. Check the Gateway page.',
|
|
12
|
+
billing: 'The provider refused billing. Check the Gateway page.',
|
|
13
|
+
rate_limit: 'The provider rate limit was reached. Retry later or check the Gateway page.',
|
|
14
|
+
provider_unavailable: 'The provider is unavailable. Retry later or check the Gateway page.',
|
|
15
|
+
invalid_request: 'The provider rejected the request. Check Conversations and the Gateway page.',
|
|
16
16
|
output_limit: 'The model reached its output token limit. Check Conversations.',
|
|
17
|
-
provider_error: 'SDK turn failed (provider_error). Check Conversations and the
|
|
18
|
-
error_during_execution: 'SDK turn failed (error_during_execution). Check Conversations and the
|
|
19
|
-
error_max_turns: 'SDK turn failed (error_max_turns). Check Conversations and the
|
|
20
|
-
error_max_budget_usd: 'SDK turn failed (error_max_budget_usd). Check Conversations and the
|
|
21
|
-
error_max_structured_output_retries: 'SDK turn failed (error_max_structured_output_retries). Check Conversations and the
|
|
17
|
+
provider_error: 'SDK turn failed (provider_error). Check Conversations and the Gateway page.',
|
|
18
|
+
error_during_execution: 'SDK turn failed (error_during_execution). Check Conversations and the Gateway page.',
|
|
19
|
+
error_max_turns: 'SDK turn failed (error_max_turns). Check Conversations and the Gateway page.',
|
|
20
|
+
error_max_budget_usd: 'SDK turn failed (error_max_budget_usd). Check Conversations and the Gateway page.',
|
|
21
|
+
error_max_structured_output_retries: 'SDK turn failed (error_max_structured_output_retries). Check Conversations and the Gateway page.',
|
|
22
22
|
read_token_limit: 'Read exceeded its token limit. Read a smaller range with offset and limit, or search the file.',
|
|
23
23
|
tool_error: 'A tool failed. See its result in Conversations.',
|
|
24
24
|
task_error: 'A background task failed. See Conversations and Terminal.',
|
|
@@ -27,12 +27,12 @@ const FAILURE = {
|
|
|
27
27
|
saved_state: 'The saved Agent SDK state cannot be read. Restore it before starting.',
|
|
28
28
|
executable: 'The SDK executable could not start. Prepare the SDK runtime again.',
|
|
29
29
|
module: 'The SDK runtime could not load a required module. Prepare the SDK runtime again.',
|
|
30
|
-
configuration: 'The SDK launch configuration is missing or invalid. Check Harness and the
|
|
30
|
+
configuration: 'The SDK launch configuration is missing or invalid. Check Harness and the Gateway page before starting.',
|
|
31
31
|
process_exit: 'The SDK process exited unexpectedly. Check its reported exit code or signal before retrying.',
|
|
32
32
|
connection: 'The SDK connection failed. Check the server and provider connection.',
|
|
33
33
|
session_ended: 'The Agent SDK session ended unexpectedly.',
|
|
34
34
|
interrupted: 'The SDK stopped before finishing a chat request. Check Conversations before retrying; actions may already have run.',
|
|
35
|
-
session_error: 'The Agent SDK session failed. Check Terminal and the
|
|
35
|
+
session_error: 'The Agent SDK session failed. Check Terminal and the Gateway page.',
|
|
36
36
|
} as const;
|
|
37
37
|
export type RunnerFailureCode = keyof typeof FAILURE;
|
|
38
38
|
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@metro-labs/core": "workspace:*",
|
|
27
27
|
"@metro-labs/http": "workspace:*",
|
|
28
|
+
"@metro-labs/inference": "workspace:*",
|
|
28
29
|
"@metro-labs/discord-bot": "workspace:*",
|
|
29
30
|
"@metro-labs/telegram": "workspace:*",
|
|
30
31
|
"@metro-labs/telegram-bot": "workspace:*",
|
|
@@ -58,7 +58,7 @@ import type { StationName } from '@metro-labs/core/station-names';
|
|
|
58
58
|
import { startUploadReaper } from '../files/upload-store.js';
|
|
59
59
|
import { startAttachReaper } from '../files/attach-reaper.js';
|
|
60
60
|
import { startCloudWatchPublisher } from '../server/cloudwatch.js';
|
|
61
|
-
import { watchFallbackUsage } from '
|
|
61
|
+
import { watchFallbackUsage } from '@metro-labs/inference/usage-refresh';
|
|
62
62
|
import { startOrgModels, stopOrgModels } from '../gateway/org-models.js';
|
|
63
63
|
import { effectiveSetup, watchModelChanges } from '../gateway/effective.js';
|
|
64
64
|
import { removeModelLeftovers } from '../gateway/model-leftovers.js';
|
|
@@ -11,11 +11,12 @@ import { isRecord } from '@metro-labs/core/is-record';
|
|
|
11
11
|
import { readJson, writeJson } from '@metro-labs/core/secure-fs';
|
|
12
12
|
import { METRO_VERSION } from '@metro-labs/core/version';
|
|
13
13
|
import { agentsDir, listAgentFiles } from '../agents/files.js';
|
|
14
|
-
import {
|
|
14
|
+
import { primaryRoute, type ModelSetup } from '@metro-labs/inference/model-config';
|
|
15
15
|
import { effectiveSetup } from '../gateway/effective.js';
|
|
16
16
|
import { inSessionScope } from './memory.js';
|
|
17
17
|
import { runningAsRoot } from '../metro-user/privilege.js';
|
|
18
18
|
import { agentUser, agentUserExpected, agentViewDir, asAgent, claudeHome } from '../agent-user/user.js';
|
|
19
|
+
import { routeReadiness } from '../gateway/box-gateway.js';
|
|
19
20
|
|
|
20
21
|
function sessionEnv(): Record<string, string> {
|
|
21
22
|
const user = agentUser();
|
|
@@ -128,12 +129,12 @@ export function prepareRunnerStore(): Promise<void> {
|
|
|
128
129
|
});
|
|
129
130
|
}
|
|
130
131
|
|
|
131
|
-
const NO_MODEL = "the organization routes this agent to no model yet: choose one on the organization's
|
|
132
|
+
const NO_MODEL = "the organization routes this agent to no model yet: choose one on the organization's Gateway page";
|
|
132
133
|
|
|
133
134
|
function credentialReady(deps: SessionDeps): string | null {
|
|
134
135
|
try {
|
|
135
136
|
const primary = primaryRoute((deps.model ?? (() => effectiveSetup()))());
|
|
136
|
-
return primary === null ? NO_MODEL :
|
|
137
|
+
return primary === null ? NO_MODEL : routeReadiness(primary);
|
|
137
138
|
} catch (err) {
|
|
138
139
|
return `the model setup is not readable (${errMsg(err)})`;
|
|
139
140
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import { log } from '@metro-labs/core/log';
|
|
3
|
+
import { handleGatewayRequest, type GatewayDeps } from '@metro-labs/inference/gateway';
|
|
4
|
+
import { notReady, setupNotReady, type Route } from '@metro-labs/inference/model-config';
|
|
5
|
+
import { agentsDir } from '../agents/files.js';
|
|
6
|
+
import { agentIdForKey } from '../agents/keys.js';
|
|
7
|
+
import { reregisterMcpToken } from '../mcp-server/link.js';
|
|
8
|
+
import { readMcpToken } from '../mcp-server/token.js';
|
|
9
|
+
import { effectiveSetup } from './effective.js';
|
|
10
|
+
import { orgModels } from './org-models.js';
|
|
11
|
+
import { gatewayUrl, handleGatewayRelay } from './relay.js';
|
|
12
|
+
|
|
13
|
+
type Handler = (req: IncomingMessage, res: ServerResponse) => boolean;
|
|
14
|
+
|
|
15
|
+
const identify = (key: string): boolean => agentIdForKey(key) !== undefined;
|
|
16
|
+
|
|
17
|
+
export const routeReadiness = (route: Route): string | null => (gatewayUrl() === null ? notReady(route) : setupNotReady(route));
|
|
18
|
+
|
|
19
|
+
export const boxGateway = (): GatewayDeps => ({
|
|
20
|
+
setup: () => effectiveSetup(),
|
|
21
|
+
identify,
|
|
22
|
+
codex: { refresh: (conn) => orgModels()?.refresh(conn) ?? Promise.resolve(null) },
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export function boxGatewayHandler(): Handler {
|
|
26
|
+
const url = gatewayUrl();
|
|
27
|
+
if (url === null) {
|
|
28
|
+
const deps = boxGateway();
|
|
29
|
+
log.info('gateway: this box routes its model requests itself (METRO_GATEWAY_URL is empty)');
|
|
30
|
+
return (req, res) => handleGatewayRequest(req, res, deps);
|
|
31
|
+
}
|
|
32
|
+
log.info({ url }, 'gateway: model requests go through gateway.metro.box');
|
|
33
|
+
const deps = { url, identify, token: () => readMcpToken(agentsDir()), reregister: reregisterMcpToken };
|
|
34
|
+
return (req, res) => handleGatewayRelay(req, res, deps);
|
|
35
|
+
}
|
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
import { log } from '@metro-labs/core/log';
|
|
2
|
-
import type
|
|
3
|
-
import {
|
|
2
|
+
import { setupOf, type Delivered } from '@metro-labs/inference/delivered';
|
|
3
|
+
import { primaryRoute, runnerModel, type ModelSetup } from '@metro-labs/inference/model-config';
|
|
4
4
|
import { deliveredSetup, type OrgModels } from './org-models.js';
|
|
5
5
|
|
|
6
|
-
export function setupOf(delivered: Delivered | null): ModelSetup {
|
|
7
|
-
const route = delivered?.route ?? null;
|
|
8
|
-
if (delivered === null || route === null) return { ...emptySetup(), source: delivered?.source ?? null };
|
|
9
|
-
return { primary: route.primary, fallbacks: route.fallbacks, connections: delivered.connections, source: delivered.source, helpers: route.helpers, effort: route.effort };
|
|
10
|
-
}
|
|
11
|
-
|
|
12
6
|
export const effectiveSetup = (delivered: Delivered | null = deliveredSetup()): ModelSetup => setupOf(delivered);
|
|
13
7
|
|
|
14
8
|
export function modelViewText(setup: ModelSetup): string | null {
|
|
@@ -3,12 +3,13 @@ import { EFFORT_DEFAULTS } from '@metro-labs/core/effort';
|
|
|
3
3
|
import { ApiError } from '@metro-labs/http/api-error';
|
|
4
4
|
import { apiFailure, apiSession, cors, sendJson } from '@metro-labs/http/api-http';
|
|
5
5
|
import { effectiveSetup } from './effective.js';
|
|
6
|
-
import { chainStatus } from '
|
|
7
|
-
import { helperSetup,
|
|
6
|
+
import { chainStatus } from '@metro-labs/inference/fallback';
|
|
7
|
+
import { helperSetup, primaryRoute, type Connection, type ModelSetup } from '@metro-labs/inference/model-config';
|
|
8
|
+
import { routeReadiness } from './box-gateway.js';
|
|
8
9
|
import { orgModels, type SyncStatus } from './org-models.js';
|
|
9
|
-
import { lastServed } from '
|
|
10
|
-
import { refreshUsage, type UsageDeps } from '
|
|
11
|
-
import { usageSeen } from '
|
|
10
|
+
import { lastServed } from '@metro-labs/inference/served';
|
|
11
|
+
import { refreshUsage, type UsageDeps } from '@metro-labs/inference/usage-refresh';
|
|
12
|
+
import { usageSeen } from '@metro-labs/inference/usage';
|
|
12
13
|
|
|
13
14
|
const PATH = '/api/model';
|
|
14
15
|
const SNAPSHOT = '/api/model/snapshot';
|
|
@@ -21,7 +22,7 @@ export interface ModelApiDeps extends UsageDeps {
|
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
const NO_SYNC: SyncStatus = { enrolled: false, at: null, error: null };
|
|
24
|
-
const NO_MODEL = "No model is set up for this agent yet: choose one on the organization's
|
|
25
|
+
const NO_MODEL = "No model is set up for this agent yet: choose one on the organization's Gateway page.";
|
|
25
26
|
|
|
26
27
|
function publicConnection(c: Connection): Record<string, unknown> {
|
|
27
28
|
return {
|
|
@@ -29,8 +30,8 @@ function publicConnection(c: Connection): Record<string, unknown> {
|
|
|
29
30
|
provider: c.provider,
|
|
30
31
|
label: c.label,
|
|
31
32
|
model: c.model,
|
|
32
|
-
hasKey: c.apiKey !== '',
|
|
33
|
-
subscription: c.claudeToken !== '',
|
|
33
|
+
hasKey: c.apiKey !== '' || c.auth === 'key',
|
|
34
|
+
subscription: c.claudeToken !== '' || c.auth === 'subscription',
|
|
34
35
|
region: c.region,
|
|
35
36
|
zdr: c.zdr,
|
|
36
37
|
signedOut: c.signedOut,
|
|
@@ -52,7 +53,7 @@ function helpersBody(setup: ModelSetup): Record<string, unknown> | null {
|
|
|
52
53
|
export function settingsBody(deps: ModelApiDeps): Record<string, unknown> {
|
|
53
54
|
const setup = setupOf(deps);
|
|
54
55
|
const primary = primaryRoute(setup);
|
|
55
|
-
const reason = primary === null ? NO_MODEL :
|
|
56
|
+
const reason = primary === null ? NO_MODEL : routeReadiness(primary);
|
|
56
57
|
return {
|
|
57
58
|
source: setup.source,
|
|
58
59
|
sync: (deps.syncStatus ?? (() => orgModels()?.syncStatus() ?? NO_SYNC))(),
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { errMsg, log } from '@metro-labs/core/log';
|
|
2
2
|
import type { AgentKey } from '../connectors/agent-key.js';
|
|
3
3
|
import { agentCall, MetroApiRefusal, type MetroApi } from '../connectors/metro-api.js';
|
|
4
|
-
import { deliveredOf, refreshedConnection, type Delivered } from '
|
|
5
|
-
import type { Connection } from '
|
|
4
|
+
import { deliveredOf, refreshedConnection, type Delivered } from '@metro-labs/inference/delivered';
|
|
5
|
+
import type { Connection } from '@metro-labs/inference/model-config';
|
|
6
6
|
|
|
7
7
|
const MODELS_PATH = '/api/agents/models';
|
|
8
8
|
const REFRESH_PATH = '/api/agents/models/refresh';
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import { errMsg, log } from '@metro-labs/core/log';
|
|
3
|
+
import { cameThroughProxy } from '@metro-labs/http/api-http';
|
|
4
|
+
import { forwardedHeaders, pipeResponse, readBody, sendError, watchUpstream } from '@metro-labs/inference/forward';
|
|
5
|
+
import { usableServerUrl } from '../mcp-server/config.js';
|
|
6
|
+
|
|
7
|
+
const PREFIX = '/gateway';
|
|
8
|
+
const HELLO = '/api/hello';
|
|
9
|
+
const ROUTES = new Set(['GET /v1/models', 'POST /v1/messages', 'POST /v1/messages/count_tokens']);
|
|
10
|
+
const DEFAULT_URL = 'https://gateway.metro.box';
|
|
11
|
+
const DROPPED = ['authorization', 'x-api-key', 'cookie'];
|
|
12
|
+
const NOT_LINKED = 'metro gateway: this agent is not linked to its organization yet, so it cannot reach its models. It links once an admin of the organization opens its page on metro.box.';
|
|
13
|
+
const NOT_ACCEPTED = 'metro gateway: gateway.metro.box did not accept this agent yet. It registers again; Claude Code retries.';
|
|
14
|
+
const UNREACHABLE = 'metro gateway: gateway.metro.box could not be reached. Claude Code retries.';
|
|
15
|
+
|
|
16
|
+
export interface RelayDeps {
|
|
17
|
+
url: string;
|
|
18
|
+
identify: (key: string) => boolean;
|
|
19
|
+
token: () => string | null;
|
|
20
|
+
reregister: () => void;
|
|
21
|
+
fetch?: (url: string, init: RequestInit) => Promise<Response>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let refused: string | undefined;
|
|
25
|
+
|
|
26
|
+
export function gatewayUrl(env: NodeJS.ProcessEnv = process.env): string | null {
|
|
27
|
+
const raw = env.METRO_GATEWAY_URL?.trim() ?? DEFAULT_URL;
|
|
28
|
+
if (raw === '') return null;
|
|
29
|
+
if (usableServerUrl(raw)) return raw.replace(/\/+$/, '');
|
|
30
|
+
if (refused !== raw) {
|
|
31
|
+
refused = raw;
|
|
32
|
+
log.warn({ url: raw }, 'gateway: METRO_GATEWAY_URL must be an https url (http only on this machine), so the box routes its models itself');
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const keyOf = (req: IncomingMessage): string => {
|
|
38
|
+
const raw = req.headers['x-metro-key'];
|
|
39
|
+
return (Array.isArray(raw) ? raw[0] : raw)?.trim() ?? '';
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function upstreamHeaders(req: IncomingMessage, token: string): Record<string, string> {
|
|
43
|
+
const headers = forwardedHeaders(req);
|
|
44
|
+
for (const name of DROPPED) Reflect.deleteProperty(headers, name);
|
|
45
|
+
return { ...headers, authorization: `Bearer ${token}` };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function relay(req: IncomingMessage, res: ServerResponse, path: string, deps: RelayDeps): Promise<void> {
|
|
49
|
+
const token = deps.token();
|
|
50
|
+
if (token === null) {
|
|
51
|
+
req.resume();
|
|
52
|
+
sendError(res, 503, 'api_error', NOT_LINKED);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const body = req.method === 'POST' ? await readBody(req) : undefined;
|
|
56
|
+
const watch = watchUpstream(res);
|
|
57
|
+
let upstream: Response;
|
|
58
|
+
try {
|
|
59
|
+
upstream = await (deps.fetch ?? fetch)(`${deps.url}${path}`, {
|
|
60
|
+
method: req.method ?? 'GET',
|
|
61
|
+
headers: upstreamHeaders(req, token),
|
|
62
|
+
...(body === undefined ? {} : { body: new Uint8Array(body) }),
|
|
63
|
+
signal: watch.signal,
|
|
64
|
+
redirect: 'manual',
|
|
65
|
+
});
|
|
66
|
+
} catch (err) {
|
|
67
|
+
watch.stop();
|
|
68
|
+
log.warn({ err: errMsg(err) }, 'gateway: gateway.metro.box could not be reached');
|
|
69
|
+
sendError(res, 502, 'api_error', UNREACHABLE);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (upstream.status === 401) {
|
|
73
|
+
await upstream.body?.cancel();
|
|
74
|
+
watch.stop();
|
|
75
|
+
deps.reregister();
|
|
76
|
+
sendError(res, 503, 'api_error', NOT_ACCEPTED);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
await pipeResponse(upstream, res, watch);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function refusal(req: IncomingMessage, path: string, deps: RelayDeps): [number, string, string] | null {
|
|
83
|
+
if (cameThroughProxy(req)) return [401, 'authentication_error', 'metro gateway: the agent key works only on this machine, not through its public address'];
|
|
84
|
+
if (keyOf(req) === '' || !deps.identify(keyOf(req))) return [401, 'authentication_error', 'metro gateway: no agent key (x-metro-key); start the agent with metro agent'];
|
|
85
|
+
return ROUTES.has(`${req.method ?? 'GET'} ${path}`) ? null : [404, 'not_found_error', 'not found'];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function handleGatewayRelay(req: IncomingMessage, res: ServerResponse, deps: RelayDeps): boolean {
|
|
89
|
+
const full = req.url ?? '';
|
|
90
|
+
if (full !== PREFIX && !full.startsWith(`${PREFIX}/`)) return false;
|
|
91
|
+
const [path = '', query] = full.slice(PREFIX.length).split('?', 2);
|
|
92
|
+
if (req.method === 'HEAD' && path === HELLO) {
|
|
93
|
+
res.writeHead(200).end();
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
const refused = refusal(req, path, deps);
|
|
97
|
+
if (refused !== null) {
|
|
98
|
+
req.resume();
|
|
99
|
+
sendError(res, ...refused);
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
relay(req, res, query === undefined ? path : `${path}?${query}`, deps).catch((err: unknown) => {
|
|
103
|
+
log.warn({ err: errMsg(err) }, 'gateway: relaying a request failed');
|
|
104
|
+
if (!res.headersSent) sendError(res, 502, 'api_error', UNREACHABLE);
|
|
105
|
+
else res.end();
|
|
106
|
+
});
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
@@ -6,7 +6,7 @@ const LOOPBACK = new Set(['localhost', '127.0.0.1', '[::1]']);
|
|
|
6
6
|
|
|
7
7
|
let refused: string | undefined;
|
|
8
8
|
|
|
9
|
-
function
|
|
9
|
+
export function usableServerUrl(raw: string): boolean {
|
|
10
10
|
let url: URL;
|
|
11
11
|
try {
|
|
12
12
|
url = new URL(raw);
|
|
@@ -20,7 +20,7 @@ function usable(raw: string): boolean {
|
|
|
20
20
|
export function mcpServerUrl(env: NodeJS.ProcessEnv = process.env): string | null {
|
|
21
21
|
const raw = env.METRO_MCP_SERVER_URL?.trim() ?? DEFAULT_URL;
|
|
22
22
|
if (raw === '') return null;
|
|
23
|
-
if (
|
|
23
|
+
if (usableServerUrl(raw)) return raw.replace(/\/+$/, '');
|
|
24
24
|
if (refused !== raw) {
|
|
25
25
|
refused = raw;
|
|
26
26
|
log.warn({ url: raw }, 'mcp-server: METRO_MCP_SERVER_URL must be an https url (http only on this machine), so it is ignored');
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { handleSessionApis, type SessionApis } from './session-apis.js';
|
|
2
|
-
import { handleGatewayRequest } from '../gateway/gateway.js';
|
|
3
2
|
import { handleThreemaCallback } from './threema-callback.js';
|
|
4
3
|
import { BodyTooLargeError, readBody, WEBHOOK_BODY_MAX } from './body.js';
|
|
5
4
|
|
|
@@ -329,7 +328,7 @@ function handleSignInRoutes(
|
|
|
329
328
|
|
|
330
329
|
function handleEarlyRoutes(req: IncomingMessage, res: ServerResponse, apis: SessionApis): boolean {
|
|
331
330
|
if (handleHealth(req, res)) return true;
|
|
332
|
-
if (apis.gateway
|
|
331
|
+
if (apis.gateway?.(req, res) === true) return true;
|
|
333
332
|
if (handleSignInRoutes(req, res, apis)) return true;
|
|
334
333
|
return handleSessionApis(req, res, apis);
|
|
335
334
|
}
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
localOwner,
|
|
25
25
|
readLocalAgentFile,
|
|
26
26
|
} from '../agents/file-admin.js';
|
|
27
|
-
import {
|
|
27
|
+
import { boxGatewayHandler } from '../gateway/box-gateway.js';
|
|
28
28
|
import type { StationName } from '@metro-labs/core/station-names';
|
|
29
29
|
|
|
30
30
|
export interface LocalModeDeps {
|
|
@@ -148,7 +148,7 @@ export function localSessionApis(deps: LocalModeDeps): SessionApis {
|
|
|
148
148
|
ownerApi: { setOwner: setLocalOwner },
|
|
149
149
|
machineApi: {},
|
|
150
150
|
modelApi: {},
|
|
151
|
-
gateway:
|
|
151
|
+
gateway: boxGatewayHandler(),
|
|
152
152
|
terminalApi: {},
|
|
153
153
|
mode: localModeInfo,
|
|
154
154
|
};
|
|
@@ -11,7 +11,6 @@ import { handleControlRequest, type ControlApiDeps } from '../server/control.js'
|
|
|
11
11
|
import { handleOwnerRequest, type OwnerApiDeps } from '../server/owner.js';
|
|
12
12
|
import { handleMachineRequest, type MachineApiDeps } from '../server/machine.js';
|
|
13
13
|
import { handleModelRequest, type ModelApiDeps } from '../gateway/model-api.js';
|
|
14
|
-
import type { GatewayDeps } from '../gateway/gateway.js';
|
|
15
14
|
import { handleTerminalRequest, type TerminalApiDeps } from '../terminal/api.js';
|
|
16
15
|
import { handleSchedulesRequest } from '../agent-user/schedules-api.js';
|
|
17
16
|
import { handleFilesRequest } from '../agent-user/files-api.js';
|
|
@@ -26,7 +25,7 @@ export interface SessionApis {
|
|
|
26
25
|
ownerApi?: OwnerApiDeps;
|
|
27
26
|
machineApi?: MachineApiDeps;
|
|
28
27
|
modelApi?: ModelApiDeps;
|
|
29
|
-
gateway?:
|
|
28
|
+
gateway?: (req: IncomingMessage, res: ServerResponse) => boolean;
|
|
30
29
|
terminalApi?: TerminalApiDeps;
|
|
31
30
|
claudeApi?: ClaudeApiDeps;
|
|
32
31
|
enrollApi?: EnrollApiDeps;
|
|
@@ -94,10 +94,23 @@ export function apiFailure(
|
|
|
94
94
|
sendJson(req, res, err.status, { error: err.message });
|
|
95
95
|
return;
|
|
96
96
|
}
|
|
97
|
-
log.warn({ err: errMsg(err) }, `${label}: request failed`);
|
|
97
|
+
log.warn({ err: errMsg(err), ...causesOf(err) }, `${label}: request failed`);
|
|
98
98
|
if (!res.headersSent) sendJson(req, res, 500, { error: `${label} failed` });
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
const CAUSES_MAX = 3;
|
|
102
|
+
|
|
103
|
+
export function causesOf(err: unknown): { causes?: string[] } {
|
|
104
|
+
const causes: string[] = [];
|
|
105
|
+
let at = err instanceof Error ? err.cause : undefined;
|
|
106
|
+
while (at !== undefined && at !== null && causes.length < CAUSES_MAX) {
|
|
107
|
+
const code = typeof at === 'object' && 'code' in at && typeof at.code === 'string' ? `${at.code} ` : '';
|
|
108
|
+
causes.push(`${code}${errMsg(at)}`.slice(0, 300));
|
|
109
|
+
at = at instanceof Error ? at.cause : undefined;
|
|
110
|
+
}
|
|
111
|
+
return causes.length === 0 ? {} : { causes };
|
|
112
|
+
}
|
|
113
|
+
|
|
101
114
|
export interface SessionRoute {
|
|
102
115
|
methods: Readonly<Record<string, readonly string[]>>;
|
|
103
116
|
admin: boolean | readonly string[];
|
|
@@ -42,10 +42,19 @@ function hintsOf(annotations: Record<string, unknown>): Pick<RemoteTool, 'destru
|
|
|
42
42
|
return { ...(annotations.destructiveHint === true ? { destructive: true } : {}), ...(annotations.openWorldHint === true ? { openWorld: true } : {}) };
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
export function toolTitleOf(name: string): string {
|
|
46
|
+
const words = name.replace(/([a-z0-9])([A-Z])/g, '$1 $2').split(/[\s_\-.]+/).filter((word) => word !== '');
|
|
47
|
+
if (words.length === 0) return name;
|
|
48
|
+
const text = words.map((word, at) => (at > 0 && word !== word.toUpperCase() ? word.toLowerCase() : word)).join(' ');
|
|
49
|
+
return `${text.slice(0, 1).toUpperCase()}${text.slice(1)}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const filled = (value: unknown): string | null => (typeof value === 'string' && value.trim() !== '' ? value.trim() : null);
|
|
53
|
+
|
|
45
54
|
function toTool(raw: unknown): RemoteTool | null {
|
|
46
55
|
if (!isRecord(raw) || typeof raw.name !== 'string') return null;
|
|
47
56
|
const annotations = isRecord(raw.annotations) ? raw.annotations : {};
|
|
48
|
-
const title =
|
|
57
|
+
const title = filled(raw.title) ?? filled(annotations.title) ?? toolTitleOf(raw.name);
|
|
49
58
|
return { name: raw.name, title, description: typeof raw.description === 'string' ? raw.description : '', readOnly: annotations.readOnlyHint === true, ...hintsOf(annotations), ...(isRecord(raw.inputSchema) ? { inputSchema: raw.inputSchema } : {}) };
|
|
50
59
|
}
|
|
51
60
|
|
|
@@ -45,15 +45,29 @@ const callSchema = {
|
|
|
45
45
|
additionalProperties: false,
|
|
46
46
|
};
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
export interface ConnectorLabel {
|
|
49
|
+
key: string;
|
|
50
|
+
name: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const LABELS_MAX = 900;
|
|
54
|
+
|
|
55
|
+
export function connectorLabel(connector: ConnectorLabel): string {
|
|
56
|
+
const name = connector.name.replace(/\s+/g, ' ').trim();
|
|
57
|
+
return name === '' || name.toLowerCase() === connector.key ? connector.key : `${connector.key} (${name})`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function connectorsLine(connectors: ConnectorLabel[] | null): string {
|
|
61
|
+
if (connectors === null) return 'The connectors of this agent are named here once Metro reaches mcp.metro.box.';
|
|
62
|
+
if (connectors.length === 0) return 'This agent has no connectors it may use yet.';
|
|
63
|
+
const labels = connectors.map(connectorLabel).join(', ');
|
|
64
|
+
return `This agent's connectors: ${labels.length > LABELS_MAX ? connectors.map((connector) => connector.key).join(', ') : labels}.`;
|
|
51
65
|
}
|
|
52
66
|
|
|
53
|
-
export const toolSearchDescription = (
|
|
67
|
+
export const toolSearchDescription = (connectors: ConnectorLabel[] | null): string =>
|
|
54
68
|
[
|
|
55
69
|
'Search the tools of every connector assigned to this agent: third-party services such as Notion, n8n, Linear or Gmail, run by mcp.metro.box. Same design as Claude Code\'s ToolSearch, for connectors.',
|
|
56
|
-
connectorsLine(
|
|
70
|
+
connectorsLine(connectors),
|
|
57
71
|
'Three query forms, one example each:',
|
|
58
72
|
'- "select:notion.search" loads exact tools by name (comma separated for several).',
|
|
59
73
|
'- "notion create page" ranks tools by the words in their name and description.',
|
|
@@ -84,12 +98,12 @@ const toolCallDescription = (place: ApprovalPlace): string =>
|
|
|
84
98
|
|
|
85
99
|
export const TOOL_CALL_DESCRIPTION = toolCallDescription('session');
|
|
86
100
|
|
|
87
|
-
export function mcpServerTools(
|
|
101
|
+
export function mcpServerTools(connectors: ConnectorLabel[] | null, place: ApprovalPlace = 'session'): McpServerTool[] {
|
|
88
102
|
return [
|
|
89
103
|
{
|
|
90
104
|
name: TOOL_SEARCH,
|
|
91
105
|
title: 'Search connector tools',
|
|
92
|
-
description: toolSearchDescription(
|
|
106
|
+
description: toolSearchDescription(connectors),
|
|
93
107
|
inputSchema: searchSchema,
|
|
94
108
|
annotations: { title: 'Search connector tools', readOnlyHint: true, openWorldHint: false },
|
|
95
109
|
},
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@metro-labs/inference",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "The model router: Anthropic, Routeway, OpenRouter, Bedrock and Codex, backups with holds, effort per thread, tool search fitting and usage from headers and stream frames. Run by the daemon's /gateway today, by gateway.metro.box next (docs/GATEWAY-SERVER.md). Imports only core and http, never a station.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
"./attempts": "./src/attempts.ts",
|
|
9
|
+
"./codex": "./src/codex.ts",
|
|
10
|
+
"./codex-stream": "./src/codex-stream.ts",
|
|
11
|
+
"./codex-translate": "./src/codex-translate.ts",
|
|
12
|
+
"./codex-version": "./src/codex-version.ts",
|
|
13
|
+
"./delivered": "./src/delivered.ts",
|
|
14
|
+
"./effort": "./src/effort.ts",
|
|
15
|
+
"./eventstream": "./src/eventstream.ts",
|
|
16
|
+
"./failures": "./src/failures.ts",
|
|
17
|
+
"./fallback": "./src/fallback.ts",
|
|
18
|
+
"./forward": "./src/forward.ts",
|
|
19
|
+
"./frames": "./src/frames.ts",
|
|
20
|
+
"./gateway": "./src/gateway.ts",
|
|
21
|
+
"./model-config": "./src/model-config.ts",
|
|
22
|
+
"./served": "./src/served.ts",
|
|
23
|
+
"./tool-search": "./src/tool-search.ts",
|
|
24
|
+
"./usage": "./src/usage.ts",
|
|
25
|
+
"./usage-refresh": "./src/usage-refresh.ts"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsc",
|
|
29
|
+
"test": "tsc --noEmit"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@metro-labs/core": "workspace:*",
|
|
33
|
+
"@metro-labs/http": "workspace:*"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@stage-labs/config": "0.1.0-beta.3",
|
|
37
|
+
"eslint": "^10.3.0",
|
|
38
|
+
"knip": "^6.15.0",
|
|
39
|
+
"madge": "^8.0.0",
|
|
40
|
+
"typescript": "^5"
|
|
41
|
+
}
|
|
42
|
+
}
|