@stage-labs/metro 0.1.0-beta.164 → 0.1.0-beta.166

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (22) hide show
  1. package/package.json +1 -1
  2. package/runtime/marketplace/plugin/.claude-plugin/plugin.json +1 -1
  3. package/runtime/marketplace/plugin/orchestrator.md +6 -0
  4. package/runtime/node_modules/@metro-labs/daemon/src/claude/api.ts +2 -0
  5. package/runtime/node_modules/@metro-labs/daemon/src/claude/files.ts +8 -0
  6. package/runtime/node_modules/@metro-labs/daemon/src/claude/session.ts +3 -2
  7. package/runtime/node_modules/@metro-labs/daemon/src/claude/setup.ts +28 -13
  8. package/runtime/node_modules/@metro-labs/daemon/src/gateway/bedrock.ts +3 -3
  9. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex.ts +19 -19
  10. package/runtime/node_modules/@metro-labs/daemon/src/gateway/effort.ts +70 -0
  11. package/runtime/node_modules/@metro-labs/daemon/src/gateway/forward.ts +9 -0
  12. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gateway.ts +83 -51
  13. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-translate.ts +3 -0
  14. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini.ts +35 -18
  15. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-api.ts +66 -238
  16. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-config.ts +202 -141
  17. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-signin.ts +195 -0
  18. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-store.ts +70 -0
  19. package/runtime/node_modules/@metro-labs/daemon/src/gateway/provider-models.ts +3 -3
  20. package/runtime/node_modules/@metro-labs/daemon/src/gateway/served.ts +1 -0
  21. package/runtime/node_modules/@metro-labs/daemon/src/gateway/usage.ts +25 -23
  22. package/runtime/runtime.json +1 -1
@@ -0,0 +1,70 @@
1
+ import type { IncomingMessage } from 'node:http';
2
+ import { ApiError } from '@metro-labs/http/api-error';
3
+ import { CodexAuthError } from './codex-auth.js';
4
+ import { GeminiAuthError } from './gemini-auth.js';
5
+ import { GatewayError } from './forward.js';
6
+ import { lastServed } from './served.js';
7
+ import { usageSeen } from './usage.js';
8
+ import { ModelConfigError, connectionOf, publicModelConfig, routedConnection, type Connection, type ModelConfig, type Provider } from './model-config.js';
9
+ import type { SetupDeps } from '../claude/setup.js';
10
+
11
+ export interface ModelApiDeps {
12
+ authorize: (subject: string) => void;
13
+ read?: () => ModelConfig;
14
+ write?: (cfg: ModelConfig) => void;
15
+ issuer?: string;
16
+ fetchImpl?: typeof fetch;
17
+ codexHome?: string;
18
+ codexBase?: string;
19
+ geminiAuthBase?: string;
20
+ geminiTokenBase?: string;
21
+ geminiUserBase?: string;
22
+ geminiBase?: string;
23
+ openrouterBase?: string;
24
+ anthropicBase?: string;
25
+ bedrockControlBase?: string;
26
+ setup?: SetupDeps;
27
+ }
28
+
29
+ export interface Store {
30
+ read: () => ModelConfig;
31
+ write: (cfg: ModelConfig) => void;
32
+ }
33
+
34
+ export type Handler = (req: IncomingMessage, deps: ModelApiDeps, store: Store) => Promise<unknown>;
35
+
36
+ export interface Route {
37
+ method: 'GET' | 'POST';
38
+ run: Handler;
39
+ }
40
+
41
+ export const BODY_MAX = 16 * 1024;
42
+ export const CREDITS_TTL_MS = 5 * 60_000;
43
+
44
+ export const settingsBody = (cfg: ModelConfig): Record<string, unknown> => ({
45
+ ...publicModelConfig(cfg),
46
+ lastServed: lastServed(),
47
+ usage: usageSeen(),
48
+ });
49
+
50
+ export function asApiError(err: unknown): never {
51
+ if (err instanceof ModelConfigError || err instanceof CodexAuthError || err instanceof GeminiAuthError) throw new ApiError(err.message, 400);
52
+ if (err instanceof GatewayError) throw new ApiError(err.message, err.status >= 400 && err.status < 500 ? 400 : 502);
53
+ throw err;
54
+ }
55
+
56
+ export const askedConnection = (req: IncomingMessage): string => new URL(req.url ?? '', 'http://metro').searchParams.get('connection') ?? '';
57
+
58
+ export function connectionFor(cfg: ModelConfig, req: IncomingMessage, provider: Provider): Connection {
59
+ const asked = askedConnection(req);
60
+ if (asked !== '') {
61
+ const found = connectionOf(cfg, asked);
62
+ if (found?.provider !== provider) throw new ApiError('no such connection', 404);
63
+ return found;
64
+ }
65
+ const routed = routedConnection(cfg);
66
+ if (routed?.provider === provider) return routed;
67
+ const first = cfg.connections.find((c) => c.provider === provider);
68
+ if (first === undefined) throw new ApiError(`no ${provider} connection yet`, 400);
69
+ return first;
70
+ }
@@ -1,6 +1,6 @@
1
1
  import { isRecord } from '@metro-labs/core/is-record';
2
2
  import { GatewayError } from './forward.js';
3
- import type { AnthropicSettings, BedrockSettings } from './model-config.js';
3
+ import type { Connection } from './model-config.js';
4
4
 
5
5
  export const ANTHROPIC_API = 'https://api.anthropic.com';
6
6
  const ANTHROPIC_VERSION = '2023-06-01';
@@ -20,7 +20,7 @@ export const KNOWN_CLAUDE: ProviderModel[] = [
20
20
 
21
21
  const str = (value: unknown): string => (typeof value === 'string' ? value : '');
22
22
 
23
- export async function anthropicModels(settings: AnthropicSettings, base = ANTHROPIC_API, fetchImpl: typeof fetch = fetch): Promise<ProviderModel[]> {
23
+ export async function anthropicModels(settings: Connection, base = ANTHROPIC_API, fetchImpl: typeof fetch = fetch): Promise<ProviderModel[]> {
24
24
  if (settings.apiKey === '') return KNOWN_CLAUDE;
25
25
  const res = await fetchImpl(`${base}/v1/models?limit=${String(LIST_MAX)}`, {
26
26
  headers: { 'x-api-key': settings.apiKey, 'anthropic-version': ANTHROPIC_VERSION, accept: 'application/json' },
@@ -37,7 +37,7 @@ export async function anthropicModels(settings: AnthropicSettings, base = ANTHRO
37
37
 
38
38
  export const bedrockControlBase = (region: string): string => `https://bedrock.${region}.amazonaws.com`;
39
39
 
40
- export async function bedrockModels(settings: BedrockSettings, base?: string, fetchImpl: typeof fetch = fetch): Promise<ProviderModel[]> {
40
+ export async function bedrockModels(settings: Connection, base?: string, fetchImpl: typeof fetch = fetch): Promise<ProviderModel[]> {
41
41
  if (settings.region === '') throw new GatewayError(400, 'invalid_request_error', 'Bedrock needs a region before its models can be listed');
42
42
  if (settings.apiKey === '') throw new GatewayError(400, 'invalid_request_error', 'Bedrock needs an API key before its models can be listed');
43
43
  const res = await fetchImpl(`${base ?? bedrockControlBase(settings.region)}/inference-profiles?maxResults=${String(LIST_MAX)}`, {
@@ -1,4 +1,5 @@
1
1
  export interface Served {
2
+ connection: string;
2
3
  provider: string;
3
4
  model: string;
4
5
  at: string;
@@ -1,4 +1,4 @@
1
- export type UsageProvider = 'anthropic' | 'codex' | 'openrouter' | 'bedrock' | 'gemini';
1
+ type Key = string;
2
2
 
3
3
  export interface UsageWindow {
4
4
  label: string;
@@ -24,27 +24,29 @@ export interface ProviderUsage {
24
24
 
25
25
  type Reported = Omit<ProviderUsage, 'tally'>;
26
26
 
27
- const latest = new Map<UsageProvider, Reported>();
28
- const tallies = new Map<UsageProvider, Tally>();
27
+ const latest = new Map<Key, Reported>();
28
+ const tallies = new Map<Key, Tally>();
29
29
 
30
- export const noteUsage = (provider: UsageProvider, usage: Reported): void => {
31
- latest.set(provider, usage);
30
+ export const noteUsage = (key: Key, usage: Reported): void => {
31
+ latest.set(key, usage);
32
32
  };
33
33
 
34
- export function usageSeen(): Partial<Record<UsageProvider, ProviderUsage>> {
35
- const out: Partial<Record<UsageProvider, ProviderUsage>> = {};
36
- const providers = new Set<UsageProvider>([...latest.keys(), ...tallies.keys()]);
37
- for (const provider of providers) {
38
- const reported = latest.get(provider);
39
- const tally = tallies.get(provider) ?? null;
40
- out[provider] = reported === undefined
41
- ? { windows: [], note: null, at: tally?.since ?? new Date().toISOString(), tally }
42
- : { ...reported, tally };
34
+ export function usageSeen(): Record<Key, ProviderUsage> {
35
+ const out: Record<Key, ProviderUsage> = {};
36
+ for (const key of new Set<Key>([...latest.keys(), ...tallies.keys()])) {
37
+ const reported = latest.get(key);
38
+ const tally = tallies.get(key) ?? null;
39
+ out[key] = reported === undefined ? { windows: [], note: null, at: tally?.since ?? new Date().toISOString(), tally } : { ...reported, tally };
43
40
  }
44
41
  return out;
45
42
  }
46
43
 
47
- export const usageOf = (provider: UsageProvider): Reported | undefined => latest.get(provider);
44
+ export const usageOf = (key: Key): Reported | undefined => latest.get(key);
45
+
46
+ export const forgetOne = (key: Key): void => {
47
+ latest.delete(key);
48
+ tallies.delete(key);
49
+ };
48
50
 
49
51
  export const forgetUsage = (): void => {
50
52
  latest.clear();
@@ -57,9 +59,9 @@ export interface Counted {
57
59
  cached: number;
58
60
  }
59
61
 
60
- export function tallyTokens(provider: UsageProvider, counted: Counted, now = new Date()): void {
61
- const so = tallies.get(provider) ?? { requests: 0, input: 0, output: 0, cached: 0, since: now.toISOString() };
62
- tallies.set(provider, {
62
+ export function tallyTokens(key: Key, counted: Counted, now = new Date()): void {
63
+ const so = tallies.get(key) ?? { requests: 0, input: 0, output: 0, cached: 0, since: now.toISOString() };
64
+ tallies.set(key, {
63
65
  requests: so.requests + 1,
64
66
  input: so.input + counted.input,
65
67
  output: so.output + counted.output,
@@ -75,7 +77,7 @@ export class UsageScanner {
75
77
  private readonly max = { input: 0, output: 0, read: 0, creation: 0 };
76
78
  private touched = false;
77
79
 
78
- constructor(private readonly provider: UsageProvider) {}
80
+ constructor(private readonly key: Key) {}
79
81
 
80
82
  private scan(text: string): void {
81
83
  for (const hit of text.matchAll(TOKEN_FIELD)) {
@@ -104,7 +106,7 @@ export class UsageScanner {
104
106
  if (this.carry !== '') this.scan(this.carry);
105
107
  this.carry = '';
106
108
  if (!this.touched) return;
107
- tallyTokens(this.provider, { input: this.max.input, output: this.max.output, cached: this.max.read + this.max.creation }, now);
109
+ tallyTokens(this.key, { input: this.max.input, output: this.max.output, cached: this.max.read + this.max.creation }, now);
108
110
  this.touched = false;
109
111
  }
110
112
  }
@@ -212,9 +214,9 @@ export function codexUsage(headers: Headers, now = new Date()): Reported | null
212
214
  return { windows, note: reached === null || reached === '' ? null : reached.replaceAll('_', ' '), at: now.toISOString() };
213
215
  }
214
216
 
215
- export function noteUsageHeaders(provider: 'anthropic' | 'codex', headers: Headers, now = new Date()): void {
216
- const usage = provider === 'anthropic' ? anthropicUsage(headers, now) : codexUsage(headers, now);
217
- if (usage !== null) noteUsage(provider, usage);
217
+ export function noteUsageHeaders(kind: 'anthropic' | 'codex', key: Key, headers: Headers, now = new Date()): void {
218
+ const usage = kind === 'anthropic' ? anthropicUsage(headers, now) : codexUsage(headers, now);
219
+ if (usage !== null) noteUsage(key, usage);
218
220
  }
219
221
 
220
222
  const dollars = (value: number): string => `$${value.toFixed(2)}`;
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.1.0-beta.164"
2
+ "version": "0.1.0-beta.166"
3
3
  }