@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 +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/boot/boot.ts +5 -2
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/claude-store.ts +21 -17
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/delivered.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/effective.ts +2 -6
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/fallback.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-leftovers.ts +42 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/org-models.ts +0 -28
- package/runtime/runtime.json +1 -1
- package/runtime/sdk-runner/runner.json +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-import.ts +0 -125
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-v2.ts +0 -79
package/package.json
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
25
|
+
const modelPath = (dir: string): string => join(dir, MODEL_FILE);
|
|
25
26
|
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
53
|
-
|
|
54
|
-
|
|
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 {
|
|
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
|
|
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
|
|
package/runtime/runtime.json
CHANGED
|
@@ -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
|
-
}
|