// OpenAI-compatible stand-in for api.teamorouter.com — used by the node tests, no dependencies. import { createServer } from 'node:http'; // 1x1 transparent PNG. export const TINY_PNG_BASE64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFAAH/q842iQAAAABJRU5ErkJggg=='; export const MODELS = [ { id: 'gpt-5.6-sol', object: 'model', owned_by: 'openai', pricing: { prompt: 0.00000053, completion: 0.00000318 } }, { id: 'claude-sonnet-5', object: 'model', owned_by: 'anthropic', pricing: { prompt: 0.0000005, completion: 0.00000252 } }, { id: 'deepseek-flash-free', object: 'model', owned_by: 'deepseek' }, { id: 'gemini-3.1-pro-preview', object: 'model', owned_by: 'google', pricing: { input: 0.29, output: 1.73 } }, { id: 'gpt-image-2', object: 'model', owned_by: 'openai' }, { id: 'nano-banana-2.1', object: 'model', owned_by: 'google' }, ]; const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); /** Minimal multipart reader: enough to check what the node uploaded. */ export function parseMultipart(rawBuffer, contentType) { const boundary = /boundary=([^;]+)/.exec(contentType ?? '')?.[1]; if (boundary === undefined) return []; const parts = []; for (const chunk of rawBuffer.toString('latin1').split(`--${boundary}`)) { const separator = chunk.indexOf('\r\n\r\n'); if (separator === -1) continue; const header = chunk.slice(0, separator); const name = /name="([^"]+)"/.exec(header)?.[1]; if (name === undefined) continue; parts.push({ name, header, body: chunk.slice(separator + 4).replace(/\r\n$/, '') }); } return parts; } /** * Models whose IDs start with "fail-" make the mock answer with the matching HTTP status, * "slow-model" delays the answer, everything else echoes the user message back. */ export function createMockServer({ apiKey = 'test-key' } = {}) { const requests = []; const handle = async (request, response) => { const chunks = []; for await (const chunk of request) chunks.push(chunk); const rawBuffer = Buffer.concat(chunks); const rawBody = rawBuffer.toString('utf8'); let body; try { body = rawBody === '' ? undefined : JSON.parse(rawBody); } catch { body = undefined; } requests.push({ method: request.method, url: request.url, headers: request.headers, body, rawBody, rawBuffer }); const json = (status, payload) => { response.writeHead(status, { 'content-type': 'application/json' }); response.end(JSON.stringify(payload)); }; if (!request.url.startsWith('/v1/')) { return json(404, { error: { message: `unknown path ${request.url}` } }); } if (request.headers.authorization !== `Bearer ${apiKey}`) { return json(401, { error: { message: 'invalid api key' } }); } if (request.method === 'GET' && request.url === '/v1/models') { return json(200, { object: 'list', data: MODELS }); } if (request.method === 'POST' && request.url === '/v1/chat/completions') { const model = body?.model ?? ''; if (model.startsWith('fail-')) { const status = Number(model.replace('fail-', '')); return json(status, { error: { message: `forced failure ${status}` } }); } if (model === 'slow-model') await delay(2500); const userMessage = [...(body?.messages ?? [])].reverse().find((message) => message.role === 'user'); const contentParts = Array.isArray(userMessage?.content) ? userMessage.content : []; const promptText = Array.isArray(userMessage?.content) ? contentParts.filter((part) => part.type === 'text').map((part) => part.text).join('') : userMessage?.content ?? ''; const imageCount = contentParts.filter((part) => part.type === 'image_url').length; const content = `echo: ${promptText}${imageCount === 0 ? '' : ` (+${imageCount} image${imageCount === 1 ? '' : 's'})`}`; if (body?.stream === true) { response.writeHead(200, { 'content-type': 'text/event-stream' }); response.write( `data: ${JSON.stringify({ object: 'chat.completion.chunk', choices: [{ index: 0, delta: { content } }] })}\n\n`, ); response.write('data: [DONE]\n\n'); return response.end(); } // Stand-in for a model that answers with an image as well as text. const images = model === 'image-answering-model' ? [{ type: 'image_url', image_url: { url: `data:image/png;base64,${TINY_PNG_BASE64}` } }] : undefined; return json(200, { object: 'chat.completion', model, choices: [ { index: 0, message: { role: 'assistant', content, ...(images === undefined ? {} : { images }) }, finish_reason: 'stop', }, ], usage: { prompt_tokens: 11, completion_tokens: 4, total_tokens: 15 }, }); } // LangChain's ChatOpenAI serves "gpt-5.6*" and "codex" model IDs through the Responses API. if (request.method === 'POST' && request.url === '/v1/responses') { const userMessage = (body?.input ?? []).find?.((message) => message.role === 'user'); const content = `echo: ${typeof body?.input === 'string' ? body.input : userMessage?.content ?? ''}`; return json(200, { id: 'resp_mock', object: 'response', created_at: 1, status: 'completed', model: body?.model, output: [ { type: 'message', id: 'msg_mock', role: 'assistant', status: 'completed', content: [{ type: 'output_text', text: content, annotations: [] }], }, ], usage: { input_tokens: 11, output_tokens: 4, total_tokens: 15 }, }); } if (request.method === 'POST' && request.url === '/v1/images/generations') { if (String(body?.prompt ?? '').includes('noimage')) { return json(400, { error: { message: 'prompt rejected' } }); } if (String(body?.prompt ?? '').includes('urlonly')) { return json(200, { created: 1, data: [{ url: 'https://cdn.example.com/cat.png' }] }); } const count = Number(body?.n ?? 1) > 1 ? Number(body.n) : 1; return json(200, { created: 1, data: Array.from({ length: count }, () => ({ b64_json: TINY_PNG_BASE64 })), }); } if (request.method === 'POST' && request.url === '/v1/images/edits') { const parts = parseMultipart(rawBuffer, request.headers['content-type']); const field = (name) => parts.find((part) => part.name === name); if (field('image') === undefined || field('prompt') === undefined) { return json(400, { error: { message: 'image and prompt are required' } }); } if (field('prompt').body.includes('noimage')) { return json(400, { error: { message: 'prompt rejected' } }); } const count = Number(field('n')?.body ?? 1) > 1 ? Number(field('n').body) : 1; return json(200, { created: 1, data: Array.from({ length: count }, () => ({ b64_json: TINY_PNG_BASE64 })), }); } return json(404, { error: { message: `unknown route ${request.method} ${request.url}` } }); }; const server = createServer((request, response) => { handle(request, response).catch((error) => { response.writeHead(500, { 'content-type': 'application/json' }); response.end(JSON.stringify({ error: { message: String(error) } })); }); }); return { requests, /** Starts on `port` (0 = random) and returns the base URL, e.g. http://127.0.0.1:4123 */ async listen(port = 0) { await new Promise((resolve) => server.listen(port, '127.0.0.1', resolve)); return `http://127.0.0.1:${server.address().port}`; }, async close() { await new Promise((resolve) => server.close(resolve)); }, }; } if (process.argv[1]?.endsWith('mock-server.mjs') === true) { const mock = createMockServer({ apiKey: process.env.MOCK_API_KEY ?? 'test-key' }); const baseUrl = await mock.listen(Number(process.env.PORT ?? 4123)); console.log(`mock TeamoRouter listening on ${baseUrl}/v1 (api key: ${process.env.MOCK_API_KEY ?? 'test-key'})`); }