cachegate 1.1.0 → 1.2.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.
@@ -1,357 +0,0 @@
1
- const { test } = require('node:test');
2
- const assert = require('node:assert/strict');
3
- const http = require('http');
4
- const fs = require('fs');
5
- const os = require('os');
6
- const path = require('path');
7
-
8
- // server.js reads MODEL_ROUTER_INTERNAL_KEY / METRICS_LOG_PATH once at
9
- // require time, so set env before requiring, and use a throwaway
10
- // metrics file per test run so this suite never touches the real one.
11
- process.env.MODEL_ROUTER_INTERNAL_KEY = 'test-internal-key';
12
- process.env.METRICS_LOG_PATH = path.join(
13
- fs.mkdtempSync(path.join(os.tmpdir(), 'server-test-')),
14
- 'metrics.jsonl'
15
- );
16
- // No provider keys set - tests below never exercise an actual provider
17
- // call (that needs live API keys and real spend, and is out of scope
18
- // for this suite; see README's "Where this leaves things").
19
-
20
- const { app } = require('../server');
21
-
22
- function listen() {
23
- return new Promise((resolve) => {
24
- const server = http.createServer(app);
25
- server.listen(0, () => resolve(server));
26
- });
27
- }
28
-
29
- function request(server, options, body) {
30
- return new Promise((resolve, reject) => {
31
- const req = http.request(
32
- { host: '127.0.0.1', port: server.address().port, ...options },
33
- (res) => {
34
- let data = '';
35
- res.on('data', (chunk) => { data += chunk; });
36
- res.on('end', () => {
37
- let parsed;
38
- try { parsed = JSON.parse(data); } catch { parsed = data; }
39
- resolve({ status: res.statusCode, body: parsed });
40
- });
41
- }
42
- );
43
- req.on('error', reject);
44
- if (body) req.write(JSON.stringify(body));
45
- req.end();
46
- });
47
- }
48
-
49
- test('GET /health is public and reports routing tiers', async (t) => {
50
- const server = await listen();
51
- t.after(() => server.close());
52
-
53
- const res = await request(server, { method: 'GET', path: '/health' });
54
- assert.equal(res.status, 200);
55
- assert.equal(res.body.status, 'healthy');
56
- assert.ok(Array.isArray(res.body.routing_tiers));
57
- assert.ok(res.body.routing_tiers.includes('router:fast-cheap'));
58
- assert.equal(res.body.routing_strategy, 'cost'); // no ROUTER_STRATEGY set in this test suite - default applies
59
- });
60
-
61
- test('POST /v1/chat/completions without a key is rejected', async (t) => {
62
- const server = await listen();
63
- t.after(() => server.close());
64
-
65
- const res = await request(
66
- server,
67
- { method: 'POST', path: '/v1/chat/completions', headers: { 'Content-Type': 'application/json' } },
68
- { model: 'gpt-4o-mini', messages: [{ role: 'user', content: 'hi' }] }
69
- );
70
- assert.equal(res.status, 401);
71
- });
72
-
73
- test('POST /v1/chat/completions with the wrong key is rejected', async (t) => {
74
- const server = await listen();
75
- t.after(() => server.close());
76
-
77
- const res = await request(
78
- server,
79
- {
80
- method: 'POST',
81
- path: '/v1/chat/completions',
82
- headers: { 'Content-Type': 'application/json', Authorization: 'Bearer wrong-key' }
83
- },
84
- { model: 'gpt-4o-mini', messages: [{ role: 'user', content: 'hi' }] }
85
- );
86
- assert.equal(res.status, 401);
87
- });
88
-
89
- test('POST /v1/chat/completions with the right key but missing fields returns 400', async (t) => {
90
- const server = await listen();
91
- t.after(() => server.close());
92
-
93
- const res = await request(
94
- server,
95
- {
96
- method: 'POST',
97
- path: '/v1/chat/completions',
98
- headers: { 'Content-Type': 'application/json', Authorization: 'Bearer test-internal-key' }
99
- },
100
- { model: 'gpt-4o-mini' } // messages missing
101
- );
102
- assert.equal(res.status, 400);
103
- });
104
-
105
- test('POST /v1/chat/completions with stream:true AND tools is rejected - tool-call streaming is out of scope', async (t) => {
106
- const server = await listen();
107
- t.after(() => server.close());
108
-
109
- const res = await request(
110
- server,
111
- {
112
- method: 'POST',
113
- path: '/v1/chat/completions',
114
- headers: { 'Content-Type': 'application/json', Authorization: 'Bearer test-internal-key' }
115
- },
116
- {
117
- model: 'gpt-4o-mini',
118
- messages: [{ role: 'user', content: 'hi' }],
119
- stream: true,
120
- tools: [{ type: 'function', function: { name: 'do_thing' } }]
121
- }
122
- );
123
- assert.equal(res.status, 400);
124
- assert.match(res.body.error, /stream/i);
125
- assert.match(res.body.error, /tools/i);
126
- });
127
-
128
- test('POST /v1/chat/completions with stream:true (no tools) attempts real dispatch - missing provider key is still a clean JSON 500, not a broken stream', async (t) => {
129
- const server = await listen();
130
- t.after(() => server.close());
131
-
132
- const res = await request(
133
- server,
134
- {
135
- method: 'POST',
136
- path: '/v1/chat/completions',
137
- headers: { 'Content-Type': 'application/json', Authorization: 'Bearer test-internal-key' }
138
- },
139
- { model: 'gpt-4o-mini', messages: [{ role: 'user', content: 'hi' }], stream: true }
140
- );
141
- // No OPENAI_API_KEY is set in this test suite (see the header comment) -
142
- // the missing-key check happens before SSE headers are ever written,
143
- // so this is a normal JSON error response, not a broken/partial stream.
144
- assert.equal(res.status, 500);
145
- assert.match(res.body.error, /OPENAI_API_KEY/);
146
- });
147
-
148
- test('POST /v1/chat/completions with an unsupported model name returns 400', async (t) => {
149
- const server = await listen();
150
- t.after(() => server.close());
151
-
152
- const res = await request(
153
- server,
154
- {
155
- method: 'POST',
156
- path: '/v1/chat/completions',
157
- headers: { 'Content-Type': 'application/json', Authorization: 'Bearer test-internal-key' }
158
- },
159
- { model: 'llama-3-70b', messages: [{ role: 'user', content: 'hi' }] }
160
- );
161
- assert.equal(res.status, 400);
162
- });
163
-
164
- test('POST /v1/chat/completions with a body over JSON_BODY_LIMIT returns a clean JSON error, not a stack trace', async (t) => {
165
- const server = await listen();
166
- t.after(() => server.close());
167
-
168
- // Security-review finding (2026-08-29): before the catch-all error
169
- // handler existed, this scenario fell through to Express's own
170
- // default handler - a raw HTML page containing the full stack trace
171
- // and this server's absolute filesystem paths. Regression-guards
172
- // both the JSON shape and the absence of anything stack-trace-shaped.
173
- const res = await request(
174
- server,
175
- {
176
- method: 'POST',
177
- path: '/v1/chat/completions',
178
- headers: { 'Content-Type': 'application/json', Authorization: 'Bearer test-internal-key' }
179
- },
180
- { model: 'gpt-4o-mini', messages: [{ role: 'user', content: 'a'.repeat(3 * 1024 * 1024) }] }
181
- );
182
- assert.equal(res.status, 413);
183
- assert.equal(typeof res.body, 'object'); // JSON, not an HTML string
184
- assert.equal(res.body.error, 'Request body too large.');
185
- assert.ok(!JSON.stringify(res.body).includes('node_modules')); // no stack trace/filesystem paths leaked
186
- });
187
-
188
- test('an unauthenticated request with an oversized body still gets a plain 401, not a body-size error - auth runs before parsing', async (t) => {
189
- const server = await listen();
190
- t.after(() => server.close());
191
-
192
- const res = await request(
193
- server,
194
- { method: 'POST', path: '/v1/chat/completions', headers: { 'Content-Type': 'application/json' } },
195
- { model: 'gpt-4o-mini', messages: [{ role: 'user', content: 'a'.repeat(3 * 1024 * 1024) }] }
196
- );
197
- assert.equal(res.status, 401);
198
- });
199
-
200
- test('responses never carry an X-Powered-By header', async (t) => {
201
- const server = await listen();
202
- t.after(() => server.close());
203
-
204
- const res = await request(server, { method: 'GET', path: '/health' });
205
- assert.equal(res.status, 200);
206
- // request() only returns {status, body} - check via a raw request for headers.
207
- await new Promise((resolve, reject) => {
208
- const req = http.request(
209
- { host: '127.0.0.1', port: server.address().port, method: 'GET', path: '/health' },
210
- (rawRes) => {
211
- assert.equal(rawRes.headers['x-powered-by'], undefined);
212
- rawRes.resume();
213
- rawRes.on('end', resolve);
214
- }
215
- );
216
- req.on('error', reject);
217
- req.end();
218
- });
219
- });
220
-
221
- test('POST /v1/chat/completions with an unknown routing tier returns 400', async (t) => {
222
- const server = await listen();
223
- t.after(() => server.close());
224
-
225
- const res = await request(
226
- server,
227
- {
228
- method: 'POST',
229
- path: '/v1/chat/completions',
230
- headers: { 'Content-Type': 'application/json', Authorization: 'Bearer test-internal-key' }
231
- },
232
- { model: 'router:does-not-exist', messages: [{ role: 'user', content: 'hi' }] }
233
- );
234
- assert.equal(res.status, 400);
235
- });
236
-
237
- test('GET /stats requires the internal key and returns aggregate shape', async (t) => {
238
- const server = await listen();
239
- t.after(() => server.close());
240
-
241
- const unauthed = await request(server, { method: 'GET', path: '/stats' });
242
- assert.equal(unauthed.status, 401);
243
-
244
- const authed = await request(server, {
245
- method: 'GET',
246
- path: '/stats',
247
- headers: { Authorization: 'Bearer test-internal-key' }
248
- });
249
- assert.equal(authed.status, 200);
250
- assert.ok('sample_size' in authed.body);
251
- assert.ok('cache_hit_rate' in authed.body);
252
- assert.ok('by_provider' in authed.body);
253
- });
254
-
255
- test('GET /dashboard/data requires the internal key and returns the range-summary shape', async (t) => {
256
- const server = await listen();
257
- t.after(() => server.close());
258
-
259
- const unauthed = await request(server, { method: 'GET', path: '/dashboard/data' });
260
- assert.equal(unauthed.status, 401);
261
-
262
- const authed = await request(server, {
263
- method: 'GET',
264
- path: '/dashboard/data?days=7',
265
- headers: { Authorization: 'Bearer test-internal-key' }
266
- });
267
- assert.equal(authed.status, 200);
268
- assert.equal(authed.body.days, 7);
269
- assert.ok('sample_size' in authed.body);
270
- assert.ok('cache_hit_rate' in authed.body);
271
- assert.ok('error_rate' in authed.body);
272
- assert.ok(Array.isArray(authed.body.daily));
273
- assert.ok('by_provider' in authed.body);
274
- assert.deepEqual(authed.body.provider_alerts, [], 'a healthy deployment should report no alerts');
275
- });
276
-
277
- test('GET /dashboard/data surfaces provider_alerts once a provider has a recent classified error', async (t) => {
278
- const server = await listen();
279
- t.after(() => server.close());
280
-
281
- const metrics = require('../metrics');
282
- metrics.record({
283
- provider: 'anthropic',
284
- error: '401 {"type":"error","error":{"type":"authentication_error","message":"API key is invalid."}}',
285
- error_type: 'authentication_error'
286
- });
287
- await new Promise((resolve) => setTimeout(resolve, 50)); // let the write stream flush
288
-
289
- const authed = await request(server, {
290
- method: 'GET',
291
- path: '/dashboard/data',
292
- headers: { Authorization: 'Bearer test-internal-key' }
293
- });
294
- assert.equal(authed.status, 200);
295
- assert.equal(authed.body.provider_alerts.length, 1);
296
- assert.equal(authed.body.provider_alerts[0].provider, 'anthropic');
297
- assert.equal(authed.body.provider_alerts[0].error_type, 'authentication_error');
298
- assert.ok(authed.body.provider_alerts[0].last_error_at);
299
- });
300
-
301
- test('GET /dashboard/data clamps an out-of-range days value instead of erroring', async (t) => {
302
- const server = await listen();
303
- t.after(() => server.close());
304
-
305
- const tooMany = await request(server, {
306
- method: 'GET',
307
- path: '/dashboard/data?days=99999',
308
- headers: { Authorization: 'Bearer test-internal-key' }
309
- });
310
- assert.equal(tooMany.status, 200);
311
- assert.equal(tooMany.body.days, 90);
312
-
313
- const zero = await request(server, {
314
- method: 'GET',
315
- path: '/dashboard/data?days=0',
316
- headers: { Authorization: 'Bearer test-internal-key' }
317
- });
318
- assert.equal(zero.status, 200);
319
- assert.equal(zero.body.days, 1);
320
- });
321
-
322
- test('GET /dashboard is public and serves the dashboard page', async (t) => {
323
- const server = await listen();
324
- t.after(() => server.close());
325
-
326
- const res = await request(server, { method: 'GET', path: '/dashboard' });
327
- assert.equal(res.status, 200);
328
- assert.match(res.body, /Cost Dashboard/);
329
- });
330
-
331
- // Deliberately LAST in this file: neither provider key is set anywhere
332
- // in this suite (see the top of this file), so a virtual-model request
333
- // here always exhausts the failover loop and records a classified
334
- // error for BOTH candidates - exactly the kind of write the earlier
335
- // provider_alerts tests above assume nothing else has made yet. This
336
- // exists to prove the failover loop actually runs end-to-end through
337
- // the real HTTP route wiring (failover.js + router.js's
338
- // rankedCandidates + dispatchToProvider) and fails cleanly with one
339
- // response, not a crash or a hang, without needing a live provider
340
- // call to do it - it just has to run after everything that depends on
341
- // a clean alert slate, not before.
342
- test('POST /v1/chat/completions with a virtual model tries every candidate via failover, then fails cleanly', async (t) => {
343
- const server = await listen();
344
- t.after(() => server.close());
345
-
346
- const res = await request(
347
- server,
348
- {
349
- method: 'POST',
350
- path: '/v1/chat/completions',
351
- headers: { 'Content-Type': 'application/json', Authorization: 'Bearer test-internal-key' }
352
- },
353
- { model: 'router:fast-cheap', messages: [{ role: 'user', content: 'hi' }] }
354
- );
355
- assert.equal(res.status, 502);
356
- assert.ok(res.body.error);
357
- });
@@ -1,248 +0,0 @@
1
- const { test, before, after } = require('node:test');
2
- const assert = require('node:assert/strict');
3
- const { spawn } = require('child_process');
4
- const net = require('net');
5
- const http = require('http');
6
- const fs = require('fs');
7
- const os = require('os');
8
- const path = require('path');
9
-
10
- const streaming = require('../streaming');
11
- const anthropicProvider = require('../providers/anthropic');
12
- const openaiProvider = require('../providers/openai');
13
-
14
- // ---------- pure SSE frame-building tests (no network, no Redis) ----------
15
-
16
- test('roleChunk/deltaChunk/finalChunk/doneFrame produce valid "data: <json>\\n\\n" SSE lines', () => {
17
- const id = 'chatcmpl-test';
18
- const role = streaming.roleChunk({ id, model: 'gpt-4o-mini' });
19
- assert.match(role, /^data: /);
20
- assert.match(role, /\n\n$/);
21
- const roleJson = JSON.parse(role.slice('data: '.length).trim());
22
- assert.equal(roleJson.choices[0].delta.role, 'assistant');
23
- assert.equal(roleJson.object, 'chat.completion.chunk');
24
-
25
- const delta = streaming.deltaChunk({ id, model: 'gpt-4o-mini', content: 'Hello' });
26
- const deltaJson = JSON.parse(delta.slice('data: '.length).trim());
27
- assert.equal(deltaJson.choices[0].delta.content, 'Hello');
28
- assert.equal(deltaJson.choices[0].finish_reason, null);
29
-
30
- const final = streaming.finalChunk({
31
- id, model: 'gpt-4o-mini', usage: { input_tokens: 5, output_tokens: 3 },
32
- cost_usd: 0.001, provider: 'openai', cached: false
33
- });
34
- const finalJson = JSON.parse(final.slice('data: '.length).trim());
35
- assert.equal(finalJson.choices[0].finish_reason, 'stop');
36
- assert.equal(finalJson.cost_usd, 0.001);
37
- assert.equal(finalJson.provider, 'openai');
38
- assert.equal(finalJson.cached, false);
39
- assert.equal(finalJson.cache_type, undefined); // omitted, not set to a falsy placeholder
40
-
41
- assert.equal(streaming.doneFrame(), 'data: [DONE]\n\n');
42
- });
43
-
44
- test('finalChunk includes cache_type only when a cache_type was actually given', () => {
45
- const withType = streaming.finalChunk({ id: 'x', model: 'm', cached: true, cache_type: 'semantic' });
46
- assert.match(withType, /"cache_type":"semantic"/);
47
-
48
- const withoutType = streaming.finalChunk({ id: 'x', model: 'm', cached: false });
49
- assert.doesNotMatch(withoutType, /cache_type/);
50
- });
51
-
52
- test('errorFrame carries the message in an { error } shape distinguishable from a normal chunk', () => {
53
- const frame = streaming.errorFrame('upstream timed out');
54
- const json = JSON.parse(frame.slice('data: '.length).trim());
55
- assert.equal(json.error.message, 'upstream timed out');
56
- assert.equal(json.choices, undefined);
57
- });
58
-
59
- test('genId() produces unique ids', () => {
60
- const ids = new Set([streaming.genId(), streaming.genId(), streaming.genId()]);
61
- assert.equal(ids.size, 3);
62
- });
63
-
64
- // ---------- pure stream-event accumulator tests (no network) ----------
65
-
66
- test('anthropic applyStreamEvent accumulates text deltas and final usage from a realistic event sequence', () => {
67
- const state = { content: '', inputTokens: 0, outputTokens: 0 };
68
- const deltas = [];
69
- const onDelta = (t) => deltas.push(t);
70
-
71
- const events = [
72
- { type: 'message_start', message: { usage: { input_tokens: 12, output_tokens: 0 } } },
73
- { type: 'content_block_start' },
74
- { type: 'content_block_delta', delta: { type: 'text_delta', text: 'Hello' } },
75
- { type: 'content_block_delta', delta: { type: 'text_delta', text: ', world' } },
76
- { type: 'content_block_stop' },
77
- { type: 'message_delta', usage: { output_tokens: 4 } },
78
- { type: 'message_stop' }
79
- ];
80
- events.forEach((e) => anthropicProvider.applyStreamEvent(state, e, onDelta));
81
-
82
- assert.equal(state.content, 'Hello, world');
83
- assert.equal(state.inputTokens, 12);
84
- assert.equal(state.outputTokens, 4);
85
- assert.deepEqual(deltas, ['Hello', ', world']);
86
- });
87
-
88
- test('anthropic applyStreamEvent ignores non-text content block deltas', () => {
89
- const state = { content: '', inputTokens: 0, outputTokens: 0 };
90
- anthropicProvider.applyStreamEvent(state, { type: 'message_start', message: { usage: { input_tokens: 1 } } }, () => {});
91
- anthropicProvider.applyStreamEvent(state, { type: 'content_block_delta', delta: { type: 'input_json_delta', partial_json: '{}' } }, () => {
92
- throw new Error('onDelta should not fire for a non-text delta');
93
- });
94
- assert.equal(state.content, '');
95
- });
96
-
97
- test('openai applyStreamChunk accumulates text deltas and only reads usage from the choice-less final chunk', () => {
98
- const state = { content: '', inputTokens: 0, outputTokens: 0 };
99
- const deltas = [];
100
- const onDelta = (t) => deltas.push(t);
101
-
102
- const chunks = [
103
- { choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }] },
104
- { choices: [{ index: 0, delta: { content: 'Hi' }, finish_reason: null }] },
105
- { choices: [{ index: 0, delta: { content: ' there' }, finish_reason: null }] },
106
- { choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] },
107
- { choices: [], usage: { prompt_tokens: 8, completion_tokens: 2, total_tokens: 10 } }
108
- ];
109
- chunks.forEach((c) => openaiProvider.applyStreamChunk(state, c, onDelta));
110
-
111
- assert.equal(state.content, 'Hi there');
112
- assert.equal(state.inputTokens, 8);
113
- assert.equal(state.outputTokens, 2);
114
- assert.deepEqual(deltas, ['Hi', ' there']);
115
- });
116
-
117
- test('openai applyStreamChunk does nothing dangerous on a chunk with no choices and no usage', () => {
118
- const state = { content: '', inputTokens: 0, outputTokens: 0 };
119
- assert.doesNotThrow(() => openaiProvider.applyStreamChunk(state, { choices: [] }, () => {}));
120
- assert.equal(state.content, '');
121
- });
122
-
123
- // ---------- real end-to-end SSE test, through an ephemeral Redis-backed cache ----------
124
- // (mirrors semanticCache.test.js's self-contained pattern - no external
125
- // service assumed, no live provider API call needed since this only
126
- // exercises the cached-hit replay path)
127
-
128
- let redisProcess;
129
- let redisClient;
130
- let cache;
131
- let app;
132
-
133
- function getFreePort() {
134
- return new Promise((resolve, reject) => {
135
- const srv = net.createServer();
136
- srv.listen(0, () => {
137
- const { port } = srv.address();
138
- srv.close(() => resolve(port));
139
- });
140
- srv.on('error', reject);
141
- });
142
- }
143
-
144
- function waitForRedisReady(proc) {
145
- return new Promise((resolve, reject) => {
146
- const timeout = setTimeout(() => reject(new Error('redis-server did not become ready in time')), 10_000);
147
- proc.stdout.on('data', (chunk) => {
148
- if (chunk.toString().includes('Ready to accept connections')) {
149
- clearTimeout(timeout);
150
- resolve();
151
- }
152
- });
153
- proc.on('error', reject);
154
- });
155
- }
156
-
157
- before(async () => {
158
- const redisPort = await getFreePort();
159
- process.env.REDIS_URL = `redis://127.0.0.1:${redisPort}`;
160
- process.env.MODEL_ROUTER_INTERNAL_KEY = 'stream-test-key';
161
- process.env.METRICS_LOG_PATH = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'streaming-test-')), 'metrics.jsonl');
162
-
163
- redisProcess = spawn('redis-server', ['--port', String(redisPort), '--save', '', '--appendonly', 'no'], {
164
- stdio: ['ignore', 'pipe', 'pipe']
165
- });
166
- await waitForRedisReady(redisProcess);
167
-
168
- redisClient = require('../redisClient');
169
- await redisClient.ready;
170
- cache = require('../cache');
171
- app = require('../server').app;
172
- });
173
-
174
- after(async () => {
175
- try { await redisClient.client.quit(); } catch { /* already closed */ }
176
- if (redisProcess) redisProcess.kill();
177
- });
178
-
179
- function listen() {
180
- return new Promise((resolve) => {
181
- const server = http.createServer(app);
182
- server.listen(0, () => resolve(server));
183
- });
184
- }
185
-
186
- function postSse(server, body) {
187
- return new Promise((resolve, reject) => {
188
- const req = http.request(
189
- {
190
- host: '127.0.0.1',
191
- port: server.address().port,
192
- method: 'POST',
193
- path: '/v1/chat/completions',
194
- headers: { 'Content-Type': 'application/json', Authorization: 'Bearer stream-test-key' }
195
- },
196
- (res) => {
197
- let data = '';
198
- res.on('data', (chunk) => { data += chunk; });
199
- res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body: data }));
200
- }
201
- );
202
- req.on('error', reject);
203
- req.write(JSON.stringify(body));
204
- req.end();
205
- });
206
- }
207
-
208
- test('a streamed request that hits the exact cache replays the cached content as SSE, not a JSON body', async (t) => {
209
- const server = await listen();
210
- t.after(() => server.close());
211
-
212
- const payload = { model: 'gpt-4o-mini', messages: [{ role: 'user', content: 'stream cache replay test' }] };
213
- await cache.set(payload, {
214
- provider: 'openai',
215
- model: 'gpt-4o-mini',
216
- content: 'Hello from cache',
217
- usage: { input_tokens: 5, output_tokens: 3 }
218
- });
219
-
220
- const res = await postSse(server, { ...payload, stream: true });
221
-
222
- assert.equal(res.status, 200);
223
- assert.equal(res.headers['content-type'], 'text/event-stream');
224
- assert.match(res.body, /"delta":\{"role":"assistant"\}/);
225
- assert.match(res.body, /"content":"Hello from cache"/);
226
- assert.match(res.body, /"cached":true/);
227
- assert.match(res.body, /"cache_type":"exact"/);
228
- assert.match(res.body, /"finish_reason":"stop"/);
229
- assert.match(res.body, /data: \[DONE\]\n\n$/);
230
- });
231
-
232
- test('a streamed request that misses every cache with no provider key configured still gets a clean JSON error, not a broken stream', async (t) => {
233
- const server = await listen();
234
- t.after(() => server.close());
235
-
236
- const res = await postSse(server, {
237
- model: 'gpt-4o-mini',
238
- messages: [{ role: 'user', content: 'definitely not cached ' + Math.random() }],
239
- stream: true
240
- });
241
-
242
- // No OPENAI_API_KEY is set anywhere in this test run, and the
243
- // missing-key check runs before SSE headers are written.
244
- assert.equal(res.status, 500);
245
- assert.notEqual(res.headers['content-type'], 'text/event-stream');
246
- const parsed = JSON.parse(res.body);
247
- assert.match(parsed.error, /OPENAI_API_KEY/);
248
- });