@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.
Files changed (91) hide show
  1. package/dist/agent.js +23 -28
  2. package/dist/child.js +33 -0
  3. package/dist/claude-env.js +104 -0
  4. package/dist/cli.js +7 -21
  5. package/dist/lock.js +62 -0
  6. package/dist/memory.js +12 -27
  7. package/dist/root-release-io.js +1 -2
  8. package/dist/root-release.js +1 -1
  9. package/dist/route.js +0 -1
  10. package/hooks/guard.mjs +1 -5
  11. package/hooks/session-start.mjs +1 -1
  12. package/package.json +1 -1
  13. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/provision.ts +13 -38
  14. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/user.ts +21 -1
  15. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/view.ts +3 -9
  16. package/runtime/node_modules/@metro-labs/daemon/src/boot/boot.ts +14 -12
  17. package/runtime/node_modules/@metro-labs/daemon/src/claude/api.ts +6 -18
  18. package/runtime/node_modules/@metro-labs/daemon/src/claude/github-skills.ts +3 -7
  19. package/runtime/node_modules/@metro-labs/daemon/src/claude/login.ts +6 -5
  20. package/runtime/node_modules/@metro-labs/daemon/src/claude/onboarding.ts +1 -23
  21. package/runtime/node_modules/@metro-labs/daemon/src/claude/session.ts +39 -118
  22. package/runtime/node_modules/@metro-labs/daemon/src/claude/setup-api.ts +14 -61
  23. package/runtime/node_modules/@metro-labs/daemon/src/claude/setup.ts +10 -42
  24. package/runtime/node_modules/@metro-labs/daemon/src/claude/usage-probe.ts +2 -2
  25. package/runtime/node_modules/@metro-labs/daemon/src/gateway/bedrock.ts +1 -6
  26. package/runtime/node_modules/@metro-labs/daemon/src/gateway/claude-logins.ts +53 -29
  27. package/runtime/node_modules/@metro-labs/daemon/src/gateway/claude-store.ts +55 -0
  28. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex.ts +40 -62
  29. package/runtime/node_modules/@metro-labs/daemon/src/gateway/delivered.ts +82 -0
  30. package/runtime/node_modules/@metro-labs/daemon/src/gateway/effective.ts +47 -0
  31. package/runtime/node_modules/@metro-labs/daemon/src/gateway/fallback.ts +18 -19
  32. package/runtime/node_modules/@metro-labs/daemon/src/gateway/forward.ts +0 -1
  33. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gateway.ts +37 -77
  34. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-api.ts +86 -220
  35. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-config.ts +80 -283
  36. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-import.ts +125 -0
  37. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-lists.ts +0 -17
  38. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-v2.ts +79 -0
  39. package/runtime/node_modules/@metro-labs/daemon/src/gateway/openrouter.ts +0 -79
  40. package/runtime/node_modules/@metro-labs/daemon/src/gateway/org-models.ts +231 -0
  41. package/runtime/node_modules/@metro-labs/daemon/src/gateway/provider-models.ts +4 -44
  42. package/runtime/node_modules/@metro-labs/daemon/src/gateway/text.ts +0 -2
  43. package/runtime/node_modules/@metro-labs/daemon/src/gateway/usage-refresh.ts +37 -57
  44. package/runtime/node_modules/@metro-labs/daemon/src/gateway/usage.ts +0 -13
  45. package/runtime/node_modules/@metro-labs/daemon/src/mcp/index.ts +5 -15
  46. package/runtime/node_modules/@metro-labs/daemon/src/mcp/instructions.ts +0 -11
  47. package/runtime/node_modules/@metro-labs/daemon/src/mcp/session-slot.ts +3 -7
  48. package/runtime/node_modules/@metro-labs/daemon/src/mcp/session.ts +12 -24
  49. package/runtime/node_modules/@metro-labs/daemon/src/routes/bearer.ts +1 -4
  50. package/runtime/node_modules/@metro-labs/daemon/src/routes/http.ts +1 -2
  51. package/runtime/node_modules/@metro-labs/daemon/src/routes/local-mode.ts +3 -4
  52. package/runtime/node_modules/@metro-labs/daemon/src/voice/call.ts +9 -92
  53. package/runtime/node_modules/@metro-labs/daemon/src/voice/calls.ts +1 -4
  54. package/runtime/node_modules/@metro-labs/daemon/src/voice/scribe.ts +1 -1
  55. package/runtime/node_modules/@metro-labs/daemon/src/voice/shared.ts +0 -2
  56. package/runtime/node_modules/@metro-labs/daemon/src/voice/speech.ts +0 -4
  57. package/runtime/node_modules/@metro-labs/daemon/src/voice/store.ts +1 -14
  58. package/runtime/node_modules/@metro-labs/http/src/api-http.ts +0 -1
  59. package/runtime/runtime.json +1 -1
  60. package/runtime/sdk-runner/runner.json +1 -1
  61. package/dist/background.js +0 -141
  62. package/dist/claude.js +0 -204
  63. package/dist/mcp-config.js +0 -27
  64. package/dist/native-recovery-scan.js +0 -246
  65. package/dist/native-recovery-state.js +0 -154
  66. package/dist/native-recovery.js +0 -164
  67. package/dist/onboarding.js +0 -44
  68. package/dist/voice.js +0 -56
  69. package/runtime/node_modules/@metro-labs/daemon/src/claude/channel-check.ts +0 -56
  70. package/runtime/node_modules/@metro-labs/daemon/src/claude/pinned.ts +0 -16
  71. package/runtime/node_modules/@metro-labs/daemon/src/claude/plugin-cleanup.ts +0 -121
  72. package/runtime/node_modules/@metro-labs/daemon/src/claude/runner.ts +0 -45
  73. package/runtime/node_modules/@metro-labs/daemon/src/claude/session-continuity.ts +0 -57
  74. package/runtime/node_modules/@metro-labs/daemon/src/claude/version.ts +0 -76
  75. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-auth.ts +0 -219
  76. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-device.ts +0 -119
  77. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-auth.ts +0 -160
  78. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-client.ts +0 -46
  79. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-setup.ts +0 -150
  80. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-stream.ts +0 -139
  81. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-translate.ts +0 -236
  82. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini.ts +0 -208
  83. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-signin.ts +0 -157
  84. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-store.ts +0 -83
  85. package/runtime/node_modules/@metro-labs/daemon/src/gateway/openrouter-login.ts +0 -192
  86. package/runtime/node_modules/@metro-labs/daemon/src/gateway/openrouter-signin.ts +0 -64
  87. package/runtime/node_modules/@metro-labs/daemon/src/gateway/pkce.ts +0 -36
  88. package/runtime/node_modules/@metro-labs/daemon/src/voice/approvals.ts +0 -38
  89. package/runtime/node_modules/@metro-labs/daemon/src/voice/brain.ts +0 -146
  90. package/runtime/node_modules/@metro-labs/daemon/src/voice/speakable.ts +0 -54
  91. package/runtime/node_modules/@metro-labs/daemon/src/voice/talk.ts +0 -264
@@ -1,231 +1,105 @@
1
1
  import type { IncomingMessage, ServerResponse } from 'node:http';
2
2
  import { ApiError } from '@metro-labs/http/api-error';
3
- import { apiFailure, apiSession, cors, readJsonBody, sendJson } from '@metro-labs/http/api-http';
4
- import { isRecord } from '@metro-labs/core/is-record';
5
- import { log } from '@metro-labs/core/log';
6
- import { openrouterModels, openrouterZdrModels } from './openrouter.js';
7
- import { anthropicModels, bedrockModels } from './provider-models.js';
8
- import { routeOf, syncAvailableModelsQuietly } from '../claude/setup.js';
9
- import { harnessRunner, runnerModel, sdkAllowed } from '../claude/runner.js';
10
- import { sessionLive, sessionRunning, stopSession } from '../claude/session.js';
11
- import { forgetOne, forgetReported } from './usage.js';
12
- import {
13
- addConnection,
14
- connectionOf,
15
- parseModelConfig,
16
- readModelConfig,
17
- readModelSnapshot,
18
- removeConnection,
19
- setFallbacks,
20
- setRoute,
21
- updateConnection,
22
- writeModelConfig,
23
- type ModelConfig,
24
- } from './model-config.js';
25
- import { asApiError, BODY_MAX, claudeLoginDeps, connectionFor, settingsBody, type ModelApiDeps, type Route, type Store } from './model-store.js';
26
- import { dropUnusedClaudeLogins } from './claude-logins.js';
27
- import { CODEX_ROUTES, codexDeviceRoute, GEMINI_ROUTES } from './model-signin.js';
28
- import { refreshUsage } from './usage-refresh.js';
29
- import { handleOpenRouterSignIn } from './openrouter-signin.js';
3
+ import { apiFailure, apiSession, cors, requireAdmin, sendJson, type ApiSession } from '@metro-labs/http/api-http';
4
+ import { agentsDir } from '../agents/files.js';
5
+ import { removeClaudeLogin } from './claude-logins.js';
6
+ import { readClaudeLogins, type ClaudeLogin } from './claude-store.js';
7
+ import { effectiveSetup } from './effective.js';
8
+ import { chainStatus } from './fallback.js';
9
+ import { notReady, primaryRoute, type Connection, type ModelSetup } from './model-config.js';
10
+ import { orgModels, type SyncStatus } from './org-models.js';
11
+ import { claudeModels } from './provider-models.js';
12
+ import { lastServed } from './served.js';
13
+ import { refreshUsage, type UsageDeps } from './usage-refresh.js';
14
+ import { usageSeen } from './usage.js';
30
15
 
31
16
  const PATH = '/api/model';
32
17
  const SNAPSHOT = '/api/model/snapshot';
33
- const CONNECTIONS = '/api/model/connections';
34
- const FALLBACKS = '/api/model/fallbacks';
35
- const CODEX = '/api/model/codex/';
36
- const GEMINI = '/api/model/gemini/';
37
- const OPENROUTER = '/api/model/openrouter/';
38
- const ANTHROPIC = '/api/model/anthropic/';
39
- const BEDROCK = '/api/model/bedrock/';
40
- const BUNDLE = '/api/model/bundle';
41
- const RESTORE = '/api/model/restore';
42
- const RESTORE_MAX = 64 * 1024;
43
- const DEVICE_PREFIX = 'device/';
44
- const DEVICE_ID_RE = /^[A-Za-z0-9_-]{16,64}$/;
45
-
46
- export type { ModelApiDeps } from './model-store.js';
47
-
48
- function restartRunningSession(): boolean {
49
- if (!sessionRunning()) return false;
50
- stopSession();
51
- return true;
52
- }
53
-
54
- function switchLive(before: ModelConfig, next: ModelConfig, deps: ModelApiDeps): void {
55
- const [was, now] = [runnerModel(before), runnerModel(next)];
56
- if (was === now) return;
57
- if (deps.switchModel?.(now) === true) log.info({ was, now }, 'model-api: the model changed, so the Agent SDK session switches to it live, with no restart');
58
- else if (runnerIsLive(deps)) throw new ApiError('The running Agent SDK could not be told about this model change. Nothing was saved. Let its work finish, stop it, then save and start it again.', 409);
59
- }
60
-
61
- function followModel(before: ModelConfig, next: ModelConfig, deps: ModelApiDeps): void {
62
- const restart = deps.restartSession ?? restartRunningSession;
63
- const agents = deps.setup?.agents;
64
- if (harnessRunner(agents) === 'sdk') {
65
- if (sdkAllowed(agents, next)) switchLive(before, next, deps);
66
- return;
67
- }
68
- if (routeOf(next) !== routeOf(before) && restart())
69
- log.info({ was: routeOf(before), now: routeOf(next) }, 'model-api: the model changed, so the Claude session restarts on it');
70
- }
71
-
72
- function kept(store: Store, next: ModelConfig, deps: ModelApiDeps, note: string, fields: Record<string, unknown>): unknown {
73
- const before = store.read();
74
- store.write(next);
75
- try {
76
- followModel(before, next, deps);
77
- } catch (err) {
78
- store.write(before);
79
- throw err;
80
- }
81
- syncAvailableModelsQuietly(deps.setup ?? {}, next);
82
- log.info(fields, note);
83
- return settingsBody(next);
84
- }
85
-
86
- async function withBody(req: IncomingMessage, run: (body: Record<string, unknown>) => ModelConfig): Promise<ModelConfig> {
87
- const body = await readJsonBody(req, BODY_MAX);
88
- if (!isRecord(body)) throw new ApiError('body must be a JSON object', 400);
89
- try {
90
- return run(body);
91
- } catch (err) {
92
- return asApiError(err);
93
- }
94
- }
95
-
96
- async function chooseRoute(req: IncomingMessage, deps: ModelApiDeps, store: Store): Promise<unknown> {
97
- const next = await withBody(req, (body) => setRoute(store.read(), typeof body.route === 'string' ? body.route : ''));
98
- return kept(store, next, deps, 'model-api: route changed', { route: next.route });
99
- }
100
-
101
- async function chooseFallbacks(req: IncomingMessage, deps: ModelApiDeps, store: Store): Promise<unknown> {
102
- const next = await withBody(req, (body) => setFallbacks(store.read(), body.fallbacks));
103
- return kept(store, next, deps, 'model-api: fallback models changed', { fallbacks: (next.fallbacks ?? []).map((f) => `${f.connection}:${f.model}`) });
104
- }
105
-
106
- async function create(req: IncomingMessage, deps: ModelApiDeps, store: Store): Promise<unknown> {
107
- const next = await withBody(req, (body) => addConnection(store.read(), body));
108
- return kept(store, next, deps, 'model-api: connection added', { connection: next.connections.at(-1)?.id ?? '' });
109
- }
110
-
111
- async function change(req: IncomingMessage, deps: ModelApiDeps, store: Store, id: string): Promise<unknown> {
112
- const before = connectionOf(store.read(), id)?.apiKey;
113
- const next = await withBody(req, (body) => updateConnection(store.read(), id, body));
114
- if (connectionOf(next, id)?.apiKey !== before) forgetReported(id);
115
- return kept(store, next, deps, 'model-api: connection changed', { connection: id });
116
- }
117
-
118
- function drop(deps: ModelApiDeps, store: Store, id: string): unknown {
119
- const before = store.read();
120
- let next: ModelConfig;
121
- try {
122
- next = removeConnection(before, id);
123
- } catch (err) {
124
- asApiError(err);
125
- }
126
- forgetOne(id);
127
- const body = kept(store, next, deps, 'model-api: connection removed', { connection: id });
128
- dropUnusedClaudeLogins(before, next, claudeLoginDeps(deps));
129
- return body;
130
- }
131
-
132
- async function settingsWithUsage(deps: ModelApiDeps, store: Store): Promise<Record<string, unknown>> {
133
- await refreshUsage(deps, store);
134
- return settingsBody(store.read());
135
- }
136
-
137
- const OPENROUTER_ROUTES: Record<string, Route> = {
138
- models: { method: 'GET', run: async (_req, deps) => ({ models: await openrouterModels(deps.openrouterBase, deps.fetchImpl).catch(asApiError) }) },
139
- zdr: { method: 'GET', run: async (_req, deps) => ({ models: await openrouterZdrModels(deps.openrouterBase, deps.fetchImpl).catch(asApiError) }) },
140
- };
141
-
142
- const ANTHROPIC_ROUTES: Record<string, Route> = {
143
- models: {
144
- method: 'GET',
145
- run: async (req, deps, store) => ({ models: await anthropicModels(connectionFor(store.read(), req, 'anthropic'), deps.anthropicBase, deps.fetchImpl).catch(asApiError) }),
146
- },
147
- };
148
-
149
- const BEDROCK_ROUTES: Record<string, Route> = {
150
- models: {
151
- method: 'GET',
152
- run: async (req, deps, store) => ({ models: await bedrockModels(connectionFor(store.read(), req, 'bedrock'), deps.bedrockControlBase, deps.fetchImpl).catch(asApiError) }),
153
- },
154
- };
155
-
156
- const named = (table: Record<string, Route>, name: string, method: string | undefined): Route | number => {
157
- const route = table[name];
158
- if (route === undefined) return 404;
159
- return route.method === method ? route : 405;
160
- };
161
-
162
- async function restore(req: IncomingMessage, store: Store, deps: ModelApiDeps): Promise<unknown> {
163
- const body = await readJsonBody(req, RESTORE_MAX);
164
- if (!isRecord(body)) throw new ApiError('body must be a JSON object', 400);
165
- const before = store.read();
166
- const next = parseModelConfig(body);
167
- const answer = kept(store, next, deps, 'model-api: model setup restored from a file', {});
168
- dropUnusedClaudeLogins(before, next, claudeLoginDeps(deps));
169
- return answer;
18
+ const SYNC = '/api/model/sync';
19
+ const CLAUDE_MODELS = '/api/model/claude-models';
20
+ const CLAUDE_LOGIN = '/api/model/claude/';
21
+ const LOGIN_RE = /^[A-Za-z0-9_-]{11}$/;
22
+
23
+ export interface ModelApiDeps extends UsageDeps {
24
+ dir?: () => string;
25
+ setup?: () => ModelSetup;
26
+ sync?: () => Promise<void>;
27
+ syncStatus?: () => SyncStatus;
28
+ anthropicBase?: string;
29
+ }
30
+
31
+ const NO_SYNC: SyncStatus = { enrolled: false, at: null, error: null };
32
+
33
+ function publicConnection(c: Connection): Record<string, unknown> {
34
+ return {
35
+ id: c.id,
36
+ provider: c.provider,
37
+ label: c.label,
38
+ model: c.model,
39
+ hasKey: c.apiKey !== '',
40
+ region: c.region,
41
+ zdr: c.zdr,
42
+ signedIn: c.codex !== null || (c.claude !== null && !c.signedOut),
43
+ signedOut: c.signedOut,
44
+ account: c.account.email,
45
+ plan: c.account.plan,
46
+ method: c.account.method,
47
+ };
170
48
  }
171
49
 
172
- function bundleRoute(path: string, method: string | undefined): Route | number {
173
- if (path === BUNDLE) return method === 'GET' ? { method: 'GET', run: (_req, _deps, store) => Promise.resolve(store.read()) } : 405;
174
- return method === 'POST' ? { method: 'POST', run: (req, deps, store) => restore(req, store, deps) } : 405;
50
+ const loginsOf = (deps: ModelApiDeps): ClaudeLogin[] => readClaudeLogins(deps.dir?.() ?? agentsDir());
51
+ const setupOf = (deps: ModelApiDeps): ModelSetup => (deps.setup ?? (() => effectiveSetup(deps.dir?.())))();
52
+
53
+ export function settingsBody(deps: ModelApiDeps): Record<string, unknown> {
54
+ const setup = setupOf(deps);
55
+ const primary = primaryRoute(setup);
56
+ const reason = primary === null ? null : notReady(primary);
57
+ return {
58
+ source: setup.source,
59
+ sync: (deps.syncStatus ?? (() => orgModels()?.syncStatus() ?? NO_SYNC))(),
60
+ route: primary?.connection.id ?? '',
61
+ model: primary?.model ?? '',
62
+ ready: reason === null,
63
+ reason,
64
+ connections: setup.connections.map(publicConnection),
65
+ fallbacks: setup.fallbacks,
66
+ chain: chainStatus(setup),
67
+ claude: loginsOf(deps),
68
+ lastServed: lastServed(),
69
+ usage: usageSeen(),
70
+ };
175
71
  }
176
72
 
177
- function settingsRoute(path: string, method: string | undefined): Route | number {
178
- if (path === FALLBACKS) return method === 'PUT' ? { method: 'POST', run: chooseFallbacks } : 405;
179
- if (method === 'GET') return { method: 'GET', run: (_req, deps, store) => settingsWithUsage(deps, store) };
180
- if (method === 'PUT') return { method: 'POST', run: chooseRoute };
181
- return 405;
73
+ async function withUsage(deps: ModelApiDeps): Promise<Record<string, unknown>> {
74
+ await refreshUsage(setupOf(deps), loginsOf(deps), deps);
75
+ return settingsBody(deps);
182
76
  }
183
77
 
184
- function connectionsRoute(path: string, method: string | undefined): Route | number {
185
- if (path === CONNECTIONS) return method === 'POST' ? { method: 'POST', run: create } : 405;
186
- const id = path.slice(CONNECTIONS.length + 1);
187
- if (id === '' || id.includes('/')) return 404;
188
- if (method === 'PUT') return { method: 'POST', run: (req, deps, store) => change(req, deps, store, id) };
189
- if (method === 'DELETE') return { method: 'GET', run: (_req, deps, store) => Promise.resolve(drop(deps, store, id)) };
190
- return 405;
78
+ async function synced(deps: ModelApiDeps): Promise<Record<string, unknown>> {
79
+ await (deps.sync ?? (() => orgModels()?.requested() ?? Promise.resolve()))();
80
+ return settingsBody(deps);
191
81
  }
192
82
 
193
- function codexRoute(rest: string, method: string | undefined): Route | number {
194
- if (rest in CODEX_ROUTES) return named(CODEX_ROUTES, rest, method);
195
- const id = rest.startsWith(DEVICE_PREFIX) ? rest.slice(DEVICE_PREFIX.length) : '';
196
- if (!DEVICE_ID_RE.test(id)) return 404;
197
- return codexDeviceRoute(id, method);
83
+ function dropLogin(session: ApiSession, login: string, deps: ModelApiDeps): Record<string, unknown> {
84
+ requireAdmin(session);
85
+ removeClaudeLogin(login, { ...deps.claudeLogins, dir: deps.dir?.() ?? deps.claudeLogins?.dir });
86
+ return settingsBody(deps);
198
87
  }
199
88
 
200
- const TABLES: [string, Record<string, Route>][] = [
201
- [OPENROUTER, OPENROUTER_ROUTES],
202
- [ANTHROPIC, ANTHROPIC_ROUTES],
203
- [BEDROCK, BEDROCK_ROUTES],
204
- [GEMINI, GEMINI_ROUTES],
205
- ];
206
- const EXACT = new Set([PATH, SNAPSHOT, FALLBACKS, BUNDLE, RESTORE, CONNECTIONS]);
207
- const PREFIXES = [`${CONNECTIONS}/`, CODEX, ...TABLES.map(([prefix]) => prefix)];
89
+ type Run = (session: ApiSession) => Promise<unknown>;
208
90
 
209
- const mine = (path: string): boolean => EXACT.has(path) || PREFIXES.some((prefix) => path.startsWith(prefix));
210
-
211
- function routeFor(path: string, method: string | undefined): Route | number {
212
- if (path === SNAPSHOT) return method === 'GET' ? { method: 'GET', run: (_req, _deps, store) => Promise.resolve().then(() => settingsBody(store.read())).catch(asApiError) } : 405;
213
- if (path === PATH || path === FALLBACKS) return settingsRoute(path, method);
214
- if (path === BUNDLE || path === RESTORE) return bundleRoute(path, method);
215
- if (path === CONNECTIONS || path.startsWith(`${CONNECTIONS}/`)) return connectionsRoute(path, method);
216
- const table = TABLES.find(([prefix]) => path.startsWith(prefix));
217
- if (table !== undefined) return named(table[1], path.slice(table[0].length), method);
218
- return codexRoute(path.slice(CODEX.length), method);
91
+ function routeFor(path: string, method: string | undefined, deps: ModelApiDeps): Run | number {
92
+ const only = (wanted: string, run: Run): Run | number => (method === wanted ? run : 405);
93
+ if (path === PATH) return only('GET', () => withUsage(deps));
94
+ if (path === SNAPSHOT) return only('GET', () => Promise.resolve(settingsBody(deps)));
95
+ if (path === SYNC) return only('POST', () => synced(deps));
96
+ if (path === CLAUDE_MODELS) return only('GET', () => Promise.resolve({ models: claudeModels(deps.anthropicBase) }));
97
+ const login = path.slice(CLAUDE_LOGIN.length);
98
+ if (!LOGIN_RE.test(login)) return 404;
99
+ return only('DELETE', (session) => Promise.resolve(dropLogin(session, login, deps)));
219
100
  }
220
101
 
221
- const runnerIsLive = (deps: ModelApiDeps): boolean => (deps.sessionRunning ?? (() => sessionLive({ agents: deps.setup?.agents })))();
222
-
223
- function checkRunnerRoute(before: ModelConfig, next: ModelConfig, deps: ModelApiDeps): void {
224
- const agents = deps.setup?.agents;
225
- if (harnessRunner(agents) !== 'sdk' || !sdkAllowed(agents, before) || sdkAllowed(agents, next)) return;
226
- if (runnerIsLive(deps))
227
- throw new ApiError('The Agent SDK session is running, and this change would leave a model or fallback it may not use. Let it finish its work, stop it, then save the change.', 409);
228
- }
102
+ const mine = (path: string): boolean => path === PATH || path.startsWith(`${PATH}/`);
229
103
 
230
104
  export function handleModelRequest(req: IncomingMessage, res: ServerResponse, deps: ModelApiDeps): boolean {
231
105
  const path = (req.url ?? '').split('?')[0] ?? '';
@@ -234,15 +108,7 @@ export function handleModelRequest(req: IncomingMessage, res: ServerResponse, de
234
108
  res.writeHead(204, cors(req)).end();
235
109
  return true;
236
110
  }
237
- const store: Store = {
238
- read: deps.read ?? (path === SNAPSHOT ? readModelSnapshot : readModelConfig),
239
- write: (cfg) => {
240
- checkRunnerRoute(store.read(), cfg, deps);
241
- (deps.write ?? writeModelConfig)(cfg);
242
- },
243
- };
244
- if (handleOpenRouterSignIn(req, res, deps, store)) return true;
245
- const route = routeFor(path, req.method);
111
+ const route = path === PATH || path.startsWith(CLAUDE_LOGIN) || [SNAPSHOT, SYNC, CLAUDE_MODELS].includes(path) ? routeFor(path, req.method, deps) : 404;
246
112
  if (typeof route === 'number') {
247
113
  sendJson(req, res, route, { error: route === 404 ? 'not found' : 'method not allowed' });
248
114
  return true;
@@ -250,7 +116,7 @@ export function handleModelRequest(req: IncomingMessage, res: ServerResponse, de
250
116
  apiSession(req)
251
117
  .then(async (session) => {
252
118
  if (!session) throw new ApiError('unauthorized', 401);
253
- sendJson(req, res, 200, await route.run(req, deps, store));
119
+ sendJson(req, res, 200, await route(session));
254
120
  })
255
121
  .catch((err: unknown) => {
256
122
  apiFailure(req, res, err, 'model-api');