@stage-labs/metro 0.1.0-beta.73 → 0.1.0-beta.74
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/package.json +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/model-api.ts +6 -3
- package/runtime/node_modules/@metro-labs/mcp/src/gateway/gateway.ts +3 -0
- package/runtime/node_modules/@metro-labs/mcp/src/gateway/openrouter.ts +13 -4
- package/runtime/node_modules/@metro-labs/mcp/src/gateway/served.ts +17 -0
- package/runtime/runtime.json +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stage-labs/metro",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.74",
|
|
4
4
|
"description": "The metro command line. Sign in once per machine, then hand your MCP connector list to Claude Code without the credentials touching disk, argv or shell history.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -7,6 +7,7 @@ import { beginLogin, CodexAuthError, finishLogin, readCodexCliAuth } from '../ga
|
|
|
7
7
|
import { beginDeviceLogin, pollDeviceLogin } from '../gateway/codex-device.js';
|
|
8
8
|
import { codexModels, currentTokens, freshCodexState } from '../gateway/codex.js';
|
|
9
9
|
import { openrouterModels } from '../gateway/openrouter.js';
|
|
10
|
+
import { lastServed } from '../gateway/served.js';
|
|
10
11
|
import type { CodexTokens } from '../gateway/codex-auth.js';
|
|
11
12
|
import { GatewayError } from '../gateway/forward.js';
|
|
12
13
|
import {
|
|
@@ -49,6 +50,8 @@ interface Route {
|
|
|
49
50
|
run: Handler;
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
const settingsBody = (cfg: ModelConfig): Record<string, unknown> => ({ ...publicModelConfig(cfg), lastServed: lastServed() });
|
|
54
|
+
|
|
52
55
|
function asApiError(err: unknown): never {
|
|
53
56
|
if (err instanceof ModelConfigError || err instanceof CodexAuthError) throw new ApiError(err.message, 400);
|
|
54
57
|
if (err instanceof GatewayError) throw new ApiError(err.message, 502);
|
|
@@ -65,13 +68,13 @@ async function update(req: IncomingMessage, store: Store): Promise<unknown> {
|
|
|
65
68
|
}
|
|
66
69
|
store.write(next);
|
|
67
70
|
log.info({ provider: next.provider }, 'model-api: route updated');
|
|
68
|
-
return
|
|
71
|
+
return settingsBody(next);
|
|
69
72
|
}
|
|
70
73
|
|
|
71
74
|
function saveCodex(store: Store, cfg: ModelConfig, note: string): unknown {
|
|
72
75
|
store.write(cfg);
|
|
73
76
|
log.info({ signedIn: cfg.codex.auth !== null, plan: cfg.codex.auth?.plan ?? null }, note);
|
|
74
|
-
return
|
|
77
|
+
return settingsBody(cfg);
|
|
75
78
|
}
|
|
76
79
|
|
|
77
80
|
const modelApiState = freshCodexState();
|
|
@@ -146,7 +149,7 @@ const named = (table: Record<string, Route>, name: string, method: string | unde
|
|
|
146
149
|
};
|
|
147
150
|
|
|
148
151
|
function settingsRoute(method: string | undefined): Route | number {
|
|
149
|
-
if (method === 'GET') return { method: 'GET', run: (_req, _deps, store) => Promise.resolve(
|
|
152
|
+
if (method === 'GET') return { method: 'GET', run: (_req, _deps, store) => Promise.resolve(settingsBody(store.read())) };
|
|
150
153
|
if (method === 'PUT') return { method: 'POST', run: (req, _deps, store) => update(req, store) };
|
|
151
154
|
return 405;
|
|
152
155
|
}
|
|
@@ -13,6 +13,7 @@ import { forwardedHeaders, GatewayError, parseJson, pipeResponse, readBody, send
|
|
|
13
13
|
import { notReady, readModelConfig, resolveRoute, routeLabel, setCodexAuth, writeModelConfig, type ModelConfig, type Route } from './model-config.js';
|
|
14
14
|
import { codexCount, codexMessages, freshCodexState, type CodexDeps } from './codex.js';
|
|
15
15
|
import { OPENROUTER_BASE } from './openrouter.js';
|
|
16
|
+
import { forgetServed, noteServed } from './served.js';
|
|
16
17
|
import type { CodexTokens } from './codex-auth.js';
|
|
17
18
|
|
|
18
19
|
export const GATEWAY_PREFIX = '/gateway';
|
|
@@ -35,6 +36,7 @@ const learned: Adaptations = freshAdaptations();
|
|
|
35
36
|
const codexState = freshCodexState();
|
|
36
37
|
|
|
37
38
|
export function resetGatewayState(): void {
|
|
39
|
+
forgetServed();
|
|
38
40
|
learned.fields.clear();
|
|
39
41
|
learned.dropBetas = false;
|
|
40
42
|
Object.assign(codexState, freshCodexState());
|
|
@@ -127,6 +129,7 @@ async function dispatch(req: IncomingMessage, res: ServerResponse, path: string,
|
|
|
127
129
|
const body = parseJson(raw);
|
|
128
130
|
const route = resolveRoute(requestedModel(body), cfg);
|
|
129
131
|
log.info({ route: routeLabel(route), path }, 'gateway: routing');
|
|
132
|
+
if (path === MESSAGES) noteServed({ provider: route.provider, model: route.model, at: new Date().toISOString() });
|
|
130
133
|
if (route.provider === 'bedrock') {
|
|
131
134
|
assertBedrockReady(cfg.bedrock);
|
|
132
135
|
const up = { settings: cfg.bedrock, base: deps.bedrockBase ?? bedrockBase(cfg.bedrock.region), learned, watch: watchUpstream(res) };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isRecord } from '../daemon/is-record.js';
|
|
1
2
|
import { GatewayError } from './forward.js';
|
|
2
3
|
|
|
3
4
|
export const OPENROUTER_BASE = 'https://openrouter.ai/api';
|
|
@@ -6,15 +7,23 @@ const MODELS_MAX = 2000;
|
|
|
6
7
|
export interface OpenRouterModel {
|
|
7
8
|
id: string;
|
|
8
9
|
name: string;
|
|
10
|
+
prompt: number | null;
|
|
11
|
+
completion: number | null;
|
|
9
12
|
}
|
|
10
13
|
|
|
11
14
|
const str = (value: unknown): string => (typeof value === 'string' ? value : '');
|
|
12
15
|
|
|
16
|
+
function price(raw: unknown): number | null {
|
|
17
|
+
const value = typeof raw === 'string' ? Number(raw) : typeof raw === 'number' ? raw : Number.NaN;
|
|
18
|
+
return Number.isFinite(value) && value >= 0 ? value : null;
|
|
19
|
+
}
|
|
20
|
+
|
|
13
21
|
function modelOf(entry: unknown): OpenRouterModel | null {
|
|
14
|
-
if (
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
22
|
+
if (!isRecord(entry)) return null;
|
|
23
|
+
const id = str(entry.id);
|
|
24
|
+
if (id === '') return null;
|
|
25
|
+
const pricing = isRecord(entry.pricing) ? entry.pricing : {};
|
|
26
|
+
return { id, name: str(entry.name) || id, prompt: price(pricing.prompt), completion: price(pricing.completion) };
|
|
18
27
|
}
|
|
19
28
|
|
|
20
29
|
export async function openrouterModels(base = OPENROUTER_BASE, fetchImpl: typeof fetch = fetch): Promise<OpenRouterModel[]> {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface Served {
|
|
2
|
+
provider: string;
|
|
3
|
+
model: string;
|
|
4
|
+
at: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
let last: Served | null = null;
|
|
8
|
+
|
|
9
|
+
export const noteServed = (served: Served): void => {
|
|
10
|
+
last = served;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const lastServed = (): Served | null => last;
|
|
14
|
+
|
|
15
|
+
export const forgetServed = (): void => {
|
|
16
|
+
last = null;
|
|
17
|
+
};
|
package/runtime/runtime.json
CHANGED