@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
|
@@ -9,8 +9,9 @@ import {
|
|
|
9
9
|
freshAdaptations,
|
|
10
10
|
type Adaptations,
|
|
11
11
|
} from './bedrock.js';
|
|
12
|
-
import { anthropicHeaders, forwardedHeaders, GatewayError, parseJson, pipeResponse, readBody, sendError, watchUpstream } from './forward.js';
|
|
13
|
-
import {
|
|
12
|
+
import { addBeta, anthropicHeaders, forwardedHeaders, GatewayError, parseJson, pipeResponse, readBody, sendError, watchUpstream } from './forward.js';
|
|
13
|
+
import { BINDING_BETA, cappedEffort, effortToApply, plannedEffort, withBlockBinding, withEffort } from './effort.js';
|
|
14
|
+
import { notReady, readModelConfig, resolveRoute, routeLabel, setCodexAuth, setGeminiAuth, writeModelConfig, type Connection, type ModelConfig, type Route } from './model-config.js';
|
|
14
15
|
import { codexCount, codexMessages, freshCodexState } from './codex.js';
|
|
15
16
|
import { freshGeminiState, geminiCount, geminiMessages } from './gemini.js';
|
|
16
17
|
import type { GeminiDeps } from './gemini.js';
|
|
@@ -52,12 +53,12 @@ export function resetGatewayState(): void {
|
|
|
52
53
|
Object.assign(geminiState, freshGeminiState());
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
const saveCodexTokens = (tokens: CodexTokens): void => {
|
|
56
|
-
writeModelConfig(setCodexAuth(readModelConfig(), tokens));
|
|
56
|
+
const saveCodexTokens = (id: string, tokens: CodexTokens): void => {
|
|
57
|
+
writeModelConfig(setCodexAuth(readModelConfig(), id, tokens));
|
|
57
58
|
};
|
|
58
59
|
|
|
59
|
-
const saveGeminiTokens = (tokens: GeminiTokens): void => {
|
|
60
|
-
writeModelConfig(setGeminiAuth(readModelConfig(), tokens));
|
|
60
|
+
const saveGeminiTokens = (id: string, tokens: GeminiTokens): void => {
|
|
61
|
+
writeModelConfig(setGeminiAuth(readModelConfig(), id, tokens));
|
|
61
62
|
};
|
|
62
63
|
|
|
63
64
|
const keyOf = (req: IncomingMessage): string => {
|
|
@@ -68,18 +69,16 @@ const keyOf = (req: IncomingMessage): string => {
|
|
|
68
69
|
const defaultIdentify = (key: string): boolean => key !== '' && agentIdForKey(key) !== undefined;
|
|
69
70
|
|
|
70
71
|
function modelsBody(cfg: ModelConfig): Record<string, unknown> {
|
|
71
|
-
const data
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (cfg.openrouter.model !== '')
|
|
75
|
-
data.push({ id: `openrouter:${cfg.openrouter.model}`, display_name: `OpenRouter · ${cfg.openrouter.model}`, description: 'Through metro, billed to OpenRouter' });
|
|
76
|
-
if (cfg.codex.model !== '')
|
|
77
|
-
data.push({ id: `codex:${cfg.codex.model}`, display_name: `Codex · ${cfg.codex.model}`, description: 'Through metro, on your ChatGPT subscription' });
|
|
78
|
-
if (cfg.gemini.model !== '')
|
|
79
|
-
data.push({ id: `gemini:${cfg.gemini.model}`, display_name: `Gemini · ${cfg.gemini.model}`, description: 'Through metro, on your Google account' });
|
|
72
|
+
const data = cfg.connections
|
|
73
|
+
.filter((c) => c.provider !== 'anthropic' && c.model !== '')
|
|
74
|
+
.map((c) => ({ id: `${c.provider}:${c.model}`, display_name: `${c.label} · ${c.model}`, description: 'Through metro' }));
|
|
80
75
|
return { data, has_more: false, first_id: data[0]?.id ?? null, last_id: data.at(-1)?.id ?? null };
|
|
81
76
|
}
|
|
82
77
|
|
|
78
|
+
const PASSTHROUGH: Connection = { id: 'passthrough', provider: 'anthropic', label: 'Claude Code login', model: '', apiKey: '', region: '', zdr: false, codex: null, gemini: null };
|
|
79
|
+
|
|
80
|
+
const passthrough = (body: Record<string, unknown>): Route => ({ connection: PASSTHROUGH, model: requestedModel(body) });
|
|
81
|
+
|
|
83
82
|
function requestedModel(body: Record<string, unknown>): string {
|
|
84
83
|
const model = body.model;
|
|
85
84
|
if (typeof model !== 'string' || model === '') throw new GatewayError(400, 'invalid_request_error', 'model is required');
|
|
@@ -91,18 +90,43 @@ const standsInFor = (req: IncomingMessage): boolean => {
|
|
|
91
90
|
return typeof metroKey === 'string' && metroKey !== '' && req.headers.authorization === `Bearer ${metroKey}`;
|
|
92
91
|
};
|
|
93
92
|
|
|
93
|
+
interface Payloads {
|
|
94
|
+
metro: Buffer;
|
|
95
|
+
asSent: Buffer | null;
|
|
96
|
+
bound: boolean;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function anthropicPayloads(raw: Buffer, sent: Record<string, unknown>, shaped: Record<string, unknown>, model: string): Payloads {
|
|
100
|
+
const rewrite = typeof sent.model === 'string' && sent.model !== model;
|
|
101
|
+
const asSent = rewrite ? Buffer.from(JSON.stringify({ ...sent, model })) : raw;
|
|
102
|
+
const bound = withBlockBinding(shaped);
|
|
103
|
+
if (bound === sent) return { metro: asSent, asSent: null, bound: false };
|
|
104
|
+
return { metro: Buffer.from(JSON.stringify(rewrite ? { ...bound, model } : bound)), asSent, bound: bound !== shaped };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
type Send = (payload: Buffer, headers: Record<string, string>) => Promise<Response>;
|
|
108
|
+
|
|
109
|
+
async function asAnthropicWants(send: Send, base: Record<string, string>, payloads: Payloads, connection: string, model: string): Promise<Response> {
|
|
110
|
+
const { metro, asSent, bound } = payloads;
|
|
111
|
+
const upstream = await send(metro, bound ? addBeta(base, BINDING_BETA) : base);
|
|
112
|
+
if (upstream.status !== 400 || asSent === null) return upstream;
|
|
113
|
+
await upstream.body?.cancel();
|
|
114
|
+
log.warn({ connection, model }, 'gateway: Anthropic refused the request metro shaped, so it was sent again as Claude Code wrote it');
|
|
115
|
+
return send(asSent, base);
|
|
116
|
+
}
|
|
117
|
+
|
|
94
118
|
async function toAnthropic(
|
|
95
119
|
req: IncomingMessage,
|
|
96
120
|
res: ServerResponse,
|
|
97
121
|
raw: Buffer,
|
|
98
|
-
|
|
122
|
+
sent: Record<string, unknown>,
|
|
123
|
+
shaped: Record<string, unknown>,
|
|
99
124
|
route: Route,
|
|
100
125
|
deps: GatewayDeps,
|
|
101
126
|
): Promise<void> {
|
|
102
|
-
const
|
|
103
|
-
const payload = explicit ? Buffer.from(JSON.stringify({ ...body, model: route.model })) : raw;
|
|
127
|
+
const conn = route.connection;
|
|
104
128
|
const url = `${deps.anthropicBase ?? ANTHROPIC_BASE}${(req.url ?? '').slice(GATEWAY_PREFIX.length)}`;
|
|
105
|
-
const key =
|
|
129
|
+
const key = conn.apiKey;
|
|
106
130
|
if (key === '' && standsInFor(req))
|
|
107
131
|
throw new GatewayError(
|
|
108
132
|
403,
|
|
@@ -110,16 +134,13 @@ async function toAnthropic(
|
|
|
110
134
|
'Claude Code on this machine has no Anthropic login of its own; choose Bedrock, OpenRouter, Codex or Gemini on the Model page, or sign in on the Claude tab',
|
|
111
135
|
);
|
|
112
136
|
const watch = watchUpstream(res);
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
noteRefusal('anthropic', route.model, upstream);
|
|
121
|
-
noteUsageHeaders('anthropic', upstream.headers);
|
|
122
|
-
const scanner = new UsageScanner('anthropic');
|
|
137
|
+
const base = key === '' ? forwardedHeaders(req) : anthropicHeaders(req, key);
|
|
138
|
+
const send = (payload: Buffer, headers: Record<string, string>): Promise<Response> =>
|
|
139
|
+
fetch(url, { method: 'POST', headers, body: new Uint8Array(payload), signal: watch.signal, redirect: 'manual' });
|
|
140
|
+
const upstream = await asAnthropicWants(send, base, anthropicPayloads(raw, sent, shaped, route.model), conn.label, route.model);
|
|
141
|
+
noteRefusal(conn.label, route.model, upstream);
|
|
142
|
+
noteUsageHeaders('anthropic', conn.id, upstream.headers);
|
|
143
|
+
const scanner = new UsageScanner(conn.id);
|
|
123
144
|
await pipeResponse(upstream, res, watch, key === '' ? { scanner } : { ownCredential: true, scanner });
|
|
124
145
|
}
|
|
125
146
|
|
|
@@ -131,12 +152,13 @@ async function toOpenRouter(
|
|
|
131
152
|
cfg: ModelConfig,
|
|
132
153
|
deps: GatewayDeps,
|
|
133
154
|
): Promise<void> {
|
|
134
|
-
const
|
|
155
|
+
const conn = route.connection;
|
|
156
|
+
const reason = notReady(cfg, conn);
|
|
135
157
|
if (reason !== null) throw new GatewayError(400, 'invalid_request_error', reason);
|
|
136
158
|
const headers: Record<string, string> = {
|
|
137
159
|
'content-type': 'application/json',
|
|
138
160
|
accept: req.headers.accept ?? 'application/json',
|
|
139
|
-
authorization: `Bearer ${
|
|
161
|
+
authorization: `Bearer ${conn.apiKey}`,
|
|
140
162
|
'anthropic-version': typeof req.headers['anthropic-version'] === 'string' ? req.headers['anthropic-version'] : '2023-06-01',
|
|
141
163
|
'http-referer': 'https://metro.box',
|
|
142
164
|
'x-title': 'metro',
|
|
@@ -147,12 +169,12 @@ async function toOpenRouter(
|
|
|
147
169
|
const upstream = await fetch(`${deps.openrouterBase ?? OPENROUTER_BASE}${MESSAGES}`, {
|
|
148
170
|
method: 'POST',
|
|
149
171
|
headers,
|
|
150
|
-
body: JSON.stringify(openrouterBody(body, route.model,
|
|
172
|
+
body: JSON.stringify(openrouterBody(body, route.model, conn.zdr)),
|
|
151
173
|
signal: watch.signal,
|
|
152
174
|
redirect: 'manual',
|
|
153
175
|
});
|
|
154
|
-
noteRefusal(
|
|
155
|
-
await pipeResponse(upstream, res, watch, { keepalive: true, ownCredential: true, scanner: new UsageScanner(
|
|
176
|
+
noteRefusal(conn.label, route.model, upstream);
|
|
177
|
+
await pipeResponse(upstream, res, watch, { keepalive: true, ownCredential: true, scanner: new UsageScanner(conn.id) });
|
|
156
178
|
}
|
|
157
179
|
|
|
158
180
|
const thinkingOff = (body: Record<string, unknown>): boolean => isRecord(body.thinking) && body.thinking.type === 'disabled';
|
|
@@ -160,49 +182,59 @@ const thinkingOff = (body: Record<string, unknown>): boolean => isRecord(body.th
|
|
|
160
182
|
export function openrouterBody(body: Record<string, unknown>, model: string, zdr: boolean): Record<string, unknown> {
|
|
161
183
|
const sent: Record<string, unknown> = { ...body, model };
|
|
162
184
|
if (thinkingOff(body)) delete sent.thinking;
|
|
185
|
+
const effort = effortToApply(body);
|
|
186
|
+
if (effort !== null) sent.reasoning = { ...(isRecord(body.reasoning) ? body.reasoning : {}), effort: cappedEffort(effort) };
|
|
163
187
|
if (!zdr) return sent;
|
|
164
188
|
const provider = isRecord(body.provider) ? body.provider : {};
|
|
165
189
|
return { ...sent, provider: { ...provider, zdr: true } };
|
|
166
190
|
}
|
|
167
191
|
|
|
168
|
-
function noteRefusal(
|
|
169
|
-
if (!upstream.ok) log.warn({
|
|
192
|
+
function noteRefusal(connection: string, model: string, upstream: Response): void {
|
|
193
|
+
if (!upstream.ok) log.warn({ connection, model, status: upstream.status }, 'gateway: the provider refused the request');
|
|
170
194
|
}
|
|
171
195
|
|
|
172
|
-
async function toSubscription(req: IncomingMessage, res: ServerResponse, path: string, body: Record<string, unknown>, route: Route,
|
|
173
|
-
|
|
196
|
+
async function toSubscription(req: IncomingMessage, res: ServerResponse, path: string, body: Record<string, unknown>, route: Route, deps: GatewayDeps): Promise<void> {
|
|
197
|
+
const conn = route.connection;
|
|
198
|
+
if (conn.provider === 'gemini') {
|
|
174
199
|
if (path === COUNT) geminiCount(res, body);
|
|
175
|
-
else await geminiMessages(req, res, body, route.model,
|
|
200
|
+
else await geminiMessages(req, res, body, route.model, conn, { save: saveGeminiTokens, ...deps.gemini }, geminiState, watchUpstream(res));
|
|
176
201
|
return;
|
|
177
202
|
}
|
|
178
203
|
if (path === COUNT) codexCount(res, body);
|
|
179
|
-
else await codexMessages(req, res, body, route.model,
|
|
204
|
+
else await codexMessages(req, res, body, route.model, conn, { save: saveCodexTokens, ...deps.codex }, codexState, watchUpstream(res));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function shapedFor(req: IncomingMessage, sent: Record<string, unknown>): Record<string, unknown> {
|
|
208
|
+
const effort = plannedEffort(req, sent);
|
|
209
|
+
return effort === null ? sent : withEffort(sent, effort);
|
|
180
210
|
}
|
|
181
211
|
|
|
182
212
|
async function dispatch(req: IncomingMessage, res: ServerResponse, path: string, deps: GatewayDeps): Promise<void> {
|
|
183
213
|
const cfg = deps.config();
|
|
184
214
|
const raw = await readBody(req);
|
|
185
|
-
const
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
215
|
+
const sent = parseJson(raw);
|
|
216
|
+
const body = shapedFor(req, sent);
|
|
217
|
+
const route = resolveRoute(requestedModel(body), cfg) ?? passthrough(body);
|
|
218
|
+
const conn = route.connection;
|
|
219
|
+
log.info({ route: routeLabel(route), connection: conn.label, path }, 'gateway: routing');
|
|
220
|
+
if (path === MESSAGES) noteServed({ connection: conn.id, provider: conn.provider, model: route.model, at: new Date().toISOString() });
|
|
221
|
+
if (conn.provider === 'bedrock') {
|
|
222
|
+
assertBedrockReady(conn);
|
|
223
|
+
const up = { settings: conn, base: deps.bedrockBase ?? bedrockBase(conn.region), learned, watch: watchUpstream(res) };
|
|
192
224
|
if (path === COUNT) await bedrockCount(req, res, body, route.model, up);
|
|
193
225
|
else await bedrockMessages(req, res, body, route.model, up);
|
|
194
226
|
return;
|
|
195
227
|
}
|
|
196
|
-
if (
|
|
228
|
+
if (conn.provider === 'openrouter') {
|
|
197
229
|
if (path === COUNT) throw new GatewayError(404, 'not_found_error', 'OpenRouter does not count tokens');
|
|
198
230
|
await toOpenRouter(req, res, body, route, cfg, deps);
|
|
199
231
|
return;
|
|
200
232
|
}
|
|
201
|
-
if (
|
|
202
|
-
await toSubscription(req, res, path, body, route,
|
|
233
|
+
if (conn.provider === 'codex' || conn.provider === 'gemini') {
|
|
234
|
+
await toSubscription(req, res, path, body, route, deps);
|
|
203
235
|
return;
|
|
204
236
|
}
|
|
205
|
-
await toAnthropic(req, res, raw, body, route, deps);
|
|
237
|
+
await toAnthropic(req, res, raw, sent, body, route, deps);
|
|
206
238
|
}
|
|
207
239
|
|
|
208
240
|
function failed(res: ServerResponse, err: unknown): void {
|
|
@@ -2,6 +2,7 @@ import { randomBytes } from 'node:crypto';
|
|
|
2
2
|
import { isRecord } from '@metro-labs/core/is-record';
|
|
3
3
|
import { ToolNames } from './codex-translate.js';
|
|
4
4
|
import { CLIENT_NAME, requestId, SYSTEM_PREFIX } from './gemini-client.js';
|
|
5
|
+
import { cappedEffort, effortToApply } from './effort.js';
|
|
5
6
|
|
|
6
7
|
type Item = Record<string, unknown>;
|
|
7
8
|
|
|
@@ -190,6 +191,8 @@ function generationConfig(body: Item): Item {
|
|
|
190
191
|
if (typeof body.temperature === 'number') out.temperature = body.temperature;
|
|
191
192
|
if (typeof body.top_p === 'number') out.topP = body.top_p;
|
|
192
193
|
if (isRecord(body.thinking) && body.thinking.type === 'enabled') out.thinkingConfig = { includeThoughts: true };
|
|
194
|
+
const effort = effortToApply(body);
|
|
195
|
+
if (effort !== null) out.thinkingLevel = cappedEffort(effort);
|
|
193
196
|
return out;
|
|
194
197
|
}
|
|
195
198
|
|
|
@@ -8,8 +8,9 @@ import { toGeminiRequest } from './gemini-translate.js';
|
|
|
8
8
|
import { ToolNames } from './codex-translate.js';
|
|
9
9
|
import { assembleMessage, SseParser } from './codex-stream.js';
|
|
10
10
|
import { GatewayError, idleMessage, providerStatus, sendError, upstreamMessage, type Watch } from './forward.js';
|
|
11
|
-
import type {
|
|
11
|
+
import type { Connection } from './model-config.js';
|
|
12
12
|
import { UsageScanner } from './usage.js';
|
|
13
|
+
import { effortToApply, withoutEffort } from './effort.js';
|
|
13
14
|
|
|
14
15
|
const PING_MS = 25_000;
|
|
15
16
|
const STATUS_OF: Record<string, number> = { rate_limit_error: 429, invalid_request_error: 400, permission_error: 403, overloaded_error: 529 };
|
|
@@ -18,7 +19,7 @@ export interface GeminiDeps {
|
|
|
18
19
|
base?: Bases;
|
|
19
20
|
tokenBase?: string;
|
|
20
21
|
fetchImpl?: typeof fetch;
|
|
21
|
-
save: (tokens: GeminiTokens) => void;
|
|
22
|
+
save: (id: string, tokens: GeminiTokens) => void;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
export interface GeminiState {
|
|
@@ -30,13 +31,13 @@ export const freshGeminiState = (): GeminiState => ({ refreshing: null, latest:
|
|
|
30
31
|
|
|
31
32
|
const newerThan = (a: GeminiTokens, b: GeminiTokens): boolean => Date.parse(a.savedAt) > Date.parse(b.savedAt);
|
|
32
33
|
|
|
33
|
-
function refreshed(tokens: GeminiTokens, deps: GeminiDeps, state: GeminiState): Promise<GeminiTokens> {
|
|
34
|
+
function refreshed(id: string, tokens: GeminiTokens, deps: GeminiDeps, state: GeminiState): Promise<GeminiTokens> {
|
|
34
35
|
const latest = state.latest;
|
|
35
36
|
if (latest !== null && newerThan(latest, tokens) && !tokensStale(latest)) return Promise.resolve(latest);
|
|
36
37
|
if (state.refreshing !== null) return state.refreshing;
|
|
37
38
|
const run = refreshTokens(tokens, deps.tokenBase, deps.fetchImpl)
|
|
38
39
|
.then((fresh) => {
|
|
39
|
-
deps.save(fresh);
|
|
40
|
+
deps.save(id, fresh);
|
|
40
41
|
state.latest = fresh;
|
|
41
42
|
return fresh;
|
|
42
43
|
})
|
|
@@ -50,10 +51,10 @@ function refreshed(tokens: GeminiTokens, deps: GeminiDeps, state: GeminiState):
|
|
|
50
51
|
return run;
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
export function currentGeminiTokens(
|
|
54
|
-
const tokens =
|
|
54
|
+
export function currentGeminiTokens(conn: Connection, deps: GeminiDeps, state: GeminiState): Promise<GeminiTokens> {
|
|
55
|
+
const tokens = conn.gemini;
|
|
55
56
|
if (tokens === null) throw new GatewayError(400, 'invalid_request_error', 'Gemini is not connected: sign in with Google on the Model page.');
|
|
56
|
-
return tokensStale(tokens) ? refreshed(tokens, deps, state) : Promise.resolve(tokens);
|
|
57
|
+
return tokensStale(tokens) ? refreshed(conn.id, tokens, deps, state) : Promise.resolve(tokens);
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
function errorKind(status: number): string {
|
|
@@ -76,6 +77,7 @@ interface Call {
|
|
|
76
77
|
watch: Watch;
|
|
77
78
|
deps: GeminiDeps;
|
|
78
79
|
names: ToolNames;
|
|
80
|
+
conn: Connection;
|
|
79
81
|
}
|
|
80
82
|
|
|
81
83
|
const headersFor = (token: string, stream: boolean): Record<string, string> => ({
|
|
@@ -140,12 +142,12 @@ export function refusalMessage(text: string, status: number): string {
|
|
|
140
142
|
return notes.length === 0 ? message : `${message} (${notes.join('; ')})`;
|
|
141
143
|
}
|
|
142
144
|
|
|
143
|
-
async function reach(call: Call,
|
|
144
|
-
let tokens = await currentGeminiTokens(
|
|
145
|
+
async function reach(call: Call, conn: Connection, state: GeminiState, stream: boolean): Promise<Response> {
|
|
146
|
+
let tokens = await currentGeminiTokens(conn, call.deps, state);
|
|
145
147
|
let upstream = await send(call, tokens, stream);
|
|
146
148
|
if (upstream.status === 401) {
|
|
147
149
|
await upstream.body?.cancel();
|
|
148
|
-
tokens = await refreshed(tokens, call.deps, state);
|
|
150
|
+
tokens = await refreshed(conn.id, tokens, call.deps, state);
|
|
149
151
|
upstream = await send(call, tokens, stream);
|
|
150
152
|
}
|
|
151
153
|
return upstream;
|
|
@@ -210,7 +212,7 @@ async function relayStream(upstream: Response, res: ServerResponse, call: Call):
|
|
|
210
212
|
return;
|
|
211
213
|
}
|
|
212
214
|
const ping = setInterval(() => res.write('event: ping\ndata: {"type":"ping"}\n\n'), PING_MS);
|
|
213
|
-
const scanner = new UsageScanner(
|
|
215
|
+
const scanner = new UsageScanner(call.conn.id);
|
|
214
216
|
const emit = (frames: string): void => {
|
|
215
217
|
if (frames === '') return;
|
|
216
218
|
scanner.feed(frames);
|
|
@@ -245,7 +247,7 @@ async function relayWhole(upstream: Response, res: ServerResponse, call: Call):
|
|
|
245
247
|
const translator = new GeminiStreamTranslator(call.model, (name) => call.names.restore(name));
|
|
246
248
|
const data = parseData(await upstream.text());
|
|
247
249
|
const frames = (data === null ? '' : translator.push(data)) + translator.close();
|
|
248
|
-
const scanner = new UsageScanner(
|
|
250
|
+
const scanner = new UsageScanner(call.conn.id);
|
|
249
251
|
scanner.feed(frames);
|
|
250
252
|
scanner.done();
|
|
251
253
|
const message = assembleMessage(frames);
|
|
@@ -254,23 +256,38 @@ async function relayWhole(upstream: Response, res: ServerResponse, call: Call):
|
|
|
254
256
|
res.end(JSON.stringify(message));
|
|
255
257
|
}
|
|
256
258
|
|
|
259
|
+
const NAMES_THINKING = /thinking|thought/i;
|
|
260
|
+
|
|
261
|
+
function refuse(res: ServerResponse, model: string, status: number, text: string): void {
|
|
262
|
+
log.warn({ provider: 'gemini', model, status, body: text.slice(0, REFUSAL_LOG_MAX) }, 'gateway: the provider refused the request');
|
|
263
|
+
sendError(res, providerStatus(status), errorKind(status), refusalMessage(text, status));
|
|
264
|
+
}
|
|
265
|
+
|
|
257
266
|
export async function geminiMessages(
|
|
258
267
|
req: IncomingMessage,
|
|
259
268
|
res: ServerResponse,
|
|
260
269
|
body: Record<string, unknown>,
|
|
261
270
|
model: string,
|
|
262
|
-
|
|
271
|
+
conn: Connection,
|
|
263
272
|
deps: GeminiDeps,
|
|
264
273
|
state: GeminiState,
|
|
265
274
|
watch: Watch,
|
|
266
275
|
): Promise<void> {
|
|
267
276
|
const stream = body.stream === true;
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
if (
|
|
277
|
+
let call: Call = { body, model, promptId: promptIdOf(req), watch, deps, names: new ToolNames(), conn };
|
|
278
|
+
let upstream = await reach(call, conn, state, stream);
|
|
279
|
+
if (upstream.status === 400 && effortToApply(body) !== null) {
|
|
271
280
|
const text = await upstream.text();
|
|
272
|
-
|
|
273
|
-
|
|
281
|
+
if (!NAMES_THINKING.test(text)) {
|
|
282
|
+
refuse(res, model, 400, text);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
log.warn({ provider: 'gemini', model }, 'gateway: Gemini refused the thinking level, so metro asked again without it');
|
|
286
|
+
call = { ...call, body: withoutEffort(body) };
|
|
287
|
+
upstream = await reach(call, conn, state, stream);
|
|
288
|
+
}
|
|
289
|
+
if (!upstream.ok) {
|
|
290
|
+
refuse(res, model, upstream.status, await upstream.text());
|
|
274
291
|
return;
|
|
275
292
|
}
|
|
276
293
|
if (stream) await relayStream(upstream, res, call);
|