@stage-labs/metro 0.1.0-beta.323 → 0.1.0-beta.324
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 +1 -3
- package/runtime/node_modules/@metro-labs/daemon/src/claude/session.ts +2 -3
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-api.ts +4 -15
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/org-models.ts +1 -33
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/relay.ts +26 -7
- package/runtime/node_modules/@metro-labs/daemon/src/routes/local-mode.ts +1 -1
- package/runtime/node_modules/@metro-labs/inference/package.json +3 -4
- package/runtime/runtime.json +1 -1
- package/runtime/sdk-runner/runner.json +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/box-gateway.ts +0 -35
- package/runtime/node_modules/@metro-labs/inference/src/usage-refresh.ts +0 -51
package/package.json
CHANGED
|
@@ -58,9 +58,8 @@ import type { StationName } from '@metro-labs/core/station-names';
|
|
|
58
58
|
import { startUploadReaper } from '../files/upload-store.js';
|
|
59
59
|
import { startAttachReaper } from '../files/attach-reaper.js';
|
|
60
60
|
import { startCloudWatchPublisher } from '../server/cloudwatch.js';
|
|
61
|
-
import { watchFallbackUsage } from '@metro-labs/inference/usage-refresh';
|
|
62
61
|
import { startOrgModels, stopOrgModels } from '../gateway/org-models.js';
|
|
63
|
-
import {
|
|
62
|
+
import { watchModelChanges } from '../gateway/effective.js';
|
|
64
63
|
import { removeModelLeftovers } from '../gateway/model-leftovers.js';
|
|
65
64
|
import { readAgentKey } from '../connectors/agent-key.js';
|
|
66
65
|
import { METRO_API } from '../connectors/metro-api.js';
|
|
@@ -180,7 +179,6 @@ installBearerSessions(agentsDir(), localOwner);
|
|
|
180
179
|
});
|
|
181
180
|
watchConnectorTools();
|
|
182
181
|
startCloudWatchPublisher();
|
|
183
|
-
watchFallbackUsage(() => effectiveSetup());
|
|
184
182
|
announceLocalEndpoint();
|
|
185
183
|
tunnel?.start();
|
|
186
184
|
log.info(
|
|
@@ -11,12 +11,11 @@ import { isRecord } from '@metro-labs/core/is-record';
|
|
|
11
11
|
import { readJson, writeJson } from '@metro-labs/core/secure-fs';
|
|
12
12
|
import { METRO_VERSION } from '@metro-labs/core/version';
|
|
13
13
|
import { agentsDir, listAgentFiles } from '../agents/files.js';
|
|
14
|
-
import { primaryRoute, type ModelSetup } from '@metro-labs/inference/model-config';
|
|
14
|
+
import { primaryRoute, setupNotReady, type ModelSetup } from '@metro-labs/inference/model-config';
|
|
15
15
|
import { effectiveSetup } from '../gateway/effective.js';
|
|
16
16
|
import { inSessionScope } from './memory.js';
|
|
17
17
|
import { runningAsRoot } from '../metro-user/privilege.js';
|
|
18
18
|
import { agentUser, agentUserExpected, agentViewDir, asAgent, claudeHome } from '../agent-user/user.js';
|
|
19
|
-
import { routeReadiness } from '../gateway/box-gateway.js';
|
|
20
19
|
|
|
21
20
|
function sessionEnv(): Record<string, string> {
|
|
22
21
|
const user = agentUser();
|
|
@@ -134,7 +133,7 @@ const NO_MODEL = "the organization routes this agent to no model yet: choose one
|
|
|
134
133
|
function credentialReady(deps: SessionDeps): string | null {
|
|
135
134
|
try {
|
|
136
135
|
const primary = primaryRoute((deps.model ?? (() => effectiveSetup()))());
|
|
137
|
-
return primary === null ? NO_MODEL :
|
|
136
|
+
return primary === null ? NO_MODEL : setupNotReady(primary);
|
|
138
137
|
} catch (err) {
|
|
139
138
|
return `the model setup is not readable (${errMsg(err)})`;
|
|
140
139
|
}
|
|
@@ -4,18 +4,14 @@ import { ApiError } from '@metro-labs/http/api-error';
|
|
|
4
4
|
import { apiFailure, apiSession, cors, sendJson } from '@metro-labs/http/api-http';
|
|
5
5
|
import { effectiveSetup } from './effective.js';
|
|
6
6
|
import { chainStatus } from '@metro-labs/inference/fallback';
|
|
7
|
-
import { helperSetup, primaryRoute, type Connection, type ModelSetup } from '@metro-labs/inference/model-config';
|
|
8
|
-
import { routeReadiness } from './box-gateway.js';
|
|
7
|
+
import { helperSetup, primaryRoute, setupNotReady, type Connection, type ModelSetup } from '@metro-labs/inference/model-config';
|
|
9
8
|
import { orgModels, type SyncStatus } from './org-models.js';
|
|
10
|
-
import { lastServed } from '@metro-labs/inference/served';
|
|
11
|
-
import { refreshUsage, type UsageDeps } from '@metro-labs/inference/usage-refresh';
|
|
12
|
-
import { usageSeen } from '@metro-labs/inference/usage';
|
|
13
9
|
|
|
14
10
|
const PATH = '/api/model';
|
|
15
11
|
const SNAPSHOT = '/api/model/snapshot';
|
|
16
12
|
const SYNC = '/api/model/sync';
|
|
17
13
|
|
|
18
|
-
export interface ModelApiDeps
|
|
14
|
+
export interface ModelApiDeps {
|
|
19
15
|
setup?: () => ModelSetup;
|
|
20
16
|
sync?: () => Promise<void>;
|
|
21
17
|
syncStatus?: () => SyncStatus;
|
|
@@ -53,7 +49,7 @@ function helpersBody(setup: ModelSetup): Record<string, unknown> | null {
|
|
|
53
49
|
export function settingsBody(deps: ModelApiDeps): Record<string, unknown> {
|
|
54
50
|
const setup = setupOf(deps);
|
|
55
51
|
const primary = primaryRoute(setup);
|
|
56
|
-
const reason = primary === null ? NO_MODEL :
|
|
52
|
+
const reason = primary === null ? NO_MODEL : setupNotReady(primary);
|
|
57
53
|
return {
|
|
58
54
|
source: setup.source,
|
|
59
55
|
sync: (deps.syncStatus ?? (() => orgModels()?.syncStatus() ?? NO_SYNC))(),
|
|
@@ -66,16 +62,9 @@ export function settingsBody(deps: ModelApiDeps): Record<string, unknown> {
|
|
|
66
62
|
chain: chainStatus(setup),
|
|
67
63
|
helpers: helpersBody(setup),
|
|
68
64
|
effort: { ...EFFORT_DEFAULTS, ...setup.effort },
|
|
69
|
-
lastServed: lastServed(),
|
|
70
|
-
usage: usageSeen(),
|
|
71
65
|
};
|
|
72
66
|
}
|
|
73
67
|
|
|
74
|
-
async function withUsage(deps: ModelApiDeps): Promise<Record<string, unknown>> {
|
|
75
|
-
await refreshUsage(setupOf(deps), deps);
|
|
76
|
-
return settingsBody(deps);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
68
|
async function synced(deps: ModelApiDeps): Promise<Record<string, unknown>> {
|
|
80
69
|
await (deps.sync ?? (() => orgModels()?.requested() ?? Promise.resolve()))();
|
|
81
70
|
return settingsBody(deps);
|
|
@@ -84,7 +73,7 @@ async function synced(deps: ModelApiDeps): Promise<Record<string, unknown>> {
|
|
|
84
73
|
type Run = () => Promise<unknown>;
|
|
85
74
|
|
|
86
75
|
const ROUTES: Record<string, [string, (deps: ModelApiDeps) => Promise<unknown>]> = {
|
|
87
|
-
[PATH]: ['GET',
|
|
76
|
+
[PATH]: ['GET', (deps) => Promise.resolve(settingsBody(deps))],
|
|
88
77
|
[SNAPSHOT]: ['GET', (deps) => Promise.resolve(settingsBody(deps))],
|
|
89
78
|
[SYNC]: ['POST', synced],
|
|
90
79
|
};
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
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
|
-
import { deliveredOf,
|
|
5
|
-
import type { Connection } from '@metro-labs/inference/model-config';
|
|
4
|
+
import { deliveredOf, type Delivered } from '@metro-labs/inference/delivered';
|
|
6
5
|
|
|
7
6
|
const MODELS_PATH = '/api/agents/models';
|
|
8
|
-
const REFRESH_PATH = '/api/agents/models/refresh';
|
|
9
7
|
const SYNC_MS = 60_000;
|
|
10
8
|
const PAGE_SYNC_MS = 5_000;
|
|
11
9
|
const AGENT_KEY_MISSING = 'agent_key_missing';
|
|
@@ -32,7 +30,6 @@ export class OrgModels {
|
|
|
32
30
|
private timer: ReturnType<typeof setInterval> | null = null;
|
|
33
31
|
private trailing: ReturnType<typeof setTimeout> | null = null;
|
|
34
32
|
private pageAt = Number.NEGATIVE_INFINITY;
|
|
35
|
-
private readonly refreshing = new Map<string, Promise<Connection | null>>();
|
|
36
33
|
private readonly listeners = new Set<() => void>();
|
|
37
34
|
|
|
38
35
|
constructor(private readonly deps: OrgModelsDeps) {}
|
|
@@ -89,19 +86,6 @@ export class OrgModels {
|
|
|
89
86
|
this.trailing = null;
|
|
90
87
|
}
|
|
91
88
|
|
|
92
|
-
refresh(conn: Connection): Promise<Connection | null> {
|
|
93
|
-
const newer = this.kept?.connections.find((c) => c.id === conn.id && c.revision > conn.revision);
|
|
94
|
-
if (newer !== undefined) return Promise.resolve(newer);
|
|
95
|
-
const slot = `${conn.id}\n${String(conn.revision)}`;
|
|
96
|
-
const found = this.refreshing.get(slot);
|
|
97
|
-
if (found !== undefined) return found;
|
|
98
|
-
const run = this.askRefresh(conn).finally(() => {
|
|
99
|
-
this.refreshing.delete(slot);
|
|
100
|
-
});
|
|
101
|
-
this.refreshing.set(slot, run);
|
|
102
|
-
return run;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
89
|
private later(wait: number): ReturnType<typeof setTimeout> {
|
|
106
90
|
const timer = setTimeout(() => {
|
|
107
91
|
this.trailing = null;
|
|
@@ -166,22 +150,6 @@ export class OrgModels {
|
|
|
166
150
|
this.failed(errMsg(err), identityRefused(err));
|
|
167
151
|
}
|
|
168
152
|
}
|
|
169
|
-
|
|
170
|
-
private async askRefresh(conn: Connection): Promise<Connection | null> {
|
|
171
|
-
const key = this.enrolledKey();
|
|
172
|
-
if (key === null) return null;
|
|
173
|
-
try {
|
|
174
|
-
const fresh = refreshedConnection(await agentCall(this.deps.api, key, REFRESH_PATH, { connection: conn.id, revision: conn.revision }));
|
|
175
|
-
if (fresh?.id !== conn.id) throw new Error('api.metro.box answered the refresh with another connection');
|
|
176
|
-
const kept = this.kept;
|
|
177
|
-
if (kept !== null) this.keep({ ...kept, connections: kept.connections.map((c) => (c.id === fresh.id ? fresh : c)) });
|
|
178
|
-
log.info({ connection: conn.id, revision: fresh.revision }, 'org-models: api.metro.box renewed the sign-in of a connection');
|
|
179
|
-
return fresh;
|
|
180
|
-
} catch (err) {
|
|
181
|
-
log.warn({ connection: conn.id, err: errMsg(err) }, 'org-models: api.metro.box could not renew the sign-in of a connection');
|
|
182
|
-
return null;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
153
|
}
|
|
186
154
|
|
|
187
155
|
let shared: OrgModels | null = null;
|
|
@@ -2,7 +2,11 @@ import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
|
2
2
|
import { errMsg, log } from '@metro-labs/core/log';
|
|
3
3
|
import { cameThroughProxy } from '@metro-labs/http/api-http';
|
|
4
4
|
import { forwardedHeaders, pipeResponse, readBody, sendError, watchUpstream } from '@metro-labs/inference/forward';
|
|
5
|
+
import { agentsDir } from '../agents/files.js';
|
|
6
|
+
import { agentIdForKey } from '../agents/keys.js';
|
|
5
7
|
import { usableServerUrl } from '../mcp-server/config.js';
|
|
8
|
+
import { reregisterMcpToken } from '../mcp-server/link.js';
|
|
9
|
+
import { readMcpToken } from '../mcp-server/token.js';
|
|
6
10
|
|
|
7
11
|
const PREFIX = '/gateway';
|
|
8
12
|
const HELLO = '/api/hello';
|
|
@@ -11,10 +15,11 @@ const DEFAULT_URL = 'https://gateway.metro.box';
|
|
|
11
15
|
const DROPPED = ['authorization', 'x-api-key', 'cookie'];
|
|
12
16
|
const NOT_LINKED = 'metro gateway: this agent is not linked to its organization yet, so it cannot reach its models. It links once an admin of the organization opens its page on metro.box.';
|
|
13
17
|
const NOT_ACCEPTED = 'metro gateway: gateway.metro.box did not accept this agent yet. It registers again; Claude Code retries.';
|
|
18
|
+
const NO_GATEWAY = 'metro gateway: this Metro has no model gateway, since METRO_GATEWAY_URL is empty or not an https url.';
|
|
14
19
|
const UNREACHABLE = 'metro gateway: gateway.metro.box could not be reached. Claude Code retries.';
|
|
15
20
|
|
|
16
21
|
export interface RelayDeps {
|
|
17
|
-
url: string;
|
|
22
|
+
url: string | null;
|
|
18
23
|
identify: (key: string) => boolean;
|
|
19
24
|
token: () => string | null;
|
|
20
25
|
reregister: () => void;
|
|
@@ -29,7 +34,7 @@ export function gatewayUrl(env: NodeJS.ProcessEnv = process.env): string | null
|
|
|
29
34
|
if (usableServerUrl(raw)) return raw.replace(/\/+$/, '');
|
|
30
35
|
if (refused !== raw) {
|
|
31
36
|
refused = raw;
|
|
32
|
-
log.warn({ url: raw }, 'gateway: METRO_GATEWAY_URL must be an https url (http only on this machine), so
|
|
37
|
+
log.warn({ url: raw }, 'gateway: METRO_GATEWAY_URL must be an https url (http only on this machine), so this box has no model gateway');
|
|
33
38
|
}
|
|
34
39
|
return null;
|
|
35
40
|
}
|
|
@@ -45,20 +50,26 @@ function upstreamHeaders(req: IncomingMessage, token: string): Record<string, st
|
|
|
45
50
|
return { ...headers, authorization: `Bearer ${token}` };
|
|
46
51
|
}
|
|
47
52
|
|
|
48
|
-
|
|
53
|
+
function targetOf(deps: RelayDeps): { url: string; token: string } | string {
|
|
54
|
+
if (deps.url === null) return NO_GATEWAY;
|
|
49
55
|
const token = deps.token();
|
|
50
|
-
|
|
56
|
+
return token === null ? NOT_LINKED : { url: deps.url, token };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function relay(req: IncomingMessage, res: ServerResponse, path: string, deps: RelayDeps): Promise<void> {
|
|
60
|
+
const target = targetOf(deps);
|
|
61
|
+
if (typeof target === 'string') {
|
|
51
62
|
req.resume();
|
|
52
|
-
sendError(res, 503, 'api_error',
|
|
63
|
+
sendError(res, 503, 'api_error', target);
|
|
53
64
|
return;
|
|
54
65
|
}
|
|
55
66
|
const body = req.method === 'POST' ? await readBody(req) : undefined;
|
|
56
67
|
const watch = watchUpstream(res);
|
|
57
68
|
let upstream: Response;
|
|
58
69
|
try {
|
|
59
|
-
upstream = await (deps.fetch ?? fetch)(`${
|
|
70
|
+
upstream = await (deps.fetch ?? fetch)(`${target.url}${path}`, {
|
|
60
71
|
method: req.method ?? 'GET',
|
|
61
|
-
headers: upstreamHeaders(req, token),
|
|
72
|
+
headers: upstreamHeaders(req, target.token),
|
|
62
73
|
...(body === undefined ? {} : { body: new Uint8Array(body) }),
|
|
63
74
|
signal: watch.signal,
|
|
64
75
|
redirect: 'manual',
|
|
@@ -106,3 +117,11 @@ export function handleGatewayRelay(req: IncomingMessage, res: ServerResponse, de
|
|
|
106
117
|
});
|
|
107
118
|
return true;
|
|
108
119
|
}
|
|
120
|
+
|
|
121
|
+
export function boxGatewayHandler(): (req: IncomingMessage, res: ServerResponse) => boolean {
|
|
122
|
+
const url = gatewayUrl();
|
|
123
|
+
if (url === null) log.warn('gateway: METRO_GATEWAY_URL is empty, so model requests are refused');
|
|
124
|
+
else log.info({ url }, 'gateway: model requests go through gateway.metro.box');
|
|
125
|
+
const deps: RelayDeps = { url, identify: (key) => agentIdForKey(key) !== undefined, token: () => readMcpToken(agentsDir()), reregister: reregisterMcpToken };
|
|
126
|
+
return (req, res) => handleGatewayRelay(req, res, deps);
|
|
127
|
+
}
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
localOwner,
|
|
25
25
|
readLocalAgentFile,
|
|
26
26
|
} from '../agents/file-admin.js';
|
|
27
|
-
import { boxGatewayHandler } from '../gateway/
|
|
27
|
+
import { boxGatewayHandler } from '../gateway/relay.js';
|
|
28
28
|
import type { StationName } from '@metro-labs/core/station-names';
|
|
29
29
|
|
|
30
30
|
export interface LocalModeDeps {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@metro-labs/inference",
|
|
3
3
|
"version": "0.1.0",
|
|
4
4
|
"private": true,
|
|
5
|
-
"description": "The model router: Anthropic, Routeway, OpenRouter, Bedrock and Codex, backups with holds, effort per thread, tool search fitting and usage from headers and stream frames. Run by
|
|
5
|
+
"description": "The model router: Anthropic, Routeway, OpenRouter, Bedrock and Codex, backups with holds, effort per thread, tool search fitting and usage from headers and stream frames. Run by gateway.metro.box (apps/gateway, docs/GATEWAY-SERVER.md). Imports only core and http, never a station.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"exports": {
|
|
8
8
|
"./attempts": "./src/attempts.ts",
|
|
@@ -21,12 +21,11 @@
|
|
|
21
21
|
"./model-config": "./src/model-config.ts",
|
|
22
22
|
"./served": "./src/served.ts",
|
|
23
23
|
"./tool-search": "./src/tool-search.ts",
|
|
24
|
-
"./usage": "./src/usage.ts"
|
|
25
|
-
"./usage-refresh": "./src/usage-refresh.ts"
|
|
24
|
+
"./usage": "./src/usage.ts"
|
|
26
25
|
},
|
|
27
26
|
"scripts": {
|
|
28
27
|
"build": "tsc",
|
|
29
|
-
"test": "tsc --noEmit"
|
|
28
|
+
"test": "tsc --noEmit && bun test test/"
|
|
30
29
|
},
|
|
31
30
|
"dependencies": {
|
|
32
31
|
"@metro-labs/core": "workspace:*",
|
package/runtime/runtime.json
CHANGED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
-
import { log } from '@metro-labs/core/log';
|
|
3
|
-
import { handleGatewayRequest, type GatewayDeps } from '@metro-labs/inference/gateway';
|
|
4
|
-
import { notReady, setupNotReady, type Route } from '@metro-labs/inference/model-config';
|
|
5
|
-
import { agentsDir } from '../agents/files.js';
|
|
6
|
-
import { agentIdForKey } from '../agents/keys.js';
|
|
7
|
-
import { reregisterMcpToken } from '../mcp-server/link.js';
|
|
8
|
-
import { readMcpToken } from '../mcp-server/token.js';
|
|
9
|
-
import { effectiveSetup } from './effective.js';
|
|
10
|
-
import { orgModels } from './org-models.js';
|
|
11
|
-
import { gatewayUrl, handleGatewayRelay } from './relay.js';
|
|
12
|
-
|
|
13
|
-
type Handler = (req: IncomingMessage, res: ServerResponse) => boolean;
|
|
14
|
-
|
|
15
|
-
const identify = (key: string): boolean => agentIdForKey(key) !== undefined;
|
|
16
|
-
|
|
17
|
-
export const routeReadiness = (route: Route): string | null => (gatewayUrl() === null ? notReady(route) : setupNotReady(route));
|
|
18
|
-
|
|
19
|
-
export const boxGateway = (): GatewayDeps => ({
|
|
20
|
-
setup: () => effectiveSetup(),
|
|
21
|
-
identify,
|
|
22
|
-
codex: { refresh: (conn) => orgModels()?.refresh(conn) ?? Promise.resolve(null) },
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
export function boxGatewayHandler(): Handler {
|
|
26
|
-
const url = gatewayUrl();
|
|
27
|
-
if (url === null) {
|
|
28
|
-
const deps = boxGateway();
|
|
29
|
-
log.info('gateway: this box routes its model requests itself (METRO_GATEWAY_URL is empty)');
|
|
30
|
-
return (req, res) => handleGatewayRequest(req, res, deps);
|
|
31
|
-
}
|
|
32
|
-
log.info({ url }, 'gateway: model requests go through gateway.metro.box');
|
|
33
|
-
const deps = { url, identify, token: () => readMcpToken(agentsDir()), reregister: reregisterMcpToken };
|
|
34
|
-
return (req, res) => handleGatewayRelay(req, res, deps);
|
|
35
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { errMsg, log } from '@metro-labs/core/log';
|
|
2
|
-
import { openrouterKey } from './openrouter.js';
|
|
3
|
-
import { mayProbe, noteUsage, openrouterUsage, usageOf } from './usage.js';
|
|
4
|
-
import type { Connection, ModelSetup } from './model-config.js';
|
|
5
|
-
|
|
6
|
-
export const CREDITS_TTL_MS = 5 * 60_000;
|
|
7
|
-
const WAIT_MS = 4_000;
|
|
8
|
-
|
|
9
|
-
export interface UsageDeps {
|
|
10
|
-
fetchImpl?: typeof fetch;
|
|
11
|
-
openrouterBase?: string;
|
|
12
|
-
usageWaitMs?: number;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const fresh = (id: string, now: number): boolean => {
|
|
16
|
-
const seen = usageOf(id);
|
|
17
|
-
return seen !== undefined && now - Date.parse(seen.at) < CREDITS_TTL_MS;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
async function refreshKey(conn: Connection, deps: UsageDeps, now: number): Promise<void> {
|
|
21
|
-
if (conn.provider !== 'openrouter' || conn.apiKey === '' || fresh(conn.id, now) || !mayProbe(conn.id, now, CREDITS_TTL_MS)) return;
|
|
22
|
-
try {
|
|
23
|
-
noteUsage(conn.id, openrouterUsage(await openrouterKey(conn.apiKey, deps.openrouterBase, deps.fetchImpl), new Date(now)));
|
|
24
|
-
} catch (err) {
|
|
25
|
-
log.warn({ err: errMsg(err) }, 'model-api: could not read the OpenRouter key spend');
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async function within(ms: number, work: Promise<unknown>): Promise<void> {
|
|
30
|
-
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
31
|
-
const waited = new Promise<void>((resolve) => {
|
|
32
|
-
timer = setTimeout(resolve, ms);
|
|
33
|
-
});
|
|
34
|
-
await Promise.race([work, waited]);
|
|
35
|
-
clearTimeout(timer);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export async function refreshUsage(setup: ModelSetup, deps: UsageDeps = {}, now = Date.now()): Promise<void> {
|
|
39
|
-
await within(deps.usageWaitMs ?? WAIT_MS, Promise.all(setup.connections.map((conn) => refreshKey(conn, deps, now))));
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function watchFallbackUsage(setup: () => ModelSetup, deps: UsageDeps = {}, every = CREDITS_TTL_MS): void {
|
|
43
|
-
const run = (): void => {
|
|
44
|
-
const current = setup();
|
|
45
|
-
if (current.fallbacks.length === 0 && (current.helpers ?? null) === null) return;
|
|
46
|
-
refreshUsage(current, deps).catch((err: unknown) => {
|
|
47
|
-
log.warn({ err: errMsg(err) }, 'model-api: could not refresh the usage of the fallback models');
|
|
48
|
-
});
|
|
49
|
-
};
|
|
50
|
-
setInterval(run, every).unref();
|
|
51
|
-
}
|