@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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage-labs/metro",
3
- "version": "0.1.0-beta.319",
3
+ "version": "0.1.0-beta.320",
4
4
  "description": "The metro command line: run your agent on this machine, relaying its chat channels and connectors to Claude Code.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -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
- return errorFrame(errorKind(500, errorType), upstreamMessage(message.payload.toString('utf8'), 'Bedrock stream error'));
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 details = isRecord(response.incomplete_details) ? response.incomplete_details : {};
158
- if (details.reason === 'max_output_tokens') return this.start(response) + this.completed(response, 'max_tokens');
159
- return this.failed(response, `Codex left the response incomplete (${stringOf(details.reason) || 'no reason given'})`);
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
- return errorFrame(errorKind(code), withUsageLink(code, stringOf(error.message) || fallback));
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
- sessionId: string;
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.sessionId, names: call.names });
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.sessionId),
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): string {
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 withUsageLink(code, upstreamMessage(text, `OpenAI answered ${String(status)}`));
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, sessionId: sessionHeader(req) || randomUUID(), watch, deps, names: new ToolNames() };
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 text = await upstream.text();
107
- sendError(res, providerStatus(upstream.status), errorKind(upstream.status, INVALID), refusalOf(text, upstream.status));
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(model, (name) => call.names.restore(name));
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 (header(req, SUBAGENT_HEADER) !== '' || systemMarksSubagent(body)) return 'subagent';
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 kind = requestKind(req, body);
48
- if (kind === 'other') return null;
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
- async function pump(body: ReadableStream<Uint8Array>, res: ServerResponse, watch: Watch, scanner?: UsageScanner): Promise<void> {
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
- if (decoder !== null) scanner?.feed(decoder.decode(value, { stream: true }));
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.scanner);
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 = requestKind(req, sent) === 'subagent' ? helperSetup(setup) : null;
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
  }
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.1.0-beta.319"
2
+ "version": "0.1.0-beta.320"
3
3
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.1.0-beta.319",
2
+ "version": "0.1.0-beta.320",
3
3
  "dependencies": {
4
4
  "@anthropic-ai/claude-agent-sdk": "0.3.287",
5
5
  "@modelcontextprotocol/sdk": "^1.29.0",