offrouter-core 0.2.1 → 0.2.2
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/audit.d.ts +137 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +320 -0
- package/dist/audit.js.map +1 -0
- package/dist/auth/credential-chain.d.ts +84 -0
- package/dist/auth/credential-chain.d.ts.map +1 -0
- package/dist/auth/credential-chain.js +150 -0
- package/dist/auth/credential-chain.js.map +1 -0
- package/dist/auth/index.d.ts +10 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +7 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/keychain.d.ts +60 -0
- package/dist/auth/keychain.d.ts.map +1 -0
- package/dist/auth/keychain.js +104 -0
- package/dist/auth/keychain.js.map +1 -0
- package/dist/auth/oauth-pkce.d.ts +138 -0
- package/dist/auth/oauth-pkce.d.ts.map +1 -0
- package/dist/auth/oauth-pkce.js +375 -0
- package/dist/auth/oauth-pkce.js.map +1 -0
- package/dist/auth/oauth-server.d.ts +36 -0
- package/dist/auth/oauth-server.d.ts.map +1 -0
- package/dist/auth/oauth-server.js +210 -0
- package/dist/auth/oauth-server.js.map +1 -0
- package/dist/config.d.ts +76 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +355 -0
- package/dist/config.js.map +1 -0
- package/dist/delegation.d.ts +123 -0
- package/dist/delegation.d.ts.map +1 -0
- package/dist/delegation.js +455 -0
- package/dist/delegation.js.map +1 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/policy.d.ts +45 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +318 -0
- package/dist/policy.js.map +1 -0
- package/{src/protocol.ts → dist/protocol.d.ts} +2 -2
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +6 -0
- package/dist/protocol.js.map +1 -0
- package/dist/providers/adapter.d.ts +137 -0
- package/dist/providers/adapter.d.ts.map +1 -0
- package/dist/providers/adapter.js +163 -0
- package/dist/providers/adapter.js.map +1 -0
- package/dist/providers/catalog-data.d.ts +128 -0
- package/dist/providers/catalog-data.d.ts.map +1 -0
- package/dist/providers/catalog-data.js +397 -0
- package/dist/providers/catalog-data.js.map +1 -0
- package/dist/providers/catalog.d.ts +63 -0
- package/dist/providers/catalog.d.ts.map +1 -0
- package/dist/providers/catalog.js +394 -0
- package/dist/providers/catalog.js.map +1 -0
- package/dist/providers/fake.d.ts +46 -0
- package/dist/providers/fake.d.ts.map +1 -0
- package/dist/providers/fake.js +234 -0
- package/dist/providers/fake.js.map +1 -0
- package/dist/providers/openai-compatible.d.ts +65 -0
- package/dist/providers/openai-compatible.d.ts.map +1 -0
- package/dist/providers/openai-compatible.js +434 -0
- package/dist/providers/openai-compatible.js.map +1 -0
- package/dist/proxy/responses-mapper.d.ts +366 -0
- package/dist/proxy/responses-mapper.d.ts.map +1 -0
- package/dist/proxy/responses-mapper.js +517 -0
- package/dist/proxy/responses-mapper.js.map +1 -0
- package/dist/proxy/responses-server.d.ts +29 -0
- package/dist/proxy/responses-server.d.ts.map +1 -0
- package/dist/proxy/responses-server.js +360 -0
- package/dist/proxy/responses-server.js.map +1 -0
- package/dist/router.d.ts +18 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +296 -0
- package/dist/router.js.map +1 -0
- package/dist/schemas.d.ts +560 -0
- package/dist/schemas.d.ts.map +1 -0
- package/{src/schemas.ts → dist/schemas.js} +83 -103
- package/dist/schemas.js.map +1 -0
- package/dist/secrets.d.ts +112 -0
- package/dist/secrets.d.ts.map +1 -0
- package/dist/secrets.js +326 -0
- package/dist/secrets.js.map +1 -0
- package/dist/types.d.ts +117 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/usage-file.d.ts +59 -0
- package/dist/usage-file.d.ts.map +1 -0
- package/dist/usage-file.js +316 -0
- package/dist/usage-file.js.map +1 -0
- package/dist/usage.d.ts +235 -0
- package/dist/usage.d.ts.map +1 -0
- package/dist/usage.js +517 -0
- package/dist/usage.js.map +1 -0
- package/package.json +7 -2
- package/src/audit.test.ts +0 -302
- package/src/audit.ts +0 -553
- package/src/auth/credential-chain.test.ts +0 -326
- package/src/auth/credential-chain.ts +0 -235
- package/src/auth/index.ts +0 -45
- package/src/auth/keychain.test.ts +0 -265
- package/src/auth/keychain.ts +0 -168
- package/src/auth/oauth-pkce.test.ts +0 -329
- package/src/auth/oauth-pkce.ts +0 -571
- package/src/auth/oauth-server.test.ts +0 -83
- package/src/auth/oauth-server.ts +0 -296
- package/src/config.test.ts +0 -479
- package/src/config.ts +0 -505
- package/src/delegation.test.ts +0 -368
- package/src/delegation.ts +0 -605
- package/src/index.ts +0 -280
- package/src/policy.test.ts +0 -634
- package/src/policy.ts +0 -420
- package/src/providers/adapter.test.ts +0 -293
- package/src/providers/adapter.ts +0 -328
- package/src/providers/catalog-data.test.ts +0 -258
- package/src/providers/catalog-data.ts +0 -498
- package/src/providers/catalog.test.ts +0 -84
- package/src/providers/catalog.ts +0 -483
- package/src/providers/fake.ts +0 -312
- package/src/providers/openai-compatible.test.ts +0 -366
- package/src/providers/openai-compatible.ts +0 -554
- package/src/proxy/responses-mapper.test.ts +0 -290
- package/src/proxy/responses-mapper.ts +0 -736
- package/src/proxy/responses-server.test.ts +0 -322
- package/src/proxy/responses-server.ts +0 -469
- package/src/router.test.ts +0 -699
- package/src/router.ts +0 -352
- package/src/schemas.test.ts +0 -291
- package/src/secrets.test.ts +0 -271
- package/src/secrets.ts +0 -461
- package/src/types.ts +0 -173
- package/src/usage-file.test.ts +0 -243
- package/src/usage-file.ts +0 -435
- package/src/usage.test.ts +0 -335
- package/src/usage.ts +0 -859
- package/tsconfig.json +0 -9
|
@@ -1,322 +0,0 @@
|
|
|
1
|
-
import http from "node:http";
|
|
2
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
3
|
-
import { InMemoryAuditStore } from "../audit.js";
|
|
4
|
-
import { ProviderError } from "../providers/adapter.js";
|
|
5
|
-
import { createFakeProvider } from "../providers/fake.js";
|
|
6
|
-
import {
|
|
7
|
-
createResponsesProxyContext,
|
|
8
|
-
type ResponsesProxyContext,
|
|
9
|
-
} from "./responses-mapper.js";
|
|
10
|
-
import {
|
|
11
|
-
startResponsesProxy,
|
|
12
|
-
type ResponsesProxyHandle,
|
|
13
|
-
} from "./responses-server.js";
|
|
14
|
-
import type { PolicyConfig, ProviderCandidate } from "../types.js";
|
|
15
|
-
|
|
16
|
-
const POLICY: PolicyConfig = {
|
|
17
|
-
allowlistedProfiles: ["proxy"],
|
|
18
|
-
deniedProfilePatterns: ["*-work"],
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
function fakeCandidate(): ProviderCandidate {
|
|
22
|
-
return {
|
|
23
|
-
providerId: "fake",
|
|
24
|
-
modelId: "fake-coder",
|
|
25
|
-
authTier: "api-key",
|
|
26
|
-
authScope: "third-party",
|
|
27
|
-
health: "healthy",
|
|
28
|
-
supportsTools: true,
|
|
29
|
-
supportsStreaming: true,
|
|
30
|
-
supportsJson: true,
|
|
31
|
-
supportsImages: false,
|
|
32
|
-
estimatedCostUsd: 0.01,
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function makeContext(
|
|
37
|
-
adapter = createFakeProvider({ id: "fake" }),
|
|
38
|
-
): ResponsesProxyContext {
|
|
39
|
-
return createResponsesProxyContext({
|
|
40
|
-
policy: POLICY,
|
|
41
|
-
candidates: [fakeCandidate()],
|
|
42
|
-
adapters: { fake: adapter },
|
|
43
|
-
delegation: { audit: new InMemoryAuditStore() },
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
interface PostResult {
|
|
48
|
-
status: number;
|
|
49
|
-
headers: http.IncomingHttpHeaders;
|
|
50
|
-
text: string;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function post(
|
|
54
|
-
handle: ResponsesProxyHandle,
|
|
55
|
-
path: string,
|
|
56
|
-
body: unknown,
|
|
57
|
-
options: { token?: string } = {},
|
|
58
|
-
): Promise<PostResult> {
|
|
59
|
-
return new Promise((resolve, reject) => {
|
|
60
|
-
const req = http.request(
|
|
61
|
-
{
|
|
62
|
-
host: "127.0.0.1",
|
|
63
|
-
port: handle.port,
|
|
64
|
-
path,
|
|
65
|
-
method: "POST",
|
|
66
|
-
headers: {
|
|
67
|
-
"content-type": "application/json",
|
|
68
|
-
...(options.token
|
|
69
|
-
? { authorization: `Bearer ${options.token}` }
|
|
70
|
-
: {}),
|
|
71
|
-
},
|
|
72
|
-
},
|
|
73
|
-
(res) => {
|
|
74
|
-
const chunks: Buffer[] = [];
|
|
75
|
-
res.on("data", (c: Buffer) => chunks.push(c));
|
|
76
|
-
res.on("end", () =>
|
|
77
|
-
resolve({
|
|
78
|
-
status: res.statusCode ?? 0,
|
|
79
|
-
headers: res.headers,
|
|
80
|
-
text: Buffer.concat(chunks).toString("utf8"),
|
|
81
|
-
}),
|
|
82
|
-
);
|
|
83
|
-
},
|
|
84
|
-
);
|
|
85
|
-
req.on("error", reject);
|
|
86
|
-
req.end(JSON.stringify(body));
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function sleep(ms: number): Promise<void> {
|
|
91
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const handles: ResponsesProxyHandle[] = [];
|
|
95
|
-
afterEach(async () => {
|
|
96
|
-
while (handles.length > 0) {
|
|
97
|
-
const h = handles.pop();
|
|
98
|
-
if (h) await h.close();
|
|
99
|
-
}
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
async function start(
|
|
103
|
-
context: ResponsesProxyContext,
|
|
104
|
-
options: Parameters<typeof startResponsesProxy>[1] = {},
|
|
105
|
-
): Promise<ResponsesProxyHandle> {
|
|
106
|
-
const handle = await startResponsesProxy(context, {
|
|
107
|
-
port: 0,
|
|
108
|
-
harness: { kind: "other", profile: "proxy" },
|
|
109
|
-
workspace: { cwd: "/tmp/project", trusted: true },
|
|
110
|
-
...options,
|
|
111
|
-
});
|
|
112
|
-
handles.push(handle);
|
|
113
|
-
return handle;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
describe("responses proxy", () => {
|
|
117
|
-
it("streams a successful Responses SSE sequence", async () => {
|
|
118
|
-
const adapter = createFakeProvider({
|
|
119
|
-
id: "fake",
|
|
120
|
-
streamChunks: ["hello ", "world"],
|
|
121
|
-
});
|
|
122
|
-
const handle = await start(makeContext(adapter));
|
|
123
|
-
|
|
124
|
-
const res = await post(handle, "/v1/responses", {
|
|
125
|
-
input: "hi",
|
|
126
|
-
stream: true,
|
|
127
|
-
});
|
|
128
|
-
expect(res.status).toBe(200);
|
|
129
|
-
expect(res.headers["content-type"]).toContain("text/event-stream");
|
|
130
|
-
expect(res.text).toContain("response.created");
|
|
131
|
-
expect(res.text).toContain("response.output_text.delta");
|
|
132
|
-
expect(res.text).toContain("response.completed");
|
|
133
|
-
expect(res.text).toContain("hello ");
|
|
134
|
-
expect(res.text).toContain('"output_text":"hello world"');
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it("returns 403 for a denied work profile", async () => {
|
|
138
|
-
const handle = await start(makeContext(), {
|
|
139
|
-
harness: { kind: "other", profile: "claude-work" },
|
|
140
|
-
});
|
|
141
|
-
const res = await post(handle, "/v1/responses", { input: "hi" });
|
|
142
|
-
expect(res.status).toBe(403);
|
|
143
|
-
const json = JSON.parse(res.text) as { error: { message: string } };
|
|
144
|
-
expect(json.error.message).toMatch(/denied/i);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it("returns 503 when no provider is configured", async () => {
|
|
148
|
-
const context = createResponsesProxyContext({
|
|
149
|
-
policy: POLICY,
|
|
150
|
-
candidates: [],
|
|
151
|
-
adapters: {},
|
|
152
|
-
delegation: { audit: new InMemoryAuditStore() },
|
|
153
|
-
});
|
|
154
|
-
const handle = await start(context);
|
|
155
|
-
const res = await post(handle, "/v1/responses", { input: "hi" });
|
|
156
|
-
expect(res.status).toBe(503);
|
|
157
|
-
const json = JSON.parse(res.text) as { error: { message: string } };
|
|
158
|
-
expect(json.error).toBeDefined();
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it("maps a provider error to an HTTP status before streaming starts", async () => {
|
|
162
|
-
const adapter = createFakeProvider({
|
|
163
|
-
id: "fake",
|
|
164
|
-
error: new ProviderError({
|
|
165
|
-
code: "rate_limited",
|
|
166
|
-
message: "slow down",
|
|
167
|
-
retryable: true,
|
|
168
|
-
httpStatus: 429,
|
|
169
|
-
}),
|
|
170
|
-
});
|
|
171
|
-
const handle = await start(makeContext(adapter));
|
|
172
|
-
const res = await post(handle, "/v1/responses", { input: "hi" });
|
|
173
|
-
expect(res.status).toBe(429);
|
|
174
|
-
const json = JSON.parse(res.text) as {
|
|
175
|
-
error: { message: string; type: string };
|
|
176
|
-
};
|
|
177
|
-
expect(json.error.message).toContain("slow down");
|
|
178
|
-
expect(json.error.type).toBe("rate_limit_error");
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it("cancels the provider call when the client disconnects", async () => {
|
|
182
|
-
const adapter = createFakeProvider({
|
|
183
|
-
id: "fake",
|
|
184
|
-
hangUntilCancel: true,
|
|
185
|
-
});
|
|
186
|
-
const context = makeContext(adapter);
|
|
187
|
-
const handle = await start(context);
|
|
188
|
-
|
|
189
|
-
const req = http.request({
|
|
190
|
-
host: "127.0.0.1",
|
|
191
|
-
port: handle.port,
|
|
192
|
-
path: "/v1/responses",
|
|
193
|
-
method: "POST",
|
|
194
|
-
headers: { "content-type": "application/json" },
|
|
195
|
-
});
|
|
196
|
-
req.on("error", () => undefined);
|
|
197
|
-
req.end(JSON.stringify({ input: "hang please" }));
|
|
198
|
-
await sleep(60);
|
|
199
|
-
req.destroy();
|
|
200
|
-
|
|
201
|
-
let cancelled = false;
|
|
202
|
-
for (let i = 0; i < 150; i++) {
|
|
203
|
-
await sleep(20);
|
|
204
|
-
if (context.runtime.listTasks().some((t) => t.status === "cancelled")) {
|
|
205
|
-
cancelled = true;
|
|
206
|
-
break;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
expect(cancelled).toBe(true);
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
it("enforces a bearer auth token", async () => {
|
|
213
|
-
const handle = await start(makeContext(), { token: "sekret" });
|
|
214
|
-
|
|
215
|
-
const noToken = await post(handle, "/v1/responses", { input: "hi" });
|
|
216
|
-
expect(noToken.status).toBe(401);
|
|
217
|
-
|
|
218
|
-
const wrong = await post(
|
|
219
|
-
handle,
|
|
220
|
-
"/v1/responses",
|
|
221
|
-
{ input: "hi" },
|
|
222
|
-
{ token: "nope" },
|
|
223
|
-
);
|
|
224
|
-
expect(wrong.status).toBe(401);
|
|
225
|
-
|
|
226
|
-
const ok = await post(
|
|
227
|
-
handle,
|
|
228
|
-
"/v1/responses",
|
|
229
|
-
{ input: "hi" },
|
|
230
|
-
{ token: "sekret" },
|
|
231
|
-
);
|
|
232
|
-
expect(ok.status).toBe(200);
|
|
233
|
-
expect(ok.text).toContain("response.completed");
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
it("rejects unknown paths with 404", async () => {
|
|
237
|
-
const handle = await start(makeContext());
|
|
238
|
-
const res = await post(handle, "/v1/unknown", { input: "hi" });
|
|
239
|
-
expect(res.status).toBe(404);
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
it("accepts the chat/completions alias and streams delta chunks", async () => {
|
|
243
|
-
const adapter = createFakeProvider({
|
|
244
|
-
id: "fake",
|
|
245
|
-
streamChunks: ["alpha", "beta"],
|
|
246
|
-
});
|
|
247
|
-
const handle = await start(makeContext(adapter));
|
|
248
|
-
const res = await post(handle, "/v1/chat/completions", {
|
|
249
|
-
messages: [{ role: "user", content: "hi" }],
|
|
250
|
-
stream: true,
|
|
251
|
-
});
|
|
252
|
-
expect(res.status).toBe(200);
|
|
253
|
-
expect(res.text).toContain("chat.completion.chunk");
|
|
254
|
-
expect(res.text).toContain("alpha");
|
|
255
|
-
expect(res.text).toContain('"finish_reason":"stop"');
|
|
256
|
-
expect(res.text).toContain("[DONE]");
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
it("returns a single JSON response object when stream is false", async () => {
|
|
260
|
-
const adapter = createFakeProvider({
|
|
261
|
-
id: "fake",
|
|
262
|
-
streamChunks: ["hello ", "world"],
|
|
263
|
-
});
|
|
264
|
-
const handle = await start(makeContext(adapter));
|
|
265
|
-
const res = await post(handle, "/v1/responses", {
|
|
266
|
-
input: "hi",
|
|
267
|
-
stream: false,
|
|
268
|
-
});
|
|
269
|
-
expect(res.status).toBe(200);
|
|
270
|
-
expect(res.headers["content-type"]).toContain("application/json");
|
|
271
|
-
expect(res.headers["content-type"]).not.toContain("text/event-stream");
|
|
272
|
-
// No SSE framing.
|
|
273
|
-
expect(res.text).not.toContain("data: ");
|
|
274
|
-
expect(res.text).not.toContain("response.created");
|
|
275
|
-
const json = JSON.parse(res.text);
|
|
276
|
-
expect(json.object).toBe("response");
|
|
277
|
-
expect(json.status).toBe("completed");
|
|
278
|
-
expect(json.output_text).toBe("hello world");
|
|
279
|
-
const msg = json.output[0];
|
|
280
|
-
expect(msg?.content[0]?.text).toBe("hello world");
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
it("returns a single chat completion JSON object when stream is false", async () => {
|
|
284
|
-
const adapter = createFakeProvider({
|
|
285
|
-
id: "fake",
|
|
286
|
-
streamChunks: ["alpha", "beta"],
|
|
287
|
-
});
|
|
288
|
-
const handle = await start(makeContext(adapter));
|
|
289
|
-
const res = await post(handle, "/v1/chat/completions", {
|
|
290
|
-
messages: [{ role: "user", content: "hi" }],
|
|
291
|
-
stream: false,
|
|
292
|
-
});
|
|
293
|
-
expect(res.status).toBe(200);
|
|
294
|
-
expect(res.headers["content-type"]).toContain("application/json");
|
|
295
|
-
expect(res.text).not.toContain("[DONE]");
|
|
296
|
-
const json = JSON.parse(res.text);
|
|
297
|
-
expect(json.object).toBe("chat.completion");
|
|
298
|
-
expect(json.choices[0].message.role).toBe("assistant");
|
|
299
|
-
expect(json.choices[0].message.content).toBe("alphabeta");
|
|
300
|
-
expect(json.choices[0].finish_reason).toBe("stop");
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
it("returns a 400 OpenAI-style error for an invalid request body", async () => {
|
|
304
|
-
const handle = await start(makeContext());
|
|
305
|
-
// Unknown top-level keys are stripped (OpenAI-compat interop), not rejected.
|
|
306
|
-
const unknownKey = await post(handle, "/v1/responses", {
|
|
307
|
-
input: "hi",
|
|
308
|
-
bogus: true,
|
|
309
|
-
tools: [],
|
|
310
|
-
store: false,
|
|
311
|
-
});
|
|
312
|
-
expect(unknownKey.status).toBe(200);
|
|
313
|
-
|
|
314
|
-
const badType = await post(handle, "/v1/responses", {
|
|
315
|
-
input: "hi",
|
|
316
|
-
max_output_tokens: "lots",
|
|
317
|
-
});
|
|
318
|
-
expect(badType.status).toBe(400);
|
|
319
|
-
const json2 = JSON.parse(badType.text);
|
|
320
|
-
expect(json2.error.type).toBe("invalid_request_error");
|
|
321
|
-
});
|
|
322
|
-
});
|