@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.
- package/package.json +1 -1
- package/runtime/marketplace/plugin/.claude-plugin/plugin.json +1 -1
- package/runtime/marketplace/plugin/orchestrator.md +6 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/api.ts +2 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/files.ts +8 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/session.ts +3 -2
- package/runtime/node_modules/@metro-labs/daemon/src/claude/setup.ts +28 -13
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/bedrock.ts +3 -3
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex.ts +19 -19
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/effort.ts +70 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/forward.ts +9 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gateway.ts +83 -51
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-translate.ts +3 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini.ts +35 -18
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-api.ts +66 -238
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-config.ts +202 -141
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-signin.ts +195 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-store.ts +70 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/provider-models.ts +3 -3
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/served.ts +1 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/usage.ts +25 -23
- 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 {
|
|
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:
|
|
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:
|
|
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,4 @@
|
|
|
1
|
-
|
|
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<
|
|
28
|
-
const tallies = new Map<
|
|
27
|
+
const latest = new Map<Key, Reported>();
|
|
28
|
+
const tallies = new Map<Key, Tally>();
|
|
29
29
|
|
|
30
|
-
export const noteUsage = (
|
|
31
|
-
latest.set(
|
|
30
|
+
export const noteUsage = (key: Key, usage: Reported): void => {
|
|
31
|
+
latest.set(key, usage);
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
-
export function usageSeen():
|
|
35
|
-
const out:
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
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 = (
|
|
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(
|
|
61
|
-
const so = tallies.get(
|
|
62
|
-
tallies.set(
|
|
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
|
|
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.
|
|
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(
|
|
216
|
-
const usage =
|
|
217
|
-
if (usage !== null) noteUsage(
|
|
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)}`;
|
package/runtime/runtime.json
CHANGED