@stage-labs/metro 0.1.0-beta.319 → 0.1.0-beta.320
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/claude-env.js +2 -0
- package/package.json +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/bedrock.ts +8 -5
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-stream.ts +12 -5
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex.ts +26 -10
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/effort.ts +35 -8
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/failures.ts +40 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/forward.ts +19 -4
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gateway.ts +6 -5
- package/runtime/runtime.json +1 -1
- package/runtime/sdk-runner/runner.json +1 -1
package/dist/claude-env.js
CHANGED
|
@@ -11,6 +11,7 @@ const RM_PROMPT_OFF = 'CLAUDE_CODE_DISABLE_INLINE_SHELL_RM_PROMPT';
|
|
|
11
11
|
const OWN_CREDENTIALS = ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN'];
|
|
12
12
|
const CACHE_TTLS = ['CLAUDE_CODE_PROMPT_CACHE_TTL', 'CLAUDE_CODE_SUBAGENT_PROMPT_CACHE_TTL'];
|
|
13
13
|
const LONG_CACHE = '1h';
|
|
14
|
+
const HINT_HEADERS = 'CLAUDE_CODE_GATEWAY_HINT_HEADERS';
|
|
14
15
|
export const AGENT_VIEW_OFF = 'CLAUDE_CODE_DISABLE_AGENT_VIEW';
|
|
15
16
|
export const NO_UPDATES = { DISABLE_AUTOUPDATER: '1' };
|
|
16
17
|
const set = (env, name) => (env[name] ?? '').trim() !== '';
|
|
@@ -29,6 +30,7 @@ export function gatewayEnv(base, agentKey, port) {
|
|
|
29
30
|
ANTHROPIC_BASE_URL: `http://127.0.0.1:${String(port)}/gateway`,
|
|
30
31
|
ANTHROPIC_CUSTOM_HEADERS: own === '' ? mine : `${own}\n${mine}`,
|
|
31
32
|
CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY: '1',
|
|
33
|
+
[HINT_HEADERS]: set(base, HINT_HEADERS) ? base[HINT_HEADERS] : '1',
|
|
32
34
|
};
|
|
33
35
|
}
|
|
34
36
|
export const toolSearchEnv = (env) => set(env, 'ENABLE_TOOL_SEARCH') ? env : { ...env, ENABLE_TOOL_SEARCH: 'true' };
|
package/package.json
CHANGED
|
@@ -2,6 +2,7 @@ import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
|
2
2
|
import { log } from '@metro-labs/core/log';
|
|
3
3
|
import { withoutCacheTtl, withoutTtlBeta } from './cache-ttl.js';
|
|
4
4
|
import { EventStreamDecoder, type EventStreamMessage } from './eventstream.js';
|
|
5
|
+
import { noteFailure, type FailureSite } from './failures.js';
|
|
5
6
|
import { errorFrame, providerStatus, relayFrames, sendError, upstreamMessage, type Watch } from './forward.js';
|
|
6
7
|
import type { Connection } from './model-config.js';
|
|
7
8
|
import { estimateTokens } from './subscription.js';
|
|
@@ -177,7 +178,7 @@ function eventTypeOf(event: string): string {
|
|
|
177
178
|
}
|
|
178
179
|
}
|
|
179
180
|
|
|
180
|
-
function eventFrame(message: EventStreamMessage): string {
|
|
181
|
+
function eventFrame(message: EventStreamMessage, site: FailureSite): string {
|
|
181
182
|
if (message.headers[':message-type'] === 'event') {
|
|
182
183
|
const parsed = JSON.parse(message.payload.toString('utf8')) as { bytes?: unknown };
|
|
183
184
|
if (typeof parsed.bytes !== 'string') return '';
|
|
@@ -185,14 +186,16 @@ function eventFrame(message: EventStreamMessage): string {
|
|
|
185
186
|
return `event: ${eventTypeOf(event)}\ndata: ${event}\n\n`;
|
|
186
187
|
}
|
|
187
188
|
const errorType = message.headers[':exception-type'] ?? message.headers[':error-code'] ?? null;
|
|
188
|
-
|
|
189
|
+
const text = upstreamMessage(message.payload.toString('utf8'), 'Bedrock stream error');
|
|
190
|
+
noteFailure(site, { code: errorType ?? '', message: text }, 'gateway: the provider sent an error in the middle of its stream');
|
|
191
|
+
return errorFrame(errorKind(500, errorType), text);
|
|
189
192
|
}
|
|
190
193
|
|
|
191
|
-
function relayStream(upstream: Response, res: ServerResponse, watch: Watch, key: string): Promise<void> {
|
|
194
|
+
function relayStream(upstream: Response, res: ServerResponse, watch: Watch, key: string, site: FailureSite): Promise<void> {
|
|
192
195
|
const decoder = new EventStreamDecoder();
|
|
193
196
|
return relayFrames(upstream, res, watch, key, {
|
|
194
197
|
push: (chunk, emit) => {
|
|
195
|
-
for (const message of decoder.push(Buffer.from(chunk))) emit(eventFrame(message));
|
|
198
|
+
for (const message of decoder.push(Buffer.from(chunk))) emit(eventFrame(message, site));
|
|
196
199
|
},
|
|
197
200
|
close: () => '',
|
|
198
201
|
idle: (message) => errorFrame('api_error', message),
|
|
@@ -218,7 +221,7 @@ export async function bedrockMessages(
|
|
|
218
221
|
return;
|
|
219
222
|
}
|
|
220
223
|
if (rewritten.stream) {
|
|
221
|
-
await relayStream(upstream, res, up.watch, up.settings.id);
|
|
224
|
+
await relayStream(upstream, res, up.watch, up.settings.id, { connection: up.settings.label, model });
|
|
222
225
|
return;
|
|
223
226
|
}
|
|
224
227
|
const text = await upstream.text();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isRecord } from '@metro-labs/core/is-record';
|
|
2
2
|
import { encodeSignature } from './codex-translate.js';
|
|
3
3
|
import { errorFrame } from './forward.js';
|
|
4
|
+
import type { Failure } from './failures.js';
|
|
4
5
|
import { frame, messageEnd, messageStart, type Usage } from './frames.js';
|
|
5
6
|
import { stringOf } from '@metro-labs/http/api-http';
|
|
6
7
|
|
|
@@ -13,6 +14,7 @@ interface Block {
|
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
const responseOf = (data: Item): Item => (isRecord(data.response) ? data.response : {});
|
|
17
|
+
const reasonOf = (response: Item): string => (isRecord(response.incomplete_details) ? stringOf(response.incomplete_details.reason) : '');
|
|
16
18
|
const itemOf = (data: Item): Item => (isRecord(data.item) ? data.item : {});
|
|
17
19
|
|
|
18
20
|
const PLAN_LIMIT = 'subscription_sharing_usage_limit_exceeded';
|
|
@@ -53,6 +55,7 @@ export class CodexEventTranslator {
|
|
|
53
55
|
private done = false;
|
|
54
56
|
private readonly model: string;
|
|
55
57
|
private readonly restore: (name: string) => string;
|
|
58
|
+
private readonly report: (failure: Failure) => void;
|
|
56
59
|
private readonly handlers: Record<string, (data: Item) => string> = {
|
|
57
60
|
'response.created': (d) => this.start(responseOf(d)),
|
|
58
61
|
'response.output_item.added': (d) => this.start({}) + this.itemAdded(itemOf(d)),
|
|
@@ -67,9 +70,10 @@ export class CodexEventTranslator {
|
|
|
67
70
|
error: (d) => this.failed({ error: d }, 'Codex stream error'),
|
|
68
71
|
};
|
|
69
72
|
|
|
70
|
-
constructor(model: string, restore: (name: string) => string = (name) => name) {
|
|
73
|
+
constructor(model: string, restore: (name: string) => string = (name) => name, report: (failure: Failure) => void = () => undefined) {
|
|
71
74
|
this.model = model;
|
|
72
75
|
this.restore = restore;
|
|
76
|
+
this.report = report;
|
|
73
77
|
}
|
|
74
78
|
|
|
75
79
|
get finished(): boolean {
|
|
@@ -154,16 +158,18 @@ export class CodexEventTranslator {
|
|
|
154
158
|
}
|
|
155
159
|
|
|
156
160
|
private incomplete(response: Item): string {
|
|
157
|
-
const
|
|
158
|
-
if (
|
|
159
|
-
return this.failed(response, `Codex left the response incomplete (${
|
|
161
|
+
const reason = reasonOf(response);
|
|
162
|
+
if (reason === 'max_output_tokens') return this.start(response) + this.completed(response, 'max_tokens');
|
|
163
|
+
return this.failed(response, `Codex left the response incomplete (${reason || 'no reason given'})`);
|
|
160
164
|
}
|
|
161
165
|
|
|
162
166
|
private failed(response: Item, fallback: string): string {
|
|
163
167
|
this.done = true;
|
|
164
168
|
const error = isRecord(response.error) ? response.error : {};
|
|
165
169
|
const code = stringOf(error.code);
|
|
166
|
-
|
|
170
|
+
const message = stringOf(error.message) || fallback;
|
|
171
|
+
this.report({ code: code || reasonOf(response), message });
|
|
172
|
+
return errorFrame(errorKind(code), withUsageLink(code, message));
|
|
167
173
|
}
|
|
168
174
|
|
|
169
175
|
push(event: string, data: Item): string {
|
|
@@ -174,6 +180,7 @@ export class CodexEventTranslator {
|
|
|
174
180
|
close(message = 'Codex ended the stream before completing the response'): string {
|
|
175
181
|
if (this.done) return '';
|
|
176
182
|
this.done = true;
|
|
183
|
+
this.report({ code: '', message });
|
|
177
184
|
return errorFrame('api_error', message);
|
|
178
185
|
}
|
|
179
186
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
3
3
|
import { arch, platform, release } from 'node:os';
|
|
4
4
|
import { isRecord } from '@metro-labs/core/is-record';
|
|
@@ -7,6 +7,8 @@ import { stringOf } from '@metro-labs/http/api-http';
|
|
|
7
7
|
import { codexVersion, learnCodexVersion } from './codex-version.js';
|
|
8
8
|
import { CodexEventTranslator, withUsageLink } from './codex-stream.js';
|
|
9
9
|
import { ToolNames, toResponsesRequest } from './codex-translate.js';
|
|
10
|
+
import { agentIdOf } from './effort.js';
|
|
11
|
+
import { noteFailure, type Failure, type FailureSite } from './failures.js';
|
|
10
12
|
import { GatewayError, providerStatus, sendError, upstreamMessage, type Watch } from './forward.js';
|
|
11
13
|
import { parseEvent, SseParser, type SseEvent } from './frames.js';
|
|
12
14
|
import { CODEX_SIGN_IN, type CodexAccess, type Connection } from './model-config.js';
|
|
@@ -46,24 +48,30 @@ function headersFor(access: CodexAccess, sessionId: string): Record<string, stri
|
|
|
46
48
|
interface Call {
|
|
47
49
|
body: Record<string, unknown>;
|
|
48
50
|
model: string;
|
|
49
|
-
|
|
51
|
+
threadKey: string;
|
|
50
52
|
watch: Watch;
|
|
51
53
|
deps: CodexDeps;
|
|
52
54
|
names: ToolNames;
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
function threadKey(req: IncomingMessage): string {
|
|
58
|
+
const session = sessionHeader(req) || randomUUID();
|
|
59
|
+
const agent = agentIdOf(req);
|
|
60
|
+
return agent === '' ? session : createHash('sha256').update(`${session}\n${agent}`).digest('base64url');
|
|
61
|
+
}
|
|
62
|
+
|
|
55
63
|
function send(call: Call, access: CodexAccess): Promise<Response> {
|
|
56
|
-
const request = toResponsesRequest(call.body, call.model, { promptCacheKey: call.
|
|
64
|
+
const request = toResponsesRequest(call.body, call.model, { promptCacheKey: call.threadKey, names: call.names });
|
|
57
65
|
return (call.deps.fetchImpl ?? fetch)(`${baseFor(access, call.deps)}/responses`, {
|
|
58
66
|
method: 'POST',
|
|
59
|
-
headers: headersFor(access, call.
|
|
67
|
+
headers: headersFor(access, call.threadKey),
|
|
60
68
|
body: JSON.stringify(request),
|
|
61
69
|
signal: call.watch.signal,
|
|
62
70
|
redirect: 'manual',
|
|
63
71
|
});
|
|
64
72
|
}
|
|
65
73
|
|
|
66
|
-
function refusalOf(text: string, status: number):
|
|
74
|
+
function refusalOf(text: string, status: number): Failure {
|
|
67
75
|
let code = '';
|
|
68
76
|
try {
|
|
69
77
|
const parsed: unknown = JSON.parse(text);
|
|
@@ -71,7 +79,7 @@ function refusalOf(text: string, status: number): string {
|
|
|
71
79
|
} catch {
|
|
72
80
|
code = '';
|
|
73
81
|
}
|
|
74
|
-
return
|
|
82
|
+
return { code, message: upstreamMessage(text, `OpenAI answered ${String(status)}`), status };
|
|
75
83
|
}
|
|
76
84
|
|
|
77
85
|
let learning: Promise<unknown> | null = null;
|
|
@@ -99,15 +107,23 @@ async function reach(call: Call, conn: Connection): Promise<Response> {
|
|
|
99
107
|
}
|
|
100
108
|
|
|
101
109
|
export async function codexMessages(req: IncomingMessage, res: ServerResponse, body: Record<string, unknown>, model: string, conn: Connection, deps: CodexDeps, watch: Watch): Promise<void> {
|
|
102
|
-
const call: Call = { body, model,
|
|
110
|
+
const call: Call = { body, model, threadKey: threadKey(req), watch, deps, names: new ToolNames() };
|
|
111
|
+
const site: FailureSite = { connection: conn.label, model };
|
|
103
112
|
const upstream = await reach(call, conn);
|
|
104
113
|
noteUsageHeaders('codex', conn.id, upstream.headers);
|
|
105
114
|
if (!upstream.ok) {
|
|
106
|
-
const
|
|
107
|
-
|
|
115
|
+
const refusal = refusalOf(await upstream.text(), upstream.status);
|
|
116
|
+
noteFailure(site, refusal, 'gateway: Codex refused the request');
|
|
117
|
+
sendError(res, providerStatus(upstream.status), errorKind(upstream.status, INVALID), withUsageLink(refusal.code, refusal.message));
|
|
108
118
|
return;
|
|
109
119
|
}
|
|
110
|
-
const translator = new CodexEventTranslator(
|
|
120
|
+
const translator = new CodexEventTranslator(
|
|
121
|
+
model,
|
|
122
|
+
(name) => call.names.restore(name),
|
|
123
|
+
(failure) => {
|
|
124
|
+
noteFailure(site, failure, 'gateway: Codex did not complete the response');
|
|
125
|
+
},
|
|
126
|
+
);
|
|
111
127
|
const onEvent = (raw: SseEvent): string => {
|
|
112
128
|
const parsed = parseEvent(raw);
|
|
113
129
|
return parsed === null ? '' : translator.push(parsed.event, parsed.data);
|
|
@@ -4,11 +4,15 @@ import { isRecord } from '@metro-labs/core/is-record';
|
|
|
4
4
|
|
|
5
5
|
export const BINDING_BETA = 'thinking-binding-controls-2026-08-01';
|
|
6
6
|
|
|
7
|
+
const COMPACT_EFFORT = 'low';
|
|
7
8
|
const SUBAGENT_HEADER = 'x-claude-code-agent-id';
|
|
8
9
|
const SUBAGENT_MARK = 'cc_is_subagent=true';
|
|
10
|
+
const REQUEST_CLASS_HEADER = 'x-claude-code-request-class';
|
|
11
|
+
const COMPACTION_CLASS = 'compaction';
|
|
12
|
+
const COMPACT_PROMPT = 'CRITICAL: Respond with TEXT ONLY. Do NOT call any tools.';
|
|
9
13
|
const ABOVE_HIGH = new Set(['xhigh', 'max']);
|
|
10
14
|
|
|
11
|
-
export type RequestKind = 'main' | 'subagent' | 'other';
|
|
15
|
+
export type RequestKind = 'main' | 'subagent' | 'compact' | 'other';
|
|
12
16
|
|
|
13
17
|
type Body = Record<string, unknown>;
|
|
14
18
|
|
|
@@ -32,22 +36,45 @@ function systemMarksSubagent(body: Body): boolean {
|
|
|
32
36
|
|
|
33
37
|
const chore = (body: Body): boolean => record(body.output_config).format !== undefined || record(body.thinking).type === 'disabled';
|
|
34
38
|
|
|
39
|
+
const messagesOf = (body: Body): unknown[] => (Array.isArray(body.messages) ? body.messages : []);
|
|
40
|
+
|
|
41
|
+
function textsOf(content: unknown): string[] {
|
|
42
|
+
if (typeof content === 'string') return [content];
|
|
43
|
+
if (!Array.isArray(content)) return [];
|
|
44
|
+
return content.filter(isRecord).flatMap((block) => (block.type === 'text' && typeof block.text === 'string' ? [block.text] : []));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function asksForSummary(body: Body): boolean {
|
|
48
|
+
const last = messagesOf(body).map(record).findLast((message) => message.role === 'user');
|
|
49
|
+
return textsOf(last?.content).some((text) => text.trimStart().startsWith(COMPACT_PROMPT));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const agentIdOf = (req: IncomingMessage): string => header(req, SUBAGENT_HEADER);
|
|
53
|
+
|
|
54
|
+
export const helperRequest = (req: IncomingMessage, body: Body): boolean => agentIdOf(req) !== '' || systemMarksSubagent(body);
|
|
55
|
+
|
|
56
|
+
export const compactionRequest = (req: IncomingMessage, body: Body): boolean => header(req, REQUEST_CLASS_HEADER) === COMPACTION_CLASS || asksForSummary(body);
|
|
57
|
+
|
|
35
58
|
export function requestKind(req: IncomingMessage, body: Body): RequestKind {
|
|
36
|
-
if (
|
|
59
|
+
if (compactionRequest(req, body)) return 'compact';
|
|
60
|
+
if (helperRequest(req, body)) return 'subagent';
|
|
37
61
|
return chore(body) ? 'other' : 'main';
|
|
38
62
|
}
|
|
39
63
|
|
|
40
|
-
const messagesOf = (body: Body): unknown[] => (Array.isArray(body.messages) ? body.messages : []);
|
|
41
|
-
|
|
42
64
|
const askedEfforts = (body: Body): string[] => [body, ...messagesOf(body).map(record)].map(effortOf).filter((effort) => effort !== '');
|
|
43
65
|
|
|
66
|
+
function wantedEffort(kind: RequestKind, setting: RouteEffort): string | null {
|
|
67
|
+
if (kind === 'compact') return COMPACT_EFFORT;
|
|
68
|
+
if (kind === 'other') return null;
|
|
69
|
+
const wanted = kind === 'subagent' ? (setting.helpers ?? EFFORT_DEFAULTS.helpers) : (setting.main ?? EFFORT_DEFAULTS.main);
|
|
70
|
+
return wanted === 'asked' ? null : wanted;
|
|
71
|
+
}
|
|
72
|
+
|
|
44
73
|
export function plannedEffort(req: IncomingMessage, body: Body, setting: RouteEffort = {}): string | null {
|
|
45
74
|
const asked = askedEfforts(body);
|
|
46
75
|
if (asked.length === 0) return null;
|
|
47
|
-
const
|
|
48
|
-
if (
|
|
49
|
-
const wanted = kind === 'subagent' ? (setting.helpers ?? EFFORT_DEFAULTS.helpers) : (setting.main ?? EFFORT_DEFAULTS.main);
|
|
50
|
-
if (wanted === 'asked') return null;
|
|
76
|
+
const wanted = wantedEffort(requestKind(req, body), setting);
|
|
77
|
+
if (wanted === null) return null;
|
|
51
78
|
return asked.every((effort) => effort === wanted) ? null : wanted;
|
|
52
79
|
}
|
|
53
80
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { isRecord } from '@metro-labs/core/is-record';
|
|
2
|
+
import { log } from '@metro-labs/core/log';
|
|
3
|
+
import { stringOf } from '@metro-labs/http/api-http';
|
|
4
|
+
import { parseEvent, SseParser } from './frames.js';
|
|
5
|
+
import type { Route } from './model-config.js';
|
|
6
|
+
|
|
7
|
+
const MESSAGE_MAX = 300;
|
|
8
|
+
const ERROR_DATA = /^\{\s*"type"\s*:\s*"error"/;
|
|
9
|
+
|
|
10
|
+
export interface FailureSite {
|
|
11
|
+
connection: string;
|
|
12
|
+
model: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface Failure {
|
|
16
|
+
code: string;
|
|
17
|
+
message: string;
|
|
18
|
+
status?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const siteOf = (route: Route): FailureSite => ({ connection: route.connection.label, model: route.model });
|
|
22
|
+
|
|
23
|
+
export function noteFailure(site: FailureSite, failure: Failure, what: string): void {
|
|
24
|
+
log.warn({ connection: site.connection, model: site.model, ...failure, message: failure.message.slice(0, MESSAGE_MAX) }, what);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class StreamErrors {
|
|
28
|
+
private readonly parser = new SseParser();
|
|
29
|
+
|
|
30
|
+
constructor(private readonly site: FailureSite) {}
|
|
31
|
+
|
|
32
|
+
feed(text: string): void {
|
|
33
|
+
for (const raw of this.parser.push(text)) {
|
|
34
|
+
if (raw.event !== 'error' && !ERROR_DATA.test(raw.data)) continue;
|
|
35
|
+
const data = parseEvent(raw)?.data ?? {};
|
|
36
|
+
const error = isRecord(data.error) ? data.error : {};
|
|
37
|
+
noteFailure(this.site, { code: stringOf(error.type) || stringOf(error.code), message: stringOf(error.message) }, 'gateway: the provider sent an error in the middle of its stream');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { IncomingMessage, OutgoingHttpHeader, OutgoingHttpHeaders, ServerResponse } from 'node:http';
|
|
2
|
+
import { StreamErrors, type FailureSite } from './failures.js';
|
|
2
3
|
import { UsageScanner } from './usage.js';
|
|
3
4
|
|
|
4
5
|
export class GatewayError extends Error {
|
|
@@ -156,6 +157,7 @@ export interface PipeOptions {
|
|
|
156
157
|
keepalive?: boolean;
|
|
157
158
|
ownCredential?: boolean;
|
|
158
159
|
scanner?: UsageScanner;
|
|
160
|
+
site?: FailureSite;
|
|
159
161
|
}
|
|
160
162
|
|
|
161
163
|
function openResponse(upstream: Response, res: ServerResponse, opts: PipeOptions): boolean {
|
|
@@ -167,14 +169,27 @@ function openResponse(upstream: Response, res: ServerResponse, opts: PipeOptions
|
|
|
167
169
|
return (upstream.headers.get('content-type') ?? '').startsWith('text/event-stream');
|
|
168
170
|
}
|
|
169
171
|
|
|
170
|
-
|
|
172
|
+
type Tap = (chunk: Uint8Array) => void;
|
|
173
|
+
|
|
174
|
+
function tapOf(opts: PipeOptions, streaming: boolean): Tap | null {
|
|
175
|
+
const scanner = opts.scanner;
|
|
176
|
+
const errors = streaming && opts.site !== undefined ? new StreamErrors(opts.site) : null;
|
|
177
|
+
if (scanner === undefined && errors === null) return null;
|
|
178
|
+
const decoder = new TextDecoder();
|
|
179
|
+
return (chunk) => {
|
|
180
|
+
const text = decoder.decode(chunk, { stream: true });
|
|
181
|
+
scanner?.feed(text);
|
|
182
|
+
errors?.feed(text);
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function pump(body: ReadableStream<Uint8Array>, res: ServerResponse, watch: Watch, tap: Tap | null): Promise<void> {
|
|
171
187
|
const reader = body.getReader();
|
|
172
|
-
const decoder = scanner === undefined ? null : new TextDecoder();
|
|
173
188
|
for (;;) {
|
|
174
189
|
const { done, value } = await reader.read();
|
|
175
190
|
if (done) return;
|
|
176
191
|
watch.touch();
|
|
177
|
-
|
|
192
|
+
tap?.(value);
|
|
178
193
|
res.write(value);
|
|
179
194
|
}
|
|
180
195
|
}
|
|
@@ -188,7 +203,7 @@ export async function pipeResponse(upstream: Response, res: ServerResponse, watc
|
|
|
188
203
|
}
|
|
189
204
|
const ping = opts.keepalive === true && streaming ? setInterval(() => res.write(PING_FRAME), PING_MS) : null;
|
|
190
205
|
try {
|
|
191
|
-
await pump(body, res, watch, opts
|
|
206
|
+
await pump(body, res, watch, tapOf(opts, streaming));
|
|
192
207
|
} catch (err) {
|
|
193
208
|
if (!watch.idle()) throw err;
|
|
194
209
|
if (streaming) res.write(errorFrame('api_error', idleMessage(watch.ms)));
|
|
@@ -33,9 +33,9 @@ import {
|
|
|
33
33
|
cappedEffort,
|
|
34
34
|
effortToApply,
|
|
35
35
|
forgetLearnedThinking,
|
|
36
|
+
helperRequest,
|
|
36
37
|
learnAlwaysThinks,
|
|
37
38
|
plannedEffort,
|
|
38
|
-
requestKind,
|
|
39
39
|
thinkingOff,
|
|
40
40
|
withBlockBinding,
|
|
41
41
|
withEffort,
|
|
@@ -50,6 +50,7 @@ import { isRecord } from '@metro-labs/core/is-record';
|
|
|
50
50
|
import { forgetServed, noteServed } from './served.js';
|
|
51
51
|
import { forgetUsage, noteUsageHeaders, UsageScanner } from './usage.js';
|
|
52
52
|
import { fitToolSearch } from './tool-search.js';
|
|
53
|
+
import { siteOf } from './failures.js';
|
|
53
54
|
import { orgModels } from './org-models.js';
|
|
54
55
|
|
|
55
56
|
const GATEWAY_PREFIX = '/gateway';
|
|
@@ -183,7 +184,7 @@ async function toAnthropic(req: IncomingMessage, res: ServerResponse, body: Shap
|
|
|
183
184
|
await refusedSubscription(conn, upstream, watch);
|
|
184
185
|
noteRefusal(conn.label, route.model, upstream);
|
|
185
186
|
noteUsageHeaders('anthropic', conn.id, upstream.headers);
|
|
186
|
-
await pipeResponse(upstream, res, watch, { ownCredential: true, scanner: new UsageScanner(conn.id) });
|
|
187
|
+
await pipeResponse(upstream, res, watch, { ownCredential: true, scanner: new UsageScanner(conn.id), site: siteOf(route) });
|
|
187
188
|
}
|
|
188
189
|
|
|
189
190
|
async function toRouteway(req: IncomingMessage, res: ServerResponse, body: Shaped, route: Route, deps: GatewayDeps): Promise<void> {
|
|
@@ -196,7 +197,7 @@ async function toRouteway(req: IncomingMessage, res: ServerResponse, body: Shape
|
|
|
196
197
|
noteRefusal(conn.label, route.model, upstream);
|
|
197
198
|
await noteRoutewayRefusal(upstream);
|
|
198
199
|
await refusedWithPage(upstream, watch);
|
|
199
|
-
await pipeResponse(upstream, res, watch, { keepalive: true, ownCredential: true, scanner: new UsageScanner(conn.id) });
|
|
200
|
+
await pipeResponse(upstream, res, watch, { keepalive: true, ownCredential: true, scanner: new UsageScanner(conn.id), site: siteOf(route) });
|
|
200
201
|
}
|
|
201
202
|
|
|
202
203
|
async function toOpenRouter(req: IncomingMessage, res: ServerResponse, body: Record<string, unknown>, route: Route, deps: GatewayDeps): Promise<void> {
|
|
@@ -220,7 +221,7 @@ async function toOpenRouter(req: IncomingMessage, res: ServerResponse, body: Rec
|
|
|
220
221
|
redirect: 'manual',
|
|
221
222
|
});
|
|
222
223
|
noteRefusal(conn.label, route.model, upstream);
|
|
223
|
-
await pipeResponse(upstream, res, watch, { keepalive: true, ownCredential: true, scanner: new UsageScanner(conn.id) });
|
|
224
|
+
await pipeResponse(upstream, res, watch, { keepalive: true, ownCredential: true, scanner: new UsageScanner(conn.id), site: siteOf(route) });
|
|
224
225
|
}
|
|
225
226
|
|
|
226
227
|
function openrouterBody(body: Record<string, unknown>, model: string, zdr: boolean): Record<string, unknown> {
|
|
@@ -254,7 +255,7 @@ interface Chosen {
|
|
|
254
255
|
}
|
|
255
256
|
|
|
256
257
|
function chosenChain(req: IncomingMessage, sent: Record<string, unknown>, setup: ModelSetup, requested: string): Chosen {
|
|
257
|
-
const helpers =
|
|
258
|
+
const helpers = helperRequest(req, sent) ? helperSetup(setup) : null;
|
|
258
259
|
if (helpers === null) return { lane: 'main', setup, primary: resolveRoute(requested, setup) ?? noRoute() };
|
|
259
260
|
return { lane: 'helpers', setup: helpers, primary: pinnedRoute(requested, helpers) ?? noRoute() };
|
|
260
261
|
}
|
package/runtime/runtime.json
CHANGED