@stage-labs/metro 0.1.0-beta.145 → 0.1.0-beta.147
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/route.js +1 -1
- package/package.json +1 -1
- package/runtime/marketplace/plugin/.claude-plugin/plugin.json +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/agents/admin.ts +0 -4
- package/runtime/node_modules/@metro-labs/daemon/src/agents/api.ts +3 -32
- package/runtime/node_modules/@metro-labs/daemon/src/agents/file-admin.ts +2 -16
- package/runtime/node_modules/@metro-labs/daemon/src/agents/keys.ts +0 -5
- package/runtime/node_modules/@metro-labs/daemon/src/boot/boot.ts +1 -8
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gateway.ts +28 -10
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-auth.ts +160 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-setup.ts +67 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-stream.ts +148 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-translate.ts +190 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini.ts +196 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-api.ts +43 -2
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-config.ts +31 -6
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/usage.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/index.ts +0 -5
- package/runtime/node_modules/@metro-labs/daemon/src/routes/local-mode.ts +0 -10
- package/runtime/runtime.json +1 -1
|
@@ -3,8 +3,9 @@ import { readJson, writeSecure } from '@metro-labs/core/secure-fs';
|
|
|
3
3
|
import { agentsDir } from '../agents/files.js';
|
|
4
4
|
import { isRecord } from '@metro-labs/core/is-record';
|
|
5
5
|
import type { CodexTokens } from './codex-auth.js';
|
|
6
|
+
import { tokensFromDisk as geminiTokensFromDisk, type GeminiTokens } from './gemini-auth.js';
|
|
6
7
|
|
|
7
|
-
export const PROVIDERS = ['anthropic', 'bedrock', 'openrouter', 'codex'] as const;
|
|
8
|
+
export const PROVIDERS = ['anthropic', 'bedrock', 'openrouter', 'codex', 'gemini'] as const;
|
|
8
9
|
export type Provider = (typeof PROVIDERS)[number];
|
|
9
10
|
|
|
10
11
|
export interface AnthropicSettings {
|
|
@@ -29,6 +30,11 @@ export interface CodexSettings {
|
|
|
29
30
|
auth: CodexTokens | null;
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
export interface GeminiSettings {
|
|
34
|
+
model: string;
|
|
35
|
+
auth: GeminiTokens | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
32
38
|
export interface ModelConfig {
|
|
33
39
|
version: 1;
|
|
34
40
|
provider: Provider;
|
|
@@ -36,6 +42,7 @@ export interface ModelConfig {
|
|
|
36
42
|
bedrock: BedrockSettings;
|
|
37
43
|
openrouter: OpenRouterSettings;
|
|
38
44
|
codex: CodexSettings;
|
|
45
|
+
gemini: GeminiSettings;
|
|
39
46
|
}
|
|
40
47
|
|
|
41
48
|
export interface Route {
|
|
@@ -47,7 +54,7 @@ export class ModelConfigError extends Error {}
|
|
|
47
54
|
|
|
48
55
|
export const MODEL_FILE = 'model.json';
|
|
49
56
|
const MAX_FIELD = 512;
|
|
50
|
-
const PREFIX_RE = /^(anthropic|bedrock|openrouter|codex):(.+)$/;
|
|
57
|
+
const PREFIX_RE = /^(anthropic|bedrock|openrouter|codex|gemini):(.+)$/;
|
|
51
58
|
const SMALL_RE = /haiku/i;
|
|
52
59
|
|
|
53
60
|
const empty = (): ModelConfig => ({
|
|
@@ -57,6 +64,7 @@ const empty = (): ModelConfig => ({
|
|
|
57
64
|
bedrock: { region: '', apiKey: '', model: '' },
|
|
58
65
|
openrouter: { apiKey: '', model: '', zdr: false },
|
|
59
66
|
codex: { model: '', auth: null },
|
|
67
|
+
gemini: { model: '', auth: null },
|
|
60
68
|
});
|
|
61
69
|
|
|
62
70
|
const isProvider = (value: unknown): value is Provider =>
|
|
@@ -89,6 +97,7 @@ export function parseModelConfig(raw: unknown): ModelConfig {
|
|
|
89
97
|
const bedrock = isRecord(raw.bedrock) ? raw.bedrock : {};
|
|
90
98
|
const openrouter = isRecord(raw.openrouter) ? raw.openrouter : {};
|
|
91
99
|
const codex = isRecord(raw.codex) ? raw.codex : {};
|
|
100
|
+
const gemini = isRecord(raw.gemini) ? raw.gemini : {};
|
|
92
101
|
return {
|
|
93
102
|
version: 1,
|
|
94
103
|
provider: isProvider(raw.provider) ? raw.provider : 'anthropic',
|
|
@@ -96,6 +105,7 @@ export function parseModelConfig(raw: unknown): ModelConfig {
|
|
|
96
105
|
bedrock: { region: text(bedrock.region), apiKey: text(bedrock.apiKey), model: text(bedrock.model) },
|
|
97
106
|
openrouter: { apiKey: text(openrouter.apiKey), model: text(openrouter.model), zdr: openrouter.zdr === true },
|
|
98
107
|
codex: { model: text(codex.model), auth: tokensFromDisk(codex.auth) },
|
|
108
|
+
gemini: { model: text(gemini.model), auth: geminiTokensFromDisk(gemini.auth) },
|
|
99
109
|
};
|
|
100
110
|
}
|
|
101
111
|
|
|
@@ -130,6 +140,7 @@ export function applyModelUpdate(cfg: ModelConfig, patch: unknown): ModelConfig
|
|
|
130
140
|
const bedrock = isRecord(patch.bedrock) ? patch.bedrock : {};
|
|
131
141
|
const openrouter = isRecord(patch.openrouter) ? patch.openrouter : {};
|
|
132
142
|
const codex = isRecord(patch.codex) ? patch.codex : {};
|
|
143
|
+
const gemini = isRecord(patch.gemini) ? patch.gemini : {};
|
|
133
144
|
return {
|
|
134
145
|
version: 1,
|
|
135
146
|
provider,
|
|
@@ -148,11 +159,14 @@ export function applyModelUpdate(cfg: ModelConfig, patch: unknown): ModelConfig
|
|
|
148
159
|
zdr: flag(openrouter, 'zdr', cfg.openrouter.zdr, 'OpenRouter zero data retention'),
|
|
149
160
|
},
|
|
150
161
|
codex: { model: field(codex, 'model', cfg.codex.model, 'Codex model'), auth: cfg.codex.auth },
|
|
162
|
+
gemini: { model: field(gemini, 'model', cfg.gemini.model, 'Gemini model'), auth: cfg.gemini.auth },
|
|
151
163
|
};
|
|
152
164
|
}
|
|
153
165
|
|
|
154
166
|
export const setCodexAuth = (cfg: ModelConfig, auth: CodexTokens | null): ModelConfig => ({ ...cfg, codex: { ...cfg.codex, auth } });
|
|
155
167
|
|
|
168
|
+
export const setGeminiAuth = (cfg: ModelConfig, auth: GeminiTokens | null): ModelConfig => ({ ...cfg, gemini: { ...cfg.gemini, auth } });
|
|
169
|
+
|
|
156
170
|
const CHECKS: Record<Provider, [(cfg: ModelConfig) => boolean, string][]> = {
|
|
157
171
|
anthropic: [],
|
|
158
172
|
bedrock: [
|
|
@@ -167,6 +181,10 @@ const CHECKS: Record<Provider, [(cfg: ModelConfig) => boolean, string][]> = {
|
|
|
167
181
|
[(cfg) => cfg.codex.auth === null, 'Codex is not connected: sign in with ChatGPT on the Model page.'],
|
|
168
182
|
[(cfg) => cfg.codex.model === '', 'Codex needs a model id: choose one on the Model page.'],
|
|
169
183
|
],
|
|
184
|
+
gemini: [
|
|
185
|
+
[(cfg) => cfg.gemini.auth === null, 'Gemini is not connected: sign in with Google on the Model page.'],
|
|
186
|
+
[(cfg) => cfg.gemini.model === '', 'Gemini needs a model id: choose one on the Model page.'],
|
|
187
|
+
],
|
|
170
188
|
};
|
|
171
189
|
|
|
172
190
|
export function notReady(cfg: ModelConfig, provider: Provider = cfg.provider): string | null {
|
|
@@ -175,6 +193,7 @@ export function notReady(cfg: ModelConfig, provider: Provider = cfg.provider): s
|
|
|
175
193
|
|
|
176
194
|
export function publicModelConfig(cfg: ModelConfig): Record<string, unknown> {
|
|
177
195
|
const auth = cfg.codex.auth;
|
|
196
|
+
const gemini = cfg.gemini.auth;
|
|
178
197
|
return {
|
|
179
198
|
provider: cfg.provider,
|
|
180
199
|
ready: notReady(cfg) === null,
|
|
@@ -183,16 +202,22 @@ export function publicModelConfig(cfg: ModelConfig): Record<string, unknown> {
|
|
|
183
202
|
bedrock: { region: cfg.bedrock.region, model: cfg.bedrock.model, hasKey: cfg.bedrock.apiKey !== '' },
|
|
184
203
|
openrouter: { model: cfg.openrouter.model, hasKey: cfg.openrouter.apiKey !== '', zdr: cfg.openrouter.zdr },
|
|
185
204
|
codex: { model: cfg.codex.model, signedIn: auth !== null, account: auth?.email ?? null, plan: auth?.plan ?? null },
|
|
205
|
+
gemini: { model: cfg.gemini.model, signedIn: gemini !== null, account: gemini?.email ?? null, plan: gemini?.tier ?? null },
|
|
186
206
|
};
|
|
187
207
|
}
|
|
188
208
|
|
|
189
209
|
export const isSmallModel = (requested: string): boolean => SMALL_RE.test(requested);
|
|
190
210
|
|
|
211
|
+
const DEFAULTS: Record<Provider, (requested: string, cfg: ModelConfig) => string> = {
|
|
212
|
+
openrouter: (requested, cfg) => (requested.includes('/') ? requested : cfg.openrouter.model),
|
|
213
|
+
bedrock: (requested, cfg) => (cfg.bedrock.model === '' ? requested : cfg.bedrock.model),
|
|
214
|
+
codex: (requested, cfg) => (requested.startsWith('gpt-') ? requested : cfg.codex.model),
|
|
215
|
+
gemini: (requested, cfg) => (requested.startsWith('gemini-') ? requested : cfg.gemini.model),
|
|
216
|
+
anthropic: (requested, cfg) => (cfg.anthropic.model === '' || isSmallModel(requested) ? requested : cfg.anthropic.model),
|
|
217
|
+
};
|
|
218
|
+
|
|
191
219
|
function defaultModelFor(provider: Provider, requested: string, cfg: ModelConfig): string {
|
|
192
|
-
|
|
193
|
-
if (provider === 'bedrock') return cfg.bedrock.model === '' ? requested : cfg.bedrock.model;
|
|
194
|
-
if (provider === 'codex') return requested.startsWith('gpt-') ? requested : cfg.codex.model;
|
|
195
|
-
return cfg.anthropic.model === '' || isSmallModel(requested) ? requested : cfg.anthropic.model;
|
|
220
|
+
return DEFAULTS[provider](requested, cfg);
|
|
196
221
|
}
|
|
197
222
|
|
|
198
223
|
export function resolveRoute(requested: string, cfg: ModelConfig): Route {
|
|
@@ -138,11 +138,6 @@ export function agentLiveness(): Map<string, AgentLiveness> {
|
|
|
138
138
|
return activeRegistry?.liveness() ?? new Map<string, AgentLiveness>();
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
export async function closeAgentSession(agentId: string): Promise<boolean> {
|
|
142
|
-
const scopeKey = sessionScopeKey({ kind: 'agent', agentId });
|
|
143
|
-
return (await activeRegistry?.closeScope(scopeKey)) ?? false;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
141
|
export async function createMetroMcp(): Promise<{
|
|
147
142
|
httpHandler: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
148
143
|
startInbound: () => void;
|
|
@@ -43,7 +43,6 @@ import {
|
|
|
43
43
|
localListAgents,
|
|
44
44
|
localOwnedAgentOrThrow,
|
|
45
45
|
localOwner,
|
|
46
|
-
localResetAgentKey,
|
|
47
46
|
readLocalAgentFile,
|
|
48
47
|
} from '../agents/file-admin.js';
|
|
49
48
|
import { readModelConfig } from '../gateway/model-config.js';
|
|
@@ -54,8 +53,6 @@ export interface LocalModeDeps {
|
|
|
54
53
|
reloadAgents: () => Promise<void>;
|
|
55
54
|
restart: () => void;
|
|
56
55
|
stop: () => void;
|
|
57
|
-
closeAgentSession: (id: string) => Promise<boolean>;
|
|
58
|
-
restartClaudeSession: () => boolean;
|
|
59
56
|
gatherAccounts: AgentApiDeps['gatherAccounts'];
|
|
60
57
|
capabilities: AgentApiDeps['capabilities'];
|
|
61
58
|
liveness: AgentApiDeps['liveness'];
|
|
@@ -89,13 +86,6 @@ function agentApi(deps: LocalModeDeps): AgentApiDeps {
|
|
|
89
86
|
attachSessions: attachSessions(deps),
|
|
90
87
|
listAgents: localListAgents,
|
|
91
88
|
deleteAgent: localDeleteAgent,
|
|
92
|
-
resetKey: async (subject, id) => {
|
|
93
|
-
const reset = await localResetAgentKey(subject, id);
|
|
94
|
-
const closed = await deps.closeAgentSession(id);
|
|
95
|
-
const restarted = deps.restartClaudeSession();
|
|
96
|
-
log.info({ agent: reset.name, id, sessionClosed: closed, claudeRestarted: restarted }, 'local: key rotated');
|
|
97
|
-
return reset;
|
|
98
|
-
},
|
|
99
89
|
gatherAccounts: deps.gatherAccounts,
|
|
100
90
|
capabilities: deps.capabilities,
|
|
101
91
|
attachable: ATTACHABLE.filter((s) => s !== 'webhook'),
|
package/runtime/runtime.json
CHANGED