@stage-labs/metro 0.1.0-beta.307 → 0.1.0-beta.309
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/agent.js +23 -28
- package/dist/child.js +33 -0
- package/dist/claude-env.js +104 -0
- package/dist/cli.js +7 -21
- package/dist/lock.js +62 -0
- package/dist/memory.js +12 -27
- package/dist/root-release-io.js +1 -2
- package/dist/root-release.js +1 -1
- package/dist/route.js +0 -1
- package/hooks/guard.mjs +1 -5
- package/hooks/session-start.mjs +1 -1
- package/package.json +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/provision.ts +13 -38
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/user.ts +21 -1
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/view.ts +3 -9
- package/runtime/node_modules/@metro-labs/daemon/src/boot/boot.ts +14 -12
- package/runtime/node_modules/@metro-labs/daemon/src/claude/api.ts +6 -18
- package/runtime/node_modules/@metro-labs/daemon/src/claude/github-skills.ts +3 -7
- package/runtime/node_modules/@metro-labs/daemon/src/claude/login.ts +6 -5
- package/runtime/node_modules/@metro-labs/daemon/src/claude/onboarding.ts +1 -23
- package/runtime/node_modules/@metro-labs/daemon/src/claude/session.ts +39 -118
- package/runtime/node_modules/@metro-labs/daemon/src/claude/setup-api.ts +14 -61
- package/runtime/node_modules/@metro-labs/daemon/src/claude/setup.ts +10 -42
- package/runtime/node_modules/@metro-labs/daemon/src/claude/usage-probe.ts +2 -2
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/bedrock.ts +1 -6
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/claude-logins.ts +53 -29
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/claude-store.ts +55 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex.ts +40 -62
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/delivered.ts +82 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/effective.ts +47 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/fallback.ts +18 -19
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/forward.ts +0 -1
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gateway.ts +37 -77
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-api.ts +86 -220
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-config.ts +80 -283
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-import.ts +125 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-lists.ts +0 -17
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-v2.ts +79 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/openrouter.ts +0 -79
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/org-models.ts +231 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/provider-models.ts +4 -44
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/text.ts +0 -2
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/usage-refresh.ts +37 -57
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/usage.ts +0 -13
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/index.ts +5 -15
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/instructions.ts +0 -11
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/session-slot.ts +3 -7
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/session.ts +12 -24
- package/runtime/node_modules/@metro-labs/daemon/src/routes/bearer.ts +1 -4
- 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 +3 -4
- package/runtime/node_modules/@metro-labs/daemon/src/voice/call.ts +9 -92
- package/runtime/node_modules/@metro-labs/daemon/src/voice/calls.ts +1 -4
- package/runtime/node_modules/@metro-labs/daemon/src/voice/scribe.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/voice/shared.ts +0 -2
- package/runtime/node_modules/@metro-labs/daemon/src/voice/speech.ts +0 -4
- package/runtime/node_modules/@metro-labs/daemon/src/voice/store.ts +1 -14
- package/runtime/node_modules/@metro-labs/http/src/api-http.ts +0 -1
- package/runtime/runtime.json +1 -1
- package/runtime/sdk-runner/runner.json +1 -1
- package/dist/background.js +0 -141
- package/dist/claude.js +0 -204
- package/dist/mcp-config.js +0 -27
- package/dist/native-recovery-scan.js +0 -246
- package/dist/native-recovery-state.js +0 -154
- package/dist/native-recovery.js +0 -164
- package/dist/onboarding.js +0 -44
- package/dist/voice.js +0 -56
- package/runtime/node_modules/@metro-labs/daemon/src/claude/channel-check.ts +0 -56
- package/runtime/node_modules/@metro-labs/daemon/src/claude/pinned.ts +0 -16
- package/runtime/node_modules/@metro-labs/daemon/src/claude/plugin-cleanup.ts +0 -121
- package/runtime/node_modules/@metro-labs/daemon/src/claude/runner.ts +0 -45
- package/runtime/node_modules/@metro-labs/daemon/src/claude/session-continuity.ts +0 -57
- package/runtime/node_modules/@metro-labs/daemon/src/claude/version.ts +0 -76
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-auth.ts +0 -219
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-device.ts +0 -119
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-auth.ts +0 -160
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-client.ts +0 -46
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-setup.ts +0 -150
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-stream.ts +0 -139
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-translate.ts +0 -236
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini.ts +0 -208
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-signin.ts +0 -157
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-store.ts +0 -83
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/openrouter-login.ts +0 -192
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/openrouter-signin.ts +0 -64
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/pkce.ts +0 -36
- package/runtime/node_modules/@metro-labs/daemon/src/voice/approvals.ts +0 -38
- package/runtime/node_modules/@metro-labs/daemon/src/voice/brain.ts +0 -146
- package/runtime/node_modules/@metro-labs/daemon/src/voice/speakable.ts +0 -54
- package/runtime/node_modules/@metro-labs/daemon/src/voice/talk.ts +0 -264
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { isRecord } from '@metro-labs/core/is-record';
|
|
2
|
+
import { isProvider, LOCAL_PREFIX, type Account, type CodexAccess, type Connection, type Fallback, type Provider, type RouteSource } from './model-config.js';
|
|
3
|
+
|
|
4
|
+
export interface DeliveredRoute {
|
|
5
|
+
primary: Fallback | null;
|
|
6
|
+
fallbacks: Fallback[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface Delivered {
|
|
10
|
+
route: DeliveredRoute | null;
|
|
11
|
+
source: RouteSource | null;
|
|
12
|
+
connections: Connection[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const MAX_FALLBACKS = 8;
|
|
16
|
+
const MAX_CONNECTIONS = 20;
|
|
17
|
+
|
|
18
|
+
const text = (value: unknown): string => (typeof value === 'string' ? value.trim() : '');
|
|
19
|
+
const maybe = (value: unknown): string | null => (typeof value === 'string' && value !== '' ? value : null);
|
|
20
|
+
|
|
21
|
+
function refOf(raw: unknown): Fallback | null {
|
|
22
|
+
if (!isRecord(raw)) return null;
|
|
23
|
+
const ref = { connection: text(raw.connection), model: text(raw.model) };
|
|
24
|
+
return ref.connection === '' ? null : ref;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function routeOf(raw: unknown): DeliveredRoute | null {
|
|
28
|
+
if (!isRecord(raw)) return null;
|
|
29
|
+
const fallbacks = Array.isArray(raw.fallbacks) ? raw.fallbacks.flatMap((f: unknown) => refOf(f) ?? []) : [];
|
|
30
|
+
return { primary: refOf(raw.primary), fallbacks: fallbacks.filter((f) => f.model !== '').slice(0, MAX_FALLBACKS) };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function accountOf(raw: unknown): Account {
|
|
34
|
+
const account = isRecord(raw) ? raw : {};
|
|
35
|
+
const method = account.method === 'chatgpt' || account.method === 'code' ? account.method : null;
|
|
36
|
+
return { email: maybe(account.email), plan: maybe(account.plan), method };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const numberOf = (value: unknown): number => (typeof value === 'number' && Number.isFinite(value) ? value : 0);
|
|
40
|
+
|
|
41
|
+
function codexOf(raw: Record<string, unknown>): CodexAccess | null {
|
|
42
|
+
const accessToken = text(raw.accessToken);
|
|
43
|
+
if (accessToken === '' || (raw.method !== 'chatgpt' && raw.method !== 'code')) return null;
|
|
44
|
+
return { method: raw.method, accessToken, expiresAt: numberOf(raw.expiresAt), accountId: maybe(raw.accountId) };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function credentialOf(provider: Provider, raw: unknown): { apiKey: string; codex: CodexAccess | null } {
|
|
48
|
+
if (!isRecord(raw)) return { apiKey: '', codex: null };
|
|
49
|
+
return provider === 'codex' ? { apiKey: '', codex: codexOf(raw) } : { apiKey: text(raw.apiKey), codex: null };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function deliveredConnection(raw: unknown): Connection | null {
|
|
53
|
+
if (!isRecord(raw) || !isProvider(raw.provider)) return null;
|
|
54
|
+
const id = text(raw.id);
|
|
55
|
+
if (id === '' || id.startsWith(LOCAL_PREFIX)) return null;
|
|
56
|
+
const { apiKey, codex } = credentialOf(raw.provider, raw.credential);
|
|
57
|
+
return {
|
|
58
|
+
id,
|
|
59
|
+
provider: raw.provider,
|
|
60
|
+
label: text(raw.label) === '' ? raw.provider : text(raw.label),
|
|
61
|
+
model: text(raw.model),
|
|
62
|
+
apiKey,
|
|
63
|
+
region: text(raw.region),
|
|
64
|
+
zdr: raw.zdr === true,
|
|
65
|
+
revision: numberOf(raw.revision),
|
|
66
|
+
codex,
|
|
67
|
+
claude: null,
|
|
68
|
+
account: accountOf(raw.account),
|
|
69
|
+
signedOut: codex === null && apiKey === '',
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function deliveredOf(raw: Record<string, unknown>): Delivered | null {
|
|
74
|
+
if (!Array.isArray(raw.connections) || (raw.route !== null && !isRecord(raw.route))) return null;
|
|
75
|
+
const source = raw.source === 'agent' || raw.source === 'organization' ? raw.source : null;
|
|
76
|
+
const connections = raw.connections.flatMap((c: unknown) => deliveredConnection(c) ?? []).slice(0, MAX_CONNECTIONS);
|
|
77
|
+
return { route: routeOf(raw.route), source, connections };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function refreshedConnection(raw: Record<string, unknown>): Connection | null {
|
|
81
|
+
return deliveredConnection(isRecord(raw.connection) ? raw.connection : raw);
|
|
82
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { log } from '@metro-labs/core/log';
|
|
2
|
+
import { agentsDir } from '../agents/files.js';
|
|
3
|
+
import { claudeLoginsIn, isV2, readModelFile, type ClaudeLogin } from './claude-store.js';
|
|
4
|
+
import type { Delivered } from './delivered.js';
|
|
5
|
+
import { emptySetup, LOCAL_PREFIX, loginConnection, primaryRoute, runnerModel, SESSION_LOGIN, sessionConnection, type Connection, type ModelSetup } from './model-config.js';
|
|
6
|
+
import { v2Setup } from './model-v2.js';
|
|
7
|
+
import { deliveredSetup, type OrgModels } from './org-models.js';
|
|
8
|
+
|
|
9
|
+
function localOf(id: string, logins: ClaudeLogin[]): Connection | null {
|
|
10
|
+
if (id === SESSION_LOGIN) return sessionConnection();
|
|
11
|
+
if (!id.startsWith(LOCAL_PREFIX)) return null;
|
|
12
|
+
const login = id.slice(LOCAL_PREFIX.length);
|
|
13
|
+
return loginConnection(login, logins.find((l) => l.id === login));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function setupOf(delivered: Delivered | null, logins: ClaudeLogin[]): ModelSetup {
|
|
17
|
+
const route = delivered?.route ?? null;
|
|
18
|
+
if (delivered === null || route === null) return { ...emptySetup(), source: delivered?.source ?? null };
|
|
19
|
+
const named = [...(route.primary === null ? [] : [route.primary]), ...route.fallbacks].map((r) => r.connection);
|
|
20
|
+
const locals = [...new Set(named)].flatMap((id) => localOf(id, logins) ?? []);
|
|
21
|
+
return { primary: route.primary, fallbacks: route.fallbacks, connections: [...delivered.connections, ...locals], source: delivered.source };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function effectiveSetup(dir = agentsDir(), delivered: Delivered | null = deliveredSetup()): ModelSetup {
|
|
25
|
+
const raw = readModelFile(dir);
|
|
26
|
+
return isV2(raw) ? v2Setup(raw) : setupOf(delivered, claudeLoginsIn(raw));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function modelViewText(setup: ModelSetup): string | null {
|
|
30
|
+
const primary = primaryRoute(setup);
|
|
31
|
+
if (primary === null) return null;
|
|
32
|
+
const connections = [{ id: primary.connection.id, provider: primary.connection.provider, model: primary.model }];
|
|
33
|
+
return `${JSON.stringify({ version: 2, route: primary.connection.id, connections }, null, 2)}\n`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function watchModelChanges(models: Pick<OrgModels, 'onChange'>, switchModel: (model: string | null) => boolean, setup: () => ModelSetup = effectiveSetup): () => void {
|
|
37
|
+
let last = runnerModel(setup());
|
|
38
|
+
const changed = (): void => {
|
|
39
|
+
const now = runnerModel(setup());
|
|
40
|
+
if (now === last) return;
|
|
41
|
+
const was = last;
|
|
42
|
+
last = now;
|
|
43
|
+
if (switchModel(now)) log.info({ was, now }, 'org-models: the model changed, so the Agent SDK session switches to it live, with no restart');
|
|
44
|
+
else log.info({ was, now }, 'org-models: the model changed while no Agent SDK session listens; its next start loads it');
|
|
45
|
+
};
|
|
46
|
+
return models.onChange(changed);
|
|
47
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { log } from '@metro-labs/core/log';
|
|
2
2
|
import { lastServed } from './served.js';
|
|
3
3
|
import { PER_MINUTE, usageOf, type UsageWindow } from './usage.js';
|
|
4
|
-
import { bareModel, connectionOf, isSmallModel, notReady,
|
|
4
|
+
import { bareModel, connectionOf, isSmallModel, notReady, primaryRoute, routeLabel, type Connection, type Fallback, type ModelSetup, type Route } from './model-config.js';
|
|
5
5
|
|
|
6
6
|
export const USAGE_LIMIT = 0.95;
|
|
7
7
|
const COOLDOWN_MS = 60_000;
|
|
@@ -36,7 +36,6 @@ function scopeOf(label: string): string | null {
|
|
|
36
36
|
function applies(window: UsageWindow, route: Route, now: number): boolean {
|
|
37
37
|
if (window.used === null || window.label === PER_MINUTE) return false;
|
|
38
38
|
if (window.resetAt !== null && Date.parse(window.resetAt) <= now) return false;
|
|
39
|
-
if (route.connection.provider === 'gemini') return window.label === route.model;
|
|
40
39
|
const scope = scopeOf(window.label);
|
|
41
40
|
return scope === null || route.model.toLowerCase().split(/[^a-z0-9]+/).includes(scope);
|
|
42
41
|
}
|
|
@@ -59,16 +58,16 @@ function fallbackModel(requested: string, conn: Connection, model: string): stri
|
|
|
59
58
|
return conn.provider === 'anthropic' && isSmallModel(bare) ? bare : model;
|
|
60
59
|
}
|
|
61
60
|
|
|
62
|
-
function fallbackRoute(
|
|
63
|
-
const conn = connectionOf(
|
|
64
|
-
if (conn === null || notReady(
|
|
61
|
+
function fallbackRoute(setup: ModelSetup, fallback: Fallback, requested: string): Route | null {
|
|
62
|
+
const conn = connectionOf(setup, fallback.connection);
|
|
63
|
+
if (conn === null || notReady({ connection: conn, model: fallback.model }) !== null) return null;
|
|
65
64
|
return { connection: conn, model: fallbackModel(requested, conn, fallback.model) };
|
|
66
65
|
}
|
|
67
66
|
|
|
68
67
|
const unique = (routes: Route[]): Route[] => routes.filter((route, at) => routes.findIndex((other) => keyOf(other) === keyOf(route)) === at);
|
|
69
68
|
|
|
70
|
-
export const chainOf = (primary: Route,
|
|
71
|
-
unique([primary, ...
|
|
69
|
+
export const chainOf = (primary: Route, setup: ModelSetup, requested: string): Route[] =>
|
|
70
|
+
unique([primary, ...setup.fallbacks.flatMap((f) => fallbackRoute(setup, f, requested) ?? [])]);
|
|
72
71
|
|
|
73
72
|
export function routesToTry(chain: Route[], now = Date.now()): Route[] {
|
|
74
73
|
const open = chain.filter((route) => holdOf(route, now) === null);
|
|
@@ -120,28 +119,28 @@ export interface ChainRow {
|
|
|
120
119
|
active: boolean;
|
|
121
120
|
}
|
|
122
121
|
|
|
123
|
-
function primaryOf(
|
|
124
|
-
const
|
|
125
|
-
if (
|
|
122
|
+
function primaryOf(setup: ModelSetup): Route | null {
|
|
123
|
+
const primary = primaryRoute(setup);
|
|
124
|
+
if (primary === null) return null;
|
|
126
125
|
const served = lastServed();
|
|
127
|
-
const model =
|
|
128
|
-
return { connection:
|
|
126
|
+
const model = primary.model !== '' ? primary.model : served?.connection === primary.connection.id ? served.model : '';
|
|
127
|
+
return { connection: primary.connection, model };
|
|
129
128
|
}
|
|
130
129
|
|
|
131
|
-
function rowOf(
|
|
132
|
-
const reason = notReady(
|
|
130
|
+
function rowOf(route: Route, now: number): Omit<ChainRow, 'active'> {
|
|
131
|
+
const reason = notReady(route);
|
|
133
132
|
const used = windowsFor(route, now).reduce<number | null>((most, w) => Math.max(most ?? 0, w.used ?? 0), null);
|
|
134
133
|
return { connection: route.connection.id, model: route.model, used, hold: reason === null ? holdOf(route, now) : { reason, until: null } };
|
|
135
134
|
}
|
|
136
135
|
|
|
137
|
-
export function chainStatus(
|
|
138
|
-
const primary = primaryOf(
|
|
136
|
+
export function chainStatus(setup: ModelSetup, now = Date.now()): ChainRow[] {
|
|
137
|
+
const primary = primaryOf(setup);
|
|
139
138
|
if (primary === null) return [];
|
|
140
|
-
const listed =
|
|
141
|
-
const conn = connectionOf(
|
|
139
|
+
const listed = setup.fallbacks.flatMap((f) => {
|
|
140
|
+
const conn = connectionOf(setup, f.connection);
|
|
142
141
|
return conn === null ? [] : [{ connection: conn, model: f.model }];
|
|
143
142
|
});
|
|
144
|
-
const rows = unique([primary, ...listed]).map((route) => rowOf(
|
|
143
|
+
const rows = unique([primary, ...listed]).map((route) => rowOf(route, now));
|
|
145
144
|
const active = Math.max(0, rows.findIndex((row) => row.hold === null));
|
|
146
145
|
return rows.map((row, at) => ({ ...row, active: at === active }));
|
|
147
146
|
}
|
|
@@ -2,9 +2,7 @@ import type { IncomingHttpHeaders, IncomingMessage, ServerResponse } from 'node:
|
|
|
2
2
|
import { errMsg, log } from '@metro-labs/core/log';
|
|
3
3
|
import { cameThroughProxy } from '@metro-labs/http/api-http';
|
|
4
4
|
import { agentIdForKey } from '../agents/keys.js';
|
|
5
|
-
import { sdkAllowed, sdkConnectionAllowed, SDK_NEEDS_KEY } from '../claude/runner.js';
|
|
6
5
|
import {
|
|
7
|
-
assertBedrockReady,
|
|
8
6
|
bedrockBase,
|
|
9
7
|
bedrockCount,
|
|
10
8
|
bedrockMessages,
|
|
@@ -38,21 +36,17 @@ import {
|
|
|
38
36
|
withEffort,
|
|
39
37
|
withThinkingFor,
|
|
40
38
|
} from './effort.js';
|
|
41
|
-
import { notReady,
|
|
42
|
-
import { codexMessages,
|
|
43
|
-
import { geminiMessages, sharedGeminiState } from './gemini.js';
|
|
39
|
+
import { notReady, resolveRoute, routeLabel, sessionConnection, type Connection, type ModelSetup, type Route } from './model-config.js';
|
|
40
|
+
import { codexMessages, type CodexDeps } from './codex.js';
|
|
44
41
|
import { countTokens } from './subscription.js';
|
|
45
|
-
import type { GeminiDeps } from './gemini.js';
|
|
46
|
-
import type { CodexDeps } from './codex.js';
|
|
47
|
-
import type { GeminiTokens } from './gemini-auth.js';
|
|
48
42
|
import { OPENROUTER_BASE } from './openrouter.js';
|
|
49
43
|
import { isRecord } from '@metro-labs/core/is-record';
|
|
50
44
|
import { forgetServed, noteServed } from './served.js';
|
|
51
45
|
import { forgetUsage, noteUsageHeaders, UsageScanner } from './usage.js';
|
|
52
46
|
import { fitToolSearch } from './tool-search.js';
|
|
53
47
|
import { ANTHROPIC_API, refreshLoginModels } from './provider-models.js';
|
|
54
|
-
import type { CodexTokens } from './codex-auth.js';
|
|
55
48
|
import { forgetClaudeTokens, withClaudeLogin, type ClaudeLoginDeps } from './claude-logins.js';
|
|
49
|
+
import { orgModels } from './org-models.js';
|
|
56
50
|
|
|
57
51
|
const GATEWAY_PREFIX = '/gateway';
|
|
58
52
|
const MESSAGES = '/v1/messages';
|
|
@@ -61,13 +55,12 @@ const MODELS = '/v1/models';
|
|
|
61
55
|
const HELLO = '/api/hello';
|
|
62
56
|
|
|
63
57
|
export interface GatewayDeps {
|
|
64
|
-
|
|
58
|
+
setup: () => ModelSetup;
|
|
65
59
|
identify?: (key: string) => boolean;
|
|
66
60
|
anthropicBase?: string;
|
|
67
61
|
bedrockBase?: string;
|
|
68
62
|
openrouterBase?: string;
|
|
69
63
|
codex?: Partial<CodexDeps>;
|
|
70
|
-
gemini?: Partial<GeminiDeps>;
|
|
71
64
|
claudeLogins?: ClaudeLoginDeps;
|
|
72
65
|
}
|
|
73
66
|
|
|
@@ -80,52 +73,35 @@ export function resetGatewayState(): void {
|
|
|
80
73
|
learned.fields.clear();
|
|
81
74
|
learned.dropBetas = false;
|
|
82
75
|
forgetLearnedThinking();
|
|
83
|
-
sharedCodexState.clear();
|
|
84
|
-
sharedGeminiState.clear();
|
|
85
76
|
forgetClaudeTokens();
|
|
86
77
|
}
|
|
87
78
|
|
|
88
|
-
const
|
|
89
|
-
writeModelConfig(setCodexAuth(readModelConfig(), id, tokens));
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
const saveGeminiTokens = (id: string, tokens: GeminiTokens): void => {
|
|
93
|
-
writeModelConfig(setGeminiAuth(readModelConfig(), id, tokens));
|
|
94
|
-
};
|
|
79
|
+
const refreshCodex = (conn: Connection): Promise<Connection | null> => orgModels()?.refresh(conn) ?? Promise.resolve(null);
|
|
95
80
|
|
|
96
81
|
const keyOf = (req: IncomingMessage): string => {
|
|
97
82
|
const raw = req.headers['x-metro-key'];
|
|
98
83
|
return (Array.isArray(raw) ? raw[0] : raw)?.trim() ?? '';
|
|
99
84
|
};
|
|
100
85
|
|
|
101
|
-
const fromSdkRunner = (req: IncomingMessage): boolean => {
|
|
102
|
-
const raw = req.headers['x-metro-runner'];
|
|
103
|
-
return (Array.isArray(raw) ? raw[0] : raw)?.trim() === 'sdk';
|
|
104
|
-
};
|
|
105
|
-
|
|
106
86
|
const defaultIdentify = (key: string): boolean => key !== '' && agentIdForKey(key) !== undefined;
|
|
107
87
|
|
|
108
|
-
function
|
|
109
|
-
|
|
110
|
-
.
|
|
111
|
-
|
|
112
|
-
|
|
88
|
+
function routesOf(setup: ModelSetup): Route[] {
|
|
89
|
+
return [...(setup.primary === null ? [] : [setup.primary]), ...setup.fallbacks].flatMap((ref) => {
|
|
90
|
+
const conn = setup.connections.find((c) => c.id === ref.connection);
|
|
91
|
+
const model = ref.model === '' ? (conn?.model ?? '') : ref.model;
|
|
92
|
+
return conn === undefined || model === '' ? [] : [{ connection: conn, model }];
|
|
93
|
+
});
|
|
113
94
|
}
|
|
114
95
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
zdr: false,
|
|
123
|
-
codex: null,
|
|
124
|
-
gemini: null,
|
|
125
|
-
claude: null,
|
|
126
|
-
};
|
|
96
|
+
function modelsBody(setup: ModelSetup): Record<string, unknown> {
|
|
97
|
+
const listed = routesOf(setup)
|
|
98
|
+
.filter((r) => r.connection.provider !== 'anthropic')
|
|
99
|
+
.map((r) => ({ id: routeLabel(r), display_name: `${r.connection.label} · ${r.model}`, description: 'Through metro' }));
|
|
100
|
+
const data = listed.filter((m, at) => listed.findIndex((o) => o.id === m.id) === at);
|
|
101
|
+
return { data, has_more: false, first_id: data[0]?.id ?? null, last_id: data.at(-1)?.id ?? null };
|
|
102
|
+
}
|
|
127
103
|
|
|
128
|
-
const passthrough = (body: Record<string, unknown>): Route => ({ connection:
|
|
104
|
+
const passthrough = (body: Record<string, unknown>): Route => ({ connection: sessionConnection(), model: requestedModel(body) });
|
|
129
105
|
|
|
130
106
|
function requestedModel(body: Record<string, unknown>): string {
|
|
131
107
|
const model = body.model;
|
|
@@ -181,7 +157,7 @@ function listLoginModels(headers: IncomingHttpHeaders, anthropicBase: string): v
|
|
|
181
157
|
function sendAnthropic(req: IncomingMessage, conn: Connection, anthropicBase: string, attempt: (base: Record<string, string>) => Promise<Response>, deps: GatewayDeps): Promise<Response> {
|
|
182
158
|
if (conn.apiKey !== '') return attempt(anthropicHeaders(req, conn.apiKey));
|
|
183
159
|
if (conn.claude === null) return attempt(forwardedHeaders(req));
|
|
184
|
-
return withClaudeLogin(conn.claude
|
|
160
|
+
return withClaudeLogin(conn.claude, deps.claudeLogins ?? {}, (token) => {
|
|
185
161
|
listLoginModels({ authorization: `Bearer ${token}` }, anthropicBase);
|
|
186
162
|
return attempt(loginHeaders(req, token));
|
|
187
163
|
});
|
|
@@ -190,7 +166,7 @@ function sendAnthropic(req: IncomingMessage, conn: Connection, anthropicBase: st
|
|
|
190
166
|
async function refusedLogin(conn: Connection, upstream: Response): Promise<void> {
|
|
191
167
|
if (conn.apiKey !== '' || conn.claude === null || upstream.status !== 401) return;
|
|
192
168
|
await upstream.body?.cancel();
|
|
193
|
-
throw new GatewayError(403, 'permission_error', `Anthropic refused the Claude login
|
|
169
|
+
throw new GatewayError(403, 'permission_error', `Anthropic refused the Claude login ${conn.label}: sign in again on the Model page`);
|
|
194
170
|
}
|
|
195
171
|
|
|
196
172
|
async function toAnthropic(
|
|
@@ -210,7 +186,7 @@ async function toAnthropic(
|
|
|
210
186
|
throw new GatewayError(
|
|
211
187
|
403,
|
|
212
188
|
'permission_error',
|
|
213
|
-
|
|
189
|
+
"Claude Code on this machine has no Anthropic login of its own; route this agent to a model on the organization's Models page, or sign in a Claude login on the Model page",
|
|
214
190
|
);
|
|
215
191
|
const watch = watchUpstream(res);
|
|
216
192
|
if (passthrough) listLoginModels(req.headers, anthropicBase);
|
|
@@ -229,17 +205,8 @@ async function toAnthropic(
|
|
|
229
205
|
await pipeResponse(upstream, res, watch, passthrough ? { scanner } : { ownCredential: true, scanner });
|
|
230
206
|
}
|
|
231
207
|
|
|
232
|
-
async function toOpenRouter(
|
|
233
|
-
req: IncomingMessage,
|
|
234
|
-
res: ServerResponse,
|
|
235
|
-
body: Record<string, unknown>,
|
|
236
|
-
route: Route,
|
|
237
|
-
cfg: ModelConfig,
|
|
238
|
-
deps: GatewayDeps,
|
|
239
|
-
): Promise<void> {
|
|
208
|
+
async function toOpenRouter(req: IncomingMessage, res: ServerResponse, body: Record<string, unknown>, route: Route, deps: GatewayDeps): Promise<void> {
|
|
240
209
|
const conn = route.connection;
|
|
241
|
-
const reason = notReady(cfg, conn);
|
|
242
|
-
if (reason !== null) throw new GatewayError(400, 'invalid_request_error', reason);
|
|
243
210
|
const headers: Record<string, string> = {
|
|
244
211
|
'content-type': 'application/json',
|
|
245
212
|
accept: req.headers.accept ?? 'application/json',
|
|
@@ -276,11 +243,9 @@ function noteRefusal(connection: string, model: string, upstream: Response): voi
|
|
|
276
243
|
if (!upstream.ok) log.warn({ connection, model, status: upstream.status }, 'gateway: the provider refused the request');
|
|
277
244
|
}
|
|
278
245
|
|
|
279
|
-
async function
|
|
280
|
-
const conn = route.connection;
|
|
246
|
+
async function toCodex(req: IncomingMessage, res: ServerResponse, path: string, body: Record<string, unknown>, route: Route, deps: GatewayDeps): Promise<void> {
|
|
281
247
|
if (path === COUNT) countTokens(res, body);
|
|
282
|
-
else
|
|
283
|
-
else await codexMessages(req, res, body, route.model, conn, { save: saveCodexTokens, ...deps.codex }, sharedCodexState, watchUpstream(res));
|
|
248
|
+
else await codexMessages(req, res, body, route.model, route.connection, { refresh: refreshCodex, ...deps.codex }, watchUpstream(res));
|
|
284
249
|
}
|
|
285
250
|
|
|
286
251
|
function shapedFor(req: IncomingMessage, sent: Record<string, unknown>): Record<string, unknown> {
|
|
@@ -296,22 +261,17 @@ interface Request {
|
|
|
296
261
|
parsed: Record<string, unknown>;
|
|
297
262
|
sent: Record<string, unknown>;
|
|
298
263
|
body: Record<string, unknown>;
|
|
299
|
-
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function checkSdkRoute(req: IncomingMessage, route: Route, deps: GatewayDeps): void {
|
|
303
|
-
if (fromSdkRunner(req) && (!sdkAllowed(undefined, deps.config()) || !sdkConnectionAllowed(route.connection)))
|
|
304
|
-
throw new GatewayError(403, 'permission_error', SDK_NEEDS_KEY);
|
|
264
|
+
setup: ModelSetup;
|
|
305
265
|
}
|
|
306
266
|
|
|
307
267
|
async function attempt(req: IncomingMessage, res: ServerResponse, ask: Request, route: Route, deps: GatewayDeps): Promise<void> {
|
|
308
268
|
const { path, body } = ask;
|
|
309
269
|
const conn = route.connection;
|
|
310
|
-
checkSdkRoute(req, route, deps);
|
|
311
270
|
log.info({ route: routeLabel(route), connection: conn.label, path }, 'gateway: routing');
|
|
271
|
+
const reason = notReady(route);
|
|
272
|
+
if (reason !== null) throw new GatewayError(400, 'invalid_request_error', reason);
|
|
312
273
|
if (path === MESSAGES) noteServed({ connection: conn.id, provider: conn.provider, model: route.model, at: new Date().toISOString() });
|
|
313
274
|
if (conn.provider === 'bedrock') {
|
|
314
|
-
assertBedrockReady(conn);
|
|
315
275
|
const up = { settings: conn, base: deps.bedrockBase ?? bedrockBase(conn.region), learned, watch: watchUpstream(res) };
|
|
316
276
|
if (path === COUNT) await bedrockCount(req, res, body, route.model, up);
|
|
317
277
|
else await bedrockMessages(req, res, body, route.model, up);
|
|
@@ -319,25 +279,25 @@ async function attempt(req: IncomingMessage, res: ServerResponse, ask: Request,
|
|
|
319
279
|
}
|
|
320
280
|
if (conn.provider === 'openrouter') {
|
|
321
281
|
if (path === COUNT) throw new GatewayError(404, 'not_found_error', 'OpenRouter does not count tokens');
|
|
322
|
-
await toOpenRouter(req, res, body, route,
|
|
282
|
+
await toOpenRouter(req, res, body, route, deps);
|
|
323
283
|
return;
|
|
324
284
|
}
|
|
325
|
-
if (conn.provider === 'codex'
|
|
326
|
-
await
|
|
285
|
+
if (conn.provider === 'codex') {
|
|
286
|
+
await toCodex(req, res, path, body, route, deps);
|
|
327
287
|
return;
|
|
328
288
|
}
|
|
329
289
|
await toAnthropic(req, res, bytesOf(ask.raw, ask.parsed, ask.sent), ask.sent, body, route, deps);
|
|
330
290
|
}
|
|
331
291
|
|
|
332
292
|
async function dispatch(req: IncomingMessage, res: ServerResponse, path: string, deps: GatewayDeps): Promise<void> {
|
|
333
|
-
const
|
|
293
|
+
const setup = deps.setup();
|
|
334
294
|
const raw = await readBody(req);
|
|
335
295
|
const parsed = parseJson(raw);
|
|
336
296
|
const sent = fitToolSearch(req, parsed);
|
|
337
297
|
const requested = requestedModel(sent);
|
|
338
|
-
const primary = resolveRoute(requested,
|
|
339
|
-
const ask: Request = { path, raw, parsed, sent, body: shapedFor(req, sent),
|
|
340
|
-
const routes = routesToTry(chainOf(primary,
|
|
298
|
+
const primary = resolveRoute(requested, setup) ?? passthrough(sent);
|
|
299
|
+
const ask: Request = { path, raw, parsed, sent, body: shapedFor(req, sent), setup };
|
|
300
|
+
const routes = routesToTry(chainOf(primary, setup, requested));
|
|
341
301
|
for (const [at, route] of routes.entries()) {
|
|
342
302
|
const next = routes[at + 1];
|
|
343
303
|
if (path === MESSAGES) noteChoice(primary, route);
|
|
@@ -369,7 +329,7 @@ function failed(res: ServerResponse, err: unknown): void {
|
|
|
369
329
|
function answer(req: IncomingMessage, res: ServerResponse, path: string, deps: GatewayDeps): void {
|
|
370
330
|
if (req.method === 'GET' && path === MODELS) {
|
|
371
331
|
res.writeHead(200, { 'content-type': 'application/json' });
|
|
372
|
-
res.end(JSON.stringify(modelsBody(deps.
|
|
332
|
+
res.end(JSON.stringify(modelsBody(deps.setup())));
|
|
373
333
|
return;
|
|
374
334
|
}
|
|
375
335
|
if (req.method === 'POST' && (path === MESSAGES || path === COUNT)) {
|
|
@@ -391,6 +351,6 @@ export function handleGatewayRequest(req: IncomingMessage, res: ServerResponse,
|
|
|
391
351
|
}
|
|
392
352
|
if (cameThroughProxy(req)) sendError(res, 401, 'authentication_error', 'metro gateway: the agent key works only on this machine, not through its public address');
|
|
393
353
|
else if ((deps.identify ?? defaultIdentify)(keyOf(req))) answer(req, res, path, deps);
|
|
394
|
-
else sendError(res, 401, 'authentication_error', 'metro gateway: no agent key (x-metro-key); start
|
|
354
|
+
else sendError(res, 401, 'authentication_error', 'metro gateway: no agent key (x-metro-key); start the agent with metro agent');
|
|
395
355
|
return true;
|
|
396
356
|
}
|