llm-switcher 1.1.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 (51) hide show
  1. package/.gitattributes +16 -0
  2. package/LICENSE +21 -0
  3. package/README.md +587 -0
  4. package/README.vi.md +585 -0
  5. package/blindfold/blindfold.mjs +633 -0
  6. package/blindfold/make-certs.sh +88 -0
  7. package/blindfold/wsframe.mjs +176 -0
  8. package/codex-catalog-template.json +1 -0
  9. package/config.example.json +84 -0
  10. package/contract-exclusions.json +41 -0
  11. package/contract.mjs +561 -0
  12. package/docs/LLM-RESPONSE-MATRIX.md +165 -0
  13. package/docs/TOKEN-OPTIMIZER-INTEROP.md +110 -0
  14. package/docs/codex-blindfold.md +214 -0
  15. package/docs/cross-platform.md +136 -0
  16. package/docs/diagrams/blindfold-request-routing.html +14972 -0
  17. package/docs/diagrams/blindfold-request-routing.sequence.json +175 -0
  18. package/docs/diagrams/blindfold-switch-lifecycle.html +14958 -0
  19. package/docs/diagrams/blindfold-switch-lifecycle.lifecycle.json +159 -0
  20. package/docs/diagrams/codex-model-name-resolution.html +15005 -0
  21. package/docs/diagrams/codex-model-name-resolution.workflow.json +71 -0
  22. package/docs/response-matrix.json +1131 -0
  23. package/formats.mjs +2308 -0
  24. package/mcp.mjs +340 -0
  25. package/package.json +36 -0
  26. package/proxy.mjs +1743 -0
  27. package/service.mjs +132 -0
  28. package/shim.mjs +292 -0
  29. package/skills/llm-switcher/SKILL.md +88 -0
  30. package/state.mjs +978 -0
  31. package/switch +5 -0
  32. package/switch.cmd +2 -0
  33. package/switch.mjs +930 -0
  34. package/tests/blindfold.test.mjs +307 -0
  35. package/tests/blindfold.wire.test.mjs +170 -0
  36. package/tests/contract/run.test.mjs +214 -0
  37. package/tests/contract-check.test.mjs +458 -0
  38. package/tests/contract-lab.test.mjs +755 -0
  39. package/tests/datadir.test.mjs +37 -0
  40. package/tests/formats.test.mjs +794 -0
  41. package/tests/gateway.e2e.test.mjs +999 -0
  42. package/tests/helpers.mjs +24 -0
  43. package/tests/lifecycle.test.mjs +416 -0
  44. package/tests/live-optimizer-interop.mjs +205 -0
  45. package/tests/mcp.test.mjs +91 -0
  46. package/tests/service.test.mjs +69 -0
  47. package/tests/shim.test.mjs +228 -0
  48. package/tests/state.test.mjs +675 -0
  49. package/tests/switch.test.mjs +156 -0
  50. package/tests/wsframe.test.mjs +154 -0
  51. package/ui.html +2234 -0
@@ -0,0 +1,755 @@
1
+ // Contract lab, llm-switcher half: tagging, the bounded upload queue and the policy cache.
2
+ // The e2e tests prove the hard rule: a slow, broken or absent intact never changes the bytes
3
+ // a coding tool receives, and never delays them.
4
+ import { test, before, after } from 'node:test';
5
+ import assert from 'node:assert/strict';
6
+ import http from 'node:http';
7
+ import net from 'node:net';
8
+ import crypto from 'node:crypto';
9
+ import fs from 'node:fs';
10
+ import os from 'node:os';
11
+ import path from 'node:path';
12
+ import { spawn, execFileSync } from 'node:child_process';
13
+ import { fileURLToPath } from 'node:url';
14
+ import { createFrameReader } from '../blindfold/wsframe.mjs';
15
+ import {
16
+ createContractLab, createHalfTap, newTraceId, switcherVersion, toolVersionFromUA, capJson,
17
+ finishHalf, TRACE_ID_RE, SWITCHER_VERSION_RE, MAX_QUEUE,
18
+ PROBE_FORMATS, probeVariants, probeRequest, probeModels, runProbe
19
+ } from '../contract.mjs';
20
+ import { contractLabSettings, MASKED_KEY } from '../state.mjs';
21
+
22
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
23
+
24
+ const sleep = (ms) => new Promise(r => setTimeout(r, ms));
25
+
26
+ async function waitFor(fn, what, ms = 5000) {
27
+ const deadline = Date.now() + ms;
28
+ while (Date.now() < deadline) {
29
+ if (await fn()) return true;
30
+ await sleep(10);
31
+ }
32
+ throw new Error(`timed out waiting for ${what}`);
33
+ }
34
+
35
+ function freePort() {
36
+ return new Promise((resolve, reject) => {
37
+ const s = http.createServer();
38
+ s.listen(0, '127.0.0.1', () => {
39
+ const { port } = s.address();
40
+ s.close(() => resolve(port));
41
+ });
42
+ s.on('error', reject);
43
+ });
44
+ }
45
+
46
+ // ---------------------------------------------------------------- unit: ids and versions
47
+
48
+ test('a trace id matches the regex intact accepts, and two ids differ', () => {
49
+ const a = newTraceId();
50
+ assert.match(a, TRACE_ID_RE);
51
+ assert.notEqual(a, newTraceId());
52
+ });
53
+
54
+ test('the switcher version matches the shape the half route demands', () => {
55
+ assert.match(switcherVersion(), SWITCHER_VERSION_RE);
56
+ const stamp = switcherVersion().split('+')[1];
57
+ const iso = `${stamp.slice(0, 4)}-${stamp.slice(4, 6)}-${stamp.slice(6, 11)}:${stamp.slice(11, 13)}:${stamp.slice(13, 15)}Z`;
58
+ const when = new Date(iso);
59
+ assert.ok(!Number.isNaN(when.getTime()), `${stamp} must parse as a UTC time`);
60
+ assert.ok(when.getTime() <= Date.now() + 24 * 3600 * 1000, 'never later than now plus one day');
61
+ });
62
+
63
+ test('the tool version is read from the User-Agent, and a bad one is dropped', () => {
64
+ assert.equal(toolVersionFromUA('claude-cli/1.2.3 (external, cli)'), '1.2.3');
65
+ assert.equal(toolVersionFromUA('codex_cli_rs/0.47.0-alpha.1'), '0.47.0-alpha.1');
66
+ assert.equal(toolVersionFromUA('claude-cli/not-a-version x'), '');
67
+ assert.equal(toolVersionFromUA('curl'), '');
68
+ assert.equal(toolVersionFromUA(undefined), '');
69
+ });
70
+
71
+ // ---------------------------------------------------------------- unit: config block
72
+
73
+ test('contractLab is absent by default, and a saved block survives a rewrite', () => {
74
+ assert.deepEqual(contractLabSettings({}), { url: '', apiKey: '', enabled: false });
75
+ assert.deepEqual(contractLabSettings(null), { url: '', apiKey: '', enabled: false });
76
+ // Enabled needs a real http(s) URL and a key: a half-filled block stays off.
77
+ assert.equal(contractLabSettings({ contractLab: { enabled: true } }).enabled, false);
78
+ assert.equal(contractLabSettings({ contractLab: { url: 'file:///etc/passwd', apiKey: 'k', enabled: true } }).enabled, false);
79
+ assert.equal(contractLabSettings({ contractLab: { url: 'http://127.0.0.1:9/', apiKey: 'k', enabled: true } }).enabled, true);
80
+ assert.equal(contractLabSettings({ contractLab: { url: 'http://127.0.0.1:9/', apiKey: 'k', enabled: true } }).url, 'http://127.0.0.1:9');
81
+
82
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'llm-switcher-lab-'));
83
+ const cfgPath = path.join(dir, 'config.json');
84
+ const cfg = { port: 3456, profiles: {}, contractLab: { url: 'http://127.0.0.1:20142', apiKey: 'sk-lab', enabled: true } };
85
+ fs.writeFileSync(cfgPath, JSON.stringify(cfg));
86
+ const out = JSON.parse(spawnNodeSync(`
87
+ import fs from 'node:fs';
88
+ import { loadConfig, saveConfig, contractLabSettings, redactConfig } from '${ROOT}/state.mjs';
89
+ saveConfig({ ...loadConfig(), debug: true });
90
+ const again = JSON.parse(fs.readFileSync(process.env.LLM_SWITCHER_CONFIG, 'utf8'));
91
+ console.log(JSON.stringify({ settings: contractLabSettings(again), masked: redactConfig(again).contractLab }));
92
+ `, cfgPath, dir));
93
+ assert.deepEqual(out.settings, { url: 'http://127.0.0.1:20142', apiKey: 'sk-lab', enabled: true });
94
+ // The dashboard must never read the intact key back out of /api/config.
95
+ assert.equal(out.masked.apiKey, MASKED_KEY);
96
+ assert.equal(out.masked.hasApiKey, true);
97
+ fs.rmSync(dir, { recursive: true, force: true });
98
+ });
99
+
100
+ // state.mjs reads its config path once per process, so a config test runs in its own process.
101
+ function spawnNodeSync(code, cfgPath, stateDir) {
102
+ const file = path.join(stateDir, 'probe.mjs');
103
+ fs.writeFileSync(file, code);
104
+ return execFileSync(process.execPath, [file], {
105
+ encoding: 'utf8',
106
+ env: { ...process.env, LLM_SWITCHER_CONFIG: cfgPath, LLM_SWITCHER_STATE_DIR: stateDir }
107
+ });
108
+ }
109
+
110
+ // ---------------------------------------------------------------- unit: queue and policy
111
+
112
+ const labSettings = (url) => () => ({ url, apiKey: 'sk-lab', enabled: true });
113
+
114
+ function fakeFetch(handler) {
115
+ const calls = [];
116
+ const fn = async (url, opts = {}) => {
117
+ calls.push({ url: String(url), opts });
118
+ return handler(String(url), opts, calls.length);
119
+ };
120
+ fn.calls = calls;
121
+ return fn;
122
+ }
123
+
124
+ const jsonRes = (status, body) => ({
125
+ ok: status >= 200 && status < 300,
126
+ status,
127
+ json: async () => body,
128
+ arrayBuffer: async () => new ArrayBuffer(0)
129
+ });
130
+
131
+ const half = { toolRequest: '{"a":1}', toolResponse: '{"b":2}', toolVersion: '1.2.3', inFormat: 'anthropic', outFormat: 'openai-chat' };
132
+
133
+ test('the upload queue holds 32 waiting halves and drops the oldest at the 33rd', () => {
134
+ // The pump starts on a later turn, so all 33 uploads of this turn meet a queue that never drains.
135
+ const lab = createContractLab({ settings: labSettings('http://127.0.0.1:1'), fetchImpl: fakeFetch(() => jsonRes(202, {})), log: () => {} });
136
+ const ids = [];
137
+ for (let i = 0; i < MAX_QUEUE + 1; i++) {
138
+ const id = newTraceId();
139
+ ids.push(id);
140
+ lab.upload(id, half);
141
+ }
142
+ const pending = lab.pending();
143
+ assert.equal(pending.length, MAX_QUEUE);
144
+ assert.equal(lab.stats().dropped, 1);
145
+ assert.ok(!pending.includes(ids[0]), 'the oldest waiting half is the one dropped');
146
+ assert.equal(pending.at(-1), ids.at(-1));
147
+ });
148
+
149
+ test('finishHalf uploads only a sampled turn that ran to the end', () => {
150
+ const calls = [];
151
+ const lab = { upload: (traceId, h) => calls.push([traceId, h]) };
152
+ const id = newTraceId();
153
+ finishHalf(lab, null, false, half);
154
+ finishHalf(lab, id, true, half);
155
+ assert.equal(calls.length, 0, 'an untagged or aborted turn uploads nothing');
156
+ finishHalf(lab, id, false, half);
157
+ assert.deepEqual(calls, [[id, half]]);
158
+ });
159
+
160
+ test('a 409 half counts as done and is never sent again', async () => {
161
+ const fetchImpl = fakeFetch(() => jsonRes(409, {}));
162
+ const lab = createContractLab({ settings: labSettings('http://127.0.0.1:1'), fetchImpl, log: () => {} });
163
+ const id = newTraceId();
164
+ lab.upload(id, half);
165
+ await waitFor(() => lab.stats().done === 1, 'the 409 to count as done');
166
+ await sleep(50);
167
+ assert.equal(fetchImpl.calls.length, 1, 'no retry');
168
+ assert.equal(lab.stats().failed, 0);
169
+ assert.equal(lab.pending().length, 0);
170
+ const sent = fetchImpl.calls[0];
171
+ assert.equal(sent.url, `http://127.0.0.1:1/api/contracts/traces/${id}/half`);
172
+ assert.equal(sent.opts.method, 'POST');
173
+ assert.equal(sent.opts.headers.authorization, 'Bearer sk-lab');
174
+ const body = JSON.parse(sent.opts.body);
175
+ assert.deepEqual(body.converter, { inFormat: 'anthropic', outFormat: 'openai-chat' });
176
+ assert.equal(body.toolRequest, '{"a":1}');
177
+ assert.equal(body.toolResponse, '{"b":2}');
178
+ assert.equal(body.toolVersion, '1.2.3');
179
+ assert.match(body.switcherVersion, SWITCHER_VERSION_RE);
180
+ });
181
+
182
+ test('a failed policy refresh keeps the last policy, and no policy samples nothing', async () => {
183
+ let clock = 0;
184
+ let fail = false;
185
+ const fetchImpl = fakeFetch(() => (fail ? Promise.reject(new Error('intact down')) : jsonRes(200, { models: { 'ag/flash': 1 }, default: 0 })));
186
+ const lab = createContractLab({
187
+ settings: labSettings('http://127.0.0.1:1'), fetchImpl, log: () => {},
188
+ now: () => clock, random: () => 0
189
+ });
190
+ // Before the first policy arrives nothing is sampled.
191
+ assert.equal(lab.traceFor('ag/flash'), null);
192
+ await waitFor(() => lab.policy() !== null, 'the first policy');
193
+ assert.match(lab.traceFor('ag/flash'), TRACE_ID_RE);
194
+ assert.equal(lab.traceFor('other-model'), null, 'a model with rate 0 is not sampled');
195
+
196
+ fail = true;
197
+ clock += 10 * 60 * 1000 + 1;
198
+ // The refresh runs beside the request, never in front of it: the cached policy still decides.
199
+ assert.match(lab.traceFor('ag/flash'), TRACE_ID_RE);
200
+ await waitFor(() => fetchImpl.calls.length === 2, 'the second policy call');
201
+ await sleep(20);
202
+ assert.deepEqual(lab.policy(), { models: { 'ag/flash': 1 }, default: 0 }, 'the last policy is kept');
203
+ assert.match(lab.traceFor('ag/flash'), TRACE_ID_RE);
204
+ });
205
+
206
+ test('a lab whose config is disabled tags nothing and calls intact never', async () => {
207
+ const fetchImpl = fakeFetch(() => jsonRes(200, { default: 1 }));
208
+ const lab = createContractLab({ settings: () => ({ url: 'http://127.0.0.1:1', apiKey: 'k', enabled: false }), fetchImpl, log: () => {} });
209
+ assert.equal(lab.traceFor('m'), null);
210
+ lab.upload(newTraceId(), half);
211
+ await sleep(50);
212
+ assert.equal(fetchImpl.calls.length, 0);
213
+ assert.equal(lab.pending().length, 0);
214
+ });
215
+
216
+ test('a WS message becomes half text, and an unreadable or oversized one becomes empty', () => {
217
+ assert.equal(capJson({ type: 'response.create', model: 'main' }), '{"type":"response.create","model":"main"}');
218
+ assert.equal(capJson({ a: 'x'.repeat(40) }, 8), '', 'a side over the cap would get 413 from intact');
219
+ assert.equal(capJson(undefined), '');
220
+ assert.equal(capJson({ big: 1n }), '', 'a value JSON cannot hold never breaks the turn');
221
+ });
222
+
223
+ test('the half tap copies bytes and gives up the whole side past the cap', () => {
224
+ const tap = createHalfTap(16);
225
+ tap.push('ab');
226
+ tap.push(Buffer.from('cd'));
227
+ tap.push(new Uint8Array([101, 102]));
228
+ assert.equal(tap.text(), 'abcdef');
229
+ const small = createHalfTap(4);
230
+ small.push('abcdef');
231
+ assert.equal(small.text(), '', 'a side over the cap is dropped, intact would answer 413');
232
+ });
233
+
234
+ // ---------------------------------------------------------------- e2e with a spawned gateway
235
+
236
+ let upstream, upstreamPort, intact, intactPort, closedPort, tmpDir;
237
+ const received = []; // upstream requests
238
+ const halves = []; // half uploads intact saw
239
+ const intactState = { hangHalf: false, policy: { models: {}, default: 1 } };
240
+ const proxies = {}; // name -> { port, child }
241
+
242
+ const DIRECT_BODY = JSON.stringify({
243
+ id: 'msg_fixed', type: 'message', role: 'assistant', model: 'up-opus',
244
+ content: [{ type: 'text', text: 'direct ok' }], stop_reason: 'end_turn',
245
+ usage: { input_tokens: 3, output_tokens: 2 }
246
+ });
247
+
248
+ // The Codex WS transport always asks upstream for a stream, so the mock answers one.
249
+ const STREAM_BODY = [
250
+ ['message_start', { type: 'message_start', message: { id: 'msg_ws', type: 'message', role: 'assistant', model: 'up-opus', content: [], usage: { input_tokens: 5, output_tokens: 1 } } }],
251
+ ['content_block_start', { type: 'content_block_start', index: 0, content_block: { type: 'text', text: '' } }],
252
+ ['content_block_delta', { type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: 'ws ok' } }],
253
+ ['content_block_stop', { type: 'content_block_stop', index: 0 }],
254
+ ['message_delta', { type: 'message_delta', delta: { stop_reason: 'end_turn' }, usage: { output_tokens: 3 } }],
255
+ ['message_stop', { type: 'message_stop' }]
256
+ ].map(([e, d]) => `event: ${e}\ndata: ${JSON.stringify(d)}\n\n`).join('');
257
+
258
+ function startUpstream() {
259
+ upstream = http.createServer((req, res) => {
260
+ let body = '';
261
+ req.on('data', c => { body += c; });
262
+ req.on('end', () => {
263
+ const json = body ? JSON.parse(body) : {};
264
+ received.push({ url: req.url, headers: req.headers, body: json });
265
+ if (JSON.stringify(json.messages || '').includes('ERR_400')) {
266
+ res.writeHead(400, { 'Content-Type': 'application/json' });
267
+ return res.end(JSON.stringify({ type: 'error', error: { type: 'invalid_request_error', message: 'bad request' } }));
268
+ }
269
+ if (JSON.stringify(json.messages || '').includes('MID_STREAM_ERR')) {
270
+ res.writeHead(200, { 'Content-Type': 'text/event-stream' });
271
+ res.write('event: message_start\ndata: {"type":"message_start","message":{"id":"msg_err","type":"message","role":"assistant","model":"up-opus","content":[]}}\n\n');
272
+ setTimeout(() => res.destroy(new Error('cut off')), 50);
273
+ return;
274
+ }
275
+ if (json.stream === true) {
276
+ res.writeHead(200, { 'Content-Type': 'text/event-stream' });
277
+ return res.end(STREAM_BODY);
278
+ }
279
+ const answer = () => {
280
+ res.writeHead(200, { 'Content-Type': 'application/json' });
281
+ res.end(DIRECT_BODY);
282
+ };
283
+ if (JSON.stringify(json.messages || '').includes('SLOW')) setTimeout(answer, 400);
284
+ else answer();
285
+ });
286
+ });
287
+ return new Promise(r => upstream.listen(0, '127.0.0.1', () => { upstreamPort = upstream.address().port; r(); }));
288
+ }
289
+
290
+ function startIntact() {
291
+ intact = http.createServer((req, res) => {
292
+ let body = '';
293
+ req.on('data', c => { body += c; });
294
+ req.on('end', () => {
295
+ if (req.url === '/api/contracts/policy') {
296
+ res.writeHead(200, { 'Content-Type': 'application/json' });
297
+ return res.end(JSON.stringify(intactState.policy));
298
+ }
299
+ const m = req.url.match(/^\/api\/contracts\/traces\/([^/]+)\/half$/);
300
+ if (m && req.method === 'POST') {
301
+ halves.push({ traceId: m[1], headers: req.headers, body: JSON.parse(body) });
302
+ if (intactState.hangHalf) return; // never answers: the tool answer must not wait for it
303
+ res.writeHead(202);
304
+ return res.end('{}');
305
+ }
306
+ res.writeHead(404);
307
+ res.end('{}');
308
+ });
309
+ });
310
+ return new Promise(r => intact.listen(0, '127.0.0.1', () => { intactPort = intact.address().port; r(); }));
311
+ }
312
+
313
+ async function startProxy(name, contractLab) {
314
+ const port = await freePort();
315
+ const dir = path.join(tmpDir, name);
316
+ fs.mkdirSync(dir, { recursive: true });
317
+ const models = { opus: 'up-opus', sonnet: 'up-sonnet', haiku: 'up-haiku', fable: 'up-fable' };
318
+ const cfg = {
319
+ port,
320
+ activeProfiles: { anthropic: 'ant', responses: 'ant', 'openai-chat': 'ant', vertex: 'ant' },
321
+ profiles: {
322
+ ant: { name: 'Mock Anthropic', mode: 'direct', inFormat: 'auto', outFormat: 'anthropic', baseURL: `http://127.0.0.1:${upstreamPort}/ant`, apiKey: 'sk-secret-ant', defaultModels: models }
323
+ },
324
+ ...(contractLab ? { contractLab } : {})
325
+ };
326
+ fs.writeFileSync(path.join(dir, 'config.json'), JSON.stringify(cfg, null, 2));
327
+ const child = spawn(process.execPath, [path.join(ROOT, 'proxy.mjs'), '--port', String(port)], {
328
+ env: { ...process.env, LLM_SWITCHER_CONFIG: path.join(dir, 'config.json'), LLM_SWITCHER_STATE_DIR: dir, CLAUDE_CONFIG_DIR: path.join(dir, 'claude'), LLM_SWITCHER_PORT: '' },
329
+ stdio: ['ignore', 'pipe', 'pipe']
330
+ });
331
+ let log = '';
332
+ child.stdout.on('data', d => { log += d; });
333
+ child.stderr.on('data', d => { log += d; });
334
+ proxies[name] = { port, child };
335
+ for (let i = 0; i < 100; i++) {
336
+ try {
337
+ const r = await fetch(`http://127.0.0.1:${port}/health`);
338
+ if (r.ok) return proxies[name];
339
+ } catch {}
340
+ await sleep(50);
341
+ }
342
+ throw new Error(`proxy ${name} did not start:\n${log}`);
343
+ }
344
+
345
+ const ask = (name, headers = {}, opts = {}) => fetch(`http://127.0.0.1:${proxies[name].port}/v1/messages`, {
346
+ method: 'POST',
347
+ headers: { 'Content-Type': 'application/json', 'user-agent': 'claude-cli/1.2.3 (external, cli)', ...headers },
348
+ body: JSON.stringify({
349
+ model: 'claude-opus-4-6',
350
+ max_tokens: 64,
351
+ ...(opts.stream ? { stream: true } : {}),
352
+ messages: [{ role: 'user', content: opts.text || 'hello' }]
353
+ }),
354
+ ...(opts.signal ? { signal: opts.signal } : {})
355
+ });
356
+
357
+ before(async () => {
358
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'llm-switcher-contract-'));
359
+ await startUpstream();
360
+ await startIntact();
361
+ closedPort = await freePort();
362
+ await startProxy('off', null);
363
+ await startProxy('on', { url: `http://127.0.0.1:${intactPort}`, apiKey: 'sk-lab', enabled: true });
364
+ await startProxy('down', { url: `http://127.0.0.1:${closedPort}`, apiKey: 'sk-lab', enabled: true });
365
+ });
366
+
367
+ after(() => {
368
+ for (const p of Object.values(proxies)) p.child?.kill();
369
+ upstream?.close();
370
+ intact?.close();
371
+ if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true });
372
+ });
373
+
374
+ test('a client that sends its own X-Intact-Trace never reaches the upstream with it', async () => {
375
+ const chosen = 'client-chosen-trace-id-0001';
376
+ const res = await ask('off', { 'X-Intact-Trace': chosen });
377
+ assert.equal(res.status, 200);
378
+ assert.equal(received.at(-1).headers['x-intact-trace'], undefined, 'the lab is off: no trace header at all');
379
+
380
+ // Sampled, the switcher still refuses the client's id and uses its own.
381
+ await waitFor(async () => {
382
+ await ask('on', { 'X-Intact-Trace': chosen });
383
+ return received.at(-1).headers['x-intact-trace'] !== undefined;
384
+ }, 'the policy to arrive and a request to be sampled');
385
+ const tagged = received.at(-1).headers['x-intact-trace'];
386
+ assert.notEqual(tagged, chosen);
387
+ assert.match(tagged, TRACE_ID_RE);
388
+ });
389
+
390
+ test('the half of a sampled request reaches intact with the exchange and the versions', async () => {
391
+ await waitFor(async () => {
392
+ await ask('on');
393
+ return halves.length > 0;
394
+ }, 'a sampled request to reach intact');
395
+ const last = halves.at(-1);
396
+ assert.match(last.traceId, TRACE_ID_RE);
397
+ assert.equal(last.headers.authorization, 'Bearer sk-lab');
398
+ assert.deepEqual(last.body.converter, { inFormat: 'anthropic', outFormat: 'anthropic' });
399
+ assert.equal(last.body.toolVersion, '1.2.3');
400
+ assert.match(last.body.switcherVersion, SWITCHER_VERSION_RE);
401
+ assert.equal(JSON.parse(last.body.toolRequest).messages[0].content, 'hello');
402
+ assert.equal(JSON.parse(last.body.toolResponse).content[0].text, 'direct ok');
403
+ });
404
+
405
+ test('with intact at a closed port the tool answer is byte-identical to the answer with the lab off', async () => {
406
+ const [a, b] = [await ask('off'), await ask('down')];
407
+ assert.equal(a.status, b.status);
408
+ const [ab, bb] = [Buffer.from(await a.arrayBuffer()), Buffer.from(await b.arrayBuffer())];
409
+ assert.equal(Buffer.compare(ab, bb), 0, 'byte-identical answer');
410
+ assert.equal(ab.toString('utf8'), DIRECT_BODY);
411
+ // Nothing was tagged, because no policy could be read.
412
+ assert.equal(received.at(-1).headers['x-intact-trace'], undefined);
413
+ });
414
+
415
+ test('a client that leaves in the middle uploads no half: that answer never completed', async () => {
416
+ const before = halves.length;
417
+ const seen = received.length;
418
+ const ac = new AbortController();
419
+ const pending = ask('on', {}, { text: 'SLOW please', signal: ac.signal }).catch(() => 'aborted');
420
+ await waitFor(() => received.length > seen, 'the upstream to receive the sampled request');
421
+ assert.match(received.at(-1).headers['x-intact-trace'], TRACE_ID_RE, 'the request was sampled');
422
+ ac.abort();
423
+ assert.equal(await pending, 'aborted');
424
+ await sleep(700);
425
+ assert.equal(halves.length, before, 'a cut-off answer would diff as a loss that the converter never made');
426
+ });
427
+
428
+ test('failed exchanges upload no half: 400 upstream and mid-stream error are ignored', async () => {
429
+ await waitFor(async () => {
430
+ await ask('on');
431
+ return received.at(-1)?.headers['x-intact-trace'] !== undefined;
432
+ }, 'the policy to arrive and requests to be sampled');
433
+
434
+ const initialHalves = halves.length;
435
+
436
+ // 1. 400 upstream
437
+ const res400 = await ask('on', {}, { text: 'ERR_400' });
438
+ assert.equal(res400.status, 400);
439
+ const trace400 = received.at(-1)?.headers['x-intact-trace'];
440
+ assert.ok(trace400, 'the 400 request had a trace id');
441
+
442
+ // 2. mid-stream error
443
+ const resStream = await ask('on', {}, { text: 'MID_STREAM_ERR', stream: true });
444
+ await resStream.text().catch(() => {});
445
+ const traceStream = received.at(-1)?.headers['x-intact-trace'];
446
+ assert.ok(traceStream, 'the stream request had a trace id');
447
+
448
+ await sleep(300);
449
+
450
+ const newHalves = halves.slice(initialHalves);
451
+ assert.equal(newHalves.some(h => h.traceId === trace400), false, 'fake intact received no half for 400 trace');
452
+ assert.equal(newHalves.some(h => h.traceId === traceStream), false, 'fake intact received no half for mid-stream error trace');
453
+ });
454
+
455
+ test('a converted stream cut mid-way uploads no half, a complete one does', async () => {
456
+ const chat = (text) => fetch(`http://127.0.0.1:${proxies.on.port}/v1/chat/completions`, {
457
+ method: 'POST',
458
+ headers: { 'Content-Type': 'application/json', 'user-agent': 'codex/1.0.0' },
459
+ body: JSON.stringify({ model: 'claude-opus-4-6', stream: true, messages: [{ role: 'user', content: text }] })
460
+ });
461
+ let traceOk;
462
+ await waitFor(async () => {
463
+ await (await chat('hello')).text();
464
+ traceOk = received.at(-1)?.headers['x-intact-trace'];
465
+ return traceOk !== undefined;
466
+ }, 'a sampled converted request');
467
+ await waitFor(() => halves.some(h => h.traceId === traceOk), 'the complete converted stream to upload its half');
468
+
469
+ const before = halves.length;
470
+ await (await chat('MID_STREAM_ERR')).text().catch(() => {});
471
+ const traceCut = received.at(-1)?.headers['x-intact-trace'];
472
+ assert.ok(traceCut, 'the cut request was sampled');
473
+ await sleep(300);
474
+ assert.equal(halves.slice(before).some(h => h.traceId === traceCut), false, 'a cut converted stream would diff as a loss');
475
+ });
476
+
477
+ // ---------------------------------------------------------------- e2e over the Codex WS transport
478
+
479
+ // A masked text frame. The gateway rejects an unmasked client frame, as the WS standard demands.
480
+ function clientFrame(text) {
481
+ const data = Buffer.from(text, 'utf8');
482
+ const len = data.length;
483
+ const head = len < 126
484
+ ? Buffer.from([0x81, 0x80 | len])
485
+ : Buffer.concat([Buffer.from([0x81, 0x80 | 126]), (() => { const b = Buffer.alloc(2); b.writeUInt16BE(len); return b; })()]);
486
+ const mask = crypto.randomBytes(4);
487
+ return Buffer.concat([head, mask, Buffer.from(data.map((b, i) => b ^ mask[i & 3]))]);
488
+ }
489
+
490
+ // One Codex WS turn against a spawned gateway: upgrade, one response.create, every frame back.
491
+ function wsTurn(name, input, headers = {}) {
492
+ const port = proxies[name].port;
493
+ return new Promise((resolve, reject) => {
494
+ const socket = net.connect(port, '127.0.0.1');
495
+ const timer = setTimeout(() => { socket.destroy(); reject(new Error(`ws turn timed out: ${input}`)); }, 10000);
496
+ const extra = Object.entries({ 'user-agent': 'codex_cli_rs/0.47.0', ...headers }).map(([k, v]) => `${k}: ${v}\r\n`).join('');
497
+ const read = createFrameReader();
498
+ const events = [];
499
+ const parts = [];
500
+ let head = Buffer.alloc(0);
501
+ const finish = () => {
502
+ clearTimeout(timer);
503
+ socket.destroy();
504
+ resolve({ events, text: Buffer.concat(parts).toString('utf8') });
505
+ };
506
+ const collect = (chunk) => {
507
+ for (const f of read(chunk)) {
508
+ if (f.type !== 'text') continue;
509
+ parts.push(f.payload);
510
+ const msg = JSON.parse(f.payload.toString('utf8'));
511
+ events.push(msg);
512
+ if (msg.type === 'response.completed' || msg.type === 'response.failed') finish();
513
+ }
514
+ };
515
+ const onHead = (chunk) => {
516
+ head = Buffer.concat([head, chunk]);
517
+ const end = head.indexOf('\r\n\r\n');
518
+ if (end < 0) return;
519
+ socket.off('data', onHead);
520
+ socket.on('data', collect);
521
+ socket.write(clientFrame(JSON.stringify({ type: 'response.create', model: 'main', input })));
522
+ const rest = head.subarray(end + 4);
523
+ if (rest.length) collect(rest);
524
+ };
525
+ socket.on('error', reject);
526
+ socket.on('data', onHead);
527
+ socket.write(`GET /v1/responses HTTP/1.1\r\nHost: 127.0.0.1:${port}\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: ${crypto.randomBytes(16).toString('base64')}\r\n${extra}\r\n`);
528
+ });
529
+ }
530
+
531
+ // Every turn builds its own response, item and reasoning ids and its own created_at.
532
+ const stableWs = (text) => text
533
+ .replace(/"(resp|msg|rs|fc|ctc|lsh)_[A-Za-z0-9]+"/g, '"<id>"')
534
+ .replace(/"created_at":\d+/g, '"created_at":0');
535
+
536
+ test('a sampled Codex WS turn is tagged and its half reaches intact', async () => {
537
+ const chosen = 'client-chosen-trace-id-0003';
538
+ let turn;
539
+ await waitFor(async () => {
540
+ turn = await wsTurn('on', 'hello over ws', { 'X-Intact-Trace': chosen });
541
+ return received.at(-1).headers['x-intact-trace'] !== undefined;
542
+ }, 'a sampled WS turn');
543
+ const traceId = received.at(-1).headers['x-intact-trace'];
544
+ assert.notEqual(traceId, chosen, 'the id of the client is refused, the switcher uses its own');
545
+ assert.match(traceId, TRACE_ID_RE);
546
+ assert.equal(turn.events.at(-1).type, 'response.completed');
547
+
548
+ await waitFor(() => halves.some(h => h.traceId === traceId), 'the half of the WS turn');
549
+ const body = halves.find(h => h.traceId === traceId).body;
550
+ assert.deepEqual(body.converter, { inFormat: 'responses', outFormat: 'anthropic' });
551
+ assert.equal(body.toolVersion, '0.47.0');
552
+ assert.match(body.switcherVersion, SWITCHER_VERSION_RE);
553
+ const asked = JSON.parse(body.toolRequest);
554
+ assert.equal(asked.type, 'response.create');
555
+ assert.equal(asked.input, 'hello over ws');
556
+ assert.ok(body.toolResponse.includes('event: response.completed'), 'the half holds the events the turn wrote');
557
+ assert.ok(body.toolResponse.includes('ws ok'), 'the answer text is in the half');
558
+ });
559
+
560
+ test('a Codex WS client can never choose the trace id, and a down intact leaves the turn unchanged', async () => {
561
+ const chosen = 'client-chosen-trace-id-0002';
562
+ const a = await wsTurn('off', 'ws same bytes', { 'X-Intact-Trace': chosen });
563
+ const offTag = received.at(-1).headers['x-intact-trace'];
564
+ const b = await wsTurn('down', 'ws same bytes', { 'X-Intact-Trace': chosen });
565
+ const downTag = received.at(-1).headers['x-intact-trace'];
566
+ assert.equal(offTag, undefined, 'the lab is off: no trace header at all');
567
+ assert.equal(downTag, undefined, 'no policy could be read: nothing is sampled');
568
+ assert.equal(stableWs(a.text), stableWs(b.text), 'the frames of the turn are the same');
569
+ assert.equal(a.events.at(-1).type, 'response.completed');
570
+ });
571
+
572
+ test('an intact that never answers the half changes neither the bytes nor the timing of the answer', async () => {
573
+ intactState.hangHalf = true;
574
+ const before = halves.length;
575
+ const started = Date.now();
576
+ const res = await ask('on');
577
+ const body = Buffer.from(await res.arrayBuffer());
578
+ const took = Date.now() - started;
579
+ intactState.hangHalf = false;
580
+ assert.equal(res.status, 200);
581
+ assert.equal(body.toString('utf8'), DIRECT_BODY, 'the tap copies bytes, it never changes them');
582
+ assert.ok(took < 2000, `the answer must not wait for intact (took ${took} ms)`);
583
+ await waitFor(() => halves.length > before, 'the half to be posted after the answer');
584
+ });
585
+
586
+ // ---------------------------------------------------------------- probe: the six variants
587
+
588
+ test('the probe matrix is the six variants of the response matrix, in both tool formats', () => {
589
+ const matrix = JSON.parse(fs.readFileSync(path.join(ROOT, 'docs/response-matrix.json'), 'utf8'));
590
+ assert.deepEqual(probeVariants(), matrix.variants);
591
+ assert.deepEqual(PROBE_FORMATS, ['anthropic', 'responses']);
592
+ });
593
+
594
+ test('each variant sets its own knob in both formats', () => {
595
+ const ant = (v) => probeRequest({ format: 'anthropic', variant: v, model: 'm' }).body;
596
+ const res = (v) => probeRequest({ format: 'responses', variant: v, model: 'm' }).body;
597
+ assert.equal(probeRequest({ format: 'anthropic', variant: 'base-stream', model: 'm' }).path, '/v1/messages');
598
+ assert.equal(probeRequest({ format: 'responses', variant: 'base-stream', model: 'm' }).path, '/v1/responses');
599
+
600
+ assert.equal(ant('base-stream').stream, true);
601
+ assert.equal(ant('base-stream').thinking, undefined);
602
+ assert.equal(ant('think-stream').thinking.type, 'enabled');
603
+ assert.ok(ant('think-stream').thinking.budget_tokens >= 1024, 'a budget under 1024 is refused');
604
+ assert.equal(ant('think-nostream').stream, false);
605
+ assert.equal(ant('think-nostream').thinking.type, 'enabled');
606
+ assert.equal(ant('tool-stream').tools.length, 1);
607
+ assert.equal(ant('trunc-stream').max_tokens, 40);
608
+ assert.equal(ant('effort-stream').stream, true);
609
+ assert.equal(ant('base-stream').model, 'm');
610
+
611
+ assert.equal(res('base-stream').stream, true);
612
+ assert.equal(res('think-nostream').stream, false);
613
+ assert.equal(res('tool-stream').tools[0].type, 'function');
614
+ assert.equal(res('trunc-stream').max_output_tokens, 40);
615
+ assert.equal(res('effort-stream').reasoning.effort, 'high');
616
+ assert.equal(typeof res('base-stream').input, 'string');
617
+ });
618
+
619
+ test('Anthropic probe with thinking sets max_tokens greater than budget_tokens', () => {
620
+ for (const v of ['think-stream', 'think-nostream', 'effort-stream']) {
621
+ const req = probeRequest({ format: 'anthropic', variant: v, model: 'm' });
622
+ assert.ok(
623
+ req.body.max_tokens > req.body.thinking.budget_tokens,
624
+ `${v}: max_tokens (${req.body.max_tokens}) must exceed budget_tokens (${req.body.thinking.budget_tokens})`
625
+ );
626
+ }
627
+ });
628
+
629
+ test('the probe models are every mapped model of the active profiles, once each', () => {
630
+ const cfg = {
631
+ activeProfiles: { anthropic: 'a', responses: 'a', 'openai-chat': 'b', vertex: null },
632
+ profiles: {
633
+ a: { inFormat: 'auto', defaultModels: { opus: 'up-opus', sonnet: 'up-sonnet', haiku: '', fable: 'up-opus' } },
634
+ b: { inFormat: 'openai-chat', defaultModels: { default: 'up-chat' } },
635
+ unused: { inFormat: 'auto', defaultModels: { opus: 'never-probed' } }
636
+ }
637
+ };
638
+ assert.deepEqual(probeModels(cfg), ['up-opus', 'up-sonnet', 'up-chat']);
639
+ assert.deepEqual(probeModels({}), []);
640
+ });
641
+
642
+ // ---------------------------------------------------------------- probe: e2e through the gateway
643
+
644
+ const adminTokenOf = (name) => fs.readFileSync(path.join(tmpDir, name, 'admin.token'), 'utf8').trim();
645
+ const configOf = (name) => JSON.parse(fs.readFileSync(path.join(tmpDir, name, 'config.json'), 'utf8'));
646
+
647
+ test('the probe sends six variants in two formats per model, each with a fresh trace id', async () => {
648
+ const seen = received.length;
649
+ const lines = [];
650
+ const out = await runProbe({
651
+ port: proxies.off.port, token: adminTokenOf('off'), config: configOf('off'),
652
+ log: (line) => lines.push(line)
653
+ });
654
+
655
+ const models = probeModels(configOf('off'));
656
+ assert.equal(models.length, 4);
657
+ assert.equal(out.unreachable, false);
658
+ assert.equal(out.rows.length, models.length * 12, 'six variants x two formats per model');
659
+ assert.equal(lines.length, out.rows.length, 'one printed line per request');
660
+
661
+ const sent = received.slice(seen);
662
+ assert.equal(sent.length, out.rows.length, 'every probe request reached the upstream');
663
+ const ids = new Set();
664
+ for (const row of out.rows) {
665
+ assert.match(row.traceId, TRACE_ID_RE);
666
+ assert.equal(row.status, 200, `${row.model} ${row.format} ${row.variant}`);
667
+ ids.add(row.traceId);
668
+ assert.ok(lines.some(l => l.includes(row.traceId) && l.includes(row.variant) && l.includes(row.format)), 'the line names the request');
669
+ }
670
+ assert.equal(ids.size, out.rows.length, 'every request carries its own id');
671
+ for (const id of sent.map(r => r.headers['x-intact-trace'])) assert.ok(ids.has(id), `upstream saw an unknown trace id ${id}`);
672
+ for (const r of sent) {
673
+ assert.equal(r.headers['x-intact-probe'], undefined, 'the probe marker never leaves the gateway');
674
+ assert.equal(r.headers['x-llm-switcher-token'], undefined, 'the admin token never leaves the gateway');
675
+ }
676
+
677
+ for (const model of models) {
678
+ const rows = out.rows.filter(r => r.model === model);
679
+ assert.deepEqual([...new Set(rows.map(r => r.variant))], probeVariants());
680
+ assert.deepEqual([...new Set(rows.map(r => r.format))], [...PROBE_FORMATS]);
681
+ }
682
+ });
683
+
684
+ test('--model limits the probe to that one model', async () => {
685
+ const out = await runProbe({
686
+ port: proxies.off.port, token: adminTokenOf('off'), config: configOf('off'),
687
+ model: 'up-haiku', log: () => {}
688
+ });
689
+ assert.equal(out.rows.length, 12);
690
+ assert.deepEqual([...new Set(out.rows.map(r => r.model))], ['up-haiku']);
691
+ });
692
+
693
+ test('a probe run on a lab-enabled gateway uploads a half per row to intact', async () => {
694
+ const initialHalves = halves.length;
695
+ const out = await runProbe({
696
+ port: proxies.on.port, token: adminTokenOf('on'), config: configOf('on'),
697
+ model: 'up-haiku', log: () => {}
698
+ });
699
+ assert.equal(out.unreachable, false);
700
+ assert.equal(out.rows.length, 12);
701
+ const probeIds = new Set(out.rows.map(r => r.traceId));
702
+ await waitFor(() => {
703
+ const uploadedIds = new Set(halves.slice(initialHalves).map(h => h.traceId));
704
+ return [...probeIds].every(id => uploadedIds.has(id));
705
+ }, 'fake intact to receive one half per row traceId');
706
+ const uploaded = halves.slice(initialHalves);
707
+ for (const id of probeIds) {
708
+ assert.ok(uploaded.some(h => h.traceId === id), `trace id ${id} reached intact`);
709
+ }
710
+ });
711
+
712
+ test('without the admin token a client can neither probe nor choose a trace id', async () => {
713
+ const chosen = 'probe-marker-chosen-by-a-client';
714
+ const res = await fetch(`http://127.0.0.1:${proxies.off.port}/v1/messages`, {
715
+ method: 'POST',
716
+ headers: { 'content-type': 'application/json', 'x-intact-probe': chosen, 'x-llm-switcher-token': 'wrong-token' },
717
+ body: JSON.stringify({ model: 'up-opus', max_tokens: 16, messages: [{ role: 'user', content: 'hi' }] })
718
+ });
719
+ assert.equal(res.status, 200);
720
+ await res.arrayBuffer();
721
+ assert.equal(received.at(-1).headers['x-intact-trace'], undefined, 'no capture without the token');
722
+ assert.equal(received.at(-1).headers['x-intact-probe'], undefined, 'the marker never reaches the upstream');
723
+ });
724
+
725
+ test('a probe id that is not a valid trace id is refused by the gateway', async () => {
726
+ const res = await fetch(`http://127.0.0.1:${proxies.off.port}/v1/messages`, {
727
+ method: 'POST',
728
+ headers: { 'content-type': 'application/json', 'x-intact-probe': 'short', 'x-llm-switcher-token': adminTokenOf('off') },
729
+ body: JSON.stringify({ model: 'up-opus', max_tokens: 16, messages: [{ role: 'user', content: 'hi' }] })
730
+ });
731
+ assert.equal(res.status, 200);
732
+ await res.arrayBuffer();
733
+ assert.equal(received.at(-1).headers['x-intact-trace'], undefined);
734
+ });
735
+
736
+ test('switch contract-probe prints the run and fails only when the gateway is unreachable', async () => {
737
+ const dir = path.join(tmpDir, 'off');
738
+ const env = { ...process.env, LLM_SWITCHER_CONFIG: path.join(dir, 'config.json'), LLM_SWITCHER_STATE_DIR: dir, CLAUDE_CONFIG_DIR: path.join(dir, 'claude'), LLM_SWITCHER_PORT: '', PORT: '' };
739
+ const run = (args) => new Promise(resolve => {
740
+ const child = spawn(process.execPath, [path.join(ROOT, 'switch.mjs'), ...args], { env, stdio: ['ignore', 'pipe', 'pipe'] });
741
+ let stdout = '';
742
+ let stderr = '';
743
+ child.stdout.on('data', d => { stdout += d; });
744
+ child.stderr.on('data', d => { stderr += d; });
745
+ child.on('close', status => resolve({ status, stdout, stderr }));
746
+ });
747
+
748
+ const ok = await run(['contract-probe', '--model', 'up-sonnet', '--port', String(proxies.off.port)]);
749
+ assert.equal(ok.status, 0, ok.stderr);
750
+ assert.equal(ok.stdout.split('\n').filter(l => l.includes('up-sonnet')).length, 12, ok.stdout);
751
+ assert.ok(ok.stdout.includes('anthropic') && ok.stdout.includes('responses'), ok.stdout);
752
+
753
+ const bad = await run(['contract-probe', '--model', 'up-sonnet', '--port', String(closedPort)]);
754
+ assert.notEqual(bad.status, 0, 'a gateway that does not answer is the only failure');
755
+ });