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.
Files changed (139) hide show
  1. package/dist/audit.d.ts +137 -0
  2. package/dist/audit.d.ts.map +1 -0
  3. package/dist/audit.js +320 -0
  4. package/dist/audit.js.map +1 -0
  5. package/dist/auth/credential-chain.d.ts +84 -0
  6. package/dist/auth/credential-chain.d.ts.map +1 -0
  7. package/dist/auth/credential-chain.js +150 -0
  8. package/dist/auth/credential-chain.js.map +1 -0
  9. package/dist/auth/index.d.ts +10 -0
  10. package/dist/auth/index.d.ts.map +1 -0
  11. package/dist/auth/index.js +7 -0
  12. package/dist/auth/index.js.map +1 -0
  13. package/dist/auth/keychain.d.ts +60 -0
  14. package/dist/auth/keychain.d.ts.map +1 -0
  15. package/dist/auth/keychain.js +104 -0
  16. package/dist/auth/keychain.js.map +1 -0
  17. package/dist/auth/oauth-pkce.d.ts +138 -0
  18. package/dist/auth/oauth-pkce.d.ts.map +1 -0
  19. package/dist/auth/oauth-pkce.js +375 -0
  20. package/dist/auth/oauth-pkce.js.map +1 -0
  21. package/dist/auth/oauth-server.d.ts +36 -0
  22. package/dist/auth/oauth-server.d.ts.map +1 -0
  23. package/dist/auth/oauth-server.js +210 -0
  24. package/dist/auth/oauth-server.js.map +1 -0
  25. package/dist/config.d.ts +76 -0
  26. package/dist/config.d.ts.map +1 -0
  27. package/dist/config.js +355 -0
  28. package/dist/config.js.map +1 -0
  29. package/dist/delegation.d.ts +123 -0
  30. package/dist/delegation.d.ts.map +1 -0
  31. package/dist/delegation.js +455 -0
  32. package/dist/delegation.js.map +1 -0
  33. package/dist/index.d.ts +41 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +26 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/policy.d.ts +45 -0
  38. package/dist/policy.d.ts.map +1 -0
  39. package/dist/policy.js +318 -0
  40. package/dist/policy.js.map +1 -0
  41. package/{src/protocol.ts → dist/protocol.d.ts} +2 -2
  42. package/dist/protocol.d.ts.map +1 -0
  43. package/dist/protocol.js +6 -0
  44. package/dist/protocol.js.map +1 -0
  45. package/dist/providers/adapter.d.ts +137 -0
  46. package/dist/providers/adapter.d.ts.map +1 -0
  47. package/dist/providers/adapter.js +163 -0
  48. package/dist/providers/adapter.js.map +1 -0
  49. package/dist/providers/catalog-data.d.ts +128 -0
  50. package/dist/providers/catalog-data.d.ts.map +1 -0
  51. package/dist/providers/catalog-data.js +397 -0
  52. package/dist/providers/catalog-data.js.map +1 -0
  53. package/dist/providers/catalog.d.ts +63 -0
  54. package/dist/providers/catalog.d.ts.map +1 -0
  55. package/dist/providers/catalog.js +394 -0
  56. package/dist/providers/catalog.js.map +1 -0
  57. package/dist/providers/fake.d.ts +46 -0
  58. package/dist/providers/fake.d.ts.map +1 -0
  59. package/dist/providers/fake.js +234 -0
  60. package/dist/providers/fake.js.map +1 -0
  61. package/dist/providers/openai-compatible.d.ts +65 -0
  62. package/dist/providers/openai-compatible.d.ts.map +1 -0
  63. package/dist/providers/openai-compatible.js +434 -0
  64. package/dist/providers/openai-compatible.js.map +1 -0
  65. package/dist/proxy/responses-mapper.d.ts +366 -0
  66. package/dist/proxy/responses-mapper.d.ts.map +1 -0
  67. package/dist/proxy/responses-mapper.js +517 -0
  68. package/dist/proxy/responses-mapper.js.map +1 -0
  69. package/dist/proxy/responses-server.d.ts +29 -0
  70. package/dist/proxy/responses-server.d.ts.map +1 -0
  71. package/dist/proxy/responses-server.js +360 -0
  72. package/dist/proxy/responses-server.js.map +1 -0
  73. package/dist/router.d.ts +18 -0
  74. package/dist/router.d.ts.map +1 -0
  75. package/dist/router.js +296 -0
  76. package/dist/router.js.map +1 -0
  77. package/dist/schemas.d.ts +560 -0
  78. package/dist/schemas.d.ts.map +1 -0
  79. package/{src/schemas.ts → dist/schemas.js} +83 -103
  80. package/dist/schemas.js.map +1 -0
  81. package/dist/secrets.d.ts +112 -0
  82. package/dist/secrets.d.ts.map +1 -0
  83. package/dist/secrets.js +326 -0
  84. package/dist/secrets.js.map +1 -0
  85. package/dist/types.d.ts +117 -0
  86. package/dist/types.d.ts.map +1 -0
  87. package/dist/types.js +6 -0
  88. package/dist/types.js.map +1 -0
  89. package/dist/usage-file.d.ts +59 -0
  90. package/dist/usage-file.d.ts.map +1 -0
  91. package/dist/usage-file.js +316 -0
  92. package/dist/usage-file.js.map +1 -0
  93. package/dist/usage.d.ts +235 -0
  94. package/dist/usage.d.ts.map +1 -0
  95. package/dist/usage.js +517 -0
  96. package/dist/usage.js.map +1 -0
  97. package/package.json +7 -2
  98. package/src/audit.test.ts +0 -302
  99. package/src/audit.ts +0 -553
  100. package/src/auth/credential-chain.test.ts +0 -326
  101. package/src/auth/credential-chain.ts +0 -235
  102. package/src/auth/index.ts +0 -45
  103. package/src/auth/keychain.test.ts +0 -265
  104. package/src/auth/keychain.ts +0 -168
  105. package/src/auth/oauth-pkce.test.ts +0 -329
  106. package/src/auth/oauth-pkce.ts +0 -571
  107. package/src/auth/oauth-server.test.ts +0 -83
  108. package/src/auth/oauth-server.ts +0 -296
  109. package/src/config.test.ts +0 -479
  110. package/src/config.ts +0 -505
  111. package/src/delegation.test.ts +0 -368
  112. package/src/delegation.ts +0 -605
  113. package/src/index.ts +0 -280
  114. package/src/policy.test.ts +0 -634
  115. package/src/policy.ts +0 -420
  116. package/src/providers/adapter.test.ts +0 -293
  117. package/src/providers/adapter.ts +0 -328
  118. package/src/providers/catalog-data.test.ts +0 -258
  119. package/src/providers/catalog-data.ts +0 -498
  120. package/src/providers/catalog.test.ts +0 -84
  121. package/src/providers/catalog.ts +0 -483
  122. package/src/providers/fake.ts +0 -312
  123. package/src/providers/openai-compatible.test.ts +0 -366
  124. package/src/providers/openai-compatible.ts +0 -554
  125. package/src/proxy/responses-mapper.test.ts +0 -290
  126. package/src/proxy/responses-mapper.ts +0 -736
  127. package/src/proxy/responses-server.test.ts +0 -322
  128. package/src/proxy/responses-server.ts +0 -469
  129. package/src/router.test.ts +0 -699
  130. package/src/router.ts +0 -352
  131. package/src/schemas.test.ts +0 -291
  132. package/src/secrets.test.ts +0 -271
  133. package/src/secrets.ts +0 -461
  134. package/src/types.ts +0 -173
  135. package/src/usage-file.test.ts +0 -243
  136. package/src/usage-file.ts +0 -435
  137. package/src/usage.test.ts +0 -335
  138. package/src/usage.ts +0 -859
  139. 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
- });