// End-to-end gateway test: fake Ollama (OpenAI-compatible) + real server + real agent + HTTP client. import http from 'node:http'; import crypto from 'node:crypto'; import fs from 'node:fs'; const [apiKey, pw, secret] = process.argv.slice(2); const seen = { streamOptions: null, bodies: [] }; const mock = http.createServer((req, res) => { let body = ''; req.on('data', d => body += d); req.on('end', () => { const j = body ? JSON.parse(body) : {}; const json = o => { res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(o)); }; if (req.url === '/api/version') return json({ version: 'mock' }); if (req.url === '/api/tags') return json({ models: [{ name: 'tiny:1b', size: 1e9 }] }); if (req.url === '/api/ps') return json({ models: [{ name: 'tiny:1b', size: 1e9, expires_at: 'x' }] }); if (req.url === '/v1/chat/completions') { seen.bodies.push(j); if (j.model === 'slow:latest') return; // never answers; used for the cancel test if (!j.stream) return json({ id: 'c1', object: 'chat.completion', model: j.model, choices: [{ index: 0, message: { role: 'assistant', content: 'hello there' }, finish_reason: 'stop' }], usage: { prompt_tokens: 7, completion_tokens: 3, total_tokens: 10 } }); seen.streamOptions = j.stream_options; res.setHeader('Content-Type', 'text/event-stream'); const words = ['hel', 'lo ', 'there']; let i = 0; const t = setInterval(() => { if (i < words.length) res.write('data: ' + JSON.stringify({ choices: [{ delta: { content: words[i++] } }] }) + '\n\n'); else { if (j.stream_options?.include_usage) res.write('data: ' + JSON.stringify({ choices: [], usage: { prompt_tokens: 7, completion_tokens: 3, total_tokens: 10 } }) + '\n\n'); res.write('data: [DONE]\n\n'); res.end(); clearInterval(t); } }, 50); return; } res.statusCode = 404; res.end('nope'); }); }).listen(11434, '127.0.0.1'); const must = (c, msg) => { if (!c) { console.log('FAIL', msg); process.exitCode = 1; } else console.log('ok ', msg); }; const api = (path, opts = {}) => fetch('http://127.0.0.1:8080' + path, { ...opts, headers: { Host: 'api.wxch.org', ...(opts.headers || {}) } }); const H = { Authorization: 'Bearer ' + apiKey, 'Content-Type': 'application/json' }; // fetch cannot override Host; use node:http for the api host function call(method, path, headers, body) { return new Promise((resolve, reject) => { const req = http.request({ host: '127.0.0.1', port: 8080, method, path, headers: { Host: 'api.wxch.org', ...headers } }, res => { const chunks = []; res.on('data', c => chunks.push(c)); res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, text: Buffer.concat(chunks).toString() })); }); req.on('error', reject); if (body) req.write(body); req.end(); }); } await new Promise(r => setTimeout(r, 7000)); // let the agent report its first telemetry sample let r = await call('GET', '/v1/models', {}); must(r.status === 401, 'no key -> 401'); r = await call('GET', '/v1/models', { Authorization: 'Bearer wxk_wrong' }); must(r.status === 401, 'wrong key -> 401'); r = await call('GET', '/v1/models', H); must(r.status === 200 && JSON.parse(r.text).data.some(m => m.id === 'tiny:1b'), '/v1/models lists tiny:1b: ' + r.text.slice(0, 120)); r = await call('POST', '/v1/chat/completions', H, JSON.stringify({ model: 'tiny:1b', messages: [{ role: 'user', content: 'hi' }] })); let o = JSON.parse(r.text); must(r.status === 200 && o.choices[0].message.content === 'hello there', 'non-stream chat works'); must(r.headers['x-wxch-node'] === 'pgx', 'response says which machine answered'); r = await call('POST', '/v1/chat/completions', H, JSON.stringify({ model: 'tiny', messages: [{ role: 'user', content: 'hi' }] })); must(r.status === 404 && JSON.parse(r.text).error.code === 'model_not_found', "'tiny' means tiny:latest, which is not installed -> 404"); r = await call('POST', '/v1/chat/completions', H, JSON.stringify({ model: 'tiny:1b', stream: true, messages: [{ role: 'user', content: 'hi' }] })); must(r.status === 200 && r.headers['content-type'].startsWith('text/event-stream'), 'stream: SSE content type'); must(r.text.includes('[DONE]') && (r.text.match(/data: /g) || []).length >= 4, 'stream: all events + [DONE] arrive'); must(seen.streamOptions && seen.streamOptions.include_usage === true, 'gateway injected stream_options.include_usage'); r = await call('POST', '/v1/chat/completions', H, JSON.stringify({ model: 'does-not-exist:1b', messages: [] })); must(r.status === 404 && JSON.parse(r.text).error.code === 'model_not_found', 'unknown model -> 404 model_not_found'); r = await call('POST', '/v1/chat/completions', H, 'not json'); must(r.status === 400, 'bad JSON -> 400'); r = await call('POST', '/v1/embeddings', H, JSON.stringify({ model: 'tiny:1b', input: 'x' })); must(r.headers['x-wxch-node'] === 'pgx', 'embeddings path is routed to the machine (the mock has no handler, so its own 404 comes back)'); // client disconnect mid-request must cancel the job on the machine await new Promise(res => { const req = http.request({ host: '127.0.0.1', port: 8080, method: 'POST', path: '/v1/chat/completions', headers: { Host: 'api.wxch.org', ...H } }); req.on('error', () => {}); req.write(JSON.stringify({ model: 'tiny:1b', messages: [] })); req.end(); setTimeout(() => { req.destroy(); res(); }, 300); }); // metering via the cookie-authed management API function totp(secret) { const alpha = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; let bits = ''; for (const c of secret) bits += alpha.indexOf(c).toString(2).padStart(5, '0'); const key = Buffer.from(bits.match(/.{8}/g).map(b => parseInt(b, 2))); const ctr = Buffer.alloc(8); ctr.writeBigUInt64BE(BigInt(Math.floor(Date.now() / 30000))); const h = crypto.createHmac('sha1', key).update(ctr).digest(); const o = h[19] & 15; return String((h.readUInt32BE(o) & 0x7fffffff) % 1000000).padStart(6, '0'); } const lr = await fetch('http://127.0.0.1:8080/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: pw, code: totp(secret) }) }); const cookie = lr.headers.get('set-cookie').split(';')[0]; const keys = await (await fetch('http://127.0.0.1:8080/api/keys', { headers: { Cookie: cookie } })).json(); const u = keys[0].usage; console.log('usage:', JSON.stringify(u)); must(u.requests >= 4 && u.prompt_tokens >= 14 && u.completion_tokens >= 6, 'tokens were metered for the key (non-stream + stream)'); must(!JSON.stringify(keys).includes('"hash":"') || keys.every(k => !k.hash), 'key hashes are not exposed by the API'); const recent = await (await fetch('http://127.0.0.1:8080/api/keys/recent', { headers: { Cookie: cookie } })).json(); must(recent.length >= 4 && recent.some(x => x.status === 200 && x.node === 'pgx' && x.total_ms >= 0), 'recent requests are logged: ' + recent.length + ' entries'); // create/disable/delete a key through the UI API const created = await (await fetch('http://127.0.0.1:8080/api/keys', { method: 'POST', headers: { Cookie: cookie, 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'second' }) })).json(); must(created.key?.startsWith('wxk_'), 'UI API can create a key'); r = await call('GET', '/v1/models', { Authorization: 'Bearer ' + created.key }); must(r.status === 200, 'new key works'); await fetch(`http://127.0.0.1:8080/api/keys/${created.id}/disable?on=1`, { method: 'POST', headers: { Cookie: cookie } }); r = await call('GET', '/v1/models', { Authorization: 'Bearer ' + created.key }); must(r.status === 401, 'disabled key is rejected'); await fetch(`http://127.0.0.1:8080/api/keys/${created.id}`, { method: 'DELETE', headers: { Cookie: cookie } }); mock.close();