@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
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import type { IncomingMessage } from 'node:http';
|
|
2
|
-
import { log } from '@metro-labs/core/log';
|
|
3
2
|
import { isRecord } from '@metro-labs/core/is-record';
|
|
4
3
|
import { ApiError } from '@metro-labs/http/api-error';
|
|
5
|
-
import { readJsonBody
|
|
4
|
+
import { readJsonBody } from '@metro-labs/http/api-http';
|
|
6
5
|
import {
|
|
7
6
|
claudeSetupStatus,
|
|
8
7
|
ensureClaudeSetup,
|
|
@@ -19,12 +18,10 @@ import {
|
|
|
19
18
|
type PermissionMode,
|
|
20
19
|
type SetupDeps,
|
|
21
20
|
} from './setup.js';
|
|
22
|
-
import {
|
|
23
|
-
import { assertSdkDrained, assertSessionStopped, prepareSessionRunner, setAutostart, withSessionChange, type SessionDeps } from './session.js';
|
|
21
|
+
import { assertSessionStopped, type SessionDeps } from './session.js';
|
|
24
22
|
import { tryMemoryJob } from './memory-routine.js';
|
|
25
23
|
|
|
26
24
|
const SETUP_BODY_MAX = SYSTEM_PROMPT_MAX * 2;
|
|
27
|
-
const OPERATOR_ONLY = 'only the Metro operator can allow the Agent SDK on a Claude login';
|
|
28
25
|
|
|
29
26
|
export interface SetupApiDeps {
|
|
30
27
|
session?: SessionDeps;
|
|
@@ -36,8 +33,6 @@ export interface SetupApiDeps {
|
|
|
36
33
|
interface SetupChange {
|
|
37
34
|
privacy?: boolean;
|
|
38
35
|
permissionMode?: PermissionMode;
|
|
39
|
-
runner?: HarnessRunner;
|
|
40
|
-
sdkOnLogin?: boolean;
|
|
41
36
|
systemPrompt?: string;
|
|
42
37
|
liveEvents?: boolean;
|
|
43
38
|
memoryRoutine?: boolean;
|
|
@@ -62,19 +57,11 @@ function modeChange(raw: unknown): PermissionMode | undefined {
|
|
|
62
57
|
return raw;
|
|
63
58
|
}
|
|
64
59
|
|
|
65
|
-
function runnerChange(raw: unknown): HarnessRunner | undefined {
|
|
66
|
-
if (raw === undefined) return undefined;
|
|
67
|
-
if (!isHarnessRunner(raw)) throw new ApiError('runner must be cli or sdk', 400);
|
|
68
|
-
return raw;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
60
|
function setupChange(body: unknown): SetupChange {
|
|
72
61
|
if (!isRecord(body)) throw new ApiError('a body is required', 400);
|
|
73
62
|
const change: SetupChange = {
|
|
74
63
|
privacy: flagChange(body.privacy, 'privacy'),
|
|
75
64
|
permissionMode: modeChange(body.permissionMode),
|
|
76
|
-
runner: runnerChange(body.runner),
|
|
77
|
-
sdkOnLogin: flagChange(body.sdkOnLogin, 'sdkOnLogin'),
|
|
78
65
|
systemPrompt: promptChange(body.systemPrompt),
|
|
79
66
|
liveEvents: flagChange(body.liveEvents, 'liveEvents'),
|
|
80
67
|
memoryRoutine: flagChange(body.memoryRoutine, 'memoryRoutine'),
|
|
@@ -84,44 +71,11 @@ function setupChange(body: unknown): SetupChange {
|
|
|
84
71
|
return given;
|
|
85
72
|
}
|
|
86
73
|
|
|
87
|
-
function checkRunnerChange(change: SetupChange, session: ApiSession, agents: string | undefined): void {
|
|
88
|
-
if (change.sdkOnLogin !== undefined && session.operator !== true) throw new ApiError(OPERATOR_ONLY, 403);
|
|
89
|
-
if (change.runner === 'sdk' && !sdkAllowed(agents, undefined, change.sdkOnLogin ?? sdkOnLogin(agents))) throw new ApiError(SDK_NEEDS_KEY, 403);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function nextRunner(change: SetupChange, agents: string | undefined): HarnessRunner {
|
|
93
|
-
const runner = change.runner ?? harnessRunner(agents);
|
|
94
|
-
return runner === 'sdk' && change.sdkOnLogin === false && !sdkAllowed(agents, undefined, false) ? 'cli' : runner;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
74
|
function changesSession(change: SetupChange, agents: string | undefined): boolean {
|
|
98
|
-
return
|
|
99
|
-
|| (change.permissionMode !== undefined && change.permissionMode !== permissionMode(agents))
|
|
75
|
+
return (change.permissionMode !== undefined && change.permissionMode !== permissionMode(agents))
|
|
100
76
|
|| (change.systemPrompt !== undefined && change.systemPrompt.trim() !== systemPrompt(agents));
|
|
101
77
|
}
|
|
102
78
|
|
|
103
|
-
function saveSessionSettings(change: SetupChange, runner: HarnessRunner, agents: string | undefined): void {
|
|
104
|
-
if (change.sdkOnLogin !== undefined && change.sdkOnLogin !== sdkOnLogin(agents)) {
|
|
105
|
-
setSdkOnLogin(change.sdkOnLogin, agents);
|
|
106
|
-
log.info({ on: change.sdkOnLogin }, 'claude-setup: the operator changed whether the Agent SDK may run on this box’s Claude login');
|
|
107
|
-
}
|
|
108
|
-
if (runner !== harnessRunner(agents)) {
|
|
109
|
-
setAutostart(false, agents);
|
|
110
|
-
setHarnessRunner(runner, agents);
|
|
111
|
-
}
|
|
112
|
-
if (change.permissionMode !== undefined) setPermissionMode(change.permissionMode, agents);
|
|
113
|
-
if (change.systemPrompt !== undefined) setSystemPrompt(change.systemPrompt, agents);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function checkStoppedChange(change: SetupChange, deps: SetupApiDeps): HarnessRunner {
|
|
117
|
-
const agents = deps.setup?.agents;
|
|
118
|
-
const session = { ...deps.session, agents: deps.session?.agents ?? agents };
|
|
119
|
-
const runner = nextRunner(change, agents);
|
|
120
|
-
if (changesSession(change, agents)) assertSessionStopped(session);
|
|
121
|
-
if (runner !== harnessRunner(agents) && harnessRunner(agents) === 'sdk') assertSdkDrained(session);
|
|
122
|
-
return runner;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
79
|
function applyLiveEvents(on: boolean, deps: SetupApiDeps): void {
|
|
126
80
|
const agents = deps.setup?.agents;
|
|
127
81
|
if (on !== liveEvents(agents)) setLiveEvents(on, agents);
|
|
@@ -133,29 +87,28 @@ function applyMemoryRoutine(on: boolean, deps: SetupApiDeps): void {
|
|
|
133
87
|
(deps.memoryJob ?? tryMemoryJob)(on);
|
|
134
88
|
}
|
|
135
89
|
|
|
136
|
-
|
|
90
|
+
function saveSessionSettings(change: SetupChange, deps: SetupApiDeps): void {
|
|
91
|
+
const agents = deps.setup?.agents;
|
|
92
|
+
if (changesSession(change, agents)) assertSessionStopped({ ...deps.session, agents: deps.session?.agents ?? agents });
|
|
93
|
+
if (change.permissionMode !== undefined) setPermissionMode(change.permissionMode, agents);
|
|
94
|
+
if (change.systemPrompt !== undefined) setSystemPrompt(change.systemPrompt, agents);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function applySetupChange(change: SetupChange, deps: SetupApiDeps): void {
|
|
137
98
|
const setup = deps.setup ?? {};
|
|
138
|
-
|
|
139
|
-
const runner = checkStoppedChange(change, deps);
|
|
140
|
-
if (runner !== harnessRunner(setup.agents)) {
|
|
141
|
-
await prepareSessionRunner(runner, deps.session);
|
|
142
|
-
checkRunnerChange(change, session, setup.agents);
|
|
143
|
-
if (checkStoppedChange(change, deps) !== runner) throw new ApiError('The model setup changed during preparation. Choose the runner again.', 409);
|
|
144
|
-
}
|
|
145
|
-
if (req.socket.destroyed) throw new ApiError('The request disconnected during preparation. The runner choice was not changed.', 409);
|
|
99
|
+
saveSessionSettings(change, deps);
|
|
146
100
|
if (change.privacy !== undefined) setPrivacy(change.privacy, setup.agents);
|
|
147
101
|
if (change.liveEvents !== undefined) applyLiveEvents(change.liveEvents, deps);
|
|
148
102
|
if (change.memoryRoutine !== undefined) applyMemoryRoutine(change.memoryRoutine, deps);
|
|
149
|
-
saveSessionSettings(change, runner, setup.agents);
|
|
150
103
|
ensureClaudeSetup(setup);
|
|
151
104
|
}
|
|
152
105
|
|
|
153
|
-
export async function setupAnswer(req: IncomingMessage, deps: SetupApiDeps
|
|
106
|
+
export async function setupAnswer(req: IncomingMessage, deps: SetupApiDeps): Promise<unknown> {
|
|
154
107
|
const setup = deps.setup ?? {};
|
|
155
108
|
const method = req.method ?? 'GET';
|
|
156
109
|
if (method === 'GET') return claudeSetupStatus(setup);
|
|
157
110
|
if (method !== 'POST') throw new ApiError('method not allowed', 405);
|
|
158
111
|
const change = setupChange(await readJsonBody(req, SETUP_BODY_MAX));
|
|
159
|
-
|
|
112
|
+
applySetupChange(change, deps);
|
|
160
113
|
return claudeSetupStatus(setup);
|
|
161
114
|
}
|
|
@@ -11,8 +11,6 @@ import { claudeDir } from './files.js';
|
|
|
11
11
|
import { memoryJobStatus, type MemoryJob } from './memory-routine.js';
|
|
12
12
|
import { cliPackageDir, hooksDir, skillsDir } from './cli-files.js';
|
|
13
13
|
import { hasMetroGuard, withMetroHooks } from './hooks.js';
|
|
14
|
-
import { readModelConfig, routedConnection, type ModelConfig } from '../gateway/model-config.js';
|
|
15
|
-
import { harnessRunner, runnerModel, sdkAllowed, sdkOnLogin, type HarnessRunner } from './runner.js';
|
|
16
14
|
import { readSetupState, writeSetupState } from './setup-state.js';
|
|
17
15
|
|
|
18
16
|
export const PRIVACY_ENV: Record<string, string> = {
|
|
@@ -69,9 +67,6 @@ export interface SetupReport {
|
|
|
69
67
|
export interface SetupStatus {
|
|
70
68
|
privacy: boolean;
|
|
71
69
|
permissionMode: PermissionMode;
|
|
72
|
-
runner: HarnessRunner;
|
|
73
|
-
runnerAllowed: boolean;
|
|
74
|
-
sdkOnLogin: boolean;
|
|
75
70
|
systemPrompt: string;
|
|
76
71
|
liveEvents: boolean;
|
|
77
72
|
memoryRoutine: boolean;
|
|
@@ -218,16 +213,23 @@ interface SettingsApplied {
|
|
|
218
213
|
guard: boolean;
|
|
219
214
|
}
|
|
220
215
|
|
|
221
|
-
|
|
216
|
+
const MODELS_KEYS = new Set(['availableModels', 'enforceAvailableModels']);
|
|
217
|
+
|
|
218
|
+
const withoutMetroModels = (settings: Record<string, unknown>, metroWrote: boolean): Record<string, unknown> =>
|
|
219
|
+
metroWrote ? Object.fromEntries(Object.entries(settings).filter(([key]) => !MODELS_KEYS.has(key))) : settings;
|
|
220
|
+
|
|
221
|
+
function applySettings(dir: string, agents: string, privacy: boolean, hooks: string | null): SettingsApplied {
|
|
222
|
+
const metroModels = readSetupState(agents).modelsByMetro === true;
|
|
222
223
|
let guard = false;
|
|
223
224
|
const settings = mergeSettings(dir, (current) => {
|
|
224
|
-
const next = withPrivacy(current, privacy);
|
|
225
|
+
const next = withPrivacy(withoutMetroModels(current, metroModels), privacy);
|
|
225
226
|
const hooked = hooks === null ? null : withMetroHooks(next, hooks);
|
|
226
227
|
if (hooks !== null && hooked === null)
|
|
227
228
|
log.warn({ path: join(dir, 'settings.json') }, 'claude-setup: the hooks in settings.json are not in the shape Claude Code reads, so metro did not add its own');
|
|
228
229
|
guard = hasMetroGuard(hooked ?? next);
|
|
229
230
|
return hooked ?? next;
|
|
230
231
|
});
|
|
232
|
+
if (metroModels && settings !== 'unreadable') writeSetupState(agents, { modelsByMetro: false });
|
|
231
233
|
return { settings, guard };
|
|
232
234
|
}
|
|
233
235
|
|
|
@@ -280,9 +282,8 @@ export function ensureClaudeSetup(deps: SetupDeps = {}): SetupReport {
|
|
|
280
282
|
const skill = placeSkill(dir, skills, METRO_SKILL);
|
|
281
283
|
const stage = placeSkill(dir, skills, STAGE_SKILL);
|
|
282
284
|
const memory = placeSkill(dir, skills, MEMORY_SKILL);
|
|
283
|
-
const applied = applySettings(dir, privacy, hooks);
|
|
285
|
+
const applied = applySettings(dir, agents, privacy, hooks);
|
|
284
286
|
const report: SetupReport = { privacy, guard: applied.guard, worker, skill, stage, memory, settings: applied.settings };
|
|
285
|
-
syncAvailableModelsQuietly({ ...deps, dir, agents });
|
|
286
287
|
if ([report.worker, report.skill, report.stage, report.memory].some(written) || report.settings === 'written')
|
|
287
288
|
log.info(report, 'claude-setup: applied the Claude Code setup for a metro box');
|
|
288
289
|
if (report.settings === 'unreadable') log.warn({ path: join(dir, 'settings.json') }, 'claude-setup: settings.json is not valid JSON, so the privacy settings and metro hooks were not written');
|
|
@@ -299,9 +300,6 @@ export function claudeSetupStatus(deps: SetupDeps = {}): SetupStatus {
|
|
|
299
300
|
return {
|
|
300
301
|
privacy: privacyEnabled(agents),
|
|
301
302
|
permissionMode: permissionMode(agents),
|
|
302
|
-
runner: harnessRunner(agents),
|
|
303
|
-
runnerAllowed: sdkAllowed(agents),
|
|
304
|
-
sdkOnLogin: sdkOnLogin(agents),
|
|
305
303
|
systemPrompt: systemPrompt(agents),
|
|
306
304
|
liveEvents: liveEvents(agents),
|
|
307
305
|
memoryRoutine: memoryRoutine(agents),
|
|
@@ -323,33 +321,3 @@ export function tryClaudeSetup(deps: SetupDeps = {}): void {
|
|
|
323
321
|
log.warn({ err: errMsg(err) }, 'claude-setup: could not apply the Claude Code setup');
|
|
324
322
|
}
|
|
325
323
|
}
|
|
326
|
-
|
|
327
|
-
const MODELS_KEY = 'availableModels';
|
|
328
|
-
const ENFORCE_KEY = 'enforceAvailableModels';
|
|
329
|
-
|
|
330
|
-
export const routeOf = (cfg: ModelConfig): string | null => (routedConnection(cfg)?.provider === 'anthropic' ? null : runnerModel(cfg));
|
|
331
|
-
|
|
332
|
-
function withAvailableModels(settings: Record<string, unknown>, route: string | null, metroWrote: boolean): Record<string, unknown> {
|
|
333
|
-
if (route !== null) return { ...settings, [MODELS_KEY]: [route], [ENFORCE_KEY]: true };
|
|
334
|
-
if (!metroWrote) return settings;
|
|
335
|
-
return Object.fromEntries(Object.entries(settings).filter(([key]) => key !== MODELS_KEY && key !== ENFORCE_KEY));
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
export function syncAvailableModels(cfg: ModelConfig, deps: SetupDeps = {}): SettingsOutcome {
|
|
339
|
-
const dir = deps.dir ?? claudeDir();
|
|
340
|
-
const agents = deps.agents ?? agentsDir();
|
|
341
|
-
const route = routeOf(cfg);
|
|
342
|
-
const metroWrote = readSetupState(agents).modelsByMetro === true;
|
|
343
|
-
return mergeSettings(dir, (current) => {
|
|
344
|
-
writeSetupState(agents, { modelsByMetro: route !== null });
|
|
345
|
-
return withAvailableModels(current, route, metroWrote);
|
|
346
|
-
});
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
export function syncAvailableModelsQuietly(deps: SetupDeps = {}, cfg?: ModelConfig): void {
|
|
350
|
-
try {
|
|
351
|
-
syncAvailableModels(cfg ?? readModelConfig(deps.agents ?? agentsDir()), deps);
|
|
352
|
-
} catch (err) {
|
|
353
|
-
log.warn({ err: errMsg(err) }, 'claude-setup: could not sync the model allowlist');
|
|
354
|
-
}
|
|
355
|
-
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { isRecord } from '@metro-labs/core/is-record';
|
|
3
3
|
import { errMsg, log } from '@metro-labs/core/log';
|
|
4
|
-
import { agentCommand, agentUser, agentUserExpected, claudeBin } from '../agent-user/user.js';
|
|
4
|
+
import { agentCommand, agentUser, agentUserExpected, CLAUDE_ENV, claudeBin } from '../agent-user/user.js';
|
|
5
5
|
import { runningAsRoot } from '../metro-user/privilege.js';
|
|
6
6
|
import { PRIVACY_ENV } from './setup.js';
|
|
7
7
|
|
|
@@ -24,7 +24,7 @@ const ARGS = [
|
|
|
24
24
|
const BLOCKS_USAGE = 'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC';
|
|
25
25
|
const PROBE_ENV: Record<string, string> = {
|
|
26
26
|
...Object.fromEntries(Object.entries(PRIVACY_ENV).filter(([name]) => name !== BLOCKS_USAGE)),
|
|
27
|
-
|
|
27
|
+
...CLAUDE_ENV,
|
|
28
28
|
DISABLE_BUG_COMMAND: '1',
|
|
29
29
|
ENABLE_CLAUDEAI_MCP_SERVERS: 'false',
|
|
30
30
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
2
|
import { log } from '@metro-labs/core/log';
|
|
3
3
|
import { EventStreamDecoder, type EventStreamMessage } from './eventstream.js';
|
|
4
|
-
import { errorFrame,
|
|
4
|
+
import { errorFrame, providerStatus, relayFrames, sendError, upstreamMessage, type Watch } from './forward.js';
|
|
5
5
|
import type { Connection } from './model-config.js';
|
|
6
6
|
import { estimateTokens } from './subscription.js';
|
|
7
7
|
import { UsageScanner } from './usage.js';
|
|
@@ -226,8 +226,3 @@ export async function bedrockCount(
|
|
|
226
226
|
res.writeHead(200, { 'content-type': 'application/json' });
|
|
227
227
|
res.end(JSON.stringify({ input_tokens: counted ?? estimateTokens(rewritten.body) }));
|
|
228
228
|
}
|
|
229
|
-
|
|
230
|
-
export function assertBedrockReady(settings: Connection): void {
|
|
231
|
-
if (settings.apiKey === '' || settings.region === '')
|
|
232
|
-
throw new GatewayError(400, 'invalid_request_error', 'Bedrock needs an API key and a region: add them on the Model page.');
|
|
233
|
-
}
|
|
@@ -5,13 +5,14 @@ import { isRecord } from '@metro-labs/core/is-record';
|
|
|
5
5
|
import { errMsg, log } from '@metro-labs/core/log';
|
|
6
6
|
import { ApiError } from '@metro-labs/http/api-error';
|
|
7
7
|
import { readFileSync } from '../agent-user/agent-fs.js';
|
|
8
|
-
import { removeHome } from '../agent-user/home-fs.js';
|
|
8
|
+
import { moveHome, removeHome } from '../agent-user/home-fs.js';
|
|
9
9
|
import { claudeHome } from '../agent-user/user.js';
|
|
10
10
|
import { claudeAccountIn, type ClaudeAccount, type LoginTarget } from '../claude/login.js';
|
|
11
11
|
import { readClaudeUsage } from '../claude/usage-probe.js';
|
|
12
12
|
import { currentOf, reach, refreshed, type TokenSource, type TokenState } from './subscription.js';
|
|
13
|
-
import { forgetReported } from './usage.js';
|
|
14
|
-
import {
|
|
13
|
+
import { forgetOne, forgetReported } from './usage.js';
|
|
14
|
+
import { readClaudeLogins, writeClaudeLogins, type ClaudeLogin } from './claude-store.js';
|
|
15
|
+
import { loginRef } from './model-config.js';
|
|
15
16
|
|
|
16
17
|
const CREDENTIALS = '.credentials.json';
|
|
17
18
|
const RENEW_MARGIN_MS = 60_000;
|
|
@@ -22,8 +23,7 @@ export interface ClaudeLoginDeps {
|
|
|
22
23
|
root?: string;
|
|
23
24
|
probe?: (env: Record<string, string>) => Promise<unknown>;
|
|
24
25
|
account?: (env: Record<string, string>) => ClaudeAccount;
|
|
25
|
-
|
|
26
|
-
write?: (cfg: ModelConfig) => void;
|
|
26
|
+
dir?: string;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
interface ClaudeTokens {
|
|
@@ -110,56 +110,80 @@ export function withClaudeLogin(login: string, deps: ClaudeLoginDeps, send: (tok
|
|
|
110
110
|
);
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
|
|
113
|
+
const folderOf = (login: string, deps: ClaudeLoginDeps): string => claudeLoginEnv(login, deps).CLAUDE_CONFIG_DIR ?? '';
|
|
114
|
+
|
|
115
|
+
function forgetTokens(login: string): void {
|
|
114
116
|
seen.delete(login);
|
|
115
117
|
state.delete(login);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function dropFolder(login: string, deps: ClaudeLoginDeps): void {
|
|
121
|
+
forgetTokens(login);
|
|
116
122
|
try {
|
|
117
|
-
removeHome(
|
|
123
|
+
removeHome(folderOf(login, deps), true);
|
|
118
124
|
} catch (err) {
|
|
119
|
-
log.warn({ err: errMsg(err) }, 'claude-logins: could not remove a Claude login that is no longer used');
|
|
125
|
+
log.warn({ err: errMsg(err) }, 'claude-logins: could not remove the folder of a Claude login that is no longer used');
|
|
120
126
|
}
|
|
121
127
|
}
|
|
122
128
|
|
|
123
|
-
export function
|
|
124
|
-
const
|
|
125
|
-
|
|
129
|
+
export function removeClaudeLogin(login: string, deps: ClaudeLoginDeps = {}): void {
|
|
130
|
+
const logins = readClaudeLogins(deps.dir);
|
|
131
|
+
if (!logins.some((l) => l.id === login)) throw new ApiError('no such Claude login on this box', 404);
|
|
132
|
+
writeClaudeLogins(logins.filter((l) => l.id !== login), deps.dir);
|
|
133
|
+
dropFolder(login, deps);
|
|
134
|
+
forgetOne(loginRef(login));
|
|
135
|
+
log.info({ login }, 'claude-logins: a Claude login was removed from this box');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function takeOver(fresh: string, replaced: string, deps: ClaudeLoginDeps): void {
|
|
139
|
+
const aside = `${folderOf(replaced, deps)}.old-${newId()}`;
|
|
140
|
+
moveHome(folderOf(replaced, deps), aside);
|
|
141
|
+
try {
|
|
142
|
+
moveHome(folderOf(fresh, deps), folderOf(replaced, deps));
|
|
143
|
+
} catch (err) {
|
|
144
|
+
moveHome(aside, folderOf(replaced, deps));
|
|
145
|
+
throw err;
|
|
146
|
+
}
|
|
147
|
+
forgetTokens(replaced);
|
|
148
|
+
try {
|
|
149
|
+
removeHome(aside, true);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
log.warn({ err: errMsg(err) }, 'claude-logins: could not remove the folder of the login that was signed in again');
|
|
152
|
+
}
|
|
126
153
|
}
|
|
127
154
|
|
|
128
|
-
function keep(login: string,
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
(
|
|
135
|
-
|
|
136
|
-
forgetReported(id);
|
|
137
|
-
log.info({ connection: id, plan: account.plan, added: connection === null }, 'claude-logins: the Claude login was kept for its own connection');
|
|
155
|
+
function keep(login: string, replacing: string | null, account: ClaudeAccount, deps: ClaudeLoginDeps): string | null {
|
|
156
|
+
const logins = readClaudeLogins(deps.dir);
|
|
157
|
+
if (replacing !== null && !logins.some((l) => l.id === replacing)) return 'that Claude login was removed during the sign-in';
|
|
158
|
+
if (replacing !== null) takeOver(login, replacing, deps);
|
|
159
|
+
const entry: ClaudeLogin = { id: replacing ?? login, email: account.account, plan: account.plan, savedAt: new Date().toISOString() };
|
|
160
|
+
writeClaudeLogins(replacing === null ? [...logins, entry] : logins.map((l) => (l.id === replacing ? entry : l)), deps.dir);
|
|
161
|
+
forgetReported(loginRef(entry.id));
|
|
162
|
+
log.info({ login: entry.id, plan: account.plan, added: replacing === null }, 'claude-logins: the Claude login was kept on this box');
|
|
138
163
|
return null;
|
|
139
164
|
}
|
|
140
165
|
|
|
141
|
-
function settle(ok: boolean, login: string,
|
|
166
|
+
function settle(ok: boolean, login: string, replacing: string | null, deps: ClaudeLoginDeps): Promise<string | null> {
|
|
142
167
|
if (!ok) {
|
|
143
|
-
|
|
168
|
+
dropFolder(login, deps);
|
|
144
169
|
return Promise.resolve(null);
|
|
145
170
|
}
|
|
146
171
|
let problem: string | null;
|
|
147
172
|
try {
|
|
148
173
|
const account = (deps.account ?? claudeAccountIn)(claudeLoginEnv(login, deps));
|
|
149
|
-
problem = account.signedIn ? keep(login,
|
|
174
|
+
problem = account.signedIn ? keep(login, replacing, account, deps) : 'the sign-in finished but Claude Code holds no login; try again';
|
|
150
175
|
} catch (err) {
|
|
151
176
|
problem = errMsg(err);
|
|
152
177
|
}
|
|
153
|
-
if (problem !== null)
|
|
178
|
+
if (problem !== null) dropFolder(login, deps);
|
|
154
179
|
return Promise.resolve(problem);
|
|
155
180
|
}
|
|
156
181
|
|
|
157
182
|
export function claudeLoginTarget(asked: string, deps: ClaudeLoginDeps = {}): LoginTarget {
|
|
158
|
-
const
|
|
159
|
-
if (
|
|
160
|
-
throw new ApiError('no such Anthropic connection', 404);
|
|
183
|
+
const replacing = asked === NEW ? null : asked;
|
|
184
|
+
if (replacing !== null && !readClaudeLogins(deps.dir).some((l) => l.id === replacing)) throw new ApiError('no such Claude login on this box', 404);
|
|
161
185
|
const login = newId();
|
|
162
|
-
return { key: asked, env: claudeLoginEnv(login, deps), settle: (ok) => settle(ok, login,
|
|
186
|
+
return { key: asked, env: claudeLoginEnv(login, deps), settle: (ok) => settle(ok, login, replacing, deps) };
|
|
163
187
|
}
|
|
164
188
|
|
|
165
189
|
export function forgetClaudeTokens(): void {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { ID_RE, newId } from '@metro-labs/core/ids';
|
|
3
|
+
import { isRecord } from '@metro-labs/core/is-record';
|
|
4
|
+
import { readJson, writeSecure } from '@metro-labs/core/secure-fs';
|
|
5
|
+
import { agentsDir } from '../agents/files.js';
|
|
6
|
+
|
|
7
|
+
export interface ClaudeLogin {
|
|
8
|
+
id: string;
|
|
9
|
+
email: string | null;
|
|
10
|
+
plan: string | null;
|
|
11
|
+
savedAt: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const MODEL_FILE = 'model.json';
|
|
15
|
+
|
|
16
|
+
const text = (value: unknown): string => (typeof value === 'string' ? value.trim() : '');
|
|
17
|
+
const maybe = (value: unknown): string | null => (typeof value === 'string' && value !== '' ? value : null);
|
|
18
|
+
|
|
19
|
+
export function claudeLoginOf(raw: unknown): ClaudeLogin | null {
|
|
20
|
+
if (!isRecord(raw) || !ID_RE.test(text(raw.id))) return null;
|
|
21
|
+
return { id: text(raw.id), email: maybe(raw.email), plan: maybe(raw.plan), savedAt: text(raw.savedAt) };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const modelPath = (dir: string): string => join(dir, MODEL_FILE);
|
|
25
|
+
|
|
26
|
+
export const readModelFile = (dir = agentsDir()): unknown =>
|
|
27
|
+
readJson<unknown>(modelPath(dir), null, { warn: 'model.json is unreadable, so the Claude logins of this box are not seen until it is fixed' });
|
|
28
|
+
|
|
29
|
+
export const isV2 = (raw: unknown): raw is Record<string, unknown> & { connections: unknown[] } =>
|
|
30
|
+
isRecord(raw) && raw.version !== 3 && Array.isArray(raw.connections);
|
|
31
|
+
|
|
32
|
+
const unique = (logins: ClaudeLogin[]): ClaudeLogin[] => logins.filter((l, at) => logins.findIndex((o) => o.id === l.id) === at);
|
|
33
|
+
|
|
34
|
+
export function claudeLoginsIn(raw: unknown): ClaudeLogin[] {
|
|
35
|
+
if (isV2(raw)) return unique(raw.connections.flatMap((c: unknown) => (isRecord(c) && c.provider === 'anthropic' ? (claudeLoginOf(c.claude) ?? []) : [])));
|
|
36
|
+
if (!isRecord(raw) || !Array.isArray(raw.claude)) return [];
|
|
37
|
+
return unique(raw.claude.flatMap((c: unknown) => claudeLoginOf(c) ?? []));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const readClaudeLogins = (dir = agentsDir()): ClaudeLogin[] => claudeLoginsIn(readModelFile(dir));
|
|
41
|
+
|
|
42
|
+
export const v3Text = (logins: ClaudeLogin[]): string => JSON.stringify({ version: 3, claude: logins }, null, 2);
|
|
43
|
+
|
|
44
|
+
function v2With(raw: Record<string, unknown> & { connections: unknown[] }, logins: ClaudeLogin[]): Record<string, unknown> {
|
|
45
|
+
const holds = (c: unknown): c is Record<string, unknown> => isRecord(c) && c.provider === 'anthropic' && claudeLoginOf(c.claude) !== null;
|
|
46
|
+
const held = new Map(raw.connections.filter(holds).map((c) => [claudeLoginOf(c.claude)?.id ?? '', c]));
|
|
47
|
+
const others = raw.connections.filter((c) => !holds(c));
|
|
48
|
+
const logged = logins.map((login) => ({ ...(held.get(login.id) ?? { id: newId(), provider: 'anthropic', label: 'Anthropic', model: '' }), claude: login }));
|
|
49
|
+
return { ...raw, connections: [...others, ...logged] };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function writeClaudeLogins(logins: ClaudeLogin[], dir = agentsDir()): void {
|
|
53
|
+
const raw = readModelFile(dir);
|
|
54
|
+
writeSecure(modelPath(dir), isV2(raw) ? JSON.stringify(v2With(raw, logins), null, 2) : v3Text(logins));
|
|
55
|
+
}
|
|
@@ -2,54 +2,40 @@ import { randomUUID } from 'node:crypto';
|
|
|
2
2
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
3
3
|
import { arch, platform, release } from 'node:os';
|
|
4
4
|
import { isRecord } from '@metro-labs/core/is-record';
|
|
5
|
+
import { errMsg, log } from '@metro-labs/core/log';
|
|
5
6
|
import { stringOf } from '@metro-labs/http/api-http';
|
|
6
|
-
import { OPENAI_API, CODEX_SIGN_IN, refreshTokens, tokensStale, type CodexTokens } from './codex-auth.js';
|
|
7
|
-
import { refreshCodeTokens } from './codex-device.js';
|
|
8
7
|
import { codexVersion, learnCodexVersion } from './codex-version.js';
|
|
9
8
|
import { CodexEventTranslator, withUsageLink } from './codex-stream.js';
|
|
10
9
|
import { ToolNames, toResponsesRequest } from './codex-translate.js';
|
|
11
10
|
import { GatewayError, providerStatus, sendError, upstreamMessage, type Watch } from './forward.js';
|
|
12
11
|
import { parseEvent, SseParser, type SseEvent } from './frames.js';
|
|
13
|
-
import type
|
|
14
|
-
import {
|
|
15
|
-
import { answerWhole, currentOf, errorKind, reach, refreshed, relayTranslated, sessionHeader, type TokenSource, type TokenState } from './subscription.js';
|
|
12
|
+
import { CODEX_SIGN_IN, type CodexAccess, type Connection } from './model-config.js';
|
|
13
|
+
import { answerWhole, errorKind, relayTranslated, sessionHeader } from './subscription.js';
|
|
16
14
|
import { noteUsageHeaders } from './usage.js';
|
|
17
15
|
|
|
18
16
|
const CODEX_BACKEND = 'https://chatgpt.com/backend-api/codex';
|
|
17
|
+
const OPENAI_API = 'https://api.openai.com/v1';
|
|
19
18
|
const INVALID = [400, 404, 422];
|
|
19
|
+
const RENEW_FAILED = "Codex refused its sign-in and api.metro.box could not renew it: sign in again on the organization's Models page.";
|
|
20
20
|
|
|
21
21
|
export interface CodexDeps {
|
|
22
22
|
base?: string;
|
|
23
|
-
issuer?: string;
|
|
24
23
|
fetchImpl?: typeof fetch;
|
|
25
|
-
|
|
24
|
+
refresh: (conn: Connection) => Promise<Connection | null>;
|
|
26
25
|
}
|
|
27
26
|
|
|
28
|
-
export type CodexState = TokenState<CodexTokens>;
|
|
29
|
-
|
|
30
|
-
export const sharedCodexState: CodexState = new Map();
|
|
31
|
-
|
|
32
|
-
const codexLists = listCache<string>('codex');
|
|
33
|
-
|
|
34
|
-
const sourceOf = (deps: CodexDeps): TokenSource<CodexTokens> => ({
|
|
35
|
-
label: 'Codex',
|
|
36
|
-
stale: (tokens) => tokensStale(tokens),
|
|
37
|
-
refresh: (tokens) => (tokens.method === 'code' ? refreshCodeTokens(tokens, deps.issuer, deps.fetchImpl) : refreshTokens(tokens, deps.issuer, deps.fetchImpl)),
|
|
38
|
-
save: deps.save,
|
|
39
|
-
});
|
|
40
|
-
|
|
41
27
|
const OS_NAMES: Record<string, string> = { darwin: 'Mac OS', linux: 'Linux', win32: 'Windows' };
|
|
42
28
|
|
|
43
29
|
export const userAgent = (): string => `codex_cli_rs/${codexVersion()} (${OS_NAMES[platform()] ?? platform()} ${release()}; ${arch()}) metro`;
|
|
44
30
|
|
|
45
|
-
const baseFor = (
|
|
31
|
+
const baseFor = (access: CodexAccess, deps: Pick<CodexDeps, 'base'>): string => deps.base ?? (access.method === 'code' ? CODEX_BACKEND : OPENAI_API);
|
|
46
32
|
|
|
47
|
-
function headersFor(
|
|
48
|
-
const plain = { authorization: `Bearer ${
|
|
49
|
-
if (
|
|
33
|
+
function headersFor(access: CodexAccess, sessionId: string): Record<string, string> {
|
|
34
|
+
const plain = { authorization: `Bearer ${access.accessToken}`, 'content-type': 'application/json', accept: 'text/event-stream' };
|
|
35
|
+
if (access.method === 'chatgpt') return plain;
|
|
50
36
|
return {
|
|
51
37
|
...plain,
|
|
52
|
-
'chatgpt-account-id':
|
|
38
|
+
'chatgpt-account-id': access.accountId ?? '',
|
|
53
39
|
originator: 'codex_cli_rs',
|
|
54
40
|
'user-agent': userAgent(),
|
|
55
41
|
'openai-beta': 'responses=experimental',
|
|
@@ -57,9 +43,6 @@ function headersFor(tokens: CodexTokens, accept: string, sessionId: string): Rec
|
|
|
57
43
|
};
|
|
58
44
|
}
|
|
59
45
|
|
|
60
|
-
export const currentTokens = (conn: Connection, deps: CodexDeps, state: CodexState): Promise<CodexTokens> =>
|
|
61
|
-
currentOf(state, conn.id, conn.codex, sourceOf(deps), CODEX_SIGN_IN);
|
|
62
|
-
|
|
63
46
|
interface Call {
|
|
64
47
|
body: Record<string, unknown>;
|
|
65
48
|
model: string;
|
|
@@ -69,11 +52,11 @@ interface Call {
|
|
|
69
52
|
names: ToolNames;
|
|
70
53
|
}
|
|
71
54
|
|
|
72
|
-
function send(call: Call,
|
|
55
|
+
function send(call: Call, access: CodexAccess): Promise<Response> {
|
|
73
56
|
const request = toResponsesRequest(call.body, call.model, { promptCacheKey: call.sessionId, names: call.names });
|
|
74
|
-
return (call.deps.fetchImpl ?? fetch)(`${baseFor(
|
|
57
|
+
return (call.deps.fetchImpl ?? fetch)(`${baseFor(access, call.deps)}/responses`, {
|
|
75
58
|
method: 'POST',
|
|
76
|
-
headers: headersFor(
|
|
59
|
+
headers: headersFor(access, call.sessionId),
|
|
77
60
|
body: JSON.stringify(request),
|
|
78
61
|
signal: call.watch.signal,
|
|
79
62
|
redirect: 'manual',
|
|
@@ -91,22 +74,33 @@ function refusalOf(text: string, status: number): string {
|
|
|
91
74
|
return withUsageLink(code, upstreamMessage(text, `OpenAI answered ${String(status)}`));
|
|
92
75
|
}
|
|
93
76
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
77
|
+
let learning: Promise<unknown> | null = null;
|
|
78
|
+
|
|
79
|
+
function learnVersion(access: CodexAccess): void {
|
|
80
|
+
if (access.method !== 'code' || learning !== null) return;
|
|
81
|
+
learning = learnCodexVersion()
|
|
82
|
+
.catch((err: unknown) => {
|
|
83
|
+
log.warn({ err: errMsg(err) }, 'gateway: the latest Codex version could not be learned');
|
|
84
|
+
})
|
|
85
|
+
.finally(() => {
|
|
86
|
+
learning = null;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function reach(call: Call, conn: Connection): Promise<Response> {
|
|
91
|
+
if (conn.codex === null) throw new GatewayError(400, 'invalid_request_error', CODEX_SIGN_IN);
|
|
92
|
+
learnVersion(conn.codex);
|
|
93
|
+
const upstream = await send(call, conn.codex);
|
|
94
|
+
if (upstream.status !== 401) return upstream;
|
|
95
|
+
await upstream.body?.cancel();
|
|
96
|
+
const fresh = await call.deps.refresh(conn);
|
|
97
|
+
if (fresh?.codex === null || fresh?.codex === undefined) throw new GatewayError(403, 'permission_error', RENEW_FAILED);
|
|
98
|
+
return send(call, fresh.codex);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function codexMessages(req: IncomingMessage, res: ServerResponse, body: Record<string, unknown>, model: string, conn: Connection, deps: CodexDeps, watch: Watch): Promise<void> {
|
|
104
102
|
const call: Call = { body, model, sessionId: sessionHeader(req) || randomUUID(), watch, deps, names: new ToolNames() };
|
|
105
|
-
const upstream = await reach(
|
|
106
|
-
() => currentTokens(conn, deps, state),
|
|
107
|
-
(tokens) => refreshed(state, conn.id, tokens, sourceOf(deps)),
|
|
108
|
-
(tokens) => send(call, tokens),
|
|
109
|
-
);
|
|
103
|
+
const upstream = await reach(call, conn);
|
|
110
104
|
noteUsageHeaders('codex', conn.id, upstream.headers);
|
|
111
105
|
if (!upstream.ok) {
|
|
112
106
|
const text = await upstream.text();
|
|
@@ -125,19 +119,3 @@ export async function codexMessages(
|
|
|
125
119
|
const frames = new SseParser().push(await upstream.text()).map(onEvent).join('');
|
|
126
120
|
answerWhole(res, frames + translator.close(), conn.id);
|
|
127
121
|
}
|
|
128
|
-
|
|
129
|
-
export async function codexModels(tokens: CodexTokens, deps: Omit<CodexDeps, 'save'>): Promise<string[]> {
|
|
130
|
-
const base = baseFor(tokens, deps);
|
|
131
|
-
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
132
|
-
if (tokens.method === 'chatgpt') return codexLists.get(`${base}:${tokens.clientId}`, () => listCodex(tokens, `${base}/models`, fetchImpl));
|
|
133
|
-
const version = await learnCodexVersion();
|
|
134
|
-
return codexLists.get(`${base}:${tokens.accountId}:${version}`, () => listCodex(tokens, `${base}/models?client_version=${version}`, fetchImpl));
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
async function listCodex(tokens: CodexTokens, url: string, fetchImpl: typeof fetch): Promise<string[]> {
|
|
138
|
-
const res = await fetchImpl(url, { headers: headersFor(tokens, 'application/json', randomUUID()), redirect: 'manual' });
|
|
139
|
-
if (!res.ok) throw new GatewayError(res.status, errorKind(res.status, INVALID), `OpenAI would not list the models of this ChatGPT plan (${String(res.status)})`);
|
|
140
|
-
const body: unknown = await res.json();
|
|
141
|
-
const models: unknown[] = isRecord(body) && Array.isArray(body.models) ? body.models : [];
|
|
142
|
-
return models.filter(isRecord).flatMap((m) => ((m.visibility ?? 'list') === 'list' && stringOf(m.slug) !== '' ? [stringOf(m.slug)] : []));
|
|
143
|
-
}
|