@stage-labs/metro 0.1.0-beta.70 → 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/bedrock.js +4 -73
- package/dist/claude-settings.js +40 -0
- package/dist/claude.js +101 -9
- package/dist/hold.js +20 -6
- package/dist/provider-flags.js +9 -0
- package/package.json +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/http.ts +9 -3
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-mode.ts +3 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/model-api.ts +169 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-apis.ts +5 -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/runtime.json +1 -1
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { readJson, writeSecure } from '../daemon/secure-fs.js';
|
|
3
|
+
import { agentsDir } from '../db/file-source.js';
|
|
4
|
+
import { isRecord } from '../daemon/is-record.js';
|
|
5
|
+
import type { CodexTokens } from './codex-auth.js';
|
|
6
|
+
|
|
7
|
+
export const PROVIDERS = ['anthropic', 'bedrock', 'openrouter', 'codex'] as const;
|
|
8
|
+
export type Provider = (typeof PROVIDERS)[number];
|
|
9
|
+
|
|
10
|
+
export interface BedrockSettings {
|
|
11
|
+
region: string;
|
|
12
|
+
apiKey: string;
|
|
13
|
+
model: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface OpenRouterSettings {
|
|
17
|
+
apiKey: string;
|
|
18
|
+
model: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface CodexSettings {
|
|
22
|
+
model: string;
|
|
23
|
+
auth: CodexTokens | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ModelConfig {
|
|
27
|
+
version: 1;
|
|
28
|
+
provider: Provider;
|
|
29
|
+
bedrock: BedrockSettings;
|
|
30
|
+
openrouter: OpenRouterSettings;
|
|
31
|
+
codex: CodexSettings;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface Route {
|
|
35
|
+
provider: Provider;
|
|
36
|
+
model: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class ModelConfigError extends Error {}
|
|
40
|
+
|
|
41
|
+
export const MODEL_FILE = 'model.json';
|
|
42
|
+
const MAX_FIELD = 512;
|
|
43
|
+
const PREFIX_RE = /^(anthropic|bedrock|openrouter|codex):(.+)$/;
|
|
44
|
+
|
|
45
|
+
const empty = (): ModelConfig => ({
|
|
46
|
+
version: 1,
|
|
47
|
+
provider: 'anthropic',
|
|
48
|
+
bedrock: { region: '', apiKey: '', model: '' },
|
|
49
|
+
openrouter: { apiKey: '', model: '' },
|
|
50
|
+
codex: { model: '', auth: null },
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const isProvider = (value: unknown): value is Provider =>
|
|
54
|
+
typeof value === 'string' && (PROVIDERS as readonly string[]).includes(value);
|
|
55
|
+
|
|
56
|
+
const text = (value: unknown): string => (typeof value === 'string' ? value.trim() : '');
|
|
57
|
+
const maybe = (value: unknown): string | null => (typeof value === 'string' && value !== '' ? value : null);
|
|
58
|
+
|
|
59
|
+
function tokensFromDisk(raw: unknown): CodexTokens | null {
|
|
60
|
+
if (!isRecord(raw)) return null;
|
|
61
|
+
const accessToken = text(raw.accessToken);
|
|
62
|
+
const refreshToken = text(raw.refreshToken);
|
|
63
|
+
const accountId = text(raw.accountId);
|
|
64
|
+
if (accessToken === '' || refreshToken === '' || accountId === '') return null;
|
|
65
|
+
return {
|
|
66
|
+
accessToken,
|
|
67
|
+
refreshToken,
|
|
68
|
+
idToken: text(raw.idToken),
|
|
69
|
+
accountId,
|
|
70
|
+
email: maybe(raw.email),
|
|
71
|
+
plan: maybe(raw.plan),
|
|
72
|
+
savedAt: text(raw.savedAt) === '' ? new Date(0).toISOString() : text(raw.savedAt),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function fromDisk(raw: unknown): ModelConfig {
|
|
77
|
+
const base = empty();
|
|
78
|
+
if (!isRecord(raw)) return base;
|
|
79
|
+
const bedrock = isRecord(raw.bedrock) ? raw.bedrock : {};
|
|
80
|
+
const openrouter = isRecord(raw.openrouter) ? raw.openrouter : {};
|
|
81
|
+
const codex = isRecord(raw.codex) ? raw.codex : {};
|
|
82
|
+
return {
|
|
83
|
+
version: 1,
|
|
84
|
+
provider: isProvider(raw.provider) ? raw.provider : 'anthropic',
|
|
85
|
+
bedrock: { region: text(bedrock.region), apiKey: text(bedrock.apiKey), model: text(bedrock.model) },
|
|
86
|
+
openrouter: { apiKey: text(openrouter.apiKey), model: text(openrouter.model) },
|
|
87
|
+
codex: { model: text(codex.model), auth: tokensFromDisk(codex.auth) },
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function readModelConfig(dir = agentsDir()): ModelConfig {
|
|
92
|
+
return fromDisk(readJson<unknown>(join(dir, MODEL_FILE), null, { warn: 'model-config: model.json is unreadable, so every request goes to Anthropic until it is fixed' }));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function writeModelConfig(cfg: ModelConfig, dir = agentsDir()): void {
|
|
96
|
+
writeSecure(join(dir, MODEL_FILE), JSON.stringify(cfg, null, 2));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function field(patch: Record<string, unknown>, key: string, current: string, what: string): string {
|
|
100
|
+
if (!(key in patch)) return current;
|
|
101
|
+
const value = patch[key];
|
|
102
|
+
if (typeof value !== 'string') throw new ModelConfigError(`${what} must be a string`);
|
|
103
|
+
if (value.length > MAX_FIELD) throw new ModelConfigError(`${what} is too long`);
|
|
104
|
+
return value.trim();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function applyModelUpdate(cfg: ModelConfig, patch: unknown): ModelConfig {
|
|
108
|
+
if (!isRecord(patch)) throw new ModelConfigError('body must be a JSON object');
|
|
109
|
+
const provider = 'provider' in patch ? patch.provider : cfg.provider;
|
|
110
|
+
if (!isProvider(provider)) throw new ModelConfigError(`provider must be one of ${PROVIDERS.join(', ')}`);
|
|
111
|
+
const bedrock = isRecord(patch.bedrock) ? patch.bedrock : {};
|
|
112
|
+
const openrouter = isRecord(patch.openrouter) ? patch.openrouter : {};
|
|
113
|
+
const codex = isRecord(patch.codex) ? patch.codex : {};
|
|
114
|
+
return {
|
|
115
|
+
version: 1,
|
|
116
|
+
provider,
|
|
117
|
+
bedrock: {
|
|
118
|
+
region: field(bedrock, 'region', cfg.bedrock.region, 'Bedrock region'),
|
|
119
|
+
apiKey: field(bedrock, 'apiKey', cfg.bedrock.apiKey, 'Bedrock API key'),
|
|
120
|
+
model: field(bedrock, 'model', cfg.bedrock.model, 'Bedrock model'),
|
|
121
|
+
},
|
|
122
|
+
openrouter: {
|
|
123
|
+
apiKey: field(openrouter, 'apiKey', cfg.openrouter.apiKey, 'OpenRouter API key'),
|
|
124
|
+
model: field(openrouter, 'model', cfg.openrouter.model, 'OpenRouter model'),
|
|
125
|
+
},
|
|
126
|
+
codex: { model: field(codex, 'model', cfg.codex.model, 'Codex model'), auth: cfg.codex.auth },
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export const setCodexAuth = (cfg: ModelConfig, auth: CodexTokens | null): ModelConfig => ({ ...cfg, codex: { ...cfg.codex, auth } });
|
|
131
|
+
|
|
132
|
+
const CHECKS: Record<Provider, [(cfg: ModelConfig) => boolean, string][]> = {
|
|
133
|
+
anthropic: [],
|
|
134
|
+
bedrock: [
|
|
135
|
+
[(cfg) => cfg.bedrock.apiKey === '', 'Bedrock needs an API key: add it on the Model page.'],
|
|
136
|
+
[(cfg) => cfg.bedrock.region === '', 'Bedrock needs a region: add it on the Model page.'],
|
|
137
|
+
],
|
|
138
|
+
openrouter: [
|
|
139
|
+
[(cfg) => cfg.openrouter.apiKey === '', 'OpenRouter needs an API key: add it on the Model page.'],
|
|
140
|
+
[(cfg) => cfg.openrouter.model === '', 'OpenRouter needs a model id: choose one on the Model page.'],
|
|
141
|
+
],
|
|
142
|
+
codex: [
|
|
143
|
+
[(cfg) => cfg.codex.auth === null, 'Codex is not connected: sign in with ChatGPT on the Model page.'],
|
|
144
|
+
[(cfg) => cfg.codex.model === '', 'Codex needs a model id: choose one on the Model page.'],
|
|
145
|
+
],
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export function notReady(cfg: ModelConfig, provider: Provider = cfg.provider): string | null {
|
|
149
|
+
return CHECKS[provider].find(([missing]) => missing(cfg))?.[1] ?? null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function publicModelConfig(cfg: ModelConfig): Record<string, unknown> {
|
|
153
|
+
const auth = cfg.codex.auth;
|
|
154
|
+
return {
|
|
155
|
+
provider: cfg.provider,
|
|
156
|
+
ready: notReady(cfg) === null,
|
|
157
|
+
reason: notReady(cfg),
|
|
158
|
+
bedrock: { region: cfg.bedrock.region, model: cfg.bedrock.model, hasKey: cfg.bedrock.apiKey !== '' },
|
|
159
|
+
openrouter: { model: cfg.openrouter.model, hasKey: cfg.openrouter.apiKey !== '' },
|
|
160
|
+
codex: { model: cfg.codex.model, signedIn: auth !== null, account: auth?.email ?? null, plan: auth?.plan ?? null },
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function defaultModelFor(provider: Provider, requested: string, cfg: ModelConfig): string {
|
|
165
|
+
if (provider === 'openrouter') return requested.includes('/') ? requested : cfg.openrouter.model;
|
|
166
|
+
if (provider === 'bedrock') return cfg.bedrock.model === '' ? requested : cfg.bedrock.model;
|
|
167
|
+
if (provider === 'codex') return requested.startsWith('gpt-') ? requested : cfg.codex.model;
|
|
168
|
+
return requested;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function resolveRoute(requested: string, cfg: ModelConfig): Route {
|
|
172
|
+
const explicit = PREFIX_RE.exec(requested);
|
|
173
|
+
if (explicit !== null && isProvider(explicit[1]))
|
|
174
|
+
return { provider: explicit[1], model: (explicit[2] ?? '').trim() };
|
|
175
|
+
return { provider: cfg.provider, model: defaultModelFor(cfg.provider, requested, cfg) };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function routeLabel(route: Route): string {
|
|
179
|
+
return route.provider === 'anthropic' ? route.model : `${route.provider}:${route.model}`;
|
|
180
|
+
}
|
package/runtime/runtime.json
CHANGED