@stage-labs/metro 0.1.0-beta.313 → 0.1.0-beta.315

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage-labs/metro",
3
- "version": "0.1.0-beta.313",
3
+ "version": "0.1.0-beta.315",
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": {
@@ -43,7 +43,7 @@ import { notReady, resolveRoute, routeLabel, type Connection, type ModelSetup, t
43
43
  import { codexMessages, type CodexDeps } from './codex.js';
44
44
  import { countTokens } from './subscription.js';
45
45
  import { OPENROUTER_BASE } from './openrouter.js';
46
- import { refusedWithPage, ROUTEWAY_BASE, routewayHeaders, routewayTools } from './routeway.js';
46
+ import { noteRoutewayRefusal, refusedWithPage, ROUTEWAY_BASE, routewayHeaders, routewayTools } from './routeway.js';
47
47
  import { isRecord } from '@metro-labs/core/is-record';
48
48
  import { forgetServed, noteServed } from './served.js';
49
49
  import { forgetUsage, noteUsageHeaders, UsageScanner } from './usage.js';
@@ -143,8 +143,8 @@ async function asAnthropicWants(send: Send, base: Record<string, string>, payloa
143
143
  const { metro, asSent, bound } = payloads;
144
144
  const upstream = await send(metro, bound ? addBeta(base, BINDING_BETA) : base);
145
145
  if (upstream.status !== 400 || asSent === null) return upstream;
146
- await upstream.body?.cancel();
147
- log.warn({ connection, model }, 'gateway: Anthropic refused the request metro shaped, so it was sent again as Claude Code wrote it');
146
+ const reason = upstreamMessage(await upstream.text().catch(() => ''), '').slice(0, 300);
147
+ log.warn({ connection, model, reason }, 'gateway: the provider refused the request metro shaped, so it was sent again as Claude Code wrote it');
148
148
  return send(asSent, base);
149
149
  }
150
150
 
@@ -193,6 +193,7 @@ async function toRouteway(req: IncomingMessage, res: ServerResponse, body: Shape
193
193
  const fitted: Shaped = { raw: sent === body.sent ? body.raw : Buffer.from(JSON.stringify(sent)), sent, shaped: routewayTools(body.shaped), bind: false };
194
194
  const upstream = await shapedAttempt(send, routewayHeaders(req, conn.apiKey), fitted, route);
195
195
  noteRefusal(conn.label, route.model, upstream);
196
+ await noteRoutewayRefusal(upstream);
196
197
  await refusedWithPage(upstream, watch);
197
198
  await pipeResponse(upstream, res, watch, { keepalive: true, ownCredential: true, scanner: new UsageScanner(conn.id) });
198
199
  }
@@ -1,5 +1,6 @@
1
1
  import type { IncomingMessage } from 'node:http';
2
2
  import { isRecord } from '@metro-labs/core/is-record';
3
+ import { log } from '@metro-labs/core/log';
3
4
  import { anthropicHeaders, GatewayError, type Watch } from './forward.js';
4
5
 
5
6
  export const ROUTEWAY_BASE = 'https://api.routeway.ai';
@@ -19,9 +20,49 @@ export async function refusedWithPage(upstream: Response, watch: Watch): Promise
19
20
  throw new GatewayError(502, 'api_error', `metro gateway: Routeway answered with a web page instead of its API (${String(upstream.status)}); try again in a moment`);
20
21
  }
21
22
 
23
+ const SCHEMA_MAPS = ['properties', 'patternProperties', '$defs', 'definitions'];
24
+ const SCHEMA_LISTS = ['anyOf', 'oneOf', 'allOf', 'prefixItems'];
25
+ const SCHEMA_ONES = ['items', 'additionalProperties', 'not', 'contains'];
26
+
27
+ function fitMap(raw: Record<string, unknown>): Record<string, unknown> {
28
+ const entries = Object.entries(raw).map(([key, value]) => [key, fitSchema(value)] as const);
29
+ return entries.some(([key, value]) => value !== raw[key]) ? Object.fromEntries(entries) : raw;
30
+ }
31
+
32
+ function fitList(raw: unknown[]): unknown[] {
33
+ const fitted = raw.map(fitSchema);
34
+ return fitted.some((value, at) => value !== raw[at]) ? fitted : raw;
35
+ }
36
+
37
+ function fitChild(key: string, value: unknown): unknown {
38
+ if (SCHEMA_MAPS.includes(key)) return isRecord(value) ? fitMap(value) : value;
39
+ if (SCHEMA_LISTS.includes(key)) return Array.isArray(value) ? fitList(value) : value;
40
+ return SCHEMA_ONES.includes(key) ? fitSchema(value) : value;
41
+ }
42
+
43
+ function singleTypes(schema: Record<string, unknown>): Record<string, unknown> {
44
+ const types: unknown = schema.type;
45
+ if (!Array.isArray(types) || !types.every((type) => typeof type === 'string')) return schema;
46
+ const rest = Object.fromEntries(Object.entries(schema).filter(([key]) => key !== 'type'));
47
+ if (types.length === 1) return { ...rest, type: types[0] };
48
+ const choices = types.map((type) => ({ type }));
49
+ if (!Object.hasOwn(rest, 'anyOf')) return { ...rest, anyOf: choices };
50
+ const { anyOf, ...others } = rest;
51
+ return { ...others, allOf: [{ anyOf: choices }, { anyOf }] };
52
+ }
53
+
54
+ function fitSchema(raw: unknown): unknown {
55
+ if (!isRecord(raw)) return raw;
56
+ const entries = Object.entries(raw).map(([key, value]) => [key, fitChild(key, value)] as const);
57
+ const children = entries.some(([key, value]) => value !== raw[key]) ? Object.fromEntries(entries) : raw;
58
+ return singleTypes(children);
59
+ }
60
+
22
61
  function withProperties(tool: unknown): unknown {
23
- if (!isRecord(tool) || !isRecord(tool.input_schema) || Object.hasOwn(tool.input_schema, 'properties')) return tool;
24
- return { ...tool, input_schema: { ...tool.input_schema, properties: {} } };
62
+ if (!isRecord(tool) || !isRecord(tool.input_schema)) return tool;
63
+ const schema = fitSchema(tool.input_schema);
64
+ const full = isRecord(schema) && !Object.hasOwn(schema, 'properties') ? { ...schema, properties: {} } : schema;
65
+ return full === tool.input_schema ? tool : { ...tool, input_schema: full };
25
66
  }
26
67
 
27
68
  export function routewayTools(body: Record<string, unknown>): Record<string, unknown> {
@@ -30,3 +71,12 @@ export function routewayTools(body: Record<string, unknown>): Record<string, unk
30
71
  const tools: unknown[] = listed.map(withProperties);
31
72
  return tools.some((tool, at) => tool !== listed[at]) ? { ...body, tools } : body;
32
73
  }
74
+
75
+ export async function noteRoutewayRefusal(upstream: Response): Promise<void> {
76
+ if (upstream.ok || !(upstream.headers.get('content-type') ?? '').includes('application/json')) return;
77
+ const body: unknown = await upstream.clone().json().catch(() => null);
78
+ const error = isRecord(body) && isRecord(body.error) ? body.error : null;
79
+ if (error === null) return;
80
+ const text = (value: unknown): string | null => (typeof value === 'string' ? value.slice(0, 300) : null);
81
+ log.warn({ status: upstream.status, reason: text(error.message), param: text(error.param) }, 'gateway: Routeway refused the request');
82
+ }
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.1.0-beta.313"
2
+ "version": "0.1.0-beta.315"
3
3
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.1.0-beta.313",
2
+ "version": "0.1.0-beta.315",
3
3
  "dependencies": {
4
4
  "@anthropic-ai/claude-agent-sdk": "0.3.287",
5
5
  "@modelcontextprotocol/sdk": "^1.29.0",