@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.
Files changed (99) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +28 -0
  3. package/dist/acp-agent.d.ts +38 -0
  4. package/dist/acp-agent.js +142 -0
  5. package/dist/acp-registry.d.ts +36 -0
  6. package/dist/acp-registry.js +85 -0
  7. package/dist/adapters/anthropic.d.ts +131 -0
  8. package/dist/adapters/anthropic.js +1195 -0
  9. package/dist/adapters/chat.d.ts +14 -0
  10. package/dist/adapters/chat.js +34 -0
  11. package/dist/adapters/cursor.d.ts +34 -0
  12. package/dist/adapters/cursor.js +305 -0
  13. package/dist/adapters/dropped.d.ts +10 -0
  14. package/dist/adapters/dropped.js +24 -0
  15. package/dist/adapters/openai-chat-wire.d.ts +93 -0
  16. package/dist/adapters/openai-chat-wire.js +143 -0
  17. package/dist/adapters/responses-stream.d.ts +7 -0
  18. package/dist/adapters/responses-stream.js +597 -0
  19. package/dist/adapters/responses.d.ts +174 -0
  20. package/dist/adapters/responses.js +778 -0
  21. package/dist/adapters/server-tool-loop.d.ts +94 -0
  22. package/dist/adapters/server-tool-loop.js +477 -0
  23. package/dist/adapters/upstream-error.d.ts +14 -0
  24. package/dist/adapters/upstream-error.js +25 -0
  25. package/dist/adapters/validate.d.ts +27 -0
  26. package/dist/adapters/validate.js +180 -0
  27. package/dist/adapters/web-search.d.ts +46 -0
  28. package/dist/adapters/web-search.js +151 -0
  29. package/dist/auth.d.ts +10 -0
  30. package/dist/auth.js +28 -0
  31. package/dist/backend.d.ts +151 -0
  32. package/dist/backend.js +143 -0
  33. package/dist/capacity-pool.d.ts +31 -0
  34. package/dist/capacity-pool.js +99 -0
  35. package/dist/cost.d.ts +49 -0
  36. package/dist/cost.js +112 -0
  37. package/dist/endpoint-health.d.ts +54 -0
  38. package/dist/endpoint-health.js +123 -0
  39. package/dist/index.d.ts +40 -0
  40. package/dist/index.js +23 -0
  41. package/dist/provenance.d.ts +31 -0
  42. package/dist/provenance.js +191 -0
  43. package/dist/provider-backends.d.ts +40 -0
  44. package/dist/provider-backends.js +1050 -0
  45. package/dist/provider-source.d.ts +40 -0
  46. package/dist/provider-source.js +293 -0
  47. package/dist/router.d.ts +168 -0
  48. package/dist/router.js +474 -0
  49. package/dist/server.d.ts +67 -0
  50. package/dist/server.js +930 -0
  51. package/dist/sse/chat-assembler.d.ts +45 -0
  52. package/dist/sse/chat-assembler.js +190 -0
  53. package/dist/sse/parse.d.ts +50 -0
  54. package/dist/sse/parse.js +149 -0
  55. package/dist/sse-wire.d.ts +10 -0
  56. package/dist/sse-wire.js +31 -0
  57. package/dist/switching-proxy.d.ts +15 -0
  58. package/dist/switching-proxy.js +232 -0
  59. package/dist/test/acp-agent.test.d.ts +1 -0
  60. package/dist/test/acp-agent.test.js +66 -0
  61. package/dist/test/acp-registry.test.d.ts +1 -0
  62. package/dist/test/acp-registry.test.js +70 -0
  63. package/dist/test/anthropic.test.d.ts +1 -0
  64. package/dist/test/anthropic.test.js +793 -0
  65. package/dist/test/auth.test.d.ts +1 -0
  66. package/dist/test/auth.test.js +25 -0
  67. package/dist/test/boundary.test.d.ts +1 -0
  68. package/dist/test/boundary.test.js +32 -0
  69. package/dist/test/chat.test.d.ts +1 -0
  70. package/dist/test/chat.test.js +418 -0
  71. package/dist/test/cost.test.d.ts +1 -0
  72. package/dist/test/cost.test.js +60 -0
  73. package/dist/test/cursor.test.d.ts +1 -0
  74. package/dist/test/cursor.test.js +100 -0
  75. package/dist/test/drain.test.d.ts +1 -0
  76. package/dist/test/drain.test.js +116 -0
  77. package/dist/test/dropped.test.d.ts +1 -0
  78. package/dist/test/dropped.test.js +80 -0
  79. package/dist/test/endpoint-health.test.d.ts +1 -0
  80. package/dist/test/endpoint-health.test.js +73 -0
  81. package/dist/test/provenance.test.d.ts +1 -0
  82. package/dist/test/provenance.test.js +176 -0
  83. package/dist/test/provider-backends.test.d.ts +1 -0
  84. package/dist/test/provider-backends.test.js +699 -0
  85. package/dist/test/responses.test.d.ts +1 -0
  86. package/dist/test/responses.test.js +813 -0
  87. package/dist/test/routed-backend.test.d.ts +1 -0
  88. package/dist/test/routed-backend.test.js +39 -0
  89. package/dist/test/router.test.d.ts +1 -0
  90. package/dist/test/router.test.js +297 -0
  91. package/dist/test/server-resilience.test.d.ts +1 -0
  92. package/dist/test/server-resilience.test.js +169 -0
  93. package/dist/test/sse-codec.test.d.ts +1 -0
  94. package/dist/test/sse-codec.test.js +186 -0
  95. package/dist/test/web-search-loop.test.d.ts +1 -0
  96. package/dist/test/web-search-loop.test.js +469 -0
  97. package/dist/test/wire-validation.test.d.ts +1 -0
  98. package/dist/test/wire-validation.test.js +140 -0
  99. package/package.json +48 -0
@@ -0,0 +1,116 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { startGateway } from "../server.js";
4
+ /**
5
+ * Graceful drain: a draining gateway must report unhealthy and reject new
6
+ * model calls while letting in-flight requests (long-lived LLM streams)
7
+ * finish within the grace, and must sever what remains once it expires.
8
+ */
9
+ /** A backend whose chat stream stays open until the test releases it. */
10
+ function heldStreamBackend() {
11
+ let controller;
12
+ return {
13
+ defaultModel: "mock-model",
14
+ chat: async () => new Response(new ReadableStream({
15
+ start(streamController) {
16
+ controller = streamController;
17
+ streamController.enqueue(Buffer.from('data: {"choices":[{"delta":{"content":"first"}}]}\n\n'));
18
+ }
19
+ }), { status: 200, headers: { "content-type": "text/event-stream" } }),
20
+ models: async () => new Response(JSON.stringify({ data: [] }), { status: 200 }),
21
+ embeddings: async () => new Response(JSON.stringify({}), { status: 200 }),
22
+ release: () => {
23
+ controller?.enqueue(Buffer.from("data: [DONE]\n\n"));
24
+ controller?.close();
25
+ }
26
+ };
27
+ }
28
+ test("drain finishes in-flight streams, rejects new work, and flips /health to 503", async () => {
29
+ const backend = heldStreamBackend();
30
+ const gateway = await startGateway({ backend });
31
+ try {
32
+ const response = await fetch(`${gateway.url()}/v1/chat/completions`, {
33
+ method: "POST",
34
+ headers: { "content-type": "application/json" },
35
+ body: JSON.stringify({
36
+ model: "mock-model",
37
+ stream: true,
38
+ messages: [{ role: "user", content: "hi" }]
39
+ })
40
+ });
41
+ const reader = response.body?.getReader();
42
+ assert.ok(reader !== undefined);
43
+ await reader.read();
44
+ // Begin the drain while the stream is in flight.
45
+ const drained = gateway.drain(5_000);
46
+ // Health flips to 503 so probes stop routing new work here.
47
+ const health = await fetch(`${gateway.url()}/health`);
48
+ assert.equal(health.status, 503);
49
+ assert.deepEqual(await health.json(), { status: "draining" });
50
+ // New model calls are rejected while draining.
51
+ const rejected = await fetch(`${gateway.url()}/v1/chat/completions`, {
52
+ method: "POST",
53
+ headers: { "content-type": "application/json" },
54
+ body: JSON.stringify({ model: "mock-model", messages: [{ role: "user", content: "hi" }] })
55
+ });
56
+ assert.equal(rejected.status, 503);
57
+ const body = (await rejected.json());
58
+ assert.equal(body.error?.type, "unavailable");
59
+ // The in-flight stream completes normally once the upstream finishes.
60
+ backend.release();
61
+ let text = "";
62
+ for (;;) {
63
+ const { done, value } = await reader.read();
64
+ if (done)
65
+ break;
66
+ text += Buffer.from(value ?? []).toString("utf8");
67
+ }
68
+ assert.match(text, /\[DONE\]/);
69
+ await drained;
70
+ }
71
+ finally {
72
+ await gateway.close();
73
+ }
74
+ });
75
+ test("drain grace expiry severs a stream that never finishes", async () => {
76
+ const backend = heldStreamBackend();
77
+ const gateway = await startGateway({ backend });
78
+ try {
79
+ const response = await fetch(`${gateway.url()}/v1/chat/completions`, {
80
+ method: "POST",
81
+ headers: { "content-type": "application/json" },
82
+ body: JSON.stringify({
83
+ model: "mock-model",
84
+ stream: true,
85
+ messages: [{ role: "user", content: "hi" }]
86
+ })
87
+ });
88
+ const reader = response.body?.getReader();
89
+ assert.ok(reader !== undefined);
90
+ await reader.read();
91
+ const started = Date.now();
92
+ await gateway.drain(300);
93
+ assert.ok(Date.now() - started >= 250, "drain waits out the grace before severing");
94
+ // The severed client observes an abnormal end, not a silent hang.
95
+ await assert.rejects(async () => {
96
+ for (;;) {
97
+ const { done } = await reader.read();
98
+ if (done)
99
+ break;
100
+ }
101
+ });
102
+ }
103
+ finally {
104
+ await gateway.close();
105
+ }
106
+ });
107
+ test("close without a prior drain remains immediate", async () => {
108
+ const backend = heldStreamBackend();
109
+ const gateway = await startGateway({ backend });
110
+ const health = await fetch(`${gateway.url()}/health`);
111
+ assert.equal(health.status, 200);
112
+ const started = Date.now();
113
+ await gateway.close();
114
+ assert.ok(Date.now() - started < 1_000, "close() must not wait for a drain grace");
115
+ await assert.rejects(fetch(`${gateway.url()}/health`));
116
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Acceptance tests for the dialect drop recorder (WS6.1): every deliberately
3
+ * untranslated field is observable on the enclosing turn span and warns
4
+ * exactly once per process.
5
+ */
6
+ import assert from "node:assert/strict";
7
+ import { beforeEach, test } from "node:test";
8
+ import { DIALECT_DROPPED_ATTRIBUTE, droppedField, resetDroppedFieldWarnings, withDroppedFieldSpan } from "../adapters/dropped.js";
9
+ function captureStderr(run) {
10
+ const original = process.stderr.write.bind(process.stderr);
11
+ let captured = "";
12
+ process.stderr.write = ((chunk) => {
13
+ captured += typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk);
14
+ return true;
15
+ });
16
+ try {
17
+ run();
18
+ }
19
+ finally {
20
+ process.stderr.write = original;
21
+ }
22
+ return captured;
23
+ }
24
+ function recordingSpan() {
25
+ const attributes = {};
26
+ return {
27
+ target: {
28
+ span: {
29
+ setAttribute(name, value) {
30
+ attributes[name] = value;
31
+ }
32
+ }
33
+ },
34
+ attributes
35
+ };
36
+ }
37
+ beforeEach(() => {
38
+ resetDroppedFieldWarnings();
39
+ });
40
+ test("warns once per distinct dialect.field, not per occurrence", () => {
41
+ const first = captureStderr(() => droppedField("anthropic", "top_k"));
42
+ const repeat = captureStderr(() => droppedField("anthropic", "top_k"));
43
+ const other = captureStderr(() => droppedField("responses", "truncation"));
44
+ assert.match(first, /anthropic field "top_k" is not translated and was dropped/);
45
+ assert.equal(repeat, "");
46
+ assert.match(other, /responses field "truncation"/);
47
+ });
48
+ test("ctx narrows the warning and the span entry", () => {
49
+ const out = captureStderr(() => droppedField("anthropic", "is_error", "tool_result"));
50
+ assert.match(out, /anthropic field "tool_result.is_error"/);
51
+ });
52
+ test("appends every occurrence to the ambient span's dropped-list attribute", () => {
53
+ const span = recordingSpan();
54
+ captureStderr(() => {
55
+ withDroppedFieldSpan(span.target, () => {
56
+ droppedField("anthropic", "top_k");
57
+ droppedField("anthropic", "metadata");
58
+ droppedField("anthropic", "top_k");
59
+ });
60
+ });
61
+ assert.deepEqual(span.attributes[DIALECT_DROPPED_ATTRIBUTE], [
62
+ "anthropic.top_k",
63
+ "anthropic.metadata",
64
+ "anthropic.top_k"
65
+ ]);
66
+ });
67
+ test("survives async boundaries within the wrapped scope", async () => {
68
+ const span = recordingSpan();
69
+ await withDroppedFieldSpan(span.target, async () => {
70
+ await Promise.resolve();
71
+ captureStderr(() => droppedField("responses", "include"));
72
+ });
73
+ assert.deepEqual(span.attributes[DIALECT_DROPPED_ATTRIBUTE], [
74
+ "responses.include"
75
+ ]);
76
+ });
77
+ test("is warn-only without an ambient span", () => {
78
+ const out = captureStderr(() => droppedField("cursor", "reasoning"));
79
+ assert.match(out, /cursor field "reasoning"/);
80
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,73 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { endpointHealthProbe, probeEndpointHealth } from "../endpoint-health.js";
4
+ function endpoint(provider, dialect, baseUrl) {
5
+ return {
6
+ endpointId: `${provider}-endpoint`,
7
+ model: `${provider}-model`,
8
+ provider,
9
+ dialect,
10
+ baseUrl
11
+ };
12
+ }
13
+ test("health probes use registry-native OpenAI, Anthropic, and Google authentication", () => {
14
+ const openai = endpointHealthProbe(endpoint("openai", "openai", "https://api.openai.com/v1"), "openai-secret");
15
+ assert.equal(openai.supported, true);
16
+ if (openai.supported) {
17
+ assert.equal(new URL(openai.probe.url).pathname, "/v1/models");
18
+ assert.equal(openai.probe.headers.authorization, "Bearer openai-secret");
19
+ }
20
+ const anthropic = endpointHealthProbe(endpoint("anthropic", "anthropic", "https://api.anthropic.com/v1"), "anthropic-secret");
21
+ assert.equal(anthropic.supported, true);
22
+ if (anthropic.supported) {
23
+ assert.equal(new URL(anthropic.probe.url).pathname, "/v1/models");
24
+ assert.equal(anthropic.probe.headers["x-api-key"], "anthropic-secret");
25
+ assert.equal(anthropic.probe.headers["anthropic-version"], "2023-06-01");
26
+ assert.equal(anthropic.probe.headers.authorization, undefined);
27
+ }
28
+ const google = endpointHealthProbe(endpoint("google", "google", "https://generativelanguage.googleapis.com/v1beta"), "google-secret");
29
+ assert.equal(google.supported, true);
30
+ if (google.supported) {
31
+ assert.equal(new URL(google.probe.url).pathname, "/v1beta/models");
32
+ assert.equal(google.probe.headers["x-goog-api-key"], "google-secret");
33
+ assert.equal(google.probe.headers.authorization, undefined);
34
+ }
35
+ });
36
+ test("custom endpoints use dialect rules while Codex avoids unsafe generation probes", async () => {
37
+ const custom = endpointHealthProbe(endpoint("private-provider", "anthropic", "https://private.example/api/v1"), "private-secret");
38
+ assert.equal(custom.supported, true);
39
+ if (custom.supported) {
40
+ assert.equal(new URL(custom.probe.url).pathname, "/api/v1/models");
41
+ assert.equal(custom.probe.headers["x-api-key"], "private-secret");
42
+ }
43
+ let requested = false;
44
+ const result = await probeEndpointHealth(endpoint("codex", "openai", "https://chatgpt.com/backend-api/codex"), {
45
+ credential: "codex-secret",
46
+ fetchImpl: async () => {
47
+ requested = true;
48
+ return new Response(null, { status: 200 });
49
+ }
50
+ });
51
+ assert.equal(requested, false);
52
+ assert.equal(result.kind, "unsupported");
53
+ if (result.kind === "unsupported")
54
+ assert.match(result.reason, /Codex responses provider/);
55
+ assert.equal(JSON.stringify(result).includes("codex-secret"), false);
56
+ const customCodex = endpointHealthProbe(endpoint("private-provider", "codex", "https://private.example/responses"), "private-secret");
57
+ assert.equal(customCodex.supported, false);
58
+ if (!customCodex.supported)
59
+ assert.match(customCodex.reason, /Codex responses dialect/);
60
+ });
61
+ test("health results classify native auth rejection without returning credentials", async () => {
62
+ const result = await probeEndpointHealth(endpoint("google", "google", "https://generativelanguage.googleapis.com/v1beta"), {
63
+ credential: "never-return-this",
64
+ fetchImpl: async () => new Response(null, { status: 400 })
65
+ });
66
+ assert.deepEqual(result, {
67
+ kind: "response",
68
+ ok: false,
69
+ status: 400,
70
+ authRejected: true
71
+ });
72
+ assert.equal(JSON.stringify(result).includes("never-return-this"), false);
73
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,176 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { buildModelCallRecord } from "../provenance.js";
4
+ import { readProducerVersion, resolveProducerGitSha, UNKNOWN_GIT_SHA } from "../provenance.js";
5
+ const GIT_SHA = /^[a-f0-9]{40}$/;
6
+ test("WS7: resolveProducerGitSha returns a real 40-hex SHA from a source checkout", () => {
7
+ const previous = process.env.ROUTEKIT_BUILD_GIT_SHA;
8
+ delete process.env.ROUTEKIT_BUILD_GIT_SHA;
9
+ try {
10
+ // The test runs from the handoffkit checkout (not node_modules), so the
11
+ // runtime `git rev-parse HEAD` fallback resolves the real producer SHA.
12
+ const sha = resolveProducerGitSha();
13
+ assert.match(sha, GIT_SHA, "a checkout resolves a real git SHA");
14
+ assert.notEqual(sha, "0".repeat(40), "never the all-zero faked-provenance placeholder");
15
+ }
16
+ finally {
17
+ if (previous !== undefined)
18
+ process.env.ROUTEKIT_BUILD_GIT_SHA = previous;
19
+ }
20
+ });
21
+ test("WS7: a build-time stamp wins over the checkout git lookup", () => {
22
+ const previous = process.env.ROUTEKIT_BUILD_GIT_SHA;
23
+ const stamped = "a".repeat(40);
24
+ process.env.ROUTEKIT_BUILD_GIT_SHA = stamped;
25
+ try {
26
+ assert.equal(resolveProducerGitSha(), stamped);
27
+ }
28
+ finally {
29
+ if (previous === undefined)
30
+ delete process.env.ROUTEKIT_BUILD_GIT_SHA;
31
+ else
32
+ process.env.ROUTEKIT_BUILD_GIT_SHA = previous;
33
+ }
34
+ });
35
+ test("WS7: an installed copy (node_modules, no stamp) falls back to the 'unknown' sentinel", () => {
36
+ const previous = process.env.ROUTEKIT_BUILD_GIT_SHA;
37
+ delete process.env.ROUTEKIT_BUILD_GIT_SHA;
38
+ try {
39
+ // A node_modules path is treated as an installed artifact: no git lookup
40
+ // (so we never mis-report the consumer's repo) → the clearly-marked sentinel.
41
+ const sha = resolveProducerGitSha("/tmp/project/node_modules/@velum-labs/routekit-gateway/dist");
42
+ assert.equal(sha, UNKNOWN_GIT_SHA);
43
+ assert.equal(sha, "unknown");
44
+ assert.notEqual(sha, "0".repeat(40), "the sentinel is never 40 zeros");
45
+ }
46
+ finally {
47
+ if (previous !== undefined)
48
+ process.env.ROUTEKIT_BUILD_GIT_SHA = previous;
49
+ }
50
+ });
51
+ test("WS7: readProducerVersion reads this package's real version", () => {
52
+ const version = readProducerVersion();
53
+ assert.match(version, /^\d+\.\d+\.\d+/, "a real semver, not the 0.1.0 stub");
54
+ });
55
+ test("WS7: a built model-call record carries normalized provenance", () => {
56
+ const record = buildModelCallRecord({
57
+ callId: "call_test_1",
58
+ dialect: "openai-chat",
59
+ requestedModel: "gpt-5.5",
60
+ model: "gpt-5.5",
61
+ stream: false,
62
+ requestBody: { messages: [{ role: "user", content: "hi" }] },
63
+ startedAt: "2026-06-27T00:00:00.000Z",
64
+ attribution: {
65
+ effective_model: "openai/gpt-5.5",
66
+ native_model: "gpt-5.5",
67
+ provider: "openai",
68
+ billing_mode: "api_key",
69
+ attempts: 1,
70
+ retries: 0,
71
+ account_failovers: 0
72
+ }
73
+ }, { statusCode: 200, durationMs: 12, responseBody: Buffer.from(JSON.stringify({ id: "x" })) });
74
+ assert.equal(record.call_id, "call_test_1");
75
+ assert.equal(record.endpoint_id, "openai-chat");
76
+ assert.equal(record.status, "succeeded");
77
+ assert.equal(record.metadata?.unknown_cost, true);
78
+ assert.deepEqual(record.metadata?.attribution, {
79
+ effective_model: "openai/gpt-5.5",
80
+ native_model: "gpt-5.5",
81
+ provider: "openai",
82
+ billing_mode: "api_key",
83
+ attempts: 1,
84
+ retries: 0,
85
+ account_failovers: 0
86
+ });
87
+ });
88
+ test("model-call provenance replaces raw upstream errors with a safe summary", () => {
89
+ const secret = "sk-secret-must-not-survive";
90
+ const record = buildModelCallRecord({
91
+ callId: "call_redacted",
92
+ dialect: "openai-chat",
93
+ requestedModel: "openai/gpt-5.5",
94
+ model: "openai/gpt-5.5",
95
+ stream: false,
96
+ requestBody: { messages: [{ role: "user", content: "hi" }] },
97
+ startedAt: "2026-06-27T00:00:00.000Z"
98
+ }, {
99
+ statusCode: 502,
100
+ durationMs: 2,
101
+ error: new Error(`authorization Bearer ${secret}`)
102
+ });
103
+ assert.equal(record.error?.message, "provider request failed");
104
+ assert.doesNotMatch(JSON.stringify(record), new RegExp(secret));
105
+ assert.doesNotMatch(JSON.stringify(record), /authorization/i);
106
+ });
107
+ test("an unconfigured router is not attributed to a provider or marked retryable", () => {
108
+ const record = buildModelCallRecord({
109
+ callId: "call_no_route",
110
+ dialect: "anthropic-messages",
111
+ requestedModel: undefined,
112
+ model: undefined,
113
+ stream: false,
114
+ requestBody: { messages: [{ role: "user", content: "hi" }] },
115
+ startedAt: "2026-07-22T00:00:00.000Z"
116
+ }, {
117
+ statusCode: 503,
118
+ durationMs: 1,
119
+ responseBody: Buffer.from(JSON.stringify({
120
+ error: {
121
+ type: "unavailable",
122
+ message: "no model is available; configure a provider"
123
+ }
124
+ }))
125
+ });
126
+ assert.equal(record.metadata?.attribution, undefined);
127
+ assert.deepEqual(record.error, {
128
+ kind: "capability_missing",
129
+ message: "no model route is configured",
130
+ retryable: false
131
+ });
132
+ });
133
+ test("model-call provenance meters aggregate buffered and Responses SSE usage", () => {
134
+ const context = {
135
+ callId: "call_aggregate",
136
+ dialect: "openai-responses",
137
+ requestedModel: "gpt-5.5",
138
+ model: "gpt-5.5",
139
+ stream: false,
140
+ requestBody: { input: "research" },
141
+ startedAt: "2026-06-27T00:00:00.000Z"
142
+ };
143
+ const buffered = buildModelCallRecord(context, {
144
+ statusCode: 200,
145
+ durationMs: 3,
146
+ responseBody: Buffer.from(JSON.stringify({
147
+ usage: {
148
+ input_tokens: 30,
149
+ output_tokens: 12,
150
+ total_tokens: 42
151
+ }
152
+ }))
153
+ });
154
+ assert.deepEqual(buffered.usage, {
155
+ prompt_tokens: 30,
156
+ completion_tokens: 12,
157
+ total_tokens: 42
158
+ });
159
+ assert.equal(buffered.metadata?.cost_estimate_usd, 0.0001575);
160
+ const streamed = buildModelCallRecord({ ...context, callId: "call_aggregate_stream", stream: true }, {
161
+ statusCode: 200,
162
+ durationMs: 3,
163
+ responseBody: Buffer.from(`data: ${JSON.stringify({
164
+ type: "response.completed",
165
+ response: {
166
+ usage: {
167
+ input_tokens: 30,
168
+ output_tokens: 12,
169
+ total_tokens: 42
170
+ }
171
+ }
172
+ })}\n\n`)
173
+ });
174
+ assert.deepEqual(streamed.usage, buffered.usage);
175
+ assert.equal(streamed.metadata?.cost_estimate_usd, 0.0001575);
176
+ });
@@ -0,0 +1 @@
1
+ export {};