@velum-labs/routekit-gateway 0.9.0
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/LICENSE +201 -0
- package/README.md +28 -0
- package/dist/acp-agent.d.ts +38 -0
- package/dist/acp-agent.js +142 -0
- package/dist/acp-registry.d.ts +36 -0
- package/dist/acp-registry.js +85 -0
- package/dist/adapters/anthropic.d.ts +131 -0
- package/dist/adapters/anthropic.js +1195 -0
- package/dist/adapters/chat.d.ts +14 -0
- package/dist/adapters/chat.js +34 -0
- package/dist/adapters/cursor.d.ts +34 -0
- package/dist/adapters/cursor.js +305 -0
- package/dist/adapters/dropped.d.ts +10 -0
- package/dist/adapters/dropped.js +24 -0
- package/dist/adapters/openai-chat-wire.d.ts +93 -0
- package/dist/adapters/openai-chat-wire.js +143 -0
- package/dist/adapters/responses-stream.d.ts +7 -0
- package/dist/adapters/responses-stream.js +597 -0
- package/dist/adapters/responses.d.ts +174 -0
- package/dist/adapters/responses.js +778 -0
- package/dist/adapters/server-tool-loop.d.ts +94 -0
- package/dist/adapters/server-tool-loop.js +477 -0
- package/dist/adapters/upstream-error.d.ts +14 -0
- package/dist/adapters/upstream-error.js +25 -0
- package/dist/adapters/validate.d.ts +27 -0
- package/dist/adapters/validate.js +180 -0
- package/dist/adapters/web-search.d.ts +46 -0
- package/dist/adapters/web-search.js +151 -0
- package/dist/auth.d.ts +10 -0
- package/dist/auth.js +28 -0
- package/dist/backend.d.ts +151 -0
- package/dist/backend.js +143 -0
- package/dist/capacity-pool.d.ts +31 -0
- package/dist/capacity-pool.js +99 -0
- package/dist/cost.d.ts +49 -0
- package/dist/cost.js +112 -0
- package/dist/endpoint-health.d.ts +54 -0
- package/dist/endpoint-health.js +123 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +23 -0
- package/dist/provenance.d.ts +31 -0
- package/dist/provenance.js +191 -0
- package/dist/provider-backends.d.ts +40 -0
- package/dist/provider-backends.js +1050 -0
- package/dist/provider-source.d.ts +40 -0
- package/dist/provider-source.js +293 -0
- package/dist/router.d.ts +168 -0
- package/dist/router.js +474 -0
- package/dist/server.d.ts +67 -0
- package/dist/server.js +930 -0
- package/dist/sse/chat-assembler.d.ts +45 -0
- package/dist/sse/chat-assembler.js +190 -0
- package/dist/sse/parse.d.ts +50 -0
- package/dist/sse/parse.js +149 -0
- package/dist/sse-wire.d.ts +10 -0
- package/dist/sse-wire.js +31 -0
- package/dist/switching-proxy.d.ts +15 -0
- package/dist/switching-proxy.js +232 -0
- package/dist/test/acp-agent.test.d.ts +1 -0
- package/dist/test/acp-agent.test.js +66 -0
- package/dist/test/acp-registry.test.d.ts +1 -0
- package/dist/test/acp-registry.test.js +70 -0
- package/dist/test/anthropic.test.d.ts +1 -0
- package/dist/test/anthropic.test.js +793 -0
- package/dist/test/auth.test.d.ts +1 -0
- package/dist/test/auth.test.js +25 -0
- package/dist/test/boundary.test.d.ts +1 -0
- package/dist/test/boundary.test.js +32 -0
- package/dist/test/chat.test.d.ts +1 -0
- package/dist/test/chat.test.js +418 -0
- package/dist/test/cost.test.d.ts +1 -0
- package/dist/test/cost.test.js +60 -0
- package/dist/test/cursor.test.d.ts +1 -0
- package/dist/test/cursor.test.js +100 -0
- package/dist/test/drain.test.d.ts +1 -0
- package/dist/test/drain.test.js +116 -0
- package/dist/test/dropped.test.d.ts +1 -0
- package/dist/test/dropped.test.js +80 -0
- package/dist/test/endpoint-health.test.d.ts +1 -0
- package/dist/test/endpoint-health.test.js +73 -0
- package/dist/test/provenance.test.d.ts +1 -0
- package/dist/test/provenance.test.js +176 -0
- package/dist/test/provider-backends.test.d.ts +1 -0
- package/dist/test/provider-backends.test.js +699 -0
- package/dist/test/responses.test.d.ts +1 -0
- package/dist/test/responses.test.js +813 -0
- package/dist/test/routed-backend.test.d.ts +1 -0
- package/dist/test/routed-backend.test.js +39 -0
- package/dist/test/router.test.d.ts +1 -0
- package/dist/test/router.test.js +297 -0
- package/dist/test/server-resilience.test.d.ts +1 -0
- package/dist/test/server-resilience.test.js +169 -0
- package/dist/test/sse-codec.test.d.ts +1 -0
- package/dist/test/sse-codec.test.js +186 -0
- package/dist/test/web-search-loop.test.d.ts +1 -0
- package/dist/test/web-search-loop.test.js +469 -0
- package/dist/test/wire-validation.test.d.ts +1 -0
- package/dist/test/wire-validation.test.js +140 -0
- package/package.json +48 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { authorizedRequest, timingSafeStringEqual, verifyBearerToken } from "../auth.js";
|
|
4
|
+
function requestWithHeaders(headers) {
|
|
5
|
+
return { headers };
|
|
6
|
+
}
|
|
7
|
+
test("timingSafeStringEqual matches only exact strings", () => {
|
|
8
|
+
assert.equal(timingSafeStringEqual("secret", "secret"), true);
|
|
9
|
+
assert.equal(timingSafeStringEqual("secret", "secre_"), false);
|
|
10
|
+
assert.equal(timingSafeStringEqual("secret", "secret-longer"), false);
|
|
11
|
+
assert.equal(timingSafeStringEqual("", ""), true);
|
|
12
|
+
});
|
|
13
|
+
test("verifyBearerToken requires the Bearer prefix and exact token", () => {
|
|
14
|
+
assert.equal(verifyBearerToken("Bearer tok", "tok"), true);
|
|
15
|
+
assert.equal(verifyBearerToken("bearer tok", "tok"), false);
|
|
16
|
+
assert.equal(verifyBearerToken("tok", "tok"), false);
|
|
17
|
+
assert.equal(verifyBearerToken(undefined, "tok"), false);
|
|
18
|
+
});
|
|
19
|
+
test("authorizedRequest accepts bearer header or x-api-key, rejects otherwise", () => {
|
|
20
|
+
assert.equal(authorizedRequest(requestWithHeaders({ authorization: "Bearer tok" }), "tok"), true);
|
|
21
|
+
assert.equal(authorizedRequest(requestWithHeaders({ "x-api-key": "tok" }), "tok"), true);
|
|
22
|
+
assert.equal(authorizedRequest(requestWithHeaders({ authorization: "Bearer nope" }), "tok"), false);
|
|
23
|
+
assert.equal(authorizedRequest(requestWithHeaders({ "x-api-key": "nope" }), "tok"), false);
|
|
24
|
+
assert.equal(authorizedRequest(requestWithHeaders({}), "tok"), false);
|
|
25
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { test } from "node:test";
|
|
6
|
+
const packageRoot = join(dirname(fileURLToPath(import.meta.url)), "../../..");
|
|
7
|
+
function productionSources(root) {
|
|
8
|
+
const files = [];
|
|
9
|
+
const visit = (directory) => {
|
|
10
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
11
|
+
if (entry.name === "test" || entry.name === "__tests__")
|
|
12
|
+
continue;
|
|
13
|
+
const path = join(directory, entry.name);
|
|
14
|
+
if (entry.isDirectory())
|
|
15
|
+
visit(path);
|
|
16
|
+
else if (entry.isFile() && entry.name.endsWith(".ts"))
|
|
17
|
+
files.push(path);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
visit(join(root, "src"));
|
|
21
|
+
return files;
|
|
22
|
+
}
|
|
23
|
+
test("RouteKit gateway and accounts have no FusionKit dependency edge", () => {
|
|
24
|
+
for (const directory of ["model-gateway", "accounts"]) {
|
|
25
|
+
const root = join(packageRoot, directory);
|
|
26
|
+
const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
27
|
+
assert.equal(Object.keys(manifest.dependencies ?? {}).some((name) => name.startsWith("@fusionkit/")), false, `${directory} manifest`);
|
|
28
|
+
for (const path of productionSources(root)) {
|
|
29
|
+
assert.doesNotMatch(readFileSync(path, "utf8"), /@fusionkit\//, path);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
import { test } from "node:test";
|
|
4
|
+
import { OpenAiBackend } from "../backend.js";
|
|
5
|
+
import { MODEL_CALL_ID_HEADER } from "../provenance.js";
|
|
6
|
+
import { collectAttribution, initialAttribution, startGateway } from "../server.js";
|
|
7
|
+
function sendJson(res, status, value) {
|
|
8
|
+
res.statusCode = status;
|
|
9
|
+
res.setHeader("content-type", "application/json");
|
|
10
|
+
res.end(Buffer.from(JSON.stringify(value), "utf8"));
|
|
11
|
+
}
|
|
12
|
+
async function readAll(req) {
|
|
13
|
+
const chunks = [];
|
|
14
|
+
for await (const chunk of req)
|
|
15
|
+
chunks.push(chunk);
|
|
16
|
+
return Buffer.concat(chunks);
|
|
17
|
+
}
|
|
18
|
+
async function captureStderr(run) {
|
|
19
|
+
const original = process.stderr.write.bind(process.stderr);
|
|
20
|
+
let stderr = "";
|
|
21
|
+
process.stderr.write = ((chunk) => {
|
|
22
|
+
stderr += typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk);
|
|
23
|
+
return true;
|
|
24
|
+
});
|
|
25
|
+
try {
|
|
26
|
+
return { result: await run(), stderr };
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
process.stderr.write = original;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function startMock() {
|
|
33
|
+
let lastChatBody;
|
|
34
|
+
let lastModelCallId;
|
|
35
|
+
const server = createServer((req, res) => {
|
|
36
|
+
void (async () => {
|
|
37
|
+
const path = new URL(req.url ?? "/", "http://localhost").pathname;
|
|
38
|
+
if (req.method === "GET" && path === "/v1/models") {
|
|
39
|
+
sendJson(res, 200, { object: "list", data: [{ id: "mock-model", object: "model" }] });
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (req.method === "POST" && path === "/v1/chat/completions") {
|
|
43
|
+
const body = JSON.parse((await readAll(req)).toString("utf8"));
|
|
44
|
+
lastChatBody = body;
|
|
45
|
+
lastModelCallId =
|
|
46
|
+
typeof req.headers[MODEL_CALL_ID_HEADER] === "string"
|
|
47
|
+
? req.headers[MODEL_CALL_ID_HEADER]
|
|
48
|
+
: undefined;
|
|
49
|
+
if (body.model === "fail-model") {
|
|
50
|
+
sendJson(res, 500, { error: { message: "upstream failed" } });
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (body.stream === true) {
|
|
54
|
+
res.statusCode = 200;
|
|
55
|
+
res.setHeader("content-type", "text/event-stream");
|
|
56
|
+
res.write('data: {"choices":[{"delta":{"content":"hi"}}]}\n\n');
|
|
57
|
+
res.write('data: {"usage":{"prompt_tokens":1,"completion_tokens":1}}\n\n');
|
|
58
|
+
res.write("data: [DONE]\n\n");
|
|
59
|
+
res.end();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
sendJson(res, 200, {
|
|
63
|
+
id: "chatcmpl-mock",
|
|
64
|
+
object: "chat.completion",
|
|
65
|
+
model: body.model,
|
|
66
|
+
choices: [
|
|
67
|
+
{ index: 0, message: { role: "assistant", content: "ok" }, finish_reason: "stop" }
|
|
68
|
+
],
|
|
69
|
+
usage: { prompt_tokens: 3, completion_tokens: 2, total_tokens: 5 }
|
|
70
|
+
});
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
sendJson(res, 404, { error: "not found" });
|
|
74
|
+
})();
|
|
75
|
+
});
|
|
76
|
+
await new Promise((resolve) => server.listen(0, "127.0.0.1", () => resolve()));
|
|
77
|
+
const address = server.address();
|
|
78
|
+
const port = typeof address === "object" && address !== null ? address.port : 0;
|
|
79
|
+
return {
|
|
80
|
+
url: `http://127.0.0.1:${port}`,
|
|
81
|
+
lastChatBody: () => lastChatBody,
|
|
82
|
+
lastModelCallId: () => lastModelCallId,
|
|
83
|
+
close: () => new Promise((resolve, reject) => server.close((e) => (e ? reject(e) : resolve())))
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
test("injects the default model and pipes the completion back", async () => {
|
|
87
|
+
const mock = await startMock();
|
|
88
|
+
const backend = new OpenAiBackend({ baseUrl: `${mock.url}/v1`, defaultModel: "mlx-default" });
|
|
89
|
+
const records = [];
|
|
90
|
+
const gateway = await startGateway({
|
|
91
|
+
backend,
|
|
92
|
+
provenance: { onModelCall: (record) => records.push(record) }
|
|
93
|
+
});
|
|
94
|
+
try {
|
|
95
|
+
const response = await fetch(`${gateway.url()}/v1/chat/completions`, {
|
|
96
|
+
method: "POST",
|
|
97
|
+
headers: { "content-type": "application/json" },
|
|
98
|
+
body: JSON.stringify({ messages: [{ role: "user", content: "hi" }] })
|
|
99
|
+
});
|
|
100
|
+
assert.equal(response.status, 200);
|
|
101
|
+
const callId = response.headers.get(MODEL_CALL_ID_HEADER);
|
|
102
|
+
assert.ok(callId?.startsWith("model_call_"));
|
|
103
|
+
assert.equal(mock.lastModelCallId(), callId);
|
|
104
|
+
const json = (await response.json());
|
|
105
|
+
assert.equal(json.object, "chat.completion");
|
|
106
|
+
assert.equal(mock.lastChatBody()?.model, "mlx-default");
|
|
107
|
+
assert.equal(records.length, 1);
|
|
108
|
+
assert.equal(records[0]?.call_id, callId);
|
|
109
|
+
assert.equal(records[0]?.metadata?.dialect, "openai-chat");
|
|
110
|
+
assert.equal(records[0]?.model, "mlx-default");
|
|
111
|
+
assert.equal(records[0]?.metadata?.stream, false);
|
|
112
|
+
assert.equal(records[0]?.usage?.total_tokens, 5);
|
|
113
|
+
assert.equal(records[0]?.metadata?.unknown_usage, false);
|
|
114
|
+
assert.equal(records[0]?.metadata?.unknown_cost, true);
|
|
115
|
+
assert.ok(!JSON.stringify(records[0]).includes('"hi"'));
|
|
116
|
+
assert.ok(!JSON.stringify(records[0]).includes('"ok"'));
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
await gateway.close();
|
|
120
|
+
await mock.close();
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
test("compound provider operations are not counted as retries", () => {
|
|
124
|
+
const attribution = collectAttribution({
|
|
125
|
+
effective_model: "codex/gpt-test",
|
|
126
|
+
native_model: "gpt-test",
|
|
127
|
+
provider: "codex",
|
|
128
|
+
billing_mode: "subscription"
|
|
129
|
+
});
|
|
130
|
+
attribution.report({
|
|
131
|
+
accountAttempt: { operationId: "step-1", seat: "seat_a" }
|
|
132
|
+
});
|
|
133
|
+
attribution.report({
|
|
134
|
+
accountAttempt: { operationId: "step-2", seat: "seat_b" }
|
|
135
|
+
});
|
|
136
|
+
assert.deepEqual(attribution.snapshot(), {
|
|
137
|
+
effective_model: "codex/gpt-test",
|
|
138
|
+
native_model: "gpt-test",
|
|
139
|
+
provider: "codex",
|
|
140
|
+
billing_mode: "subscription",
|
|
141
|
+
account: { seat: "seat_b" },
|
|
142
|
+
attempts: 2,
|
|
143
|
+
retries: 0,
|
|
144
|
+
account_failovers: 0
|
|
145
|
+
});
|
|
146
|
+
attribution.report({
|
|
147
|
+
accountAttempt: { operationId: "step-2", seat: "seat_b" }
|
|
148
|
+
});
|
|
149
|
+
attribution.report({
|
|
150
|
+
accountAttempt: { operationId: "step-2", seat: "seat_c" }
|
|
151
|
+
});
|
|
152
|
+
assert.deepEqual(attribution.snapshot()?.retries, 2);
|
|
153
|
+
assert.deepEqual(attribution.snapshot()?.account_failovers, 1);
|
|
154
|
+
});
|
|
155
|
+
test("rejected bare native aliases retain subscription attribution", () => {
|
|
156
|
+
const backend = {
|
|
157
|
+
defaultModel: undefined,
|
|
158
|
+
resolveModelRoute: () => undefined,
|
|
159
|
+
chat: async () => new Response("{}"),
|
|
160
|
+
models: async () => new Response("{}"),
|
|
161
|
+
embeddings: async () => new Response("{}")
|
|
162
|
+
};
|
|
163
|
+
assert.deepEqual(initialAttribution(backend, "missing-codex-model", "codex"), {
|
|
164
|
+
effective_model: "missing-codex-model",
|
|
165
|
+
native_model: "missing-codex-model",
|
|
166
|
+
provider: "codex",
|
|
167
|
+
billing_mode: "subscription"
|
|
168
|
+
});
|
|
169
|
+
assert.deepEqual(initialAttribution(backend, "missing-claude-model", "claude-code"), {
|
|
170
|
+
effective_model: "missing-claude-model",
|
|
171
|
+
native_model: "missing-claude-model",
|
|
172
|
+
provider: "claude-code",
|
|
173
|
+
billing_mode: "subscription"
|
|
174
|
+
});
|
|
175
|
+
assert.deepEqual(initialAttribution(backend, undefined), {});
|
|
176
|
+
});
|
|
177
|
+
test("embeddings receive a call id and sanitized attribution record", async () => {
|
|
178
|
+
const records = [];
|
|
179
|
+
const gateway = await startGateway({
|
|
180
|
+
backend: {
|
|
181
|
+
defaultModel: "openai/text-embedding-test",
|
|
182
|
+
resolveModelRoute: () => ({
|
|
183
|
+
publicId: "openai/text-embedding-test",
|
|
184
|
+
nativeId: "text-embedding-test",
|
|
185
|
+
provider: "openai"
|
|
186
|
+
}),
|
|
187
|
+
chat: async () => new Response("{}"),
|
|
188
|
+
models: async () => new Response("{}"),
|
|
189
|
+
embeddings: async (_body, _signal, options) => {
|
|
190
|
+
options?.onAttribution?.({
|
|
191
|
+
effective_model: "openai/text-embedding-test",
|
|
192
|
+
native_model: "text-embedding-test",
|
|
193
|
+
provider: "openai",
|
|
194
|
+
billing_mode: "api_key"
|
|
195
|
+
});
|
|
196
|
+
return Response.json({
|
|
197
|
+
data: [{ embedding: [0.1] }],
|
|
198
|
+
usage: { prompt_tokens: 3, total_tokens: 3 }
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
provenance: { onModelCall: (record) => records.push(record) }
|
|
203
|
+
});
|
|
204
|
+
try {
|
|
205
|
+
const response = await fetch(`${gateway.url()}/v1/embeddings`, {
|
|
206
|
+
method: "POST",
|
|
207
|
+
headers: { "content-type": "application/json" },
|
|
208
|
+
body: JSON.stringify({
|
|
209
|
+
model: "openai/text-embedding-test",
|
|
210
|
+
input: "hello"
|
|
211
|
+
})
|
|
212
|
+
});
|
|
213
|
+
assert.equal(response.status, 200);
|
|
214
|
+
assert.equal(response.headers.get(MODEL_CALL_ID_HEADER), records[0]?.call_id);
|
|
215
|
+
assert.equal(records[0]?.metadata?.dialect, "openai-embeddings");
|
|
216
|
+
assert.equal(records[0]?.usage?.total_tokens, 3);
|
|
217
|
+
assert.deepEqual(records[0]?.metadata?.attribution, {
|
|
218
|
+
effective_model: "openai/text-embedding-test",
|
|
219
|
+
native_model: "text-embedding-test",
|
|
220
|
+
provider: "openai",
|
|
221
|
+
billing_mode: "api_key",
|
|
222
|
+
attempts: 1,
|
|
223
|
+
retries: 0,
|
|
224
|
+
account_failovers: 0
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
await gateway.close();
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
test("records failed upstream responses as failed model-call records", async () => {
|
|
232
|
+
const mock = await startMock();
|
|
233
|
+
const records = [];
|
|
234
|
+
const gateway = await startGateway({
|
|
235
|
+
backend: new OpenAiBackend({ baseUrl: `${mock.url}/v1` }),
|
|
236
|
+
provenance: { onModelCall: (record) => records.push(record) }
|
|
237
|
+
});
|
|
238
|
+
try {
|
|
239
|
+
const response = await fetch(`${gateway.url()}/v1/chat/completions`, {
|
|
240
|
+
method: "POST",
|
|
241
|
+
headers: { "content-type": "application/json" },
|
|
242
|
+
body: JSON.stringify({ model: "fail-model", messages: [{ role: "user", content: "fail" }] })
|
|
243
|
+
});
|
|
244
|
+
assert.equal(response.status, 500);
|
|
245
|
+
assert.equal(records.length, 1);
|
|
246
|
+
assert.equal(records[0]?.status, "failed");
|
|
247
|
+
assert.equal(records[0]?.error?.kind, "provider_error");
|
|
248
|
+
}
|
|
249
|
+
finally {
|
|
250
|
+
await gateway.close();
|
|
251
|
+
await mock.close();
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
test("redacts thrown backend failures from stderr and the wire response", async () => {
|
|
255
|
+
const secret = "sk-live-secret-from-upstream";
|
|
256
|
+
const records = [];
|
|
257
|
+
const gateway = await startGateway({
|
|
258
|
+
backend: {
|
|
259
|
+
defaultModel: "throw-model",
|
|
260
|
+
chat: async () => {
|
|
261
|
+
throw new Error(`backend exploded with ${secret}`);
|
|
262
|
+
},
|
|
263
|
+
models: async () => new Response("{}", { status: 200 }),
|
|
264
|
+
embeddings: async () => new Response("{}", { status: 200 })
|
|
265
|
+
},
|
|
266
|
+
provenance: { onModelCall: (record) => records.push(record) }
|
|
267
|
+
});
|
|
268
|
+
try {
|
|
269
|
+
const { result: response, stderr } = await captureStderr(async () => {
|
|
270
|
+
return await fetch(`${gateway.url()}/v1/chat/completions`, {
|
|
271
|
+
method: "POST",
|
|
272
|
+
headers: { "content-type": "application/json" },
|
|
273
|
+
body: JSON.stringify({ messages: [{ role: "user", content: "fail" }] })
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
assert.equal(response.status, 502);
|
|
277
|
+
assert.deepEqual(await response.json(), {
|
|
278
|
+
error: { message: "upstream request failed", type: "upstream_error" }
|
|
279
|
+
});
|
|
280
|
+
assert.match(stderr, /routekit gateway upstream error: type=Error/);
|
|
281
|
+
assert.equal(stderr.includes(secret), false);
|
|
282
|
+
assert.equal(stderr.includes("backend exploded"), false);
|
|
283
|
+
assert.equal(response.headers.get(MODEL_CALL_ID_HEADER), records[0]?.call_id);
|
|
284
|
+
assert.equal(records.length, 1);
|
|
285
|
+
assert.equal(records[0]?.status, "failed");
|
|
286
|
+
assert.equal(records[0]?.error?.kind, "provider_error");
|
|
287
|
+
assert.equal(records[0]?.metadata?.unknown_usage, true);
|
|
288
|
+
}
|
|
289
|
+
finally {
|
|
290
|
+
await gateway.close();
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
test("preserves an explicitly requested model", async () => {
|
|
294
|
+
const mock = await startMock();
|
|
295
|
+
const gateway = await startGateway({
|
|
296
|
+
backend: new OpenAiBackend({ baseUrl: `${mock.url}/v1`, defaultModel: "mlx-default" })
|
|
297
|
+
});
|
|
298
|
+
try {
|
|
299
|
+
const response = await fetch(`${gateway.url()}/v1/chat/completions`, {
|
|
300
|
+
method: "POST",
|
|
301
|
+
headers: { "content-type": "application/json" },
|
|
302
|
+
body: JSON.stringify({ model: "explicit-model", messages: [{ role: "user", content: "hi" }] })
|
|
303
|
+
});
|
|
304
|
+
assert.equal(response.status, 200);
|
|
305
|
+
assert.equal(mock.lastChatBody()?.model, "explicit-model");
|
|
306
|
+
}
|
|
307
|
+
finally {
|
|
308
|
+
await gateway.close();
|
|
309
|
+
await mock.close();
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
test("forceModel overrides the requested model on every upstream call", async () => {
|
|
313
|
+
const mock = await startMock();
|
|
314
|
+
const gateway = await startGateway({
|
|
315
|
+
backend: new OpenAiBackend({ baseUrl: `${mock.url}/v1`, defaultModel: "advertised", forceModel: "routed-endpoint" })
|
|
316
|
+
});
|
|
317
|
+
try {
|
|
318
|
+
const response = await fetch(`${gateway.url()}/v1/chat/completions`, {
|
|
319
|
+
method: "POST",
|
|
320
|
+
headers: { "content-type": "application/json" },
|
|
321
|
+
body: JSON.stringify({ model: "whatever-the-cli-picked", messages: [{ role: "user", content: "hi" }] })
|
|
322
|
+
});
|
|
323
|
+
assert.equal(response.status, 200);
|
|
324
|
+
// The driving client's model is ignored; the dedicated capture gateway routes
|
|
325
|
+
// every call to its candidate's routed model id.
|
|
326
|
+
assert.equal(mock.lastChatBody()?.model, "routed-endpoint");
|
|
327
|
+
}
|
|
328
|
+
finally {
|
|
329
|
+
await gateway.close();
|
|
330
|
+
await mock.close();
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
test("lists models from the backend", async () => {
|
|
334
|
+
const mock = await startMock();
|
|
335
|
+
const gateway = await startGateway({
|
|
336
|
+
backend: new OpenAiBackend({ baseUrl: `${mock.url}/v1` })
|
|
337
|
+
});
|
|
338
|
+
try {
|
|
339
|
+
const response = await fetch(`${gateway.url()}/v1/models`);
|
|
340
|
+
assert.equal(response.status, 200);
|
|
341
|
+
const json = (await response.json());
|
|
342
|
+
assert.equal(json.data.length, 1);
|
|
343
|
+
}
|
|
344
|
+
finally {
|
|
345
|
+
await gateway.close();
|
|
346
|
+
await mock.close();
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
test("streams server-sent events straight through", async () => {
|
|
350
|
+
const mock = await startMock();
|
|
351
|
+
const gateway = await startGateway({
|
|
352
|
+
backend: new OpenAiBackend({ baseUrl: `${mock.url}/v1`, defaultModel: "mlx-default" })
|
|
353
|
+
});
|
|
354
|
+
try {
|
|
355
|
+
const response = await fetch(`${gateway.url()}/v1/chat/completions`, {
|
|
356
|
+
method: "POST",
|
|
357
|
+
headers: { "content-type": "application/json" },
|
|
358
|
+
body: JSON.stringify({ stream: true, messages: [{ role: "user", content: "hi" }] })
|
|
359
|
+
});
|
|
360
|
+
assert.equal(response.status, 200);
|
|
361
|
+
assert.equal(response.headers.get("content-type"), "text/event-stream");
|
|
362
|
+
const text = await response.text();
|
|
363
|
+
assert.ok(text.includes("data: [DONE]"));
|
|
364
|
+
}
|
|
365
|
+
finally {
|
|
366
|
+
await gateway.close();
|
|
367
|
+
await mock.close();
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
test("returns 404 for an unknown route", async () => {
|
|
371
|
+
const gateway = await startGateway({
|
|
372
|
+
backend: new OpenAiBackend({ baseUrl: "http://127.0.0.1:1/v1" })
|
|
373
|
+
});
|
|
374
|
+
try {
|
|
375
|
+
const response = await fetch(`${gateway.url()}/v1/unknown`, { method: "POST", body: "{}" });
|
|
376
|
+
assert.equal(response.status, 404);
|
|
377
|
+
}
|
|
378
|
+
finally {
|
|
379
|
+
await gateway.close();
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
test("rejects malformed JSON with 400", async () => {
|
|
383
|
+
const gateway = await startGateway({
|
|
384
|
+
backend: new OpenAiBackend({ baseUrl: "http://127.0.0.1:1/v1" })
|
|
385
|
+
});
|
|
386
|
+
try {
|
|
387
|
+
const response = await fetch(`${gateway.url()}/v1/chat/completions`, {
|
|
388
|
+
method: "POST",
|
|
389
|
+
headers: { "content-type": "application/json" },
|
|
390
|
+
body: "{not json"
|
|
391
|
+
});
|
|
392
|
+
assert.equal(response.status, 400);
|
|
393
|
+
}
|
|
394
|
+
finally {
|
|
395
|
+
await gateway.close();
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
test("enforces the auth token when configured", async () => {
|
|
399
|
+
const mock = await startMock();
|
|
400
|
+
const gateway = await startGateway({
|
|
401
|
+
backend: new OpenAiBackend({ baseUrl: `${mock.url}/v1`, defaultModel: "mlx-default" }),
|
|
402
|
+
authToken: "secret"
|
|
403
|
+
});
|
|
404
|
+
try {
|
|
405
|
+
const unauthorized = await fetch(`${gateway.url()}/v1/models`);
|
|
406
|
+
assert.equal(unauthorized.status, 401);
|
|
407
|
+
const authorized = await fetch(`${gateway.url()}/v1/models`, {
|
|
408
|
+
headers: { authorization: "Bearer secret" }
|
|
409
|
+
});
|
|
410
|
+
assert.equal(authorized.status, 200);
|
|
411
|
+
const health = await fetch(`${gateway.url()}/health`);
|
|
412
|
+
assert.equal(health.status, 200);
|
|
413
|
+
}
|
|
414
|
+
finally {
|
|
415
|
+
await gateway.close();
|
|
416
|
+
await mock.close();
|
|
417
|
+
}
|
|
418
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { estimateCost, lookupPricing, meterCall, parseUsage, parseUsageFromSse } from "../cost.js";
|
|
4
|
+
test("single-call metering normalizes provider usage and registry pricing", () => {
|
|
5
|
+
assert.deepEqual(parseUsage({ input_tokens: 80, output_tokens: 20 }), {
|
|
6
|
+
promptTokens: 80,
|
|
7
|
+
completionTokens: 20,
|
|
8
|
+
totalTokens: 100
|
|
9
|
+
});
|
|
10
|
+
assert.deepEqual(lookupPricing("gpt-5.5"), {
|
|
11
|
+
inputPer1mTokens: 1.25,
|
|
12
|
+
outputPer1mTokens: 10
|
|
13
|
+
});
|
|
14
|
+
assert.deepEqual(estimateCost({ promptTokens: 1_000_000, completionTokens: 1_000_000 }, lookupPricing("gpt-5.5")), { costUsd: 11.25, partialUsage: false });
|
|
15
|
+
});
|
|
16
|
+
test("SSE usage extraction retains the last provider usage block", () => {
|
|
17
|
+
const text = `data: ${JSON.stringify({ choices: [{ delta: { content: "first" } }] })}\n\n` +
|
|
18
|
+
`data: ${JSON.stringify({
|
|
19
|
+
choices: [],
|
|
20
|
+
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }
|
|
21
|
+
})}\n\n` +
|
|
22
|
+
"data: [DONE]\n\n";
|
|
23
|
+
assert.deepEqual(parseUsageFromSse(text), {
|
|
24
|
+
promptTokens: 10,
|
|
25
|
+
completionTokens: 5,
|
|
26
|
+
totalTokens: 15
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
test("SSE usage extraction reads nested Responses completion usage", () => {
|
|
30
|
+
const text = `data: ${JSON.stringify({
|
|
31
|
+
type: "response.completed",
|
|
32
|
+
response: {
|
|
33
|
+
usage: {
|
|
34
|
+
input_tokens: 30,
|
|
35
|
+
output_tokens: 12,
|
|
36
|
+
total_tokens: 42
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
})}\n\n` +
|
|
40
|
+
"data: [DONE]\n\n";
|
|
41
|
+
assert.deepEqual(parseUsageFromSse(text), {
|
|
42
|
+
promptTokens: 30,
|
|
43
|
+
completionTokens: 12,
|
|
44
|
+
totalTokens: 42
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
test("provider-reported cost is canonical over a registry estimate", () => {
|
|
48
|
+
const call = meterCall({
|
|
49
|
+
model: "gpt-5.5",
|
|
50
|
+
usage: { promptTokens: 1000, completionTokens: 1000 },
|
|
51
|
+
providerCost: {
|
|
52
|
+
source: "provider",
|
|
53
|
+
costUsd: 0.0123,
|
|
54
|
+
providerName: "provider"
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
assert.equal(call.costUsd, 0.0123);
|
|
58
|
+
assert.equal(call.providerCostUsd, 0.0123);
|
|
59
|
+
assert.equal(call.unknownCost, false);
|
|
60
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { isCursorChatBody, translateCursorRequest } from "../adapters/cursor.js";
|
|
4
|
+
import { startGateway } from "../server.js";
|
|
5
|
+
const cursorBody = {
|
|
6
|
+
model: "route-primary",
|
|
7
|
+
input: [
|
|
8
|
+
{ type: "message", role: "developer", content: "You are a coding agent." },
|
|
9
|
+
{ type: "message", role: "user", content: [{ type: "input_text", text: "fix the bug" }] },
|
|
10
|
+
{
|
|
11
|
+
type: "function_call",
|
|
12
|
+
call_id: "call_1",
|
|
13
|
+
name: "read_file",
|
|
14
|
+
arguments: "{\"path\":\"a.ts\"}"
|
|
15
|
+
},
|
|
16
|
+
{ type: "function_call_output", call_id: "call_1", output: "source" }
|
|
17
|
+
],
|
|
18
|
+
stream: true,
|
|
19
|
+
tools: [
|
|
20
|
+
{
|
|
21
|
+
type: "function",
|
|
22
|
+
name: "read_file",
|
|
23
|
+
parameters: { type: "object" }
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
};
|
|
27
|
+
test("Cursor hybrid requests translate to chat messages and tools", () => {
|
|
28
|
+
assert.equal(isCursorChatBody(cursorBody), true);
|
|
29
|
+
const translated = translateCursorRequest(cursorBody);
|
|
30
|
+
assert.equal(translated.model, "route-primary");
|
|
31
|
+
assert.equal(translated.stream, true);
|
|
32
|
+
assert.deepEqual(translated.messages, [
|
|
33
|
+
{ role: "system", content: "You are a coding agent." },
|
|
34
|
+
{ role: "user", content: "fix the bug" },
|
|
35
|
+
{
|
|
36
|
+
role: "assistant",
|
|
37
|
+
content: "",
|
|
38
|
+
tool_calls: [
|
|
39
|
+
{
|
|
40
|
+
id: "call_1",
|
|
41
|
+
type: "function",
|
|
42
|
+
function: { name: "read_file", arguments: "{\"path\":\"a.ts\"}" }
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
},
|
|
46
|
+
{ role: "tool", tool_call_id: "call_1", content: "source" }
|
|
47
|
+
]);
|
|
48
|
+
assert.equal(translated.tools[0]?.function.name, "read_file");
|
|
49
|
+
});
|
|
50
|
+
test("Cursor hybrid detection rejects unrelated bodies", () => {
|
|
51
|
+
assert.equal(isCursorChatBody({ input: "hello" }), true);
|
|
52
|
+
assert.equal(isCursorChatBody({ messages: [] }), true);
|
|
53
|
+
assert.equal(isCursorChatBody({ model: "route-primary" }), false);
|
|
54
|
+
assert.equal(isCursorChatBody(null), false);
|
|
55
|
+
});
|
|
56
|
+
test("RouteKit serves the Cursor hybrid through its neutral HTTP boundary", async () => {
|
|
57
|
+
let received;
|
|
58
|
+
const backend = {
|
|
59
|
+
defaultModel: "route-primary",
|
|
60
|
+
chat(body) {
|
|
61
|
+
received = body;
|
|
62
|
+
return Promise.resolve(Response.json({
|
|
63
|
+
id: "chatcmpl_1",
|
|
64
|
+
object: "chat.completion",
|
|
65
|
+
model: "route-primary",
|
|
66
|
+
choices: [
|
|
67
|
+
{
|
|
68
|
+
index: 0,
|
|
69
|
+
message: { role: "assistant", content: "done" },
|
|
70
|
+
finish_reason: "stop"
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
}));
|
|
74
|
+
},
|
|
75
|
+
models: () => Promise.resolve(Response.json({
|
|
76
|
+
object: "list",
|
|
77
|
+
data: [{ id: "route-primary", object: "model" }]
|
|
78
|
+
})),
|
|
79
|
+
embeddings: () => Promise.resolve(new Response(null, { status: 501 }))
|
|
80
|
+
};
|
|
81
|
+
const gateway = await startGateway({ backend });
|
|
82
|
+
try {
|
|
83
|
+
const response = await fetch(`${gateway.url()}/v1/cursor/chat/completions`, {
|
|
84
|
+
method: "POST",
|
|
85
|
+
headers: { "content-type": "application/json" },
|
|
86
|
+
body: JSON.stringify({ ...cursorBody, stream: false })
|
|
87
|
+
});
|
|
88
|
+
assert.equal(response.status, 200);
|
|
89
|
+
assert.deepEqual(received, {
|
|
90
|
+
...translateCursorRequest({ ...cursorBody, stream: false }),
|
|
91
|
+
model: "route-primary"
|
|
92
|
+
});
|
|
93
|
+
const models = await fetch(`${gateway.url()}/v1/cursor/models`);
|
|
94
|
+
assert.equal(models.status, 200);
|
|
95
|
+
assert.deepEqual((await models.json()).data.map((model) => model.id), ["route-primary"]);
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
await gateway.close();
|
|
99
|
+
}
|
|
100
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|