@stage-labs/metro 0.1.0-beta.309 → 0.1.0-beta.310

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage-labs/metro",
3
- "version": "0.1.0-beta.309",
3
+ "version": "0.1.0-beta.310",
4
4
  "description": "The metro command line: run your agent on this machine, relaying its chat channels and connectors to Claude Code.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -62,7 +62,8 @@ import { startCloudWatchPublisher } from '../server/cloudwatch.js';
62
62
  import { watchFallbackUsage } from '../gateway/usage-refresh.js';
63
63
  import { startOrgModels, stopOrgModels } from '../gateway/org-models.js';
64
64
  import { effectiveSetup, watchModelChanges } from '../gateway/effective.js';
65
- import { readClaudeLogins } from '../gateway/claude-store.js';
65
+ import { readClaudeLogins, retireV2ModelFile } from '../gateway/claude-store.js';
66
+ import { removeModelLeftovers } from '../gateway/model-leftovers.js';
66
67
  import { readAgentKey } from '../connectors/agent-key.js';
67
68
  import { METRO_API } from '../connectors/metro-api.js';
68
69
  import { isCallEvent, leaveCallsForShutdown, onCallEvent, sweepLeftoverCalls } from '../voice/calls.js';
@@ -144,6 +145,8 @@ function sessionApis(setLiveEvents: (on: boolean) => void): SessionApis {
144
145
 
145
146
  async function main(): Promise<void> {
146
147
  removeConnectorLeftovers();
148
+ removeModelLeftovers();
149
+ retireV2ModelFile();
147
150
  applyLocalOwner();
148
151
  installBearerSessions(agentsDir(), localOwner);
149
152
  watchPolicySnapshot();
@@ -154,7 +157,7 @@ installBearerSessions(agentsDir(), localOwner);
154
157
  syncGmailHost();
155
158
  supervisor.start();
156
159
  sweepLeftoverCalls();
157
- const models = startOrgModels({ owner: localOwner, key: () => readAgentKey(), api: METRO_API, dir: agentsDir });
160
+ const models = startOrgModels({ owner: localOwner, key: () => readAgentKey(), api: METRO_API });
158
161
  const metroMcp = await createMetroMcp({ liveEvents: liveEvents() });
159
162
  watchModelChanges(models, metroMcp.switchModel);
160
163
  webhookServer = await startWebhookServer(
@@ -1,6 +1,7 @@
1
1
  import { join } from 'node:path';
2
- import { ID_RE, newId } from '@metro-labs/core/ids';
2
+ import { ID_RE } from '@metro-labs/core/ids';
3
3
  import { isRecord } from '@metro-labs/core/is-record';
4
+ import { errMsg, log } from '@metro-labs/core/log';
4
5
  import { readJson, writeSecure } from '@metro-labs/core/secure-fs';
5
6
  import { agentsDir } from '../agents/files.js';
6
7
 
@@ -16,22 +17,22 @@ const MODEL_FILE = 'model.json';
16
17
  const text = (value: unknown): string => (typeof value === 'string' ? value.trim() : '');
17
18
  const maybe = (value: unknown): string | null => (typeof value === 'string' && value !== '' ? value : null);
18
19
 
19
- export function claudeLoginOf(raw: unknown): ClaudeLogin | null {
20
+ function claudeLoginOf(raw: unknown): ClaudeLogin | null {
20
21
  if (!isRecord(raw) || !ID_RE.test(text(raw.id))) return null;
21
22
  return { id: text(raw.id), email: maybe(raw.email), plan: maybe(raw.plan), savedAt: text(raw.savedAt) };
22
23
  }
23
24
 
24
- export const modelPath = (dir: string): string => join(dir, MODEL_FILE);
25
+ const modelPath = (dir: string): string => join(dir, MODEL_FILE);
25
26
 
26
- export const readModelFile = (dir = agentsDir()): unknown =>
27
+ const readModelFile = (dir: string): unknown =>
27
28
  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
 
29
- export const isV2 = (raw: unknown): raw is Record<string, unknown> & { connections: unknown[] } =>
30
+ const isV2 = (raw: unknown): raw is Record<string, unknown> & { connections: unknown[] } =>
30
31
  isRecord(raw) && raw.version !== 3 && Array.isArray(raw.connections);
31
32
 
32
33
  const unique = (logins: ClaudeLogin[]): ClaudeLogin[] => logins.filter((l, at) => logins.findIndex((o) => o.id === l.id) === at);
33
34
 
34
- export function claudeLoginsIn(raw: unknown): ClaudeLogin[] {
35
+ function claudeLoginsIn(raw: unknown): ClaudeLogin[] {
35
36
  if (isV2(raw)) return unique(raw.connections.flatMap((c: unknown) => (isRecord(c) && c.provider === 'anthropic' ? (claudeLoginOf(c.claude) ?? []) : [])));
36
37
  if (!isRecord(raw) || !Array.isArray(raw.claude)) return [];
37
38
  return unique(raw.claude.flatMap((c: unknown) => claudeLoginOf(c) ?? []));
@@ -39,17 +40,20 @@ export function claudeLoginsIn(raw: unknown): ClaudeLogin[] {
39
40
 
40
41
  export const readClaudeLogins = (dir = agentsDir()): ClaudeLogin[] => claudeLoginsIn(readModelFile(dir));
41
42
 
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] };
43
+ export function writeClaudeLogins(logins: ClaudeLogin[], dir = agentsDir()): void {
44
+ writeSecure(modelPath(dir), JSON.stringify({ version: 3, claude: logins }, null, 2));
50
45
  }
51
46
 
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));
47
+ export function retireV2ModelFile(dir = agentsDir()): boolean {
48
+ try {
49
+ const raw = readModelFile(dir);
50
+ if (!isV2(raw)) return false;
51
+ const logins = claudeLoginsIn(raw);
52
+ writeClaudeLogins(logins, dir);
53
+ log.warn({ claudeLogins: logins.length, connections: raw.connections.length }, 'org-models: model.json was still the old version 2; it keeps only its Claude logins now, and its connections are never served');
54
+ return true;
55
+ } catch (err) {
56
+ log.warn({ err: errMsg(err) }, 'org-models: the old version 2 model.json could not be rewritten; its connections are never served');
57
+ return false;
58
+ }
55
59
  }
@@ -27,7 +27,7 @@ function refOf(raw: unknown): Fallback | null {
27
27
  function routeOf(raw: unknown): DeliveredRoute | null {
28
28
  if (!isRecord(raw)) return null;
29
29
  const fallbacks = Array.isArray(raw.fallbacks) ? raw.fallbacks.flatMap((f: unknown) => refOf(f) ?? []) : [];
30
- return { primary: refOf(raw.primary), fallbacks: fallbacks.filter((f) => f.model !== '').slice(0, MAX_FALLBACKS) };
30
+ return { primary: refOf(raw.primary), fallbacks: fallbacks.filter((f) => f.model !== '' || f.connection.startsWith(LOCAL_PREFIX)).slice(0, MAX_FALLBACKS) };
31
31
  }
32
32
 
33
33
  function accountOf(raw: unknown): Account {
@@ -1,9 +1,8 @@
1
1
  import { log } from '@metro-labs/core/log';
2
2
  import { agentsDir } from '../agents/files.js';
3
- import { claudeLoginsIn, isV2, readModelFile, type ClaudeLogin } from './claude-store.js';
3
+ import { readClaudeLogins, type ClaudeLogin } from './claude-store.js';
4
4
  import type { Delivered } from './delivered.js';
5
5
  import { emptySetup, LOCAL_PREFIX, loginConnection, primaryRoute, runnerModel, SESSION_LOGIN, sessionConnection, type Connection, type ModelSetup } from './model-config.js';
6
- import { v2Setup } from './model-v2.js';
7
6
  import { deliveredSetup, type OrgModels } from './org-models.js';
8
7
 
9
8
  function localOf(id: string, logins: ClaudeLogin[]): Connection | null {
@@ -21,10 +20,7 @@ export function setupOf(delivered: Delivered | null, logins: ClaudeLogin[]): Mod
21
20
  return { primary: route.primary, fallbacks: route.fallbacks, connections: [...delivered.connections, ...locals], source: delivered.source };
22
21
  }
23
22
 
24
- export function effectiveSetup(dir = agentsDir(), delivered: Delivered | null = deliveredSetup()): ModelSetup {
25
- const raw = readModelFile(dir);
26
- return isV2(raw) ? v2Setup(raw) : setupOf(delivered, claudeLoginsIn(raw));
27
- }
23
+ export const effectiveSetup = (dir = agentsDir(), delivered: Delivered | null = deliveredSetup()): ModelSetup => setupOf(delivered, readClaudeLogins(dir));
28
24
 
29
25
  export function modelViewText(setup: ModelSetup): string | null {
30
26
  const primary = primaryRoute(setup);
@@ -55,7 +55,7 @@ export function holdOf(route: Route, now = Date.now()): Hold | null {
55
55
 
56
56
  function fallbackModel(requested: string, conn: Connection, model: string): string {
57
57
  const bare = bareModel(requested);
58
- return conn.provider === 'anthropic' && isSmallModel(bare) ? bare : model;
58
+ return model === '' || (conn.provider === 'anthropic' && isSmallModel(bare)) ? bare : model;
59
59
  }
60
60
 
61
61
  function fallbackRoute(setup: ModelSetup, fallback: Fallback, requested: string): Route | null {
@@ -0,0 +1,42 @@
1
+ import { lstatSync, readdirSync, unlinkSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { errMsg, log } from '@metro-labs/core/log';
4
+ import { agentsDir } from '../agents/files.js';
5
+
6
+ const LEFTOVER_RE = /^chatgpt-host\.json(?:\.tmp-[0-9]+)?$/;
7
+
8
+ const codeOf = (err: unknown): unknown => (err instanceof Error && 'code' in err ? err.code : undefined);
9
+
10
+ function namesIn(dir: string): string[] {
11
+ try {
12
+ return readdirSync(dir).filter((name) => LEFTOVER_RE.test(name));
13
+ } catch (err) {
14
+ if (codeOf(err) !== 'ENOENT') log.warn({ err: errMsg(err), dir }, 'org-models: could not list the old model files');
15
+ return [];
16
+ }
17
+ }
18
+
19
+ function removeOne(path: string): boolean {
20
+ try {
21
+ if (!lstatSync(path).isFile()) {
22
+ log.warn({ path }, 'org-models: left an old model file alone, it is not a regular file');
23
+ return false;
24
+ }
25
+ unlinkSync(path);
26
+ return true;
27
+ } catch (err) {
28
+ if (codeOf(err) !== 'ENOENT') log.warn({ err: errMsg(err), path }, 'org-models: could not remove an old model file');
29
+ return false;
30
+ }
31
+ }
32
+
33
+ export function removeModelLeftovers(dir = agentsDir()): number {
34
+ try {
35
+ const removed = namesIn(dir).filter((name) => removeOne(join(dir, name))).length;
36
+ log.info({ removed }, 'org-models: removed the files of the old model setup');
37
+ return removed;
38
+ } catch (err) {
39
+ log.warn({ err: errMsg(err) }, 'org-models: could not remove the files of the old model setup');
40
+ return 0;
41
+ }
42
+ }
@@ -2,7 +2,6 @@ import { errMsg, log } from '@metro-labs/core/log';
2
2
  import type { AgentKey } from '../connectors/agent-key.js';
3
3
  import { agentCall, MetroApiRefusal, type MetroApi } from '../connectors/metro-api.js';
4
4
  import { deliveredOf, refreshedConnection, type Delivered } from './delivered.js';
5
- import { importModelFile, settleModelFile, type Moved } from './model-import.js';
6
5
  import type { Connection } from './model-config.js';
7
6
 
8
7
  const MODELS_PATH = '/api/agents/models';
@@ -15,7 +14,6 @@ export interface OrgModelsDeps {
15
14
  owner: () => string | null;
16
15
  key: () => AgentKey | null;
17
16
  api: MetroApi;
18
- dir: () => string;
19
17
  pageSyncMs?: number;
20
18
  }
21
19
 
@@ -27,14 +25,6 @@ export interface SyncStatus {
27
25
 
28
26
  const identityRefused = (err: unknown): boolean => err instanceof MetroApiRefusal && err.upstreamStatus === 401 && err.code === AGENT_KEY_MISSING;
29
27
 
30
- const unrenewed = new Set<string>();
31
-
32
- function noteUnrenewed(conn: Connection): void {
33
- if (unrenewed.has(conn.id)) return;
34
- unrenewed.add(conn.id);
35
- log.warn({ connection: conn.id }, 'org-models: a Codex sign-in still read from model.json is not renewed on this box; api.metro.box renews it once the box moved its model setup to the organization');
36
- }
37
-
38
28
  export class OrgModels {
39
29
  private kept: Delivered | null = null;
40
30
  private status: SyncStatus = { enrolled: false, at: null, error: null };
@@ -100,10 +90,6 @@ export class OrgModels {
100
90
  }
101
91
 
102
92
  refresh(conn: Connection): Promise<Connection | null> {
103
- if (conn.revision < 1) {
104
- noteUnrenewed(conn);
105
- return Promise.resolve(null);
106
- }
107
93
  const newer = this.kept?.connections.find((c) => c.id === conn.id && c.revision > conn.revision);
108
94
  if (newer !== undefined) return Promise.resolve(newer);
109
95
  const slot = `${conn.id}\n${String(conn.revision)}`;
@@ -167,10 +153,6 @@ export class OrgModels {
167
153
  return;
168
154
  }
169
155
  this.status = { ...this.status, enrolled: true };
170
- const moved = await importModelFile(this.deps.api, key, this.deps.dir()).catch((err: unknown) => {
171
- log.warn({ err: errMsg(err) }, 'org-models: model.json could not be read for the move to the organization');
172
- return null;
173
- });
174
156
  try {
175
157
  const delivered = deliveredOf(await agentCall(this.deps.api, key, MODELS_PATH, {}));
176
158
  if (delivered === null) {
@@ -182,16 +164,6 @@ export class OrgModels {
182
164
  this.keep(delivered);
183
165
  } catch (err) {
184
166
  this.failed(errMsg(err), identityRefused(err));
185
- return;
186
- }
187
- if (moved !== null) this.settle(moved);
188
- }
189
-
190
- private settle(moved: Moved): void {
191
- try {
192
- if (settleModelFile(this.deps.dir(), moved)) this.notify();
193
- } catch (err) {
194
- log.warn({ err: errMsg(err) }, 'org-models: model.json could not be rewritten after the move to the organization');
195
167
  }
196
168
  }
197
169
 
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.1.0-beta.309"
2
+ "version": "0.1.0-beta.310"
3
3
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.1.0-beta.309",
2
+ "version": "0.1.0-beta.310",
3
3
  "dependencies": {
4
4
  "@anthropic-ai/claude-agent-sdk": "0.3.287",
5
5
  "@modelcontextprotocol/sdk": "^1.29.0",
@@ -1,125 +0,0 @@
1
- import { isRecord } from '@metro-labs/core/is-record';
2
- import { errMsg, log } from '@metro-labs/core/log';
3
- import { writeSecure } from '@metro-labs/core/secure-fs';
4
- import type { AgentKey } from '../connectors/agent-key.js';
5
- import { agentCall, type MetroApi } from '../connectors/metro-api.js';
6
- import { claudeLoginOf, claudeLoginsIn, isV2, modelPath, readModelFile, v3Text } from './claude-store.js';
7
- import { isProvider, type Fallback } from './model-config.js';
8
-
9
- const IMPORT_PATH = '/api/agents/models/import';
10
- let lastRefusal = '';
11
-
12
- const CODEX_FIELDS = ['clientId', 'subject', 'accountId', 'plan', 'email', 'accessToken', 'refreshToken', 'expiresAt', 'savedAt'] as const;
13
-
14
- const text = (value: unknown): string => (typeof value === 'string' ? value.trim() : '');
15
-
16
- type Row = Record<string, unknown>;
17
-
18
- interface Plan {
19
- connections: Row[];
20
- sessionOnly: Set<string>;
21
- credentialed: number;
22
- gemini: number;
23
- }
24
-
25
- function codexOf(raw: unknown): Row | null {
26
- if (!isRecord(raw) || text(raw.accessToken) === '' || text(raw.refreshToken) === '') return null;
27
- const method = text(raw.accountId) !== '' ? 'code' : text(raw.clientId) !== '' ? 'chatgpt' : '';
28
- if (method === '') return null;
29
- return { method, ...Object.fromEntries(CODEX_FIELDS.flatMap((field) => (raw[field] === undefined ? [] : [[field, raw[field]]]))) };
30
- }
31
-
32
- function credentialOf(c: Row): Row | null {
33
- if (c.provider === 'codex') {
34
- const codex = codexOf(c.codex);
35
- return codex === null ? null : { codex };
36
- }
37
- return text(c.apiKey) === '' ? null : { apiKey: text(c.apiKey) };
38
- }
39
-
40
- function addRow(plan: Plan, c: Row): void {
41
- const base = { id: text(c.id), provider: c.provider, label: text(c.label), model: text(c.model), region: text(c.region), zdr: c.zdr === true };
42
- const credential = credentialOf(c);
43
- const login = c.provider === 'anthropic' && credential === null ? claudeLoginOf(c.claude) : null;
44
- if (credential !== null) {
45
- plan.connections.push({ ...base, ...credential });
46
- plan.credentialed += 1;
47
- } else if (login !== null) plan.connections.push({ ...base, claude: { id: login.id } });
48
- else if (c.provider === 'anthropic') {
49
- plan.connections.push(base);
50
- plan.sessionOnly.add(base.id);
51
- }
52
- }
53
-
54
- function planOf(rows: Row[]): Plan {
55
- const plan: Plan = { connections: [], sessionOnly: new Set(), credentialed: 0, gemini: 0 };
56
- for (const c of rows) {
57
- if (c.provider === 'gemini') plan.gemini += 1;
58
- else if (text(c.id) !== '' && isProvider(c.provider)) addRow(plan, c);
59
- }
60
- return plan;
61
- }
62
-
63
- function fallbacksOf(raw: unknown, sent: Set<string>): Fallback[] {
64
- if (!Array.isArray(raw)) return [];
65
- return raw.flatMap((f: unknown) => {
66
- const fallback = isRecord(f) ? { connection: text(f.connection), model: text(f.model) } : null;
67
- return fallback === null || !sent.has(fallback.connection) || fallback.model === '' ? [] : [fallback];
68
- });
69
- }
70
-
71
- function routeOf(raw: Row, rows: Row[], plan: Plan, fallbacks: Fallback[]): Fallback | '' {
72
- const id = text(raw.route);
73
- const routed = rows.find((c) => text(c.id) === id);
74
- if (routed === undefined || !plan.connections.some((c) => c.id === id)) return '';
75
- const route = { connection: id, model: text(routed.model) };
76
- return plan.sessionOnly.has(id) && route.model === '' && fallbacks.length === 0 ? '' : route;
77
- }
78
-
79
- function importBody(raw: Row & { connections: unknown[] }): { body: Row | null; sent: number; gemini: number } {
80
- const rows = raw.connections.filter(isRecord);
81
- const plan = planOf(rows);
82
- const fallbacks = fallbacksOf(raw.fallbacks, new Set(plan.connections.map((c) => text(c.id))));
83
- const route = routeOf(raw, rows, plan, fallbacks);
84
- const empty = plan.credentialed === 0 && route === '' && fallbacks.length === 0;
85
- return { body: empty ? null : { connections: plan.connections, route, fallbacks }, sent: plan.credentialed, gemini: plan.gemini };
86
- }
87
-
88
- export interface Moved {
89
- sent: number;
90
- gemini: number;
91
- }
92
-
93
- function noteSkipped(raw: unknown): void {
94
- const skipped = Array.isArray(raw) ? raw.flatMap((s: unknown) => (isRecord(s) ? [{ id: text(s.id), reason: text(s.reason) }] : [])) : [];
95
- if (skipped.length > 0) log.warn({ skipped }, 'org-models: api.metro.box could not import some connections of model.json, so they are not moved');
96
- }
97
-
98
- export async function importModelFile(api: MetroApi, key: AgentKey, dir: string): Promise<Moved | null> {
99
- const raw = readModelFile(dir);
100
- if (!isV2(raw)) return null;
101
- const { body, sent, gemini } = importBody(raw);
102
- try {
103
- if (body !== null) {
104
- const answer = await agentCall(api, key, IMPORT_PATH, body);
105
- if (!isRecord(answer.ids)) throw new Error('api.metro.box answered the import without ids');
106
- noteSkipped(answer.skipped);
107
- }
108
- } catch (err) {
109
- if (errMsg(err) !== lastRefusal) log.warn({ err: errMsg(err) }, 'org-models: the model setup of this box could not move to its organization yet; it is tried again');
110
- lastRefusal = errMsg(err);
111
- return null;
112
- }
113
- lastRefusal = '';
114
- return { sent, gemini };
115
- }
116
-
117
- export function settleModelFile(dir: string, moved: Moved): boolean {
118
- const raw = readModelFile(dir);
119
- if (!isV2(raw)) return false;
120
- const logins = claudeLoginsIn(raw);
121
- writeSecure(modelPath(dir), v3Text(logins));
122
- if (moved.gemini > 0) log.info({ dropped: moved.gemini }, 'org-models: Gemini connections are no longer supported, so they were dropped');
123
- log.info({ moved: moved.sent, claudeLogins: logins.length }, 'org-models: the model setup of this box moved to its organization; model.json keeps only its Claude logins');
124
- return true;
125
- }
@@ -1,79 +0,0 @@
1
- import { isRecord } from '@metro-labs/core/is-record';
2
- import { claudeLoginOf } from './claude-store.js';
3
- import { isProvider, LOCAL_PREFIX, loginConnection, sessionConnection, type Account, type CodexAccess, type Connection, type Fallback, type ModelSetup, type Provider } from './model-config.js';
4
-
5
- const MAX_CONNECTIONS = 20;
6
- const MAX_FALLBACKS = 8;
7
- const NEW_CLIENT = 'dynamic_agent_client';
8
- const NO_ACCOUNT: Account = { email: null, plan: null, method: null };
9
-
10
- type Row = Record<string, unknown>;
11
-
12
- const text = (value: unknown): string => (typeof value === 'string' ? value.trim() : '');
13
- const maybe = (value: unknown): string | null => (typeof value === 'string' && value !== '' ? value : null);
14
-
15
- const expiryOf = (value: unknown): number => (typeof value === 'number' && Number.isFinite(value) ? value : 0);
16
-
17
- function codexOf(raw: unknown): CodexAccess | null {
18
- if (!isRecord(raw) || text(raw.accessToken) === '' || text(raw.refreshToken) === '') return null;
19
- const accountId = maybe(text(raw.accountId));
20
- const clientId = text(raw.clientId);
21
- if (accountId === null && (clientId === '' || clientId === NEW_CLIENT)) return null;
22
- return { method: accountId === null ? 'chatgpt' : 'code', accessToken: text(raw.accessToken), expiresAt: expiryOf(raw.expiresAt), accountId };
23
- }
24
-
25
- function accountOf(raw: unknown, codex: CodexAccess | null): Account {
26
- if (codex === null || !isRecord(raw)) return NO_ACCOUNT;
27
- return { email: maybe(raw.email), plan: codex.method === 'code' ? maybe(raw.plan) : null, method: codex.method };
28
- }
29
-
30
- function ownConnection(row: Row, id: string, provider: Provider): Connection {
31
- const codex = provider === 'codex' ? codexOf(row.codex) : null;
32
- const apiKey = provider === 'codex' ? '' : text(row.apiKey);
33
- const label = text(row.label);
34
- return {
35
- id,
36
- provider,
37
- label: label === '' ? provider : label,
38
- model: text(row.model),
39
- apiKey,
40
- region: text(row.region),
41
- zdr: row.zdr === true,
42
- revision: 0,
43
- codex,
44
- claude: null,
45
- account: accountOf(row.codex, codex),
46
- signedOut: codex === null && apiKey === '',
47
- };
48
- }
49
-
50
- function connectionOf(row: Row): Connection | null {
51
- const id = text(row.id);
52
- if (id === '' || id.startsWith(LOCAL_PREFIX) || !isProvider(row.provider)) return null;
53
- if (row.provider !== 'anthropic' || text(row.apiKey) !== '') return ownConnection(row, id, row.provider);
54
- const login = claudeLoginOf(row.claude);
55
- return login === null ? sessionConnection() : loginConnection(login.id, login);
56
- }
57
-
58
- function fallbacksOf(raw: unknown, linked: Map<string, Connection>): Fallback[] {
59
- if (!Array.isArray(raw)) return [];
60
- const known = raw.flatMap((f: unknown) => {
61
- const conn = isRecord(f) ? linked.get(text(f.connection)) : undefined;
62
- const model = isRecord(f) ? text(f.model) : '';
63
- return conn === undefined || model === '' ? [] : [{ connection: conn.id, model }];
64
- });
65
- return known.filter((f, at) => known.findIndex((o) => o.connection === f.connection && o.model === f.model) === at).slice(0, MAX_FALLBACKS);
66
- }
67
-
68
- export function v2Setup(raw: Row & { connections: unknown[] }): ModelSetup {
69
- const rows = raw.connections.filter((c): c is Row => isRecord(c) && (isProvider(c.provider) || c.provider === 'gemini')).slice(0, MAX_CONNECTIONS);
70
- const linked = new Map(rows.flatMap((row) => {
71
- const conn = connectionOf(row);
72
- return conn === null ? [] : [[text(row.id), conn] as const];
73
- }));
74
- const routed = rows.find((row) => text(row.id) === text(raw.route)) ?? rows[0];
75
- const head = routed === undefined ? undefined : linked.get(text(routed.id));
76
- const primary = routed === undefined || head === undefined ? null : { connection: head.id, model: text(routed.model) };
77
- const connections = [...linked.values()].filter((c, at, all) => all.findIndex((o) => o.id === c.id) === at);
78
- return { primary, fallbacks: fallbacksOf(raw.fallbacks, linked), connections, source: null };
79
- }