@stage-labs/metro 0.1.0-beta.164 → 0.1.0-beta.166
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/marketplace/plugin/.claude-plugin/plugin.json +1 -1
- package/runtime/marketplace/plugin/orchestrator.md +6 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/api.ts +2 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/files.ts +8 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/session.ts +3 -2
- package/runtime/node_modules/@metro-labs/daemon/src/claude/setup.ts +28 -13
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/bedrock.ts +3 -3
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex.ts +19 -19
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/effort.ts +70 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/forward.ts +9 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gateway.ts +83 -51
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-translate.ts +3 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini.ts +35 -18
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-api.ts +66 -238
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-config.ts +202 -141
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-signin.ts +195 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-store.ts +70 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/provider-models.ts +3 -3
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/served.ts +1 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/usage.ts +25 -23
- package/runtime/runtime.json +1 -1
|
@@ -2,33 +2,26 @@ import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
|
2
2
|
import { ApiError } from '@metro-labs/http/api-error';
|
|
3
3
|
import { apiFailure, apiSession, cors, readJsonBody, sendJson } from '@metro-labs/http/api-http';
|
|
4
4
|
import { isRecord } from '@metro-labs/core/is-record';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { beginDeviceLogin, pollDeviceLogin } from './codex-device.js';
|
|
8
|
-
import { codexModels, currentTokens, freshCodexState } from './codex.js';
|
|
9
|
-
import { beginLogin as beginGeminiLogin, exchangeCode as exchangeGeminiCode, GeminiAuthError, userEmail } from './gemini-auth.js';
|
|
10
|
-
import { onboard, parseGeminiProject } from './gemini-setup.js';
|
|
11
|
-
import { currentGeminiTokens, freshGeminiState, listGeminiModels, type GeminiDeps } from './gemini.js';
|
|
12
|
-
import { openrouterCredits, openrouterModels, openrouterZdrModels } from './openrouter.js';
|
|
5
|
+
import { log } from '@metro-labs/core/log';
|
|
6
|
+
import { openrouterModels, openrouterZdrModels } from './openrouter.js';
|
|
13
7
|
import { anthropicModels, bedrockModels } from './provider-models.js';
|
|
14
|
-
import { syncAvailableModelsQuietly
|
|
15
|
-
import {
|
|
16
|
-
import { geminiUsage, noteUsage, openrouterUsage, usageOf, usageSeen } from './usage.js';
|
|
17
|
-
import type { CodexTokens } from './codex-auth.js';
|
|
18
|
-
import { GatewayError } from './forward.js';
|
|
8
|
+
import { syncAvailableModelsQuietly } from '../claude/setup.js';
|
|
9
|
+
import { forgetOne } from './usage.js';
|
|
19
10
|
import {
|
|
20
|
-
|
|
21
|
-
ModelConfigError,
|
|
11
|
+
addConnection,
|
|
22
12
|
parseModelConfig,
|
|
23
|
-
publicModelConfig,
|
|
24
13
|
readModelConfig,
|
|
25
|
-
|
|
26
|
-
|
|
14
|
+
removeConnection,
|
|
15
|
+
setRoute,
|
|
16
|
+
updateConnection,
|
|
27
17
|
writeModelConfig,
|
|
28
18
|
type ModelConfig,
|
|
29
19
|
} from './model-config.js';
|
|
20
|
+
import { asApiError, BODY_MAX, connectionFor, settingsBody, type ModelApiDeps, type Route, type Store } from './model-store.js';
|
|
21
|
+
import { CODEX_ROUTES, codexDeviceRoute, GEMINI_ROUTES, refreshUsage } from './model-signin.js';
|
|
30
22
|
|
|
31
23
|
const PATH = '/api/model';
|
|
24
|
+
const CONNECTIONS = '/api/model/connections';
|
|
32
25
|
const CODEX = '/api/model/codex/';
|
|
33
26
|
const GEMINI = '/api/model/gemini/';
|
|
34
27
|
const OPENROUTER = '/api/model/openrouter/';
|
|
@@ -37,255 +30,75 @@ const BEDROCK = '/api/model/bedrock/';
|
|
|
37
30
|
const BUNDLE = '/api/model/bundle';
|
|
38
31
|
const RESTORE = '/api/model/restore';
|
|
39
32
|
const RESTORE_MAX = 64 * 1024;
|
|
40
|
-
const BODY_MAX = 16 * 1024;
|
|
41
33
|
const DEVICE_PREFIX = 'device/';
|
|
42
34
|
const DEVICE_ID_RE = /^[A-Za-z0-9_-]{16,64}$/;
|
|
43
35
|
|
|
44
|
-
export
|
|
45
|
-
authorize: (subject: string) => void;
|
|
46
|
-
read?: () => ModelConfig;
|
|
47
|
-
write?: (cfg: ModelConfig) => void;
|
|
48
|
-
issuer?: string;
|
|
49
|
-
fetchImpl?: typeof fetch;
|
|
50
|
-
codexHome?: string;
|
|
51
|
-
codexBase?: string;
|
|
52
|
-
geminiAuthBase?: string;
|
|
53
|
-
geminiTokenBase?: string;
|
|
54
|
-
geminiUserBase?: string;
|
|
55
|
-
geminiBase?: string;
|
|
56
|
-
openrouterBase?: string;
|
|
57
|
-
anthropicBase?: string;
|
|
58
|
-
bedrockControlBase?: string;
|
|
59
|
-
setup?: SetupDeps;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
interface Store {
|
|
63
|
-
read: () => ModelConfig;
|
|
64
|
-
write: (cfg: ModelConfig) => void;
|
|
65
|
-
}
|
|
36
|
+
export type { ModelApiDeps } from './model-store.js';
|
|
66
37
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
38
|
+
function kept(store: Store, next: ModelConfig, deps: ModelApiDeps, note: string, fields: Record<string, unknown>): unknown {
|
|
39
|
+
store.write(next);
|
|
40
|
+
syncAvailableModelsQuietly(deps.setup ?? {}, next);
|
|
41
|
+
log.info(fields, note);
|
|
42
|
+
return settingsBody(next);
|
|
72
43
|
}
|
|
73
44
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
usage: usageSeen(),
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
const CREDITS_TTL_MS = 5 * 60_000;
|
|
81
|
-
|
|
82
|
-
async function refreshCredits(cfg: ModelConfig, deps: ModelApiDeps, now = Date.now()): Promise<void> {
|
|
83
|
-
const key = cfg.openrouter.apiKey;
|
|
84
|
-
if (key === '') return;
|
|
85
|
-
const seen = usageOf('openrouter');
|
|
86
|
-
if (seen !== undefined && now - Date.parse(seen.at) < CREDITS_TTL_MS) return;
|
|
45
|
+
async function withBody(req: IncomingMessage, run: (body: Record<string, unknown>) => ModelConfig): Promise<ModelConfig> {
|
|
46
|
+
const body = await readJsonBody(req, BODY_MAX);
|
|
47
|
+
if (!isRecord(body)) throw new ApiError('body must be a JSON object', 400);
|
|
87
48
|
try {
|
|
88
|
-
|
|
89
|
-
noteUsage('openrouter', openrouterUsage(credits.total, credits.spent, new Date(now)));
|
|
49
|
+
return run(body);
|
|
90
50
|
} catch (err) {
|
|
91
|
-
|
|
51
|
+
return asApiError(err);
|
|
92
52
|
}
|
|
93
53
|
}
|
|
94
54
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
base: deps.geminiBase,
|
|
99
|
-
tokenBase: deps.geminiTokenBase,
|
|
100
|
-
fetchImpl: deps.fetchImpl,
|
|
101
|
-
save: (t) => {
|
|
102
|
-
store.write(setGeminiAuth(store.read(), t));
|
|
103
|
-
},
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
async function geminiModels(deps: ModelApiDeps, store: Store): Promise<Awaited<ReturnType<typeof listGeminiModels>>> {
|
|
107
|
-
const cfg = store.read();
|
|
108
|
-
if (cfg.gemini.auth === null) throw new ApiError('Gemini is not connected: sign in with Google first', 400);
|
|
109
|
-
const tokens = await currentGeminiTokens(cfg, geminiDeps(deps, store), geminiApiState);
|
|
110
|
-
return listGeminiModels(tokens, geminiDeps(deps, store));
|
|
55
|
+
async function chooseRoute(req: IncomingMessage, deps: ModelApiDeps, store: Store): Promise<unknown> {
|
|
56
|
+
const next = await withBody(req, (body) => setRoute(store.read(), typeof body.route === 'string' ? body.route : ''));
|
|
57
|
+
return kept(store, next, deps, 'model-api: route changed', { route: next.route });
|
|
111
58
|
}
|
|
112
59
|
|
|
113
|
-
function
|
|
114
|
-
const
|
|
115
|
-
return
|
|
60
|
+
async function create(req: IncomingMessage, deps: ModelApiDeps, store: Store): Promise<unknown> {
|
|
61
|
+
const next = await withBody(req, (body) => addConnection(store.read(), body));
|
|
62
|
+
return kept(store, next, deps, 'model-api: connection added', { connection: next.connections.at(-1)?.id ?? '' });
|
|
116
63
|
}
|
|
117
64
|
|
|
118
|
-
async function
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
const inUse = geminiModelsInUse(cfg);
|
|
122
|
-
const seen = usageOf('gemini');
|
|
123
|
-
const covers = seen !== undefined && [...inUse].every((id) => seen.windows.some((w) => w.label === id));
|
|
124
|
-
if (seen !== undefined && covers && now - Date.parse(seen.at) < CREDITS_TTL_MS) return;
|
|
125
|
-
try {
|
|
126
|
-
const rows = (await geminiModels(deps, store)).filter((m) => inUse.has(m.id));
|
|
127
|
-
const usage = geminiUsage(rows, new Date(now));
|
|
128
|
-
if (usage !== null) noteUsage('gemini', usage);
|
|
129
|
-
} catch (err) {
|
|
130
|
-
log.warn({ err: errMsg(err) }, 'model-api: could not read the Gemini quota');
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
async function settingsWithUsage(cfg: ModelConfig, deps: ModelApiDeps, store: Store): Promise<Record<string, unknown>> {
|
|
135
|
-
await refreshCredits(cfg, deps);
|
|
136
|
-
await refreshGeminiQuota(deps, store);
|
|
137
|
-
return settingsBody(cfg);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function asApiError(err: unknown): never {
|
|
141
|
-
if (err instanceof ModelConfigError || err instanceof CodexAuthError || err instanceof GeminiAuthError) throw new ApiError(err.message, 400);
|
|
142
|
-
if (err instanceof GatewayError) throw new ApiError(err.message, err.status >= 400 && err.status < 500 ? 400 : 502);
|
|
143
|
-
throw err;
|
|
65
|
+
async function change(req: IncomingMessage, deps: ModelApiDeps, store: Store, id: string): Promise<unknown> {
|
|
66
|
+
const next = await withBody(req, (body) => updateConnection(store.read(), id, body));
|
|
67
|
+
return kept(store, next, deps, 'model-api: connection changed', { connection: id });
|
|
144
68
|
}
|
|
145
69
|
|
|
146
|
-
|
|
147
|
-
const patch = await readJsonBody(req, BODY_MAX);
|
|
70
|
+
function drop(deps: ModelApiDeps, store: Store, id: string): unknown {
|
|
148
71
|
let next: ModelConfig;
|
|
149
72
|
try {
|
|
150
|
-
next =
|
|
73
|
+
next = removeConnection(store.read(), id);
|
|
151
74
|
} catch (err) {
|
|
152
75
|
asApiError(err);
|
|
153
76
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
log.info({ provider: next.provider }, 'model-api: route updated');
|
|
157
|
-
return settingsBody(next);
|
|
77
|
+
forgetOne(id);
|
|
78
|
+
return kept(store, next, deps, 'model-api: connection removed', { connection: id });
|
|
158
79
|
}
|
|
159
80
|
|
|
160
|
-
function
|
|
161
|
-
store
|
|
162
|
-
|
|
163
|
-
return settingsBody(cfg);
|
|
81
|
+
async function settingsWithUsage(deps: ModelApiDeps, store: Store): Promise<Record<string, unknown>> {
|
|
82
|
+
await refreshUsage(deps, store);
|
|
83
|
+
return settingsBody(store.read());
|
|
164
84
|
}
|
|
165
85
|
|
|
166
|
-
const modelApiState = freshCodexState();
|
|
167
|
-
|
|
168
|
-
async function pollDevice(id: string, deps: ModelApiDeps, store: Store): Promise<unknown> {
|
|
169
|
-
const result = await pollDeviceLogin(id, deps.fetchImpl).catch(asApiError);
|
|
170
|
-
if (result.status !== 'done') return result;
|
|
171
|
-
return { status: 'done', settings: saveCodex(store, setCodexAuth(store.read(), result.tokens), 'model-api: Codex connected by device code') };
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const CODEX_ROUTES: Record<string, Route> = {
|
|
175
|
-
device: {
|
|
176
|
-
method: 'POST',
|
|
177
|
-
run: async (_req, deps) => {
|
|
178
|
-
const login = await beginDeviceLogin(deps.issuer, deps.fetchImpl).catch(asApiError);
|
|
179
|
-
return { id: login.id, user_code: login.userCode, verify_url: login.verifyUrl, interval: login.interval };
|
|
180
|
-
},
|
|
181
|
-
},
|
|
182
|
-
login: {
|
|
183
|
-
method: 'POST',
|
|
184
|
-
run: (_req, deps) => Promise.resolve({ url: beginLogin(deps.issuer).url }),
|
|
185
|
-
},
|
|
186
|
-
callback: {
|
|
187
|
-
method: 'POST',
|
|
188
|
-
run: async (req, deps, store) => {
|
|
189
|
-
const body = await readJsonBody(req, BODY_MAX);
|
|
190
|
-
const raw = isRecord(body) && typeof body.url === 'string' ? body.url : '';
|
|
191
|
-
const tokens = await finishLogin(raw, deps.issuer, deps.fetchImpl).catch(asApiError);
|
|
192
|
-
return saveCodex(store, setCodexAuth(store.read(), tokens), 'model-api: Codex connected');
|
|
193
|
-
},
|
|
194
|
-
},
|
|
195
|
-
logout: {
|
|
196
|
-
method: 'POST',
|
|
197
|
-
run: (_req, _deps, store) => Promise.resolve(saveCodex(store, setCodexAuth(store.read(), null), 'model-api: Codex disconnected')),
|
|
198
|
-
},
|
|
199
|
-
import: {
|
|
200
|
-
method: 'POST',
|
|
201
|
-
run: (_req, deps, store) => {
|
|
202
|
-
let tokens;
|
|
203
|
-
try {
|
|
204
|
-
tokens = readCodexCliAuth(deps.codexHome);
|
|
205
|
-
} catch (err) {
|
|
206
|
-
asApiError(err);
|
|
207
|
-
}
|
|
208
|
-
return Promise.resolve(saveCodex(store, setCodexAuth(store.read(), tokens), 'model-api: Codex CLI login imported'));
|
|
209
|
-
},
|
|
210
|
-
},
|
|
211
|
-
models: {
|
|
212
|
-
method: 'GET',
|
|
213
|
-
run: async (_req, deps, store) => {
|
|
214
|
-
const cfg = store.read();
|
|
215
|
-
if (cfg.codex.auth === null) throw new ApiError('Codex is not connected: sign in with ChatGPT first', 400);
|
|
216
|
-
const codexDeps = { issuer: deps.issuer, fetchImpl: deps.fetchImpl, base: deps.codexBase, save: (t: CodexTokens) => { store.write(setCodexAuth(store.read(), t)); } };
|
|
217
|
-
const auth = await currentTokens(cfg, codexDeps, modelApiState).catch(asApiError);
|
|
218
|
-
const models = await codexModels(auth, codexDeps).catch(asApiError);
|
|
219
|
-
return { models };
|
|
220
|
-
},
|
|
221
|
-
},
|
|
222
|
-
};
|
|
223
|
-
|
|
224
|
-
function projectOf(body: unknown): string | null {
|
|
225
|
-
try {
|
|
226
|
-
return parseGeminiProject(isRecord(body) ? body.project : null);
|
|
227
|
-
} catch (err) {
|
|
228
|
-
return asApiError(err);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
async function connectGemini(req: IncomingMessage, deps: ModelApiDeps, store: Store): Promise<unknown> {
|
|
233
|
-
const body = await readJsonBody(req, BODY_MAX);
|
|
234
|
-
const code = isRecord(body) && typeof body.code === 'string' ? body.code : '';
|
|
235
|
-
const state = isRecord(body) && typeof body.state === 'string' ? body.state : '';
|
|
236
|
-
const project = projectOf(body);
|
|
237
|
-
const tokens = await exchangeGeminiCode(code, state, deps.geminiTokenBase, deps.fetchImpl).catch(asApiError);
|
|
238
|
-
const email = await userEmail(tokens, deps.geminiUserBase, deps.fetchImpl);
|
|
239
|
-
const onboarded = await onboard(tokens, project, deps.geminiBase, deps.fetchImpl).catch(asApiError);
|
|
240
|
-
const cfg = setGeminiAuth(store.read(), { ...tokens, email, project: onboarded.project, tier: onboarded.tier });
|
|
241
|
-
store.write(cfg);
|
|
242
|
-
log.info({ tier: onboarded.tier }, 'model-api: Gemini connected');
|
|
243
|
-
return settingsBody(cfg);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
const GEMINI_ROUTES: Record<string, Route> = {
|
|
247
|
-
login: {
|
|
248
|
-
method: 'POST',
|
|
249
|
-
run: (_req, deps) => Promise.resolve(beginGeminiLogin(deps.geminiAuthBase)),
|
|
250
|
-
},
|
|
251
|
-
code: { method: 'POST', run: connectGemini },
|
|
252
|
-
logout: {
|
|
253
|
-
method: 'POST',
|
|
254
|
-
run: (_req, _deps, store) => {
|
|
255
|
-
const cfg = setGeminiAuth(store.read(), null);
|
|
256
|
-
store.write(cfg);
|
|
257
|
-
log.info('model-api: Gemini disconnected');
|
|
258
|
-
return Promise.resolve(settingsBody(cfg));
|
|
259
|
-
},
|
|
260
|
-
},
|
|
261
|
-
models: {
|
|
262
|
-
method: 'GET',
|
|
263
|
-
run: async (_req, deps, store) => ({ models: (await geminiModels(deps, store).catch(asApiError)).map((m) => m.id) }),
|
|
264
|
-
},
|
|
265
|
-
};
|
|
266
|
-
|
|
267
86
|
const OPENROUTER_ROUTES: Record<string, Route> = {
|
|
268
|
-
models: {
|
|
269
|
-
|
|
270
|
-
run: async (_req, deps) => ({ models: await openrouterModels(deps.openrouterBase, deps.fetchImpl).catch(asApiError) }),
|
|
271
|
-
},
|
|
272
|
-
zdr: {
|
|
273
|
-
method: 'GET',
|
|
274
|
-
run: async (_req, deps) => ({ models: await openrouterZdrModels(deps.openrouterBase, deps.fetchImpl).catch(asApiError) }),
|
|
275
|
-
},
|
|
87
|
+
models: { method: 'GET', run: async (_req, deps) => ({ models: await openrouterModels(deps.openrouterBase, deps.fetchImpl).catch(asApiError) }) },
|
|
88
|
+
zdr: { method: 'GET', run: async (_req, deps) => ({ models: await openrouterZdrModels(deps.openrouterBase, deps.fetchImpl).catch(asApiError) }) },
|
|
276
89
|
};
|
|
277
90
|
|
|
278
91
|
const ANTHROPIC_ROUTES: Record<string, Route> = {
|
|
279
92
|
models: {
|
|
280
93
|
method: 'GET',
|
|
281
|
-
run: async (
|
|
94
|
+
run: async (req, deps, store) => ({ models: await anthropicModels(connectionFor(store.read(), req, 'anthropic'), deps.anthropicBase, deps.fetchImpl).catch(asApiError) }),
|
|
282
95
|
},
|
|
283
96
|
};
|
|
284
97
|
|
|
285
98
|
const BEDROCK_ROUTES: Record<string, Route> = {
|
|
286
99
|
models: {
|
|
287
100
|
method: 'GET',
|
|
288
|
-
run: async (
|
|
101
|
+
run: async (req, deps, store) => ({ models: await bedrockModels(connectionFor(store.read(), req, 'bedrock'), deps.bedrockControlBase, deps.fetchImpl).catch(asApiError) }),
|
|
289
102
|
},
|
|
290
103
|
};
|
|
291
104
|
|
|
@@ -298,11 +111,7 @@ const named = (table: Record<string, Route>, name: string, method: string | unde
|
|
|
298
111
|
async function restore(req: IncomingMessage, store: Store, deps: ModelApiDeps): Promise<unknown> {
|
|
299
112
|
const body = await readJsonBody(req, RESTORE_MAX);
|
|
300
113
|
if (!isRecord(body)) throw new ApiError('body must be a JSON object', 400);
|
|
301
|
-
|
|
302
|
-
store.write(next);
|
|
303
|
-
syncAvailableModelsQuietly(deps.setup ?? {}, next);
|
|
304
|
-
log.info({ provider: next.provider }, 'model-api: model setup restored from a file');
|
|
305
|
-
return settingsBody(next);
|
|
114
|
+
return kept(store, parseModelConfig(body), deps, 'model-api: model setup restored from a file', {});
|
|
306
115
|
}
|
|
307
116
|
|
|
308
117
|
function bundleRoute(path: string, method: string | undefined): Route | number {
|
|
@@ -311,8 +120,17 @@ function bundleRoute(path: string, method: string | undefined): Route | number {
|
|
|
311
120
|
}
|
|
312
121
|
|
|
313
122
|
function settingsRoute(method: string | undefined): Route | number {
|
|
314
|
-
if (method === 'GET') return { method: 'GET', run: (_req, deps, store) => settingsWithUsage(
|
|
315
|
-
if (method === 'PUT') return { method: 'POST', run:
|
|
123
|
+
if (method === 'GET') return { method: 'GET', run: (_req, deps, store) => settingsWithUsage(deps, store) };
|
|
124
|
+
if (method === 'PUT') return { method: 'POST', run: chooseRoute };
|
|
125
|
+
return 405;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function connectionsRoute(path: string, method: string | undefined): Route | number {
|
|
129
|
+
if (path === CONNECTIONS) return method === 'POST' ? { method: 'POST', run: create } : 405;
|
|
130
|
+
const id = path.slice(CONNECTIONS.length + 1);
|
|
131
|
+
if (id === '' || id.includes('/')) return 404;
|
|
132
|
+
if (method === 'PUT') return { method: 'POST', run: (req, deps, store) => change(req, deps, store, id) };
|
|
133
|
+
if (method === 'DELETE') return { method: 'GET', run: (_req, deps, store) => Promise.resolve(drop(deps, store, id)) };
|
|
316
134
|
return 405;
|
|
317
135
|
}
|
|
318
136
|
|
|
@@ -320,15 +138,25 @@ function codexRoute(rest: string, method: string | undefined): Route | number {
|
|
|
320
138
|
if (rest in CODEX_ROUTES) return named(CODEX_ROUTES, rest, method);
|
|
321
139
|
const id = rest.startsWith(DEVICE_PREFIX) ? rest.slice(DEVICE_PREFIX.length) : '';
|
|
322
140
|
if (!DEVICE_ID_RE.test(id)) return 404;
|
|
323
|
-
return
|
|
141
|
+
return codexDeviceRoute(id, method);
|
|
324
142
|
}
|
|
325
143
|
|
|
326
144
|
const mine = (path: string): boolean =>
|
|
327
|
-
path === PATH ||
|
|
145
|
+
path === PATH ||
|
|
146
|
+
path === BUNDLE ||
|
|
147
|
+
path === RESTORE ||
|
|
148
|
+
path === CONNECTIONS ||
|
|
149
|
+
path.startsWith(`${CONNECTIONS}/`) ||
|
|
150
|
+
path.startsWith(CODEX) ||
|
|
151
|
+
path.startsWith(GEMINI) ||
|
|
152
|
+
path.startsWith(OPENROUTER) ||
|
|
153
|
+
path.startsWith(ANTHROPIC) ||
|
|
154
|
+
path.startsWith(BEDROCK);
|
|
328
155
|
|
|
329
156
|
function routeFor(path: string, method: string | undefined): Route | number {
|
|
330
157
|
if (path === PATH) return settingsRoute(method);
|
|
331
158
|
if (path === BUNDLE || path === RESTORE) return bundleRoute(path, method);
|
|
159
|
+
if (path === CONNECTIONS || path.startsWith(`${CONNECTIONS}/`)) return connectionsRoute(path, method);
|
|
332
160
|
if (path.startsWith(OPENROUTER)) return named(OPENROUTER_ROUTES, path.slice(OPENROUTER.length), method);
|
|
333
161
|
if (path.startsWith(ANTHROPIC)) return named(ANTHROPIC_ROUTES, path.slice(ANTHROPIC.length), method);
|
|
334
162
|
if (path.startsWith(BEDROCK)) return named(BEDROCK_ROUTES, path.slice(BEDROCK.length), method);
|