@stage-labs/metro 0.1.0-beta.29 → 0.1.0-beta.30
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/bedrock-proxy.js +6 -42
- package/dist/bedrock-upstream.js +72 -0
- package/dist/bedrock.js +7 -2
- package/package.json +1 -1
- package/runtime/runtime.json +1 -1
package/dist/bedrock-proxy.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createServer, } from 'node:http';
|
|
2
2
|
import { EventStreamDecoder } from './bedrock-eventstream.js';
|
|
3
|
+
import { callBedrock, freshAdaptations, invoke, invokeUrl, upstreamMessage, } from './bedrock-upstream.js';
|
|
3
4
|
export class BedrockConfigError extends Error {
|
|
4
5
|
}
|
|
5
6
|
export class ProxyError extends Error {
|
|
@@ -108,45 +109,6 @@ async function readBody(req) {
|
|
|
108
109
|
throw new ProxyError(400, 'invalid_request_error', 'body must be JSON');
|
|
109
110
|
}
|
|
110
111
|
}
|
|
111
|
-
function invokeUrl(up, modelId, action) {
|
|
112
|
-
return `${up.base}/model/${encodeURIComponent(modelId)}/${action}`;
|
|
113
|
-
}
|
|
114
|
-
function callBedrock(up, url, body, accept, signal) {
|
|
115
|
-
return fetch(url, {
|
|
116
|
-
method: 'POST',
|
|
117
|
-
headers: {
|
|
118
|
-
authorization: `Bearer ${up.cfg.bearerToken}`,
|
|
119
|
-
'content-type': 'application/json',
|
|
120
|
-
accept,
|
|
121
|
-
},
|
|
122
|
-
body: JSON.stringify(body),
|
|
123
|
-
signal,
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
async function invoke(up, req, signal) {
|
|
127
|
-
const action = req.stream ? 'invoke-with-response-stream' : 'invoke';
|
|
128
|
-
const accept = req.stream ? 'application/vnd.amazon.eventstream' : 'application/json';
|
|
129
|
-
const url = invokeUrl(up, req.modelId, action);
|
|
130
|
-
const first = await callBedrock(up, url, req.body, accept, signal);
|
|
131
|
-
if (first.status !== 400 || req.betas.length === 0)
|
|
132
|
-
return first;
|
|
133
|
-
await first.text();
|
|
134
|
-
process.stderr.write(`metro bedrock: retrying without anthropic_beta [${req.betas.join(', ')}]\n`);
|
|
135
|
-
const stripped = Object.fromEntries(Object.entries(req.body).filter(([key]) => key !== 'anthropic_beta'));
|
|
136
|
-
return callBedrock(up, url, stripped, accept, signal);
|
|
137
|
-
}
|
|
138
|
-
function upstreamMessage(text) {
|
|
139
|
-
try {
|
|
140
|
-
const parsed = JSON.parse(text);
|
|
141
|
-
const message = parsed.message ?? parsed.Message;
|
|
142
|
-
if (typeof message === 'string' && message !== '')
|
|
143
|
-
return message;
|
|
144
|
-
}
|
|
145
|
-
catch {
|
|
146
|
-
return text === '' ? 'Bedrock returned no body' : text;
|
|
147
|
-
}
|
|
148
|
-
return text === '' ? 'Bedrock returned no body' : text;
|
|
149
|
-
}
|
|
150
112
|
async function relayError(upstream, res) {
|
|
151
113
|
const text = await upstream.text();
|
|
152
114
|
const kind = errorKind(upstream.status, upstream.headers.get('x-amzn-errortype'));
|
|
@@ -248,11 +210,11 @@ function presentedToken(req) {
|
|
|
248
210
|
const key = req.headers['x-api-key'];
|
|
249
211
|
return Array.isArray(key) ? (key[0] ?? '') : (key ?? '');
|
|
250
212
|
}
|
|
251
|
-
async function route(req, res, cfg, opts) {
|
|
213
|
+
async function route(req, res, cfg, opts, learned) {
|
|
252
214
|
if (presentedToken(req) !== opts.token)
|
|
253
215
|
throw new ProxyError(401, 'authentication_error', 'unauthorized');
|
|
254
216
|
const path = (req.url ?? '').split('?')[0] ?? '';
|
|
255
|
-
const up = { cfg, base: upstreamBase(cfg, opts.upstream) };
|
|
217
|
+
const up = { cfg, base: upstreamBase(cfg, opts.upstream), learned };
|
|
256
218
|
if (req.method === 'POST' && path === MESSAGES_PATH)
|
|
257
219
|
return handleMessages(req, res, up);
|
|
258
220
|
if (req.method === 'POST' && path === COUNT_PATH)
|
|
@@ -272,8 +234,9 @@ function failed(res, err) {
|
|
|
272
234
|
sendError(res, 502, 'api_error', `metro bedrock proxy: ${message}`);
|
|
273
235
|
}
|
|
274
236
|
export function startBedrockProxy(cfg, opts) {
|
|
237
|
+
const learned = freshAdaptations();
|
|
275
238
|
const server = createServer((req, res) => {
|
|
276
|
-
route(req, res, cfg, opts).catch((err) => {
|
|
239
|
+
route(req, res, cfg, opts, learned).catch((err) => {
|
|
277
240
|
failed(res, err);
|
|
278
241
|
});
|
|
279
242
|
});
|
|
@@ -285,6 +248,7 @@ export function startBedrockProxy(cfg, opts) {
|
|
|
285
248
|
resolve({
|
|
286
249
|
port,
|
|
287
250
|
server,
|
|
251
|
+
learned,
|
|
288
252
|
close: () => new Promise((done) => {
|
|
289
253
|
server.close(() => {
|
|
290
254
|
done();
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export const freshAdaptations = () => ({
|
|
2
|
+
fields: new Set(),
|
|
3
|
+
dropBetas: false,
|
|
4
|
+
});
|
|
5
|
+
const EXTRA_INPUT_RE = /^([A-Za-z0-9_]+)(?:\.[^:]*)?: Extra inputs are not permitted/;
|
|
6
|
+
const MAX_REPAIRS = 4;
|
|
7
|
+
export function invokeUrl(up, modelId, action) {
|
|
8
|
+
return `${up.base}/model/${encodeURIComponent(modelId)}/${action}`;
|
|
9
|
+
}
|
|
10
|
+
export function callBedrock(up, url, body, accept, signal) {
|
|
11
|
+
return fetch(url, {
|
|
12
|
+
method: 'POST',
|
|
13
|
+
headers: {
|
|
14
|
+
authorization: `Bearer ${up.cfg.bearerToken}`,
|
|
15
|
+
'content-type': 'application/json',
|
|
16
|
+
accept,
|
|
17
|
+
},
|
|
18
|
+
body: JSON.stringify(body),
|
|
19
|
+
signal,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export function upstreamMessage(text) {
|
|
23
|
+
const fallback = text === '' ? 'Bedrock returned no body' : text;
|
|
24
|
+
try {
|
|
25
|
+
const parsed = JSON.parse(text);
|
|
26
|
+
const message = parsed.message ?? parsed.Message;
|
|
27
|
+
return typeof message === 'string' && message !== '' ? message : fallback;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return fallback;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const without = (body, keys) => Object.fromEntries(Object.entries(body).filter(([key]) => !keys.has(key)));
|
|
34
|
+
function applyLearned(req, learned) {
|
|
35
|
+
const dropped = new Set(learned.fields);
|
|
36
|
+
if (learned.dropBetas)
|
|
37
|
+
dropped.add('anthropic_beta');
|
|
38
|
+
return {
|
|
39
|
+
body: without(req.body, dropped),
|
|
40
|
+
betas: learned.dropBetas ? [] : req.betas,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function repair(attempt, learned, message) {
|
|
44
|
+
const named = EXTRA_INPUT_RE.exec(message)?.[1];
|
|
45
|
+
if (named !== undefined && named in attempt.body) {
|
|
46
|
+
learned.fields.add(named);
|
|
47
|
+
process.stderr.write(`metro bedrock: Bedrock refused "${named}" (extra input); dropping it from every request\n`);
|
|
48
|
+
return { body: without(attempt.body, new Set([named])), betas: attempt.betas };
|
|
49
|
+
}
|
|
50
|
+
if (attempt.betas.length > 0) {
|
|
51
|
+
learned.dropBetas = true;
|
|
52
|
+
process.stderr.write(`metro bedrock: retrying without anthropic_beta [${attempt.betas.join(', ')}]\n`);
|
|
53
|
+
return { body: without(attempt.body, new Set(['anthropic_beta'])), betas: [] };
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
export async function invoke(up, req, signal) {
|
|
58
|
+
const action = req.stream ? 'invoke-with-response-stream' : 'invoke';
|
|
59
|
+
const accept = req.stream ? 'application/vnd.amazon.eventstream' : 'application/json';
|
|
60
|
+
const url = invokeUrl(up, req.modelId, action);
|
|
61
|
+
let attempt = applyLearned(req, up.learned);
|
|
62
|
+
for (let repairs = 0;; repairs += 1) {
|
|
63
|
+
const res = await callBedrock(up, url, attempt.body, accept, signal);
|
|
64
|
+
if (res.status !== 400 || repairs >= MAX_REPAIRS)
|
|
65
|
+
return res;
|
|
66
|
+
const text = await res.text();
|
|
67
|
+
const next = repair(attempt, up.learned, upstreamMessage(text));
|
|
68
|
+
if (next === null)
|
|
69
|
+
return new Response(text, { status: 400, headers: res.headers });
|
|
70
|
+
attempt = next;
|
|
71
|
+
}
|
|
72
|
+
}
|
package/dist/bedrock.js
CHANGED
|
@@ -56,10 +56,15 @@ export function settingsConflicts(files) {
|
|
|
56
56
|
}
|
|
57
57
|
return out;
|
|
58
58
|
}
|
|
59
|
-
export function
|
|
59
|
+
export function firstPartyModelId(bedrockId) {
|
|
60
|
+
return bedrockId.replace(/^(?:[a-z-]+\.)?anthropic\./, '').replace(/-v\d+:\d+$/, '');
|
|
61
|
+
}
|
|
62
|
+
export function claudeEnv(base, port, token, pinned = null) {
|
|
60
63
|
const kept = Object.entries(base).filter(([key]) => !SCRUBBED.includes(key));
|
|
64
|
+
const model = (base.ANTHROPIC_MODEL ?? '').trim();
|
|
61
65
|
return {
|
|
62
66
|
...Object.fromEntries(kept),
|
|
67
|
+
...(pinned !== null && model === '' && { ANTHROPIC_MODEL: firstPartyModelId(pinned) }),
|
|
63
68
|
ANTHROPIC_BASE_URL: `http://127.0.0.1:${String(port)}`,
|
|
64
69
|
ANTHROPIC_AUTH_TOKEN: token,
|
|
65
70
|
};
|
|
@@ -91,7 +96,7 @@ export async function bedrock(argv) {
|
|
|
91
96
|
const pinned = cfg.model === null ? '' : ` (${cfg.model})`;
|
|
92
97
|
process.stderr.write(`metro bedrock: Claude Code → http://127.0.0.1:${String(proxy.port)} → Bedrock ${cfg.region}${pinned}\n`);
|
|
93
98
|
try {
|
|
94
|
-
return await runClaude(claudeArgs(argv), claudeEnv(process.env, proxy.port, token));
|
|
99
|
+
return await runClaude(claudeArgs(argv), claudeEnv(process.env, proxy.port, token, cfg.model));
|
|
95
100
|
}
|
|
96
101
|
finally {
|
|
97
102
|
await proxy.close();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stage-labs/metro",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.30",
|
|
4
4
|
"description": "The metro command line. Sign in once per machine, then hand your MCP connector list to Claude Code without the credentials touching disk, argv or shell history.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
package/runtime/runtime.json
CHANGED