Repository Public
1/**2 * Hand-written fixtures of provider-native HTTP responses and SSE stream events. They are fed to the REAL3 * vendor SDKs through an injected `fetch`, so the adapters are exercised end to end without any network.4 * Nothing here is recorded from a production account; IDs and signatures are obviously synthetic.5 */67export interface RecordedRequest {8 url: string;9 method: string;10 headers: Headers;11 body: Record<string, unknown> | null;12}1314export type Responder = (req: RecordedRequest, signal: AbortSignal | undefined) => Response | Promise<Response>;1516/** A fetch double that serves queued responders in order and records every request. */17export function fakeFetch(responders: Responder[]): { fetch: typeof globalThis.fetch; requests: RecordedRequest[] } {18 const requests: RecordedRequest[] = [];19 const queue = [...responders];20 const fetch = (async (input: Parameters<typeof globalThis.fetch>[0], init?: RequestInit): Promise<Response> => {21 const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;22 const rawBody = typeof init?.body === 'string' ? init.body : null;23 const req: RecordedRequest = {24 url, method: init?.method ?? 'GET', headers: new Headers(init?.headers),25 body: rawBody ? (JSON.parse(rawBody) as Record<string, unknown>) : null,26 };27 requests.push(req);28 const next = queue.shift();29 if (!next) throw new Error(`fakeFetch: no responder left for ${req.method} ${url}`);30 return next(req, init?.signal ?? undefined);31 }) as typeof globalThis.fetch;32 return { fetch, requests };33}3435export function sseResponse(events: { event?: string; data: unknown }[], headers: Record<string, string> = {}): Response {36 const text = events.map((e) => `${e.event ? `event: ${e.event}\n` : ''}data: ${typeof e.data === 'string' ? e.data : JSON.stringify(e.data)}\n\n`).join('');37 return new Response(text, { status: 200, headers: { 'content-type': 'text/event-stream', ...headers } });38}3940export function jsonResponse(status: number, body: unknown, headers: Record<string, string> = {}): Response {41 return new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json', ...headers } });42}4344/** A fetch that never answers until the request is aborted (for AbortSignal tests). */45export const hangUntilAborted: Responder = (_req, signal) => new Promise<Response>((_resolve, reject) => {46 const fail = (): void => reject(new DOMException('This operation was aborted', 'AbortError'));47 if (signal?.aborted) fail();48 else signal?.addEventListener('abort', fail, { once: true });49});5051// ------------------------------------------------------------------------------------------------52// Anthropic Messages API5354export type AnthropicBlockFixture =55 | { type: 'text'; text: string; chunks?: string[] }56 | { type: 'thinking'; thinking: string; signature: string; chunks?: string[] }57 | { type: 'tool_use'; id: string; name: string; /** raw JSON text streamed as input_json_delta fragments */ json: string; chunks?: string[] }58 | { type: 'raw'; start: Record<string, unknown> };5960export interface AnthropicMessageFixture {61 id?: string;62 model?: string;63 content: AnthropicBlockFixture[];64 stop_reason: string;65 stop_details?: Record<string, unknown> | null;66 usage?: { input_tokens?: number; output_tokens?: number; cache_read_input_tokens?: number; cache_creation_input_tokens?: number; iterations?: unknown[] };67}6869const split = (s: string, n = 2): string[] => {70 if (s.length === 0) return [];71 const size = Math.max(1, Math.ceil(s.length / n));72 const out: string[] = [];73 for (let i = 0; i < s.length; i += size) out.push(s.slice(i, i + size));74 return out;75};7677/** The event sequence of a streamed message: message_start → content_block_* → message_delta → message_stop. */78export function anthropicEvents(m: AnthropicMessageFixture): { event: string; data: unknown }[] {79 const u = m.usage ?? {};80 const events: { event: string; data: unknown }[] = [{81 event: 'message_start',82 data: {83 type: 'message_start',84 message: {85 id: m.id ?? 'msg_fixture_01', type: 'message', role: 'assistant', model: m.model ?? 'claude-haiku-5-5', content: [],86 stop_reason: null, stop_sequence: null, stop_details: null,87 usage: {88 input_tokens: u.input_tokens ?? 10, output_tokens: 1,89 cache_read_input_tokens: u.cache_read_input_tokens ?? 0, cache_creation_input_tokens: u.cache_creation_input_tokens ?? 0,90 },91 },92 },93 }];94 m.content.forEach((b, index) => {95 const start = (content_block: unknown): void => { events.push({ event: 'content_block_start', data: { type: 'content_block_start', index, content_block } }); };96 const delta = (d: unknown): void => { events.push({ event: 'content_block_delta', data: { type: 'content_block_delta', index, delta: d } }); };97 if (b.type === 'text') {98 start({ type: 'text', text: '' });99 for (const c of b.chunks ?? split(b.text)) delta({ type: 'text_delta', text: c });100 } else if (b.type === 'thinking') {101 start({ type: 'thinking', thinking: '', signature: '' });102 for (const c of b.chunks ?? split(b.thinking)) delta({ type: 'thinking_delta', thinking: c });103 delta({ type: 'signature_delta', signature: b.signature });104 } else if (b.type === 'tool_use') {105 start({ type: 'tool_use', id: b.id, name: b.name, input: {} });106 for (const c of b.chunks ?? split(b.json, 3)) delta({ type: 'input_json_delta', partial_json: c });107 } else {108 start(b.start);109 }110 events.push({ event: 'content_block_stop', data: { type: 'content_block_stop', index } });111 });112 events.push({113 event: 'message_delta',114 data: {115 type: 'message_delta',116 delta: { stop_reason: m.stop_reason, stop_sequence: null, stop_details: m.stop_details ?? null },117 usage: { output_tokens: u.output_tokens ?? 20, ...(u.iterations ? { iterations: u.iterations } : {}) },118 },119 });120 events.push({ event: 'message_stop', data: { type: 'message_stop' } });121 return events;122}123124export const anthropicStream = (m: AnthropicMessageFixture): Responder => () => sseResponse(anthropicEvents(m), { 'request-id': 'req_fixture_01' });125126export const anthropicError = (status: number, type: string, message: string, headers: Record<string, string> = {}): Responder =>127 () => jsonResponse(status, { type: 'error', error: { type, message }, request_id: 'req_fixture_err' }, { 'request-id': 'req_fixture_err', ...headers });128129// ------------------------------------------------------------------------------------------------130// OpenAI Responses API131132export interface OpenAIResponseFixture {133 id?: string;134 model?: string;135 status?: 'completed' | 'incomplete' | 'failed';136 incomplete_details?: { reason: string } | null;137 error?: { code: string; message: string } | null;138 output: Record<string, unknown>[];139 usage?: { input_tokens: number; output_tokens: number; cached_tokens?: number; cache_write_tokens?: number; reasoning_tokens?: number } | null;140}141142export function openaiResponseObject(f: OpenAIResponseFixture): Record<string, unknown> {143 const u = f.usage === undefined ? { input_tokens: 12, output_tokens: 7 } : f.usage;144 return {145 id: f.id ?? 'resp_fixture_01', object: 'response', created_at: 1_794_000_000, model: f.model ?? 'gpt-6-luna',146 status: f.status ?? 'completed', incomplete_details: f.incomplete_details ?? null, error: f.error ?? null,147 output: f.output, store: false, parallel_tool_calls: true,148 usage: u === null ? null : {149 input_tokens: u.input_tokens, output_tokens: u.output_tokens, total_tokens: u.input_tokens + u.output_tokens,150 input_tokens_details: { cached_tokens: u.cached_tokens ?? 0, cache_write_tokens: u.cache_write_tokens ?? 0 },151 output_tokens_details: { reasoning_tokens: u.reasoning_tokens ?? 0 },152 },153 };154}155156/** The event sequence of a streamed response, with deltas for text, reasoning summaries and function arguments. */157export function openaiEvents(f: OpenAIResponseFixture): { event: string; data: unknown }[] {158 let seq = 0;159 const events: { event: string; data: unknown }[] = [];160 const push = (type: string, rest: Record<string, unknown>): void => { events.push({ event: type, data: { type, sequence_number: seq++, ...rest } }); };161 const final = openaiResponseObject(f);162 push('response.created', { response: { ...final, status: 'in_progress', output: [], usage: null } });163 f.output.forEach((item, output_index) => {164 const item_id = String(item.id ?? `item_${output_index}`);165 push('response.output_item.added', { output_index, item: { ...item, status: 'in_progress' } });166 if (item.type === 'reasoning') {167 (item.summary as { text: string }[] | undefined ?? []).forEach((part, summary_index) => {168 push('response.reasoning_summary_part.added', { item_id, output_index, summary_index, part: { type: 'summary_text', text: '' } });169 for (const c of split(part.text)) push('response.reasoning_summary_text.delta', { item_id, output_index, summary_index, delta: c });170 push('response.reasoning_summary_text.done', { item_id, output_index, summary_index, text: part.text });171 push('response.reasoning_summary_part.done', { item_id, output_index, summary_index, part });172 });173 } else if (item.type === 'message') {174 (item.content as Record<string, unknown>[]).forEach((part, content_index) => {175 if (part.type === 'output_text') {176 for (const c of split(String(part.text))) push('response.output_text.delta', { item_id, output_index, content_index, delta: c, logprobs: [] });177 push('response.output_text.done', { item_id, output_index, content_index, text: part.text, logprobs: [] });178 } else if (part.type === 'refusal') {179 push('response.refusal.delta', { item_id, output_index, content_index, delta: part.refusal });180 push('response.refusal.done', { item_id, output_index, content_index, refusal: part.refusal });181 }182 });183 } else if (item.type === 'function_call') {184 for (const c of split(String(item.arguments), 3)) push('response.function_call_arguments.delta', { item_id, output_index, delta: c });185 push('response.function_call_arguments.done', { item_id, output_index, arguments: item.arguments, name: item.name });186 }187 push('response.output_item.done', { output_index, item });188 });189 const status = f.status ?? 'completed';190 push(status === 'completed' ? 'response.completed' : status === 'incomplete' ? 'response.incomplete' : 'response.failed', { response: final });191 return events;192}193194export const openaiStream = (f: OpenAIResponseFixture): Responder => () => sseResponse(openaiEvents(f), { 'x-request-id': 'req_fixture_oa' });195196export const openaiError = (status: number, code: string | null, message: string, headers: Record<string, string> = {}, type = 'invalid_request_error'): Responder =>197 () => jsonResponse(status, { error: { message, type, param: null, code } }, { 'x-request-id': 'req_fixture_oa_err', ...headers });198199export const noSleep = (): { sleep: (ms: number) => Promise<void>; waits: number[] } => {200 const waits: number[] = [];201 return { waits, sleep: async (ms: number) => { waits.push(ms); } };202};203