@anvailabs/sandhi 0.5.1 → 0.6.1

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.
@@ -1,860 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { createServer } from "node:http";
3
- import { test } from "node:test";
4
- import { tmpdir } from "node:os";
5
- import { join } from "node:path";
6
- import { readFileSync, rmSync } from "node:fs";
7
-
8
- import { Gateway, parseUsage, wireContractVersion } from "../index.js";
9
- import { ProviderRuntime } from "../sandhi.js";
10
-
11
- function localServer(responses) {
12
- return new Promise((resolve) => {
13
- let calls = 0;
14
- let lastHeaders = {};
15
- const server = createServer((request, response) => {
16
- lastHeaders = request.headers;
17
- const item = responses[calls++];
18
- response.writeHead(item.status ?? 200, {
19
- "content-type": item.contentType,
20
- "content-length": Buffer.byteLength(item.body),
21
- });
22
- response.end(item.body);
23
- });
24
- server.listen(0, "127.0.0.1", () => {
25
- resolve({
26
- origin: `http://127.0.0.1:${server.address().port}`,
27
- baseUrl: `http://127.0.0.1:${server.address().port}/v1`,
28
- calls: () => calls,
29
- lastHeaders: () => lastHeaders,
30
- close: () => new Promise((done) => server.close(done)),
31
- });
32
- });
33
- });
34
- }
35
-
36
- test("per-call wire headers ride and cannot override the credential (TD-0022)", async () => {
37
- const server = await localServer([
38
- {
39
- contentType: "application/json",
40
- body: JSON.stringify({
41
- id: "r1",
42
- model: "gpt-test",
43
- choices: [{ message: { content: "hello" }, finish_reason: "stop" }],
44
- usage: { prompt_tokens: 10, completion_tokens: 3 },
45
- }),
46
- },
47
- ]);
48
- try {
49
- const runtime = new ProviderRuntime();
50
- const provider = runtime.openaiCompat(
51
- "openai",
52
- server.baseUrl,
53
- "real-key",
54
- undefined,
55
- 0,
56
- );
57
- const request = JSON.stringify({
58
- schema_version: "1",
59
- model: "gpt-test",
60
- messages: [{ role: "user", content: "hi" }],
61
- });
62
- const response = JSON.parse(
63
- await provider.completeJson(
64
- request,
65
- JSON.stringify({
66
- "x-sandhi-run-id": "run-9",
67
- "x-sandhi-step-id": "step-7",
68
- authorization: "Bearer attacker", // must be stripped
69
- }),
70
- ),
71
- );
72
- assert.equal(response.output.content, "hello");
73
- const sent = server.lastHeaders();
74
- assert.equal(sent["x-sandhi-run-id"], "run-9");
75
- assert.equal(sent["x-sandhi-step-id"], "step-7");
76
- assert.equal(sent.authorization, "Bearer real-key");
77
- } finally {
78
- await server.close();
79
- }
80
- });
81
-
82
- test("streamJson per-call wire headers ride and cannot override the credential (TD-0022)", async () => {
83
- // Mirror of the python binding's stream pin: the stream seam must carry per-call wire
84
- // headers exactly like completeJson, or node consumers silently lose them on streams.
85
- const server = await localServer([
86
- {
87
- contentType: "text/event-stream",
88
- body:
89
- 'data: {"id":"r2","model":"gpt-test","choices":[{"delta":{"content":"he"},"finish_reason":null}]}\n\n' +
90
- 'data: {"id":"r2","model":"gpt-test","choices":[{"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":10,"completion_tokens":3}}\n\n' +
91
- "data: [DONE]\n\n",
92
- },
93
- ]);
94
- try {
95
- const runtime = new ProviderRuntime();
96
- const provider = runtime.openaiCompat(
97
- "openai",
98
- server.baseUrl,
99
- "real-key",
100
- undefined,
101
- 0,
102
- );
103
- const request = JSON.stringify({
104
- schema_version: "1",
105
- model: "gpt-test",
106
- messages: [{ role: "user", content: "hi" }],
107
- });
108
- const events = [];
109
- for await (const event of provider.streamJson(
110
- request,
111
- JSON.stringify({
112
- "x-sandhi-run-id": "run-9",
113
- "x-sandhi-step-id": "step-7",
114
- authorization: "Bearer attacker", // must be stripped
115
- }),
116
- )) {
117
- events.push(JSON.parse(event));
118
- }
119
- assert.deepEqual(events.map((event) => event.event), [
120
- "response_start",
121
- "text_delta",
122
- "usage",
123
- "finish",
124
- ]);
125
- const sent = server.lastHeaders();
126
- assert.equal(sent["x-sandhi-run-id"], "run-9");
127
- assert.equal(sent["x-sandhi-step-id"], "step-7");
128
- assert.equal(sent.authorization, "Bearer real-key");
129
- } finally {
130
- await server.close();
131
- }
132
- });
133
-
134
- test("static headersJson reaches all four non-OpenAI families (TD-0022 D3)", async () => {
135
- // The anthropic/gemini/cohere/ollama dispatch branches dropped headers_json before this
136
- // landed — gateway consumers (victor) stamp x-sandhi-run-id/x-sandhi-session there.
137
- const seen = [];
138
- const bodies = {
139
- "/v1/messages": JSON.stringify({
140
- id: "m1", type: "message", role: "assistant",
141
- content: [{ type: "text", text: "ok" }],
142
- usage: { input_tokens: 2, output_tokens: 1 },
143
- }),
144
- "/models/gemini-x:generateContent": JSON.stringify({
145
- candidates: [{ content: { parts: [{ text: "ok" }] } }],
146
- usageMetadata: { promptTokenCount: 2, candidatesTokenCount: 1 },
147
- }),
148
- "/v2/chat": JSON.stringify({
149
- id: "r1", message: { content: [{ type: "text", text: "ok" }] },
150
- usage: { billed_units: { input_tokens: 2, output_tokens: 1 } },
151
- }),
152
- "/api/chat": JSON.stringify({
153
- message: { role: "assistant", content: "ok" }, done: true,
154
- prompt_eval_count: 2, eval_count: 1,
155
- }),
156
- };
157
- // localServer serves responses in call order and overwrites lastHeaders per request;
158
- // capture headers per call so EVERY family is asserted, not just the final one.
159
- const orderedBodies = Object.values(bodies).map((body) => ({
160
- contentType: "application/json",
161
- body,
162
- }));
163
- const server = await localServer(orderedBodies);
164
- try {
165
- const runtime = new ProviderRuntime();
166
- const cases = [
167
- ["anthropic", { schema_version: "1", model: "claude-test", max_output_tokens: 8,
168
- messages: [{ role: "user", content: "hi" }] }, "/v1/messages"],
169
- ["gemini", { schema_version: "1", model: "gemini-x",
170
- messages: [{ role: "user", content: "hi" }] }, "/models/gemini-x:generateContent"],
171
- ["cohere", { schema_version: "1", model: "command-r",
172
- messages: [{ role: "user", content: "hi" }] }, "/v2/chat"],
173
- ["ollama", { schema_version: "1", model: "llama3",
174
- messages: [{ role: "user", content: "hi" }] }, "/api/chat"],
175
- ];
176
- for (const [slug, request] of cases) {
177
- const provider = runtime.provider(
178
- slug, request.model, "k", server.origin,
179
- JSON.stringify({ "x-sandhi-run-id": "run-static" }),
180
- 0,
181
- );
182
- await provider.completeJson(JSON.stringify(request));
183
- }
184
- const headers = server.lastHeaders();
185
- assert.equal(headers["x-sandhi-run-id"], "run-static");
186
- // NOTE: lastHeaders() reflects only the final request (ollama); the per-family
187
- // guarantee is pinned by the python twin (which records every request) and the
188
- // four Rust factory wiremock tests. Sequential-order coupling documented above.
189
- } finally {
190
- await server.close();
191
- }
192
- });
193
-
194
- test("persistent typed provider completes and streams neutral documents", async () => {
195
- const server = await localServer([
196
- {
197
- contentType: "application/json",
198
- body: JSON.stringify({
199
- id: "r1",
200
- model: "gpt-test",
201
- choices: [{ message: { content: "hello" }, finish_reason: "stop" }],
202
- usage: {
203
- prompt_tokens: 10,
204
- completion_tokens: 3,
205
- prompt_tokens_details: { cached_tokens: 4 },
206
- },
207
- }),
208
- },
209
- {
210
- contentType: "text/event-stream",
211
- body:
212
- 'data: {"id":"r2","model":"gpt-test","choices":[{"delta":{"content":"he"},"finish_reason":null}]}\n\n' +
213
- 'data: {"id":"r2","model":"gpt-test","choices":[{"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":10,"completion_tokens":3,"prompt_tokens_details":{"cached_tokens":4}}}\n\n' +
214
- "data: [DONE]\n\n",
215
- },
216
- ]);
217
- try {
218
- const runtime = new ProviderRuntime();
219
- const provider = runtime.openaiCompat(
220
- "openai",
221
- server.baseUrl,
222
- "key",
223
- undefined,
224
- 0,
225
- );
226
- const request = JSON.stringify({
227
- schema_version: "1",
228
- model: "gpt-test",
229
- messages: [{ role: "user", content: "hi" }],
230
- });
231
- const response = JSON.parse(await provider.completeJson(request));
232
- assert.equal(response.output.content, "hello");
233
- assert.equal(response.usage.tokens_in, 6);
234
-
235
- const events = [];
236
- for await (const event of provider.streamJson(request)) events.push(JSON.parse(event));
237
- assert.deepEqual(events.map((event) => event.event), [
238
- "response_start",
239
- "text_delta",
240
- "usage",
241
- "finish",
242
- ]);
243
- assert.equal(server.calls(), 2);
244
- } finally {
245
- await server.close();
246
- }
247
- });
248
-
249
- test("Anthropic bearer auth is explicit across the typed binding", async () => {
250
- const server = await localServer([
251
- {
252
- contentType: "application/json",
253
- body: JSON.stringify({
254
- id: "msg_1",
255
- type: "message",
256
- role: "assistant",
257
- model: "claude-test",
258
- content: [{ type: "text", text: "ok" }],
259
- stop_reason: "end_turn",
260
- usage: { input_tokens: 2, output_tokens: 1 },
261
- }),
262
- },
263
- ]);
264
- try {
265
- const provider = new ProviderRuntime().provider(
266
- "anthropic",
267
- "claude-test",
268
- "oauth-token",
269
- server.origin,
270
- undefined,
271
- 0,
272
- undefined,
273
- undefined,
274
- "bearer",
275
- );
276
- const response = JSON.parse(
277
- await provider.completeJson(
278
- JSON.stringify({
279
- model: "claude-test",
280
- messages: [{ role: "user", content: "hi" }],
281
- max_output_tokens: 16,
282
- }),
283
- ),
284
- );
285
- assert.equal(response.output.content, "ok");
286
- assert.equal(server.lastHeaders().authorization, "Bearer oauth-token");
287
- assert.equal(server.lastHeaders()["x-api-key"], undefined);
288
- } finally {
289
- await server.close();
290
- }
291
- });
292
-
293
- test("Responses protocol stays item-shaped across the typed binding", async () => {
294
- const server = await localServer([
295
- {
296
- contentType: "application/json",
297
- body: JSON.stringify({
298
- id: "resp_1",
299
- model: "gpt-test",
300
- status: "completed",
301
- output: [
302
- {
303
- type: "message",
304
- role: "assistant",
305
- content: [{ type: "output_text", text: "ok" }],
306
- },
307
- ],
308
- usage: {
309
- input_tokens: 12,
310
- output_tokens: 3,
311
- input_tokens_details: { cached_tokens: 2 },
312
- output_tokens_details: { reasoning_tokens: 1 },
313
- },
314
- }),
315
- },
316
- ]);
317
- try {
318
- const provider = new ProviderRuntime().provider(
319
- "openai",
320
- "gpt-test",
321
- "oauth-token",
322
- server.baseUrl,
323
- JSON.stringify({ originator: "victor" }),
324
- 0,
325
- undefined,
326
- undefined,
327
- undefined,
328
- "responses",
329
- );
330
- const response = JSON.parse(
331
- await provider.completeJson(
332
- JSON.stringify({
333
- model: "gpt-test",
334
- messages: [{ role: "user", content: "hi" }],
335
- }),
336
- ),
337
- );
338
- assert.equal(response.output.content, "ok");
339
- assert.equal(response.usage.tokens_in, 10);
340
- assert.equal(response.usage.cache_read_tokens, 2);
341
- assert.equal(response.usage.reasoning_tokens, 1);
342
- assert.equal(server.lastHeaders().authorization, "Bearer oauth-token");
343
- assert.equal(server.lastHeaders().originator, "victor");
344
- } finally {
345
- await server.close();
346
- }
347
- });
348
-
349
- test("ChatGPT Responses profile aggregates its required upstream stream", async () => {
350
- const server = await localServer([
351
- {
352
- contentType: "text/event-stream",
353
- body:
354
- 'data: {"type":"response.created","response":{"id":"resp_1","model":"gpt-test"}}\n\n' +
355
- 'data: {"type":"response.output_text.delta","delta":"ok"}\n\n' +
356
- 'data: {"type":"response.completed","response":{"status":"completed","output":[],"usage":{"input_tokens":4,"output_tokens":1}}}\n\n',
357
- },
358
- ]);
359
- try {
360
- const provider = new ProviderRuntime().provider(
361
- "openai",
362
- "gpt-test",
363
- "oauth-token",
364
- server.origin,
365
- undefined,
366
- 0,
367
- undefined,
368
- undefined,
369
- undefined,
370
- "chatgpt_responses",
371
- );
372
- const response = JSON.parse(
373
- await provider.completeJson(
374
- JSON.stringify({
375
- model: "gpt-test",
376
- messages: [
377
- { role: "developer", content: "be precise" },
378
- { role: "user", content: "hi" },
379
- ],
380
- }),
381
- ),
382
- );
383
- assert.equal(response.output.content, "ok");
384
- assert.equal(response.usage.tokens_in, 4);
385
- } finally {
386
- await server.close();
387
- }
388
- });
389
-
390
- test("typed errors are instanceof SandhiProviderError on every path (TD-0021 P1)", async () => {
391
- // G22 close-out conformance: a JS consumer must be able to branch on the class
392
- // (code/httpStatus/retryable) instead of string-sniffing — on completeJson, on
393
- // streamJson SETUP failures (before any iterator exists), and on read() failures.
394
- const { SandhiProviderError, ProviderRuntime } = await import("../sandhi.js");
395
- // localServer serves responses in call order; the three sub-cases below each
396
- // make one request, so supply three identical 401s.
397
- const unauthorized = {
398
- status: 401,
399
- contentType: "application/json",
400
- body: JSON.stringify({ error: "bad key" }),
401
- };
402
- const server = await localServer([unauthorized, unauthorized, unauthorized]);
403
- try {
404
- const provider = new ProviderRuntime().openaiCompat(
405
- "openai", server.baseUrl, "k", undefined, 0,
406
- );
407
- const request = JSON.stringify({
408
- schema_version: "1", model: "gpt-test",
409
- messages: [{ role: "user", content: "hi" }],
410
- });
411
-
412
- // 1. completeJson
413
- let completeErr;
414
- await provider.completeJson(request).catch((e) => (completeErr = e));
415
- assert.ok(completeErr instanceof SandhiProviderError, "completeJson error is typed");
416
- assert.equal(completeErr.code, "authentication_error");
417
- assert.equal(completeErr.httpStatus, 401);
418
- assert.equal(typeof completeErr.retryable, "boolean");
419
-
420
- // 2. streamJson SETUP — a synchronous validation failure (malformed request
421
- // JSON) throws from the setup call itself, before any iterator exists.
422
- let setupErr;
423
- try {
424
- provider.streamJson("{}");
425
- } catch (e) {
426
- setupErr = e;
427
- }
428
- assert.ok(setupErr != null, "setup rejects synchronously on bad input");
429
- // Binding-internal validation errors are deliberately NOT rewired to the
430
- // provider class (toSandhiError passes non-typed payloads through) — the
431
- // contract is that they surface as ordinary Errors, not crashes.
432
- assert.ok(setupErr instanceof Error);
433
-
434
- // 3. TypedEventStream.read — same stream seam via the async iterator.
435
- let readErr;
436
- try {
437
- const stream = provider.streamJson(request);
438
- for await (const _event of stream) {
439
- /* drain */
440
- }
441
- } catch (e) {
442
- readErr = e;
443
- }
444
- assert.ok(readErr instanceof SandhiProviderError, "read/iterator error is typed");
445
- } finally {
446
- await server.close();
447
- }
448
- });
449
-
450
- test("raw provider transport exports are absent", async () => {
451
- const module = await import("../sandhi.js");
452
- for (const name of ["complete", "stream", "registerProvider", "ByteStream"])
453
- assert.equal(module[name], undefined);
454
- });
455
-
456
- test("catalog surface serves curated model data (TD-0004)", async () => {
457
- const { providerDescriptorJson, providerModelsJson } = await import("../sandhi.js");
458
- const descriptor = JSON.parse(providerDescriptorJson("anthropic"));
459
- assert.equal(descriptor.slug, "anthropic");
460
- const models = JSON.parse(providerModelsJson("anthropic"));
461
- assert.ok(models.some((m) => m.id === "claude-fable-5"));
462
- const fable = models.find((m) => m.id === "claude-fable-5");
463
- assert.equal(fable.max_input_tokens, 1000000);
464
- // Facts only — the catalog carries no pricing (measure-vs-price line held).
465
- assert.equal(fable.price, undefined);
466
- // Seeded compat vendor lineups resolve through aliases too.
467
- const grok = JSON.parse(providerModelsJson("grok"));
468
- assert.ok(grok.some((m) => m.id === "grok-4"));
469
- // Aggregators stay empty (dynamic hosting catalogs — live discovery).
470
- assert.deepEqual(JSON.parse(providerModelsJson("openrouter")), []);
471
- // Self-hosted InferFlux: model ids are operator config server-side, never vendor facts.
472
- assert.deepEqual(JSON.parse(providerModelsJson("inferflux")), []);
473
- assert.throws(() => providerModelsJson("unknown-provider"));
474
- });
475
-
476
- test("usage parsing and metering retain cache attribution", () => {
477
- assert.equal(wireContractVersion(), "1");
478
- const usage = parseUsage(
479
- "openai",
480
- JSON.stringify({
481
- usage: {
482
- prompt_tokens: 100,
483
- completion_tokens: 20,
484
- prompt_tokens_details: { cached_tokens: 60 },
485
- },
486
- }),
487
- );
488
- assert.equal(usage.tokensIn, 40);
489
- assert.equal(usage.cacheReadTokens, 60);
490
-
491
- const gateway = new Gateway();
492
- gateway.addVirtualKey("vk", "alice", "platform", "openai");
493
- gateway.setBudget("group:platform", 1000);
494
- const event = gateway.meterTokens("vk", "openai", "m", 40, 20, 0, 60, "s1");
495
- assert.equal(event.subjectId, "alice");
496
- assert.equal(event.sessionId, "s1");
497
- // D4 billable counts the cache split: 40 fresh in + 60 cache-read + 20 out = 120.
498
- assert.equal(gateway.spent("group:platform"), 120);
499
- });
500
-
501
- // ---------------------------------------------------------------------------
502
- // ProviderRuntime.provider() dispatch + the direct openaiResponses factory.
503
- // Handle construction is pure (no network); this covers every provider branch
504
- // and the typed-provider getter.
505
- // ---------------------------------------------------------------------------
506
-
507
- test("provider() factory dispatches named backends and exposes the slug getter", () => {
508
- const runtime = new ProviderRuntime();
509
- for (const [name, provider, model] of [
510
- ["anthropic", "anthropic", "claude-test"],
511
- ["claude alias", "claude", "claude-test"],
512
- ["gemini", "gemini", "gemini-1.5-pro"],
513
- ["google alias", "google", "gemini-1.5-pro"],
514
- ["cohere", "cohere", "command-r"],
515
- ["ollama", "ollama", "llama3"],
516
- ]) {
517
- // Args: provider, model, api_key, base_url, headers_json, max_retries.
518
- const handle = runtime.provider(provider, model, "key", undefined, undefined, 0);
519
- assert.equal(typeof handle.provider, "string", `${name} exposed a slug`);
520
- assert.ok(handle.provider.length > 0, `${name} slug non-empty`);
521
- }
522
-
523
- // api_key (default) auth scheme on Anthropic — explicit api_key spelling + default both ok.
524
- assert.ok(
525
- runtime.provider("anthropic", "claude-test", "k", undefined, undefined, 0, undefined, undefined, "api_key").provider,
526
- );
527
- assert.ok(runtime.provider("anthropic", "claude-test", "k", undefined, undefined, 0).provider);
528
- });
529
-
530
- test("provider() routes the openai-compat + responses escape hatches", () => {
531
- const runtime = new ProviderRuntime();
532
- // Unknown provider WITH a base_url → openai_compat escape hatch.
533
- const custom = runtime.provider("acme", "m", "key", "https://example.test/v1", undefined, 0);
534
- assert.equal(custom.provider, "acme");
535
-
536
- // Known catalog provider WITHOUT a base_url → known_openai_compat resolves the spec.
537
- const known = runtime.provider("deepseek", "deepseek-chat", "key", undefined, undefined, 0);
538
- assert.equal(known.provider, "deepseek");
539
-
540
- // Self-hosted InferFlux is a first-class catalog slug (ADR-0008): no base_url needed, the
541
- // spec's default (http://127.0.0.1:8080/v1) resolves.
542
- const inferflux = runtime.provider("inferflux", "llama3-8b", "local-key", undefined, undefined, 0);
543
- assert.equal(inferflux.provider, "inferflux");
544
-
545
- // openaiResponses() direct factory (Responses API bearer form).
546
- const responses = runtime.openaiResponses("openai", "https://example.test/v1", "token", undefined, 0);
547
- assert.equal(responses.provider, "openai");
548
-
549
- // Responses protocol via provider() resolves a known catalog provider's base_url when omitted.
550
- const viaProvider = runtime.provider(
551
- "deepseek",
552
- "deepseek-chat",
553
- "key",
554
- undefined,
555
- undefined,
556
- 0,
557
- undefined,
558
- undefined,
559
- undefined,
560
- "responses",
561
- );
562
- assert.equal(viaProvider.provider, "deepseek");
563
- });
564
-
565
- test("provider() rejects invalid dispatch inputs at the FFI seam", () => {
566
- const runtime = new ProviderRuntime();
567
- // Unsupported auth_scheme value on Anthropic. Args: provider,model,key,base_url,headers,retries,,,auth_scheme.
568
- assert.throws(
569
- () => runtime.provider("anthropic", "m", "k", undefined, undefined, 0, undefined, undefined, "bogus"),
570
- /auth_scheme/,
571
- );
572
- // bearer for a non-scheme family is a NO-OP (its default IS Bearer) — accepted,
573
- // not rejected (TD-0008 rule 5). Only a contradictory scheme is an error.
574
- const bearerNoop = runtime.provider("openai", "m", "k", "https://e.test/v1", undefined, 0, undefined, undefined, "bearer");
575
- assert.equal(bearerNoop.provider, "openai");
576
- assert.throws(
577
- () => runtime.provider("openai", "m", "k", "https://e.test/v1", undefined, 0, undefined, undefined, "api_key"),
578
- /Anthropic/,
579
- );
580
- // Unsupported protocol value.
581
- assert.throws(
582
- () => runtime.provider("openai", "m", "k", undefined, undefined, 0, undefined, undefined, undefined, "bogus"),
583
- /protocol/,
584
- );
585
- // Responses protocol + unknown provider + no base_url.
586
- assert.throws(
587
- () => runtime.provider("acme", "m", "k", undefined, undefined, 0, undefined, undefined, undefined, "responses"),
588
- /baseUrl/,
589
- );
590
- // Unknown catalog provider without a base_url under chat_completions → typed provider error.
591
- assert.throws(() => runtime.provider("acme", "m", "k", undefined, undefined, 0), /unknown catalog provider|acme/);
592
- // Malformed headers JSON.
593
- assert.throws(() => runtime.openaiCompat("openai", "https://e.test/v1", "k", "not-json", 0), /headers/);
594
- });
595
-
596
- // ---------------------------------------------------------------------------
597
- // TypedEventStream.read() pull API + the upstream-error propagation path.
598
- // ---------------------------------------------------------------------------
599
-
600
- test("TypedEventStream.read() drains a healthy stream to exhaustion", async () => {
601
- const server = await localServer([
602
- {
603
- contentType: "text/event-stream",
604
- body:
605
- 'data: {"id":"r2","model":"gpt-test","choices":[{"delta":{"content":"he"},"finish_reason":null}]}\n\n' +
606
- 'data: {"id":"r2","model":"gpt-test","choices":[{"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":3,"completion_tokens":1}}\n\n' +
607
- "data: [DONE]\n\n",
608
- },
609
- ]);
610
- try {
611
- const provider = new ProviderRuntime().openaiCompat("openai", server.baseUrl, "key", undefined, 0);
612
- const stream = provider.streamJson(
613
- JSON.stringify({ model: "gpt-test", messages: [{ role: "user", content: "hi" }] }),
614
- );
615
- const events = [];
616
- while (true) {
617
- const chunk = await stream.read();
618
- if (chunk === null || chunk === undefined) break;
619
- events.push(JSON.parse(chunk));
620
- }
621
- assert.deepEqual(events.map((e) => e.event), ["response_start", "text_delta", "usage", "finish"]);
622
- } finally {
623
- await server.close();
624
- }
625
- });
626
-
627
- test("streamJson surfaces an upstream error via read()", async () => {
628
- const server = await localServer([
629
- { status: 500, contentType: "application/json", body: JSON.stringify({ error: "boom" }) },
630
- ]);
631
- try {
632
- const provider = new ProviderRuntime().openaiCompat("openai", server.baseUrl, "key", undefined, 0);
633
- const stream = provider.streamJson(
634
- JSON.stringify({ model: "gpt-test", messages: [{ role: "user", content: "hi" }] }),
635
- );
636
- await assert.rejects(() => stream.read(), /status|500|boom|error/i);
637
- } finally {
638
- await server.close();
639
- }
640
- });
641
-
642
- test("completeJson surfaces an upstream HTTP error as a typed descriptor", async () => {
643
- const server = await localServer([
644
- { status: 500, contentType: "application/json", body: JSON.stringify({ error: "boom" }) },
645
- ]);
646
- try {
647
- const provider = new ProviderRuntime().openaiCompat("openai", server.baseUrl, "key", undefined, 0);
648
- await assert.rejects(
649
- () => provider.completeJson(JSON.stringify({ model: "gpt-test", messages: [{ role: "user", content: "hi" }] })),
650
- /status|500|boom|error/i,
651
- );
652
- } finally {
653
- await server.close();
654
- }
655
- });
656
-
657
- // ---------------------------------------------------------------------------
658
- // Gateway: meter() (parse-driven), events(), checkBudget(), the JSONL sink,
659
- // group-less scope, and every built-in usage parser via parseUsage.
660
- // ---------------------------------------------------------------------------
661
-
662
- test("Gateway.meter parses, attributes, records budget, and lists events", () => {
663
- const gateway = new Gateway();
664
- gateway.addVirtualKey("vk_alice", "alice", "platform", "anthropic");
665
- gateway.setBudget("group:platform", 1000);
666
-
667
- const event = gateway.meter(
668
- "vk_alice",
669
- "anthropic",
670
- "claude-x",
671
- JSON.stringify({
672
- usage: {
673
- input_tokens: 100,
674
- output_tokens: 20,
675
- cache_creation_input_tokens: 5,
676
- cache_read_input_tokens: 10,
677
- },
678
- }),
679
- "conv_1",
680
- "router",
681
- );
682
- assert.equal(event.subjectId, "alice");
683
- assert.equal(event.groupId, "platform");
684
- assert.equal(event.sessionId, "conv_1");
685
- assert.equal(event.route, "router");
686
- assert.equal(event.provider, "anthropic");
687
- assert.equal(event.usageCompleteness, "final");
688
- // 100 fresh in + 5 cache-write + 10 cache-read + 20 out (D4); narrow read 120.
689
- assert.equal(gateway.spent("group:platform"), 135);
690
-
691
- // Within budget → true; over budget → false. (135 spent of 1000 → 865 remaining.)
692
- assert.equal(gateway.checkBudget("group:platform", 864), true);
693
- assert.equal(gateway.checkBudget("group:platform", 866), false);
694
-
695
- // events() returns the in-memory buffer.
696
- const listed = gateway.events();
697
- assert.equal(listed.length, 1);
698
- assert.equal(listed[0].requestId, event.requestId);
699
-
700
- // Unknown virtual key throws.
701
- assert.throws(() => gateway.meter("ghost", "openai", "m", JSON.stringify({})), /unknown virtual key/);
702
- // Bad JSON throws.
703
- assert.throws(() => gateway.meter("vk_alice", "openai", "m", "{not json"), /valid JSON/);
704
- });
705
-
706
- test("Gateway budgets are 64-bit (no u32 truncation/overflow)", () => {
707
- // Regression: set_budget/check_budget/spent were u32, silently wrapping past ~4.29G tokens
708
- // (an enforcement bypass absent on the Python binding/core). They are now i64 (a JS safe
709
- // integer, range to ~9e15), so a budget above the old u32 ceiling survives intact.
710
- const gateway = new Gateway();
711
- gateway.addVirtualKey("vk", "alice", "platform", "openai");
712
- // 5e9 > u32::MAX (4_294_967_295): a u32 budget would truncate this to ~705M.
713
- gateway.setBudget("group:platform", 5_000_000_000);
714
- // 5e9 is within the 5e9 cap; 5e9 + 1 is not — no truncation either way.
715
- assert.equal(gateway.checkBudget("group:platform", 5_000_000_000), true);
716
- assert.equal(gateway.checkBudget("group:platform", 5_000_000_001), false);
717
- // spent round-trips the i64; meter a small amount and confirm the value is exact.
718
- gateway.meterTokens("vk", "openai", "m", 10, 5, 0, 0, "s");
719
- assert.equal(gateway.spent("group:platform"), 15);
720
- });
721
-
722
- test("Gateway meters a group-less virtual key against the vk:* scope and writes a JSONL sink", () => {
723
- const sink = join(tmpdir(), `sandhi-sink-${process.pid}-${Date.now()}.jsonl`);
724
- rmSync(sink, { force: true });
725
- const gateway = new Gateway(sink);
726
- gateway.addVirtualKey("vk_solo", "solo", undefined, "openai");
727
- const event = gateway.meterTokens("vk_solo", "openai", "gpt-test", 10, 5, 0, 0, "s");
728
- assert.ok(!event.groupId, "group-less key has no groupId");
729
- assert.equal(event.subjectId, "solo");
730
- // A group-less key records against the vk:* scope (no group budget set → no enforcement).
731
- assert.equal(gateway.spent("vk:vk_solo"), 15);
732
- // JSONL sink got exactly one line.
733
- const lines = readFileSync(sink, "utf8").trim().split("\n");
734
- assert.equal(lines.length, 1);
735
- assert.equal(JSON.parse(lines[0]).subject_id, "solo");
736
- rmSync(sink, { force: true });
737
- });
738
-
739
- // ---------------------------------------------------------------------------
740
- // Gateway.usageSnapshotJson (TD-0009 P2): the in-process aggregate surface.
741
- // The corpus is shared with the Python suite so the two cannot drift.
742
- // ---------------------------------------------------------------------------
743
-
744
- function parityCorpus() {
745
- const path = new URL("../../fixtures/usage-snapshot-parity.json", import.meta.url);
746
- return JSON.parse(readFileSync(path, "utf8"));
747
- }
748
-
749
- function drive(gateway, corpus) {
750
- for (const key of corpus.virtual_keys) {
751
- gateway.addVirtualKey(key.id, key.subject, key.group ?? undefined, key.upstream);
752
- }
753
- for (const call of corpus.calls) {
754
- gateway.meterTokens(
755
- call.virtual_key,
756
- call.provider,
757
- call.model,
758
- call.tokens_in,
759
- call.tokens_out,
760
- call.cache_creation_tokens,
761
- call.cache_read_tokens,
762
- call.session_id ?? undefined,
763
- );
764
- }
765
- }
766
-
767
- test("usageSnapshotJson matches the cross-language parity corpus", () => {
768
- // Byte-identical to the fixture the Python suite asserts against (TD-0009 D6/P2).
769
- const corpus = parityCorpus();
770
- const gateway = new Gateway();
771
- drive(gateway, corpus);
772
-
773
- for (const [dimension, expected] of Object.entries(corpus.expected_json)) {
774
- assert.equal(gateway.usageSnapshotJson(dimension), expected, dimension);
775
- }
776
-
777
- // Aliases resolve through the one core parser, so `key`/`virtualKey` and `subject`/`user`
778
- // cannot mean different things here than at /admin/usage?by=.
779
- assert.equal(gateway.usageSnapshotJson("virtual_key"), corpus.expected_json.key);
780
- assert.equal(gateway.usageSnapshotJson("user"), corpus.expected_json.subject);
781
- assert.equal(gateway.usageSnapshotJson(" TOTAL "), corpus.expected_json.total);
782
-
783
- // Neutral units only (ADR-0001): no dollars anywhere in the aggregate.
784
- const rows = JSON.parse(gateway.usageSnapshotJson("subject"));
785
- assert.equal(rows[0].key, "bob"); // ranked by billable tokens, busiest first
786
- assert.ok(rows.every((r) => !("cost" in r) && !("price" in r)));
787
- // No call reported a duration, so latency is absent rather than zeroed.
788
- assert.ok(rows.every((r) => !("latency" in r)));
789
-
790
- assert.throws(() => gateway.usageSnapshotJson("cost"), /unknown usage dimension/);
791
- });
792
-
793
- test("usageSnapshotJson cap folds overflow and keeps the total exact", () => {
794
- // TD-0009 D2: the cap loses per-key detail, never the sum.
795
- const corpus = parityCorpus().overflow;
796
- const key = corpus.virtual_key;
797
- const gateway = new Gateway();
798
- gateway.addVirtualKey(key.id, key.subject, key.group ?? undefined, key.upstream);
799
- for (const session of corpus.sessions) {
800
- gateway.meterTokens(
801
- key.id,
802
- corpus.provider,
803
- corpus.model,
804
- corpus.tokens_in,
805
- corpus.tokens_out,
806
- 0,
807
- 0,
808
- session,
809
- );
810
- }
811
-
812
- const capped = gateway.usageSnapshotJson("session", corpus.cap);
813
- assert.equal(capped, corpus.expected_session_json);
814
- const rows = JSON.parse(capped);
815
- // cap distinct keys survive; everything after them lands in one honest bucket.
816
- assert.equal(rows.length, corpus.cap + 1);
817
- const overflow = rows.find((r) => r.key === "(overflow)");
818
- assert.equal(overflow.calls, corpus.sessions.length - corpus.cap);
819
-
820
- // The sum is exact under eviction: rows still add up to the grand total.
821
- assert.equal(gateway.usageSnapshotJson("total"), corpus.expected_total_json);
822
- const total = JSON.parse(gateway.usageSnapshotJson("total"));
823
- // The payload is the serde JSON of UsageAggregateV1 — snake_case, not the napi camelCase
824
- // objects `events()` returns; the schema'd contract shape crosses the seam unchanged.
825
- assert.equal(total[0].billable_tokens, corpus.expected_total_billable);
826
- assert.equal(
827
- rows.reduce((sum, r) => sum + r.billable_tokens, 0),
828
- corpus.expected_total_billable,
829
- );
830
- assert.equal(
831
- rows.reduce((sum, r) => sum + r.calls, 0),
832
- corpus.sessions.length,
833
- );
834
-
835
- // Uncapped, every session keeps its own row.
836
- const uncapped = JSON.parse(gateway.usageSnapshotJson("session"));
837
- assert.deepEqual(uncapped.map((r) => r.key), corpus.sessions);
838
- assert.ok(uncapped.every((r) => r.key !== "(overflow)"));
839
- });
840
-
841
- test("parseUsage exercises every built-in provider parser", () => {
842
- // Anthropic-style.
843
- const anthropic = parseUsage(
844
- "anthropic",
845
- JSON.stringify({ usage: { input_tokens: 7, output_tokens: 3 } }),
846
- );
847
- assert.equal(anthropic.tokensIn, 7);
848
- assert.equal(anthropic.tokensOut, 3);
849
-
850
- // The remaining parsers are selected by slug; missing fields default to zero via unwrap_or_default,
851
- // so a minimal body still exercises each match arm.
852
- for (const provider of ["gemini", "cohere", "ollama", "bedrock", "openai_responses", "responses"]) {
853
- const got = parseUsage(provider, JSON.stringify({}));
854
- assert.equal(got.tokensIn, 0);
855
- assert.equal(got.tokensOut, 0);
856
- }
857
-
858
- // Invalid JSON throws.
859
- assert.throws(() => parseUsage("openai", "{nope"), /valid JSON/);
860
- });