offrouter-core 0.2.0 → 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 +9 -4
- 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,290 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { DelegationRuntime } from "../delegation.js";
|
|
3
|
-
import {
|
|
4
|
-
buildDelegationRequest,
|
|
5
|
-
buildRouteRequest,
|
|
6
|
-
createResponsesProxyContext,
|
|
7
|
-
extractPromptFromChatCompletions,
|
|
8
|
-
extractPromptFromResponsesInput,
|
|
9
|
-
openAiErrorPayload,
|
|
10
|
-
policyDecisionToHttpStatus,
|
|
11
|
-
promptDigest,
|
|
12
|
-
promptPreview,
|
|
13
|
-
providerErrorCodeToHttpStatus,
|
|
14
|
-
RESPONSES_PROMPT_PREVIEW_CHARS,
|
|
15
|
-
ResponsesSseBuilder,
|
|
16
|
-
responsesSseData,
|
|
17
|
-
} from "./responses-mapper.js";
|
|
18
|
-
import type { ProviderCandidate, RouteDecision } from "../types.js";
|
|
19
|
-
|
|
20
|
-
function candidate(
|
|
21
|
-
partial: Partial<ProviderCandidate> &
|
|
22
|
-
Pick<ProviderCandidate, "providerId" | "modelId" | "authTier">,
|
|
23
|
-
): ProviderCandidate {
|
|
24
|
-
return {
|
|
25
|
-
authScope: "third-party",
|
|
26
|
-
health: "healthy",
|
|
27
|
-
supportsTools: true,
|
|
28
|
-
supportsStreaming: true,
|
|
29
|
-
supportsJson: true,
|
|
30
|
-
supportsImages: false,
|
|
31
|
-
estimatedCostUsd: 0.01,
|
|
32
|
-
...partial,
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const harness = { kind: "other" as const, profile: "proxy" };
|
|
37
|
-
const workspace = { cwd: "/tmp/project", trusted: true };
|
|
38
|
-
|
|
39
|
-
describe("extractPromptFromResponsesInput", () => {
|
|
40
|
-
it("reads a plain string input", () => {
|
|
41
|
-
const parsed = extractPromptFromResponsesInput({
|
|
42
|
-
model: "gpt-test",
|
|
43
|
-
input: "explain this code",
|
|
44
|
-
});
|
|
45
|
-
expect(parsed.prompt).toBe("explain this code");
|
|
46
|
-
expect(parsed.model).toBe("gpt-test");
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("reads structured input items with content arrays", () => {
|
|
50
|
-
const parsed = extractPromptFromResponsesInput({
|
|
51
|
-
input: [
|
|
52
|
-
{
|
|
53
|
-
role: "user",
|
|
54
|
-
content: [
|
|
55
|
-
{ type: "input_text", text: "hello " },
|
|
56
|
-
{ type: "input_text", text: "world" },
|
|
57
|
-
],
|
|
58
|
-
},
|
|
59
|
-
],
|
|
60
|
-
});
|
|
61
|
-
expect(parsed.prompt).toBe("hello world");
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("merges instructions and input", () => {
|
|
65
|
-
const parsed = extractPromptFromResponsesInput({
|
|
66
|
-
instructions: "be terse",
|
|
67
|
-
input: "do the thing",
|
|
68
|
-
});
|
|
69
|
-
expect(parsed.prompt).toBe("be terse\n\ndo the thing");
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it("maps output token and temperature fields", () => {
|
|
73
|
-
const parsed = extractPromptFromResponsesInput({
|
|
74
|
-
input: "x",
|
|
75
|
-
max_output_tokens: 128,
|
|
76
|
-
temperature: 0.3,
|
|
77
|
-
stream: true,
|
|
78
|
-
});
|
|
79
|
-
expect(parsed.maxOutputTokens).toBe(128);
|
|
80
|
-
expect(parsed.temperature).toBe(0.3);
|
|
81
|
-
expect(parsed.stream).toBe(true);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it("returns empty prompt for an empty object", () => {
|
|
85
|
-
expect(extractPromptFromResponsesInput({}).prompt).toBe("");
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("rejects non-object bodies with a schema error", () => {
|
|
89
|
-
expect(() => extractPromptFromResponsesInput(null)).toThrow();
|
|
90
|
-
expect(() => extractPromptFromResponsesInput("not an object")).toThrow();
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it("strips unknown top-level keys for OpenAI-compat interop", () => {
|
|
94
|
-
// Unknown keys (bogus, tools, store, metadata) are stripped, not rejected.
|
|
95
|
-
expect(() =>
|
|
96
|
-
extractPromptFromResponsesInput({ input: "x", bogus: true, tools: [] }),
|
|
97
|
-
).not.toThrow();
|
|
98
|
-
expect(() =>
|
|
99
|
-
extractPromptFromChatCompletions({
|
|
100
|
-
messages: [],
|
|
101
|
-
tools: [],
|
|
102
|
-
}),
|
|
103
|
-
).not.toThrow();
|
|
104
|
-
// But structurally invalid values still throw.
|
|
105
|
-
expect(() =>
|
|
106
|
-
extractPromptFromResponsesInput({
|
|
107
|
-
input: "x",
|
|
108
|
-
max_output_tokens: "lots",
|
|
109
|
-
}),
|
|
110
|
-
).toThrow();
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
describe("prompt digest and preview", () => {
|
|
115
|
-
it("produces a stable sha256 digest", () => {
|
|
116
|
-
expect(promptDigest("hello")).toBe(
|
|
117
|
-
"sha256:2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",
|
|
118
|
-
);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("keeps short previews intact", () => {
|
|
122
|
-
expect(promptPreview("short", 20)).toBe("short");
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it("truncates long previews with an ASCII ellipsis", () => {
|
|
126
|
-
const long = "a".repeat(500);
|
|
127
|
-
const preview = promptPreview(long, 20);
|
|
128
|
-
expect(preview.length).toBeLessThanOrEqual(20);
|
|
129
|
-
expect(preview.endsWith("...")).toBe(true);
|
|
130
|
-
// ASCII-only: every code point is below 128.
|
|
131
|
-
expect([...preview].every((ch) => (ch.codePointAt(0) ?? 0) < 128)).toBe(
|
|
132
|
-
true,
|
|
133
|
-
);
|
|
134
|
-
});
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
describe("buildRouteRequest", () => {
|
|
138
|
-
it("carries preview and digest but never the raw prompt", () => {
|
|
139
|
-
// Put a secret marker past the preview cutoff so truncation strips it.
|
|
140
|
-
const secret = "SUPERSECRETTOKEN-DO-NOT-LEAK-123";
|
|
141
|
-
const prompt = `${"x".repeat(RESPONSES_PROMPT_PREVIEW_CHARS + 40)} ${secret}`;
|
|
142
|
-
const route = buildRouteRequest({ prompt, harness, workspace });
|
|
143
|
-
expect(route.task.promptDigest).toBe(promptDigest(prompt));
|
|
144
|
-
expect(route.task.promptPreview.endsWith("...")).toBe(true);
|
|
145
|
-
expect(route.task.promptPreview).not.toContain(secret);
|
|
146
|
-
expect(JSON.stringify(route)).not.toContain(secret);
|
|
147
|
-
expect(route.task.requiresStreaming).toBe(true);
|
|
148
|
-
expect(route.protocolVersion).toBe("offrouter.route.v1");
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it("honours an explicit request id", () => {
|
|
152
|
-
const route = buildRouteRequest({
|
|
153
|
-
prompt: "x",
|
|
154
|
-
harness,
|
|
155
|
-
workspace,
|
|
156
|
-
requestId: "req_explicit",
|
|
157
|
-
});
|
|
158
|
-
expect(route.requestId).toBe("req_explicit");
|
|
159
|
-
});
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
describe("buildDelegationRequest", () => {
|
|
163
|
-
it("carries the full prompt and stream mode", () => {
|
|
164
|
-
const route = buildRouteRequest({ prompt: "x", harness, workspace });
|
|
165
|
-
const del = buildDelegationRequest({
|
|
166
|
-
prompt: "FULL SECRET PROMPT",
|
|
167
|
-
route,
|
|
168
|
-
maxOutputTokens: 7,
|
|
169
|
-
temperature: 0.5,
|
|
170
|
-
});
|
|
171
|
-
expect(del.prompt).toBe("FULL SECRET PROMPT");
|
|
172
|
-
expect(del.mode).toBe("stream");
|
|
173
|
-
expect(del.maxOutputTokens).toBe(7);
|
|
174
|
-
expect(del.temperature).toBe(0.5);
|
|
175
|
-
expect(del.route).toBe(route);
|
|
176
|
-
});
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
describe("ResponsesSseBuilder", () => {
|
|
180
|
-
it("emits created, delta, and completed events in OpenAI shape", () => {
|
|
181
|
-
const now = () => new Date("2025-01-01T00:00:00Z");
|
|
182
|
-
const builder = new ResponsesSseBuilder("fake-coder", now);
|
|
183
|
-
const created = builder.createdEvent();
|
|
184
|
-
expect(created.type).toBe("response.created");
|
|
185
|
-
expect(created.response.id).toBe(builder.responseId);
|
|
186
|
-
expect(created.response.object).toBe("response");
|
|
187
|
-
expect(created.response.status).toBe("in_progress");
|
|
188
|
-
expect(created.response.model).toBe("fake-coder");
|
|
189
|
-
|
|
190
|
-
builder.addText("hello ");
|
|
191
|
-
builder.addText("world");
|
|
192
|
-
const delta = builder.deltaEvent("hello ");
|
|
193
|
-
expect(delta.type).toBe("response.output_text.delta");
|
|
194
|
-
expect(delta.delta).toBe("hello ");
|
|
195
|
-
expect(delta.item_id).toBe(builder.messageId);
|
|
196
|
-
|
|
197
|
-
const completed = builder.completedEvent();
|
|
198
|
-
expect(completed.type).toBe("response.completed");
|
|
199
|
-
expect(completed.response.status).toBe("completed");
|
|
200
|
-
expect(completed.response.output_text).toBe("hello world");
|
|
201
|
-
const msg = completed.response.output[0];
|
|
202
|
-
expect(msg?.type).toBe("message");
|
|
203
|
-
expect(msg?.content[0]?.text).toBe("hello world");
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it("includes usage in completed when set", () => {
|
|
207
|
-
const builder = new ResponsesSseBuilder("m", () => new Date(0));
|
|
208
|
-
builder.setUsage({ inputTokens: 3, outputTokens: 4, totalTokens: 7 });
|
|
209
|
-
const completed = builder.completedEvent();
|
|
210
|
-
expect(completed.response.usage?.total_tokens).toBe(7);
|
|
211
|
-
});
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
describe("responsesSseData", () => {
|
|
215
|
-
it("wraps a payload as an SSE data line", () => {
|
|
216
|
-
expect(responsesSseData({ type: "x" })).toBe(`data: {"type":"x"}\n\n`);
|
|
217
|
-
});
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
describe("error mapping", () => {
|
|
221
|
-
it("maps provider error codes to HTTP statuses", () => {
|
|
222
|
-
expect(providerErrorCodeToHttpStatus("auth_failed")).toBe(401);
|
|
223
|
-
expect(providerErrorCodeToHttpStatus("rate_limited")).toBe(429);
|
|
224
|
-
expect(providerErrorCodeToHttpStatus("quota_exhausted")).toBe(429);
|
|
225
|
-
expect(providerErrorCodeToHttpStatus("billing")).toBe(402);
|
|
226
|
-
expect(providerErrorCodeToHttpStatus("model_not_found")).toBe(404);
|
|
227
|
-
expect(providerErrorCodeToHttpStatus("overload")).toBe(503);
|
|
228
|
-
expect(providerErrorCodeToHttpStatus("timeout")).toBe(504);
|
|
229
|
-
expect(providerErrorCodeToHttpStatus("network")).toBe(502);
|
|
230
|
-
expect(providerErrorCodeToHttpStatus("invalid_request")).toBe(400);
|
|
231
|
-
expect(providerErrorCodeToHttpStatus("unknown")).toBe(500);
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
it("builds an OpenAI-style error payload", () => {
|
|
235
|
-
const payload = openAiErrorPayload(429, "slow down", "rate_limited");
|
|
236
|
-
expect(payload.error.message).toBe("slow down");
|
|
237
|
-
expect(payload.error.type).toBe("rate_limit_error");
|
|
238
|
-
expect(payload.error.code).toBe("rate_limited");
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
it("maps a blocked policy decision to 403", () => {
|
|
242
|
-
const decision: RouteDecision = {
|
|
243
|
-
protocolVersion: "offrouter.route.v1",
|
|
244
|
-
decisionId: "d1",
|
|
245
|
-
requestId: "r1",
|
|
246
|
-
route: null,
|
|
247
|
-
reason: "policy_blocked",
|
|
248
|
-
explanation: "denied",
|
|
249
|
-
blocked: true,
|
|
250
|
-
needsConfiguration: false,
|
|
251
|
-
};
|
|
252
|
-
expect(policyDecisionToHttpStatus(decision)).toBe(403);
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
it("maps a needs-configuration decision to 503", () => {
|
|
256
|
-
const decision: RouteDecision = {
|
|
257
|
-
protocolVersion: "offrouter.route.v1",
|
|
258
|
-
decisionId: "d1",
|
|
259
|
-
requestId: "r1",
|
|
260
|
-
route: null,
|
|
261
|
-
reason: "no_candidate_needs_configuration",
|
|
262
|
-
explanation: "configure providers",
|
|
263
|
-
blocked: true,
|
|
264
|
-
needsConfiguration: true,
|
|
265
|
-
};
|
|
266
|
-
expect(policyDecisionToHttpStatus(decision)).toBe(503);
|
|
267
|
-
});
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
describe("createResponsesProxyContext", () => {
|
|
271
|
-
it("builds a runtime-backed context mirroring the MCP context shape", () => {
|
|
272
|
-
const ctx = createResponsesProxyContext({
|
|
273
|
-
policy: {
|
|
274
|
-
allowlistedProfiles: ["proxy"],
|
|
275
|
-
deniedProfilePatterns: ["*-work"],
|
|
276
|
-
},
|
|
277
|
-
candidates: [
|
|
278
|
-
candidate({
|
|
279
|
-
providerId: "fake",
|
|
280
|
-
modelId: "fake-coder",
|
|
281
|
-
authTier: "api-key",
|
|
282
|
-
}),
|
|
283
|
-
],
|
|
284
|
-
adapters: {},
|
|
285
|
-
});
|
|
286
|
-
expect(ctx.runtime).toBeInstanceOf(DelegationRuntime);
|
|
287
|
-
expect(ctx.candidates).toHaveLength(1);
|
|
288
|
-
expect(ctx.policy.allowlistedProfiles).toContain("proxy");
|
|
289
|
-
});
|
|
290
|
-
});
|