@copilotkit/aimock 1.14.9 → 1.15.1
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.
- package/CHANGELOG.md +29 -0
- package/dist/bedrock-converse.cjs +129 -21
- package/dist/bedrock-converse.cjs.map +1 -1
- package/dist/bedrock-converse.d.cts.map +1 -1
- package/dist/bedrock-converse.d.ts.map +1 -1
- package/dist/bedrock-converse.js +131 -23
- package/dist/bedrock-converse.js.map +1 -1
- package/dist/bedrock.cjs +258 -44
- package/dist/bedrock.cjs.map +1 -1
- package/dist/bedrock.d.cts.map +1 -1
- package/dist/bedrock.d.ts.map +1 -1
- package/dist/bedrock.js +259 -46
- package/dist/bedrock.js.map +1 -1
- package/dist/cohere.cjs +287 -31
- package/dist/cohere.cjs.map +1 -1
- package/dist/cohere.d.cts +9 -0
- package/dist/cohere.d.cts.map +1 -1
- package/dist/cohere.d.ts +9 -0
- package/dist/cohere.d.ts.map +1 -1
- package/dist/cohere.js +288 -32
- package/dist/cohere.js.map +1 -1
- package/dist/config-loader.d.ts.map +1 -1
- package/dist/embeddings.cjs +21 -3
- package/dist/embeddings.cjs.map +1 -1
- package/dist/embeddings.d.cts +2 -2
- package/dist/embeddings.d.cts.map +1 -1
- package/dist/embeddings.d.ts +2 -2
- package/dist/embeddings.d.ts.map +1 -1
- package/dist/embeddings.js +21 -3
- package/dist/embeddings.js.map +1 -1
- package/dist/gemini.cjs +46 -43
- package/dist/gemini.cjs.map +1 -1
- package/dist/gemini.d.cts.map +1 -1
- package/dist/gemini.d.ts.map +1 -1
- package/dist/gemini.js +46 -43
- package/dist/gemini.js.map +1 -1
- package/dist/helpers.cjs +9 -0
- package/dist/helpers.cjs.map +1 -1
- package/dist/helpers.d.cts.map +1 -1
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +9 -0
- package/dist/helpers.js.map +1 -1
- package/dist/images.cjs +19 -1
- package/dist/images.cjs.map +1 -1
- package/dist/images.js +19 -1
- package/dist/images.js.map +1 -1
- package/dist/index.cjs +2 -0
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -3
- package/dist/jest.cjs +10 -3
- package/dist/jest.cjs.map +1 -1
- package/dist/jest.js +10 -3
- package/dist/jest.js.map +1 -1
- package/dist/journal.cjs +1 -1
- package/dist/journal.cjs.map +1 -1
- package/dist/journal.d.cts.map +1 -1
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +1 -1
- package/dist/journal.js.map +1 -1
- package/dist/messages.cjs +3 -2
- package/dist/messages.cjs.map +1 -1
- package/dist/messages.js +3 -2
- package/dist/messages.js.map +1 -1
- package/dist/ollama.cjs +125 -4
- package/dist/ollama.cjs.map +1 -1
- package/dist/ollama.d.cts.map +1 -1
- package/dist/ollama.d.ts.map +1 -1
- package/dist/ollama.js +126 -5
- package/dist/ollama.js.map +1 -1
- package/dist/recorder.cjs +224 -53
- package/dist/recorder.cjs.map +1 -1
- package/dist/recorder.js +224 -53
- package/dist/recorder.js.map +1 -1
- package/dist/responses.cjs +10 -1
- package/dist/responses.cjs.map +1 -1
- package/dist/responses.d.cts +2 -1
- package/dist/responses.d.cts.map +1 -1
- package/dist/responses.d.ts +2 -1
- package/dist/responses.d.ts.map +1 -1
- package/dist/responses.js +10 -2
- package/dist/responses.js.map +1 -1
- package/dist/router.cjs +13 -6
- package/dist/router.cjs.map +1 -1
- package/dist/router.js +13 -6
- package/dist/router.js.map +1 -1
- package/dist/server.cjs +144 -62
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +145 -63
- package/dist/server.js.map +1 -1
- package/dist/speech.cjs +19 -1
- package/dist/speech.cjs.map +1 -1
- package/dist/speech.js +19 -1
- package/dist/speech.js.map +1 -1
- package/dist/transcription.cjs +8 -4
- package/dist/transcription.cjs.map +1 -1
- package/dist/transcription.d.cts.map +1 -1
- package/dist/transcription.d.ts.map +1 -1
- package/dist/transcription.js +8 -4
- package/dist/transcription.js.map +1 -1
- package/dist/types.d.cts +1 -0
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/video.cjs +63 -1
- package/dist/video.cjs.map +1 -1
- package/dist/video.d.cts +15 -2
- package/dist/video.d.cts.map +1 -1
- package/dist/video.d.ts +15 -2
- package/dist/video.d.ts.map +1 -1
- package/dist/video.js +63 -2
- package/dist/video.js.map +1 -1
- package/dist/vitest.cjs +10 -3
- package/dist/vitest.cjs.map +1 -1
- package/dist/vitest.js +10 -3
- package/dist/vitest.js.map +1 -1
- package/package.json +1 -1
package/dist/recorder.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recorder.cjs","names":["resolveUpstreamUrl","collapseStreamingResponse","crypto","path","https","http","getLastMessageByRole","getTextContent"],"sources":["../src/recorder.ts"],"sourcesContent":["import * as http from \"node:http\";\nimport * as https from \"node:https\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as crypto from \"node:crypto\";\nimport type {\n ChatCompletionRequest,\n Fixture,\n FixtureResponse,\n RecordConfig,\n RecordProviderKey,\n ToolCall,\n} from \"./types.js\";\nimport { getLastMessageByRole, getTextContent } from \"./router.js\";\nimport type { Logger } from \"./logger.js\";\nimport { collapseStreamingResponse } from \"./stream-collapse.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport { resolveUpstreamUrl } from \"./url.js\";\n\n/** Headers to strip when proxying — hop-by-hop (RFC 2616 §13.5.1) + client-set. */\nconst STRIP_HEADERS = new Set([\n // Hop-by-hop (RFC 2616 §13.5.1)\n \"connection\",\n \"keep-alive\",\n \"transfer-encoding\",\n \"te\",\n \"trailer\",\n \"upgrade\",\n \"proxy-authorization\",\n \"proxy-authenticate\",\n // Set by HTTP client from the target URL / body\n \"host\",\n \"content-length\",\n // Not relevant for LLM APIs; avoid leaking or mismatched encoding\n \"cookie\",\n \"accept-encoding\",\n]);\n\n/**\n * Proxy an unmatched request to the real upstream provider, record the\n * response as a fixture on disk and in memory, then relay the response\n * back to the original client.\n *\n * Returns `true` if the request was proxied (provider configured),\n * `false` if no upstream URL is configured for the given provider key.\n */\nexport async function proxyAndRecord(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n request: ChatCompletionRequest,\n providerKey: RecordProviderKey,\n pathname: string,\n fixtures: Fixture[],\n defaults: {\n record?: RecordConfig;\n logger: Logger;\n requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest;\n },\n rawBody?: string,\n): Promise<boolean> {\n const record = defaults.record;\n if (!record) return false;\n\n const providers = record.providers;\n const upstreamUrl = providers[providerKey];\n\n if (!upstreamUrl) {\n defaults.logger.warn(`No upstream URL configured for provider \"${providerKey}\" — cannot proxy`);\n return false;\n }\n\n const fixturePath = record.fixturePath ?? \"./fixtures/recorded\";\n let target: URL;\n try {\n target = resolveUpstreamUrl(upstreamUrl, pathname);\n } catch {\n defaults.logger.error(`Invalid upstream URL for provider \"${providerKey}\": ${upstreamUrl}`);\n writeErrorResponse(\n res,\n 502,\n JSON.stringify({\n error: { message: `Invalid upstream URL: ${upstreamUrl}`, type: \"proxy_error\" },\n }),\n );\n return true;\n }\n\n defaults.logger.warn(`NO FIXTURE MATCH — proxying to ${upstreamUrl}${pathname}`);\n\n // Forward all request headers except hop-by-hop and client-set ones.\n const forwardHeaders: Record<string, string> = {};\n for (const [name, val] of Object.entries(req.headers)) {\n if (val !== undefined && !STRIP_HEADERS.has(name)) {\n forwardHeaders[name] = Array.isArray(val) ? val.join(\", \") : val;\n }\n }\n\n const requestBody = rawBody ?? JSON.stringify(request);\n\n // Make upstream request\n let upstreamStatus: number;\n let upstreamHeaders: http.IncomingHttpHeaders;\n let upstreamBody: string;\n let rawBuffer: Buffer;\n\n // Track whether we streamed SSE progressively to the client; if so,\n // skip the final res.writeHead/res.end relay at the bottom of this fn.\n let streamedToClient = false;\n try {\n const result = await makeUpstreamRequest(target, forwardHeaders, requestBody, res);\n upstreamStatus = result.status;\n upstreamHeaders = result.headers;\n upstreamBody = result.body;\n rawBuffer = result.rawBuffer;\n streamedToClient = result.streamedToClient;\n } catch (err) {\n const msg = err instanceof Error ? err.message : \"Unknown proxy error\";\n defaults.logger.error(`Proxy request failed: ${msg}`);\n res.writeHead(502, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: { message: `Proxy to upstream failed: ${msg}`, type: \"proxy_error\" },\n }),\n );\n return true;\n }\n\n // Detect streaming response and collapse if necessary\n const contentType = upstreamHeaders[\"content-type\"];\n const ctString = Array.isArray(contentType) ? contentType.join(\", \") : (contentType ?? \"\");\n const isBinaryStream = ctString.toLowerCase().includes(\"application/vnd.amazon.eventstream\");\n const collapsed = collapseStreamingResponse(\n ctString,\n providerKey,\n isBinaryStream ? rawBuffer : upstreamBody,\n defaults.logger,\n );\n\n let fixtureResponse: FixtureResponse;\n\n // TTS response — binary audio, not JSON\n const isAudioResponse = ctString.toLowerCase().startsWith(\"audio/\");\n if (isAudioResponse && rawBuffer.length > 0) {\n // Derive format from Content-Type (audio/mpeg→mp3, audio/opus→opus, etc.)\n const audioFormat = ctString\n .toLowerCase()\n .replace(\"audio/\", \"\")\n .replace(\"mpeg\", \"mp3\")\n .split(\";\")[0]\n .trim();\n fixtureResponse = {\n audio: rawBuffer.toString(\"base64\"),\n ...(audioFormat && audioFormat !== \"mp3\" ? { format: audioFormat } : {}),\n };\n } else if (collapsed) {\n // Streaming response — use collapsed result\n defaults.logger.warn(`Streaming response detected (${ctString}) — collapsing to fixture`);\n if (collapsed.truncated) {\n defaults.logger.warn(\"Bedrock EventStream: CRC mismatch — response may be truncated\");\n }\n if (collapsed.droppedChunks && collapsed.droppedChunks > 0) {\n defaults.logger.warn(`${collapsed.droppedChunks} chunk(s) dropped during stream collapse`);\n }\n if (collapsed.content === \"\" && (!collapsed.toolCalls || collapsed.toolCalls.length === 0)) {\n defaults.logger.warn(\"Stream collapse produced empty content — fixture may be incomplete\");\n }\n if (collapsed.toolCalls && collapsed.toolCalls.length > 0) {\n if (collapsed.content) {\n defaults.logger.warn(\n \"Collapsed response has both content and toolCalls — preferring toolCalls\",\n );\n }\n fixtureResponse = { toolCalls: collapsed.toolCalls };\n } else {\n fixtureResponse = { content: collapsed.content ?? \"\" };\n }\n } else {\n // Non-streaming — try to parse as JSON\n let parsedResponse: unknown = null;\n try {\n parsedResponse = JSON.parse(upstreamBody);\n } catch {\n // Not JSON — could be an unknown format\n defaults.logger.warn(\"Upstream response is not valid JSON — saving as error fixture\");\n }\n let encodingFormat: string | undefined;\n try {\n encodingFormat = rawBody ? JSON.parse(rawBody).encoding_format : undefined;\n } catch {\n /* not JSON */\n }\n fixtureResponse = buildFixtureResponse(parsedResponse, upstreamStatus, encodingFormat);\n }\n\n // Build the match criteria from the (optionally transformed) request\n const matchRequest = defaults.requestTransform ? defaults.requestTransform(request) : request;\n const fixtureMatch = buildFixtureMatch(matchRequest);\n\n // Build and save the fixture\n const fixture: Fixture = { match: fixtureMatch, response: fixtureResponse };\n\n // Check if the match is empty (all undefined values) — warn but still save to disk\n const matchValues = Object.values(fixtureMatch);\n const isEmptyMatch = matchValues.length === 0 || matchValues.every((v) => v === undefined);\n if (isEmptyMatch) {\n defaults.logger.warn(\n \"Recorded fixture has empty match criteria — skipping in-memory registration\",\n );\n }\n\n // In proxy-only mode, skip recording to disk and in-memory caching\n if (!defaults.record?.proxyOnly) {\n const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n const filename = `${providerKey}-${timestamp}-${crypto.randomUUID().slice(0, 8)}.json`;\n const filepath = path.join(fixturePath, filename);\n\n let writtenToDisk = false;\n try {\n // Ensure fixture directory exists\n fs.mkdirSync(fixturePath, { recursive: true });\n\n // Collect warnings for the fixture file\n const warnings: string[] = [];\n if (isEmptyMatch) {\n warnings.push(\"Empty match criteria — this fixture will not match any request\");\n }\n if (collapsed?.truncated) {\n warnings.push(\"Stream response was truncated — fixture may be incomplete\");\n }\n\n // Auth headers are forwarded to upstream but excluded from saved fixtures for security\n const fileContent: Record<string, unknown> = { fixtures: [fixture] };\n if (warnings.length > 0) {\n fileContent._warning = warnings.join(\"; \");\n }\n fs.writeFileSync(filepath, JSON.stringify(fileContent, null, 2), \"utf-8\");\n writtenToDisk = true;\n } catch (err) {\n const msg = err instanceof Error ? err.message : \"Unknown filesystem error\";\n defaults.logger.error(`Failed to save fixture to disk: ${msg}`);\n res.setHeader(\"X-LLMock-Record-Error\", msg);\n }\n\n if (writtenToDisk) {\n // Register in memory so subsequent identical requests match (skip if empty match)\n if (!isEmptyMatch) {\n fixtures.push(fixture);\n }\n defaults.logger.warn(`Response recorded → ${filepath}`);\n } else {\n defaults.logger.warn(`Response relayed but NOT saved to disk — see error above`);\n }\n } else {\n defaults.logger.info(`Proxied ${providerKey} request (proxy-only mode)`);\n }\n\n // Relay upstream response to client (skip when SSE was already streamed\n // progressively by makeUpstreamRequest — headers and body are already on\n // the wire).\n if (!streamedToClient) {\n const relayHeaders: Record<string, string> = {};\n if (ctString) {\n relayHeaders[\"Content-Type\"] = ctString;\n }\n res.writeHead(upstreamStatus, relayHeaders);\n res.end(isBinaryStream ? rawBuffer : upstreamBody);\n }\n\n return true;\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nfunction makeUpstreamRequest(\n target: URL,\n headers: Record<string, string>,\n body: string,\n clientRes?: http.ServerResponse,\n): Promise<{\n status: number;\n headers: http.IncomingHttpHeaders;\n body: string;\n rawBuffer: Buffer;\n streamedToClient: boolean;\n}> {\n return new Promise((resolve, reject) => {\n const transport = target.protocol === \"https:\" ? https : http;\n const UPSTREAM_TIMEOUT_MS = 30_000;\n const BODY_TIMEOUT_MS = 30_000;\n const req = transport.request(\n target,\n {\n method: \"POST\",\n timeout: UPSTREAM_TIMEOUT_MS,\n headers: {\n ...headers,\n \"Content-Length\": Buffer.byteLength(body).toString(),\n },\n },\n (res) => {\n res.setTimeout(BODY_TIMEOUT_MS, () => {\n req.destroy(new Error(`Upstream response timed out after ${BODY_TIMEOUT_MS / 1000}s`));\n });\n // Detect Server-Sent Events so we can tee upstream chunks to the\n // client as they arrive rather than buffering the entire stream and\n // replaying it in a single res.end() at the bottom of proxyAndRecord.\n // Buffering collapses every SSE frame into one client-visible write,\n // which defeats progressive rendering in downstream consumers.\n const ct = res.headers[\"content-type\"];\n const ctStr = Array.isArray(ct) ? ct.join(\", \") : (ct ?? \"\");\n const isSSE = ctStr.toLowerCase().includes(\"text/event-stream\");\n let streamedToClient = false;\n if (isSSE && clientRes && !clientRes.headersSent) {\n const relayHeaders: Record<string, string> = {};\n if (ctStr) relayHeaders[\"Content-Type\"] = ctStr;\n clientRes.writeHead(res.statusCode ?? 200, relayHeaders);\n // Flush headers immediately so the client starts parsing frames\n // before the first data chunk arrives.\n if (typeof clientRes.flushHeaders === \"function\") clientRes.flushHeaders();\n streamedToClient = true;\n }\n const chunks: Buffer[] = [];\n res.on(\"data\", (chunk: Buffer) => {\n chunks.push(chunk);\n if (streamedToClient) clientRes!.write(chunk);\n });\n res.on(\"error\", reject);\n res.on(\"end\", () => {\n const rawBuffer = Buffer.concat(chunks);\n if (streamedToClient) clientRes!.end();\n resolve({\n status: res.statusCode ?? 500,\n headers: res.headers,\n body: rawBuffer.toString(),\n rawBuffer,\n streamedToClient,\n });\n });\n },\n );\n req.on(\"timeout\", () => {\n req.destroy(\n new Error(\n `Upstream request timed out after ${UPSTREAM_TIMEOUT_MS / 1000}s: ${target.href}`,\n ),\n );\n });\n req.on(\"error\", reject);\n req.write(body);\n req.end();\n });\n}\n\n/**\n * Detect the response format from the parsed upstream JSON and convert\n * it into an aimock FixtureResponse.\n */\nfunction buildFixtureResponse(\n parsed: unknown,\n status: number,\n encodingFormat?: string,\n): FixtureResponse {\n if (parsed === null || parsed === undefined) {\n // Raw / unparseable response — save as error\n return {\n error: { message: \"Upstream returned non-JSON response\", type: \"proxy_error\" },\n status,\n };\n }\n\n const obj = parsed as Record<string, unknown>;\n\n // Error response\n if (obj.error) {\n const err = obj.error as Record<string, unknown>;\n return {\n error: {\n message: String(err.message ?? \"Unknown error\"),\n type: String(err.type ?? \"api_error\"),\n code: err.code ? String(err.code) : undefined,\n },\n status,\n };\n }\n\n // OpenAI embeddings: { data: [{ embedding: [...] }] }\n if (Array.isArray(obj.data) && obj.data.length > 0) {\n const first = obj.data[0] as Record<string, unknown>;\n if (Array.isArray(first.embedding)) {\n return { embedding: first.embedding as number[] };\n }\n if (typeof first.embedding === \"string\" && encodingFormat === \"base64\") {\n try {\n const buf = Buffer.from(first.embedding, \"base64\");\n const floats = new Float32Array(buf.buffer, buf.byteOffset, buf.byteLength / 4);\n return { embedding: Array.from(floats) };\n } catch {\n // Corrupted base64 or non-float32 data — fall through to error\n }\n }\n // OpenAI image generation: { created, data: [{ url, b64_json, revised_prompt }] }\n if (first.url || first.b64_json) {\n const images = (obj.data as Array<Record<string, unknown>>).map((item) => ({\n ...(item.url ? { url: String(item.url) } : {}),\n ...(item.b64_json ? { b64Json: String(item.b64_json) } : {}),\n ...(item.revised_prompt ? { revisedPrompt: String(item.revised_prompt) } : {}),\n }));\n if (images.length === 1) {\n return { image: images[0] };\n }\n return { images };\n }\n }\n\n // Gemini Imagen: { predictions: [...] }\n if (Array.isArray(obj.predictions)) {\n const images = (obj.predictions as Array<Record<string, unknown>>).map((p) => ({\n ...(p.bytesBase64Encoded ? { b64Json: String(p.bytesBase64Encoded) } : {}),\n ...(p.mimeType ? { mimeType: String(p.mimeType) } : {}),\n }));\n if (images.length === 1) {\n return { image: images[0] };\n }\n return { images };\n }\n\n // OpenAI transcription: { text: \"...\", ... }\n if (\n typeof obj.text === \"string\" &&\n (obj.task === \"transcribe\" || obj.language !== undefined || obj.duration !== undefined)\n ) {\n return {\n transcription: {\n text: obj.text as string,\n ...(obj.language ? { language: String(obj.language) } : {}),\n ...(obj.duration !== undefined ? { duration: Number(obj.duration) } : {}),\n ...(Array.isArray(obj.words) ? { words: obj.words } : {}),\n ...(Array.isArray(obj.segments) ? { segments: obj.segments } : {}),\n },\n };\n }\n\n // OpenAI video generation: { id, status, ... }\n if (\n typeof obj.id === \"string\" &&\n typeof obj.status === \"string\" &&\n (obj.status === \"completed\" || obj.status === \"in_progress\" || obj.status === \"failed\")\n ) {\n if (obj.status === \"completed\" && obj.url) {\n return {\n video: {\n id: String(obj.id),\n status: \"completed\" as const,\n url: String(obj.url),\n },\n };\n }\n return {\n video: {\n id: String(obj.id),\n status: obj.status === \"failed\" ? (\"failed\" as const) : (\"processing\" as const),\n },\n };\n }\n\n // Direct embedding: { embedding: [...] }\n if (Array.isArray(obj.embedding)) {\n return { embedding: obj.embedding as number[] };\n }\n\n // OpenAI chat completion: { choices: [{ message: { content, tool_calls } }] }\n if (Array.isArray(obj.choices) && obj.choices.length > 0) {\n const choice = obj.choices[0] as Record<string, unknown>;\n const message = choice.message as Record<string, unknown> | undefined;\n if (message) {\n // Tool calls\n if (Array.isArray(message.tool_calls) && message.tool_calls.length > 0) {\n const toolCalls: ToolCall[] = (message.tool_calls as Array<Record<string, unknown>>).map(\n (tc) => {\n const fn = tc.function as Record<string, unknown>;\n return {\n name: String(fn.name),\n arguments: String(fn.arguments),\n };\n },\n );\n return { toolCalls };\n }\n // Text content\n if (typeof message.content === \"string\") {\n return { content: message.content };\n }\n }\n }\n\n // Anthropic: { content: [{ type: \"text\", text: \"...\" }] } or tool_use\n if (Array.isArray(obj.content) && obj.content.length > 0) {\n const blocks = obj.content as Array<Record<string, unknown>>;\n // Check for tool_use blocks first\n const toolUseBlocks = blocks.filter((b) => b.type === \"tool_use\");\n if (toolUseBlocks.length > 0) {\n const toolCalls: ToolCall[] = toolUseBlocks.map((b) => ({\n name: String(b.name),\n arguments: typeof b.input === \"string\" ? b.input : JSON.stringify(b.input),\n }));\n return { toolCalls };\n }\n // Text blocks\n const textBlock = blocks.find((b) => b.type === \"text\");\n if (textBlock && typeof textBlock.text === \"string\") {\n return { content: textBlock.text };\n }\n }\n\n // Gemini: { candidates: [{ content: { parts: [{ text: \"...\" }] } }] }\n if (Array.isArray(obj.candidates) && obj.candidates.length > 0) {\n const candidate = obj.candidates[0] as Record<string, unknown>;\n const content = candidate.content as Record<string, unknown> | undefined;\n if (content && Array.isArray(content.parts)) {\n const parts = content.parts as Array<Record<string, unknown>>;\n // Tool calls (functionCall)\n const fnCallParts = parts.filter((p) => p.functionCall);\n if (fnCallParts.length > 0) {\n const toolCalls: ToolCall[] = fnCallParts.map((p) => {\n const fc = p.functionCall as Record<string, unknown>;\n return {\n name: String(fc.name),\n arguments: typeof fc.args === \"string\" ? fc.args : JSON.stringify(fc.args),\n };\n });\n return { toolCalls };\n }\n // Text\n const textPart = parts.find((p) => typeof p.text === \"string\");\n if (textPart && typeof textPart.text === \"string\") {\n return { content: textPart.text };\n }\n }\n }\n\n // Bedrock Converse: { output: { message: { role, content: [{ text }, { toolUse }] } } }\n if (obj.output && typeof obj.output === \"object\") {\n const output = obj.output as Record<string, unknown>;\n const msg = output.message as Record<string, unknown> | undefined;\n if (msg && Array.isArray(msg.content)) {\n const blocks = msg.content as Array<Record<string, unknown>>;\n const toolUseBlocks = blocks.filter((b) => b.toolUse);\n if (toolUseBlocks.length > 0) {\n const toolCalls: ToolCall[] = toolUseBlocks.map((b) => {\n const tu = b.toolUse as Record<string, unknown>;\n return {\n name: String(tu.name ?? \"\"),\n arguments: typeof tu.input === \"string\" ? tu.input : JSON.stringify(tu.input),\n };\n });\n return { toolCalls };\n }\n const textBlock = blocks.find((b) => typeof b.text === \"string\");\n if (textBlock && typeof textBlock.text === \"string\") {\n return { content: textBlock.text };\n }\n }\n }\n\n // Ollama: { message: { content: \"...\", tool_calls: [...] } }\n if (obj.message && typeof obj.message === \"object\") {\n const msg = obj.message as Record<string, unknown>;\n // Tool calls (check before content — Ollama sends content: \"\" alongside tool_calls)\n if (Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0) {\n const toolCalls: ToolCall[] = (msg.tool_calls as Array<Record<string, unknown>>)\n .filter((tc) => tc.function != null)\n .map((tc) => {\n const fn = tc.function as Record<string, unknown>;\n return {\n name: String(fn.name ?? \"\"),\n arguments:\n typeof fn.arguments === \"string\" ? fn.arguments : JSON.stringify(fn.arguments),\n };\n });\n return { toolCalls };\n }\n if (typeof msg.content === \"string\" && msg.content.length > 0) {\n return { content: msg.content };\n }\n // Ollama message with content array (like Cohere)\n if (Array.isArray(msg.content) && msg.content.length > 0) {\n const first = msg.content[0] as Record<string, unknown>;\n if (typeof first.text === \"string\") {\n return { content: first.text };\n }\n }\n }\n\n // Fallback: unknown format — save as error\n return {\n error: {\n message: \"Could not detect response format from upstream\",\n type: \"proxy_error\",\n },\n status,\n };\n}\n\n/**\n * Derive fixture match criteria from the original request.\n */\ntype EndpointType = \"chat\" | \"image\" | \"speech\" | \"transcription\" | \"video\" | \"embedding\";\n\nfunction buildFixtureMatch(request: ChatCompletionRequest): {\n userMessage?: string;\n inputText?: string;\n endpoint?: EndpointType;\n} {\n const match: { userMessage?: string; inputText?: string; endpoint?: EndpointType } = {};\n\n // Include endpoint type for multimedia fixtures\n if (request._endpointType && request._endpointType !== \"chat\") {\n match.endpoint = request._endpointType as EndpointType;\n }\n\n // Embedding request\n if (request.embeddingInput) {\n match.inputText = request.embeddingInput;\n return match;\n }\n\n // Chat/multimedia request — match on the last user message\n const lastUser = getLastMessageByRole(request.messages ?? [], \"user\");\n if (lastUser) {\n const text = getTextContent(lastUser.content);\n if (text) {\n match.userMessage = text;\n }\n }\n\n return match;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"recorder.cjs","names":["resolveUpstreamUrl","collapseStreamingResponse","crypto","path","https","http","getLastMessageByRole","getTextContent"],"sources":["../src/recorder.ts"],"sourcesContent":["import * as http from \"node:http\";\nimport * as https from \"node:https\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as crypto from \"node:crypto\";\nimport type {\n ChatCompletionRequest,\n Fixture,\n FixtureResponse,\n RecordConfig,\n RecordProviderKey,\n ToolCall,\n} from \"./types.js\";\nimport { getLastMessageByRole, getTextContent } from \"./router.js\";\nimport type { Logger } from \"./logger.js\";\nimport { collapseStreamingResponse } from \"./stream-collapse.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport { resolveUpstreamUrl } from \"./url.js\";\n\n/** Headers to strip when proxying — hop-by-hop (RFC 2616 §13.5.1) + client-set. */\nconst STRIP_HEADERS = new Set([\n // Hop-by-hop (RFC 2616 §13.5.1)\n \"connection\",\n \"keep-alive\",\n \"transfer-encoding\",\n \"te\",\n \"trailer\",\n \"upgrade\",\n \"proxy-authorization\",\n \"proxy-authenticate\",\n // Set by HTTP client from the target URL / body\n \"host\",\n \"content-length\",\n // Not relevant for LLM APIs; avoid leaking or mismatched encoding\n \"cookie\",\n \"accept-encoding\",\n]);\n\n/**\n * Proxy an unmatched request to the real upstream provider, record the\n * response as a fixture on disk and in memory, then relay the response\n * back to the original client.\n *\n * Returns `true` if the request was proxied (provider configured),\n * `false` if no upstream URL is configured for the given provider key.\n */\nexport async function proxyAndRecord(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n request: ChatCompletionRequest,\n providerKey: RecordProviderKey,\n pathname: string,\n fixtures: Fixture[],\n defaults: {\n record?: RecordConfig;\n logger: Logger;\n requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest;\n },\n rawBody?: string,\n): Promise<boolean> {\n const record = defaults.record;\n if (!record) return false;\n\n const providers = record.providers;\n const upstreamUrl = providers[providerKey];\n\n if (!upstreamUrl) {\n defaults.logger.warn(`No upstream URL configured for provider \"${providerKey}\" — cannot proxy`);\n return false;\n }\n\n const fixturePath = record.fixturePath ?? \"./fixtures/recorded\";\n let target: URL;\n try {\n target = resolveUpstreamUrl(upstreamUrl, pathname);\n } catch {\n defaults.logger.error(`Invalid upstream URL for provider \"${providerKey}\": ${upstreamUrl}`);\n writeErrorResponse(\n res,\n 502,\n JSON.stringify({\n error: { message: `Invalid upstream URL: ${upstreamUrl}`, type: \"proxy_error\" },\n }),\n );\n return true;\n }\n\n defaults.logger.warn(`NO FIXTURE MATCH — proxying to ${upstreamUrl}${pathname}`);\n\n // Forward all request headers except hop-by-hop and client-set ones.\n const forwardHeaders: Record<string, string> = {};\n for (const [name, val] of Object.entries(req.headers)) {\n if (val !== undefined && !STRIP_HEADERS.has(name)) {\n forwardHeaders[name] = Array.isArray(val) ? val.join(\", \") : val;\n }\n }\n\n const requestBody = rawBody ?? JSON.stringify(request);\n\n // Make upstream request\n let upstreamStatus: number;\n let upstreamHeaders: http.IncomingHttpHeaders;\n let upstreamBody: string;\n let rawBuffer: Buffer;\n\n // Track whether we streamed SSE progressively to the client; if so,\n // skip the final res.writeHead/res.end relay at the bottom of this fn.\n let streamedToClient = false;\n let clientDisconnected = false;\n try {\n const result = await makeUpstreamRequest(target, forwardHeaders, requestBody, res);\n upstreamStatus = result.status;\n upstreamHeaders = result.headers;\n upstreamBody = result.body;\n rawBuffer = result.rawBuffer;\n streamedToClient = result.streamedToClient;\n clientDisconnected = result.clientDisconnected;\n } catch (err) {\n const msg = err instanceof Error ? err.message : \"Unknown proxy error\";\n defaults.logger.error(`Proxy request failed: ${msg}`);\n if (!res.headersSent) {\n res.writeHead(502, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: { message: `Proxy to upstream failed: ${msg}`, type: \"proxy_error\" },\n }),\n );\n } else {\n // SSE headers already sent — gracefully close the connection\n res.end();\n }\n return true;\n }\n\n // Detect streaming response and collapse if necessary\n const contentType = upstreamHeaders[\"content-type\"];\n const ctString = Array.isArray(contentType) ? contentType.join(\", \") : (contentType ?? \"\");\n const isBinaryStream = ctString.toLowerCase().includes(\"application/vnd.amazon.eventstream\");\n const collapsed = collapseStreamingResponse(\n ctString,\n providerKey,\n isBinaryStream ? rawBuffer : upstreamBody,\n defaults.logger,\n );\n\n let fixtureResponse: FixtureResponse;\n\n // TTS response — binary audio, not JSON\n const isAudioResponse = ctString.toLowerCase().startsWith(\"audio/\");\n if (isAudioResponse && rawBuffer.length > 0) {\n // Derive format from Content-Type (audio/mpeg→mp3, audio/opus→opus, etc.)\n const audioFormat = ctString\n .toLowerCase()\n .replace(\"audio/\", \"\")\n .replace(\"mpeg\", \"mp3\")\n .split(\";\")[0]\n .trim();\n fixtureResponse = {\n audio: rawBuffer.toString(\"base64\"),\n ...(audioFormat && audioFormat !== \"mp3\" ? { format: audioFormat } : {}),\n };\n } else if (collapsed) {\n // Streaming response — use collapsed result\n defaults.logger.warn(`Streaming response detected (${ctString}) — collapsing to fixture`);\n if (collapsed.truncated) {\n defaults.logger.warn(\"Bedrock EventStream: CRC mismatch — response may be truncated\");\n }\n if (collapsed.droppedChunks && collapsed.droppedChunks > 0) {\n defaults.logger.warn(`${collapsed.droppedChunks} chunk(s) dropped during stream collapse`);\n }\n if (collapsed.content === \"\" && (!collapsed.toolCalls || collapsed.toolCalls.length === 0)) {\n defaults.logger.warn(\"Stream collapse produced empty content — fixture may be incomplete\");\n }\n const reasoningSpread = collapsed.reasoning ? { reasoning: collapsed.reasoning } : {};\n if (collapsed.toolCalls && collapsed.toolCalls.length > 0) {\n if (collapsed.content) {\n // Both content and toolCalls present — save as ContentWithToolCallsResponse\n fixtureResponse = {\n content: collapsed.content,\n toolCalls: collapsed.toolCalls,\n ...reasoningSpread,\n };\n } else {\n fixtureResponse = { toolCalls: collapsed.toolCalls, ...reasoningSpread };\n }\n } else {\n fixtureResponse = { content: collapsed.content ?? \"\", ...reasoningSpread };\n }\n } else {\n // Non-streaming — try to parse as JSON\n let parsedResponse: unknown = null;\n try {\n parsedResponse = JSON.parse(upstreamBody);\n } catch {\n // Not JSON — could be an unknown format\n defaults.logger.warn(\"Upstream response is not valid JSON — saving as error fixture\");\n }\n let encodingFormat: string | undefined;\n try {\n encodingFormat = rawBody ? JSON.parse(rawBody).encoding_format : undefined;\n } catch (err) {\n defaults.logger.debug(\n `Could not parse encoding_format from raw body: ${err instanceof Error ? err.message : \"unknown error\"}`,\n );\n }\n fixtureResponse = buildFixtureResponse(parsedResponse, upstreamStatus, encodingFormat);\n }\n\n // If the client disconnected mid-stream, the collected data is likely\n // truncated. Saving a partial fixture is worse than saving none — skip\n // fixture persistence entirely.\n if (clientDisconnected) {\n defaults.logger.warn(\n \"Client disconnected mid-stream — skipping fixture save to avoid truncated data\",\n );\n return true;\n }\n\n // Build the match criteria from the (optionally transformed) request\n const matchRequest = defaults.requestTransform ? defaults.requestTransform(request) : request;\n const fixtureMatch = buildFixtureMatch(matchRequest);\n\n // Build and save the fixture\n const fixture: Fixture = { match: fixtureMatch, response: fixtureResponse };\n\n // Check if the match is empty (all undefined values) — warn but still save to disk\n const matchValues = Object.values(fixtureMatch);\n const isEmptyMatch = matchValues.length === 0 || matchValues.every((v) => v === undefined);\n if (isEmptyMatch) {\n defaults.logger.warn(\n \"Recorded fixture has empty match criteria — skipping in-memory registration\",\n );\n }\n\n // In proxy-only mode, skip recording to disk and in-memory caching\n if (!defaults.record?.proxyOnly) {\n const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n const filename = `${providerKey}-${timestamp}-${crypto.randomUUID().slice(0, 8)}.json`;\n const filepath = path.join(fixturePath, filename);\n\n let writtenToDisk = false;\n try {\n // Ensure fixture directory exists\n fs.mkdirSync(fixturePath, { recursive: true });\n\n // Collect warnings for the fixture file\n const warnings: string[] = [];\n if (isEmptyMatch) {\n warnings.push(\"Empty match criteria — this fixture will not match any request\");\n }\n if (collapsed?.truncated) {\n warnings.push(\"Stream response was truncated — fixture may be incomplete\");\n }\n\n // Auth headers are forwarded to upstream but excluded from saved fixtures for security\n const fileContent: Record<string, unknown> = { fixtures: [fixture] };\n if (warnings.length > 0) {\n fileContent._warning = warnings.join(\"; \");\n }\n fs.writeFileSync(filepath, JSON.stringify(fileContent, null, 2), \"utf-8\");\n writtenToDisk = true;\n } catch (err) {\n const msg = err instanceof Error ? err.message : \"Unknown filesystem error\";\n defaults.logger.error(`Failed to save fixture to disk: ${msg}`);\n if (!res.headersSent) {\n res.setHeader(\"X-LLMock-Record-Error\", msg);\n } else {\n defaults.logger.warn(`Cannot set X-LLMock-Record-Error header — headers already sent`);\n }\n }\n\n if (writtenToDisk) {\n // Register in memory so subsequent identical requests match (skip if empty match)\n if (!isEmptyMatch) {\n fixtures.push(fixture);\n }\n defaults.logger.warn(`Response recorded → ${filepath}`);\n } else {\n defaults.logger.warn(`Response relayed but NOT saved to disk — see error above`);\n }\n } else {\n defaults.logger.info(`Proxied ${providerKey} request (proxy-only mode)`);\n }\n\n // Relay upstream response to client (skip when SSE was already streamed\n // progressively by makeUpstreamRequest — headers and body are already on\n // the wire).\n if (!streamedToClient) {\n const relayHeaders: Record<string, string> = {};\n if (ctString) {\n relayHeaders[\"Content-Type\"] = ctString;\n }\n res.writeHead(upstreamStatus, relayHeaders);\n const isAudioRelay = ctString.toLowerCase().startsWith(\"audio/\");\n res.end(isBinaryStream || isAudioRelay ? rawBuffer : upstreamBody);\n }\n\n return true;\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nfunction makeUpstreamRequest(\n target: URL,\n headers: Record<string, string>,\n body: string,\n clientRes?: http.ServerResponse,\n): Promise<{\n status: number;\n headers: http.IncomingHttpHeaders;\n body: string;\n rawBuffer: Buffer;\n streamedToClient: boolean;\n clientDisconnected: boolean;\n}> {\n return new Promise((resolve, reject) => {\n const transport = target.protocol === \"https:\" ? https : http;\n const UPSTREAM_TIMEOUT_MS = 30_000;\n const BODY_TIMEOUT_MS = 30_000;\n const req = transport.request(\n target,\n {\n method: \"POST\",\n timeout: UPSTREAM_TIMEOUT_MS,\n headers: {\n ...headers,\n \"Content-Length\": Buffer.byteLength(body).toString(),\n },\n },\n (res) => {\n res.setTimeout(BODY_TIMEOUT_MS, () => {\n req.destroy(new Error(`Upstream response timed out after ${BODY_TIMEOUT_MS / 1000}s`));\n });\n // Detect Server-Sent Events so we can tee upstream chunks to the\n // client as they arrive rather than buffering the entire stream and\n // replaying it in a single res.end() at the bottom of proxyAndRecord.\n // Buffering collapses every SSE frame into one client-visible write,\n // which defeats progressive rendering in downstream consumers.\n const ct = res.headers[\"content-type\"];\n const ctStr = Array.isArray(ct) ? ct.join(\", \") : (ct ?? \"\");\n const isSSE = ctStr.toLowerCase().includes(\"text/event-stream\");\n let streamedToClient = false;\n let clientDisconnected = false;\n if (isSSE && clientRes && !clientRes.headersSent) {\n const relayHeaders: Record<string, string> = {};\n if (ctStr) relayHeaders[\"Content-Type\"] = ctStr;\n clientRes.writeHead(res.statusCode ?? 200, relayHeaders);\n // Flush headers immediately so the client starts parsing frames\n // before the first data chunk arrives.\n if (typeof clientRes.flushHeaders === \"function\") clientRes.flushHeaders();\n streamedToClient = true;\n // Stop relaying if the client disconnects mid-stream\n clientRes.on(\"close\", () => {\n clientDisconnected = true;\n req.destroy();\n });\n }\n const chunks: Buffer[] = [];\n res.on(\"data\", (chunk: Buffer) => {\n chunks.push(chunk);\n if (\n streamedToClient &&\n clientRes &&\n !clientDisconnected &&\n !clientRes.destroyed &&\n !clientRes.writableEnded\n ) {\n clientRes.write(chunk);\n }\n });\n res.on(\"error\", reject);\n res.on(\"end\", () => {\n const rawBuffer = Buffer.concat(chunks);\n if (\n streamedToClient &&\n clientRes &&\n !clientDisconnected &&\n !clientRes.destroyed &&\n !clientRes.writableEnded\n ) {\n clientRes.end();\n }\n resolve({\n status: res.statusCode ?? 500,\n headers: res.headers,\n body: rawBuffer.toString(),\n rawBuffer,\n streamedToClient,\n clientDisconnected,\n });\n });\n },\n );\n req.on(\"timeout\", () => {\n req.destroy(\n new Error(\n `Upstream request timed out after ${UPSTREAM_TIMEOUT_MS / 1000}s: ${target.href}`,\n ),\n );\n });\n req.on(\"error\", reject);\n req.write(body);\n req.end();\n });\n}\n\n/**\n * Detect the response format from the parsed upstream JSON and convert\n * it into an aimock FixtureResponse.\n */\nfunction buildFixtureResponse(\n parsed: unknown,\n status: number,\n encodingFormat?: string,\n): FixtureResponse {\n if (parsed === null || parsed === undefined) {\n // Raw / unparseable response — save as error\n return {\n error: { message: \"Upstream returned non-JSON response\", type: \"proxy_error\" },\n status,\n };\n }\n\n const obj = parsed as Record<string, unknown>;\n\n // Error response — only match the actual { error: { message: \"...\" } } shape\n // used by OpenAI/Anthropic/etc., not arbitrary truthy `.error` fields.\n if (\n typeof obj.error === \"object\" &&\n obj.error !== null &&\n typeof (obj.error as Record<string, unknown>).message === \"string\"\n ) {\n const err = obj.error as Record<string, unknown>;\n return {\n error: {\n message: String(err.message ?? \"Unknown error\"),\n type: String(err.type ?? \"api_error\"),\n code: err.code ? String(err.code) : undefined,\n },\n status,\n };\n }\n\n // OpenAI embeddings: { data: [{ embedding: [...] }] }\n if (Array.isArray(obj.data) && obj.data.length > 0) {\n const first = obj.data[0] as Record<string, unknown>;\n if (Array.isArray(first.embedding)) {\n return { embedding: first.embedding as number[] };\n }\n if (typeof first.embedding === \"string\" && encodingFormat === \"base64\") {\n const buf = Buffer.from(first.embedding, \"base64\");\n const aligned = new Uint8Array(buf).buffer; // Always offset 0\n const floats = new Float32Array(aligned, 0, buf.byteLength / 4);\n return { embedding: Array.from(floats) };\n }\n // OpenAI image generation: { created, data: [{ url, b64_json, revised_prompt }] }\n if (first.url || first.b64_json) {\n const images = (obj.data as Array<Record<string, unknown>>).map((item) => ({\n ...(item.url ? { url: String(item.url) } : {}),\n ...(item.b64_json ? { b64Json: String(item.b64_json) } : {}),\n ...(item.revised_prompt ? { revisedPrompt: String(item.revised_prompt) } : {}),\n }));\n if (images.length === 1) {\n return { image: images[0] };\n }\n return { images };\n }\n }\n\n // Gemini Imagen: { predictions: [...] }\n if (Array.isArray(obj.predictions)) {\n const images = (obj.predictions as Array<Record<string, unknown>>).map((p) => ({\n ...(p.bytesBase64Encoded ? { b64Json: String(p.bytesBase64Encoded) } : {}),\n ...(p.mimeType ? { mimeType: String(p.mimeType) } : {}),\n }));\n if (images.length === 1) {\n return { image: images[0] };\n }\n return { images };\n }\n\n // OpenAI transcription: { text: \"...\", ... }\n if (\n typeof obj.text === \"string\" &&\n (obj.task === \"transcribe\" || obj.language !== undefined || obj.duration !== undefined)\n ) {\n return {\n transcription: {\n text: obj.text as string,\n ...(obj.language ? { language: String(obj.language) } : {}),\n ...(obj.duration !== undefined ? { duration: Number(obj.duration) } : {}),\n ...(Array.isArray(obj.words) ? { words: obj.words } : {}),\n ...(Array.isArray(obj.segments) ? { segments: obj.segments } : {}),\n },\n };\n }\n\n // OpenAI video generation: { id, status, ... }\n // Guard against false positives: many API responses have `id` + `status` fields\n // (e.g. chat completions, Anthropic messages). Reject if the response has fields\n // that indicate a known non-video format.\n if (\n typeof obj.id === \"string\" &&\n typeof obj.status === \"string\" &&\n (obj.status === \"completed\" || obj.status === \"in_progress\" || obj.status === \"failed\") &&\n !(\"choices\" in obj) &&\n !(\"content\" in obj) &&\n !(\"candidates\" in obj) &&\n !(\"message\" in obj) &&\n !(\"data\" in obj) &&\n !(\"object\" in obj)\n ) {\n if (obj.status === \"completed\" && obj.url) {\n return {\n video: {\n id: String(obj.id),\n status: \"completed\" as const,\n url: String(obj.url),\n },\n };\n }\n return {\n video: {\n id: String(obj.id),\n status: obj.status === \"failed\" ? (\"failed\" as const) : (\"processing\" as const),\n },\n };\n }\n\n // Direct embedding: { embedding: [...] }\n if (Array.isArray(obj.embedding)) {\n return { embedding: obj.embedding as number[] };\n }\n\n // OpenAI chat completion: { choices: [{ message: { content, tool_calls } }] }\n if (Array.isArray(obj.choices) && obj.choices.length > 0) {\n const choice = obj.choices[0] as Record<string, unknown>;\n const message = choice.message as Record<string, unknown> | undefined;\n if (message) {\n const hasToolCalls = Array.isArray(message.tool_calls) && message.tool_calls.length > 0;\n const hasContent = typeof message.content === \"string\" && message.content.length > 0;\n\n const openaiReasoning =\n typeof message.reasoning_content === \"string\" && message.reasoning_content.length > 0\n ? message.reasoning_content\n : undefined;\n\n if (hasToolCalls) {\n const toolCalls: ToolCall[] = (message.tool_calls as Array<Record<string, unknown>>).map(\n (tc) => {\n const fn = tc.function as Record<string, unknown>;\n return {\n name: String(fn.name),\n arguments: String(fn.arguments),\n ...(tc.id ? { id: String(tc.id) } : {}),\n };\n },\n );\n if (hasContent) {\n return {\n content: message.content as string,\n toolCalls,\n ...(openaiReasoning ? { reasoning: openaiReasoning } : {}),\n };\n }\n return { toolCalls, ...(openaiReasoning ? { reasoning: openaiReasoning } : {}) };\n }\n // Text content only\n if (hasContent) {\n return {\n content: message.content as string,\n ...(openaiReasoning ? { reasoning: openaiReasoning } : {}),\n };\n }\n // Recognized OpenAI shape but empty content (e.g. content filtering, zero max_tokens)\n return { content: \"\", ...(openaiReasoning ? { reasoning: openaiReasoning } : {}) };\n }\n }\n\n // Anthropic: { content: [{ type: \"text\", text: \"...\" }] } or tool_use\n if (Array.isArray(obj.content) && obj.content.length > 0) {\n const blocks = obj.content as Array<Record<string, unknown>>;\n const toolUseBlocks = blocks.filter((b) => b.type === \"tool_use\");\n const textBlocks = blocks.filter((b) => b.type === \"text\" && typeof b.text === \"string\");\n const thinkingBlocks = blocks.filter((b) => b.type === \"thinking\");\n const hasToolCalls = toolUseBlocks.length > 0;\n const joinedText = textBlocks.map((b) => String(b.text ?? \"\")).join(\"\");\n const hasContent = joinedText.length > 0;\n const anthropicReasoning =\n thinkingBlocks.length > 0\n ? thinkingBlocks.map((b) => String(b.thinking ?? \"\")).join(\"\")\n : undefined;\n\n if (hasToolCalls) {\n const toolCalls: ToolCall[] = toolUseBlocks.map((b) => ({\n name: String(b.name),\n arguments: typeof b.input === \"string\" ? b.input : JSON.stringify(b.input),\n ...(b.id ? { id: String(b.id) } : {}),\n }));\n if (hasContent) {\n return {\n content: joinedText,\n toolCalls,\n ...(anthropicReasoning ? { reasoning: anthropicReasoning } : {}),\n };\n }\n return { toolCalls, ...(anthropicReasoning ? { reasoning: anthropicReasoning } : {}) };\n }\n if (hasContent) {\n return {\n content: joinedText,\n ...(anthropicReasoning ? { reasoning: anthropicReasoning } : {}),\n };\n }\n // Thinking-only response (no text, no tool calls)\n if (anthropicReasoning) {\n return { content: \"\", reasoning: anthropicReasoning };\n }\n }\n\n // Gemini: { candidates: [{ content: { parts: [{ text: \"...\" }] } }] }\n if (Array.isArray(obj.candidates) && obj.candidates.length > 0) {\n const candidate = obj.candidates[0] as Record<string, unknown>;\n const content = candidate.content as Record<string, unknown> | undefined;\n if (content && Array.isArray(content.parts)) {\n const parts = content.parts as Array<Record<string, unknown>>;\n const fnCallParts = parts.filter((p) => p.functionCall);\n const textParts = parts.filter((p) => typeof p.text === \"string\" && !p.thought);\n const thoughtParts = parts.filter((p) => p.thought === true && typeof p.text === \"string\");\n const hasToolCalls = fnCallParts.length > 0;\n const joinedText = textParts.map((p) => String(p.text ?? \"\")).join(\"\");\n const hasContent = joinedText.length > 0;\n const geminiReasoning =\n thoughtParts.length > 0\n ? thoughtParts.map((p) => String(p.text ?? \"\")).join(\"\")\n : undefined;\n\n if (hasToolCalls) {\n const toolCalls: ToolCall[] = fnCallParts.map((p) => {\n const fc = p.functionCall as Record<string, unknown>;\n return {\n name: String(fc.name),\n arguments: typeof fc.args === \"string\" ? fc.args : JSON.stringify(fc.args),\n };\n });\n if (hasContent) {\n return {\n content: joinedText,\n toolCalls,\n ...(geminiReasoning ? { reasoning: geminiReasoning } : {}),\n };\n }\n return { toolCalls, ...(geminiReasoning ? { reasoning: geminiReasoning } : {}) };\n }\n if (hasContent) {\n return {\n content: joinedText,\n ...(geminiReasoning ? { reasoning: geminiReasoning } : {}),\n };\n }\n // Recognized Gemini shape but empty content\n return { content: \"\", ...(geminiReasoning ? { reasoning: geminiReasoning } : {}) };\n }\n }\n\n // Bedrock Converse: { output: { message: { role, content: [{ text }, { toolUse }] } } }\n if (obj.output && typeof obj.output === \"object\") {\n const output = obj.output as Record<string, unknown>;\n const msg = output.message as Record<string, unknown> | undefined;\n if (msg && Array.isArray(msg.content)) {\n const blocks = msg.content as Array<Record<string, unknown>>;\n const toolUseBlocks = blocks.filter((b) => b.toolUse);\n const textBlocks = blocks.filter((b) => typeof b.text === \"string\");\n const reasoningBlocks = blocks.filter((b) => b.reasoningContent);\n const hasToolCalls = toolUseBlocks.length > 0;\n const joinedText = textBlocks.map((b) => String(b.text ?? \"\")).join(\"\");\n const hasContent = joinedText.length > 0;\n const bedrockReasoning =\n reasoningBlocks.length > 0\n ? reasoningBlocks\n .map((b) => {\n const rc = b.reasoningContent as Record<string, unknown>;\n const rt = rc?.reasoningText as Record<string, unknown> | undefined;\n return String(rt?.text ?? \"\");\n })\n .join(\"\")\n : undefined;\n\n if (hasToolCalls) {\n const toolCalls: ToolCall[] = toolUseBlocks.map((b) => {\n const tu = b.toolUse as Record<string, unknown>;\n return {\n name: String(tu.name ?? \"\"),\n arguments: typeof tu.input === \"string\" ? tu.input : JSON.stringify(tu.input),\n ...(tu.toolUseId ? { id: String(tu.toolUseId) } : {}),\n };\n });\n if (hasContent) {\n return {\n content: joinedText,\n toolCalls,\n ...(bedrockReasoning ? { reasoning: bedrockReasoning } : {}),\n };\n }\n return { toolCalls, ...(bedrockReasoning ? { reasoning: bedrockReasoning } : {}) };\n }\n if (hasContent) {\n return {\n content: joinedText,\n ...(bedrockReasoning ? { reasoning: bedrockReasoning } : {}),\n };\n }\n // Recognized Bedrock Converse shape but empty content\n return { content: \"\", ...(bedrockReasoning ? { reasoning: bedrockReasoning } : {}) };\n }\n }\n\n // Cohere v2 chat: { finish_reason: \"...\", message: { content: [{ type: \"text\", text: \"...\" }] } }\n // Must come before Ollama since both have `message`, but Cohere has `finish_reason` at top level\n // (not nested in `choices`) and `message.content` as an array of typed objects.\n if (\n typeof obj.finish_reason === \"string\" &&\n obj.message &&\n typeof obj.message === \"object\" &&\n Array.isArray((obj.message as Record<string, unknown>).content)\n ) {\n const msg = obj.message as Record<string, unknown>;\n const contentBlocks = msg.content as Array<Record<string, unknown>>;\n const textBlock = contentBlocks.find((b) => b.type === \"text\" && typeof b.text === \"string\");\n const hasContent = textBlock && typeof textBlock.text === \"string\" && textBlock.text.length > 0;\n const toolCallBlocks = contentBlocks.filter((b) => b.type === \"tool_call\");\n\n // Also check message-level tool_calls (Cohere v2 puts tool calls here, not in content blocks)\n const msgToolCalls = Array.isArray(msg.tool_calls)\n ? (msg.tool_calls as Array<Record<string, unknown>>)\n : [];\n\n if (toolCallBlocks.length > 0) {\n const toolCalls: ToolCall[] = toolCallBlocks.map((b) => ({\n name: String(b.name ?? (b.function as Record<string, unknown>)?.name ?? \"\"),\n arguments:\n typeof b.parameters === \"string\"\n ? b.parameters\n : typeof b.parameters === \"object\"\n ? JSON.stringify(b.parameters)\n : typeof (b.function as Record<string, unknown>)?.arguments === \"string\"\n ? String((b.function as Record<string, unknown>).arguments)\n : JSON.stringify((b.function as Record<string, unknown>)?.arguments),\n ...(b.id ? { id: String(b.id) } : {}),\n }));\n if (hasContent) {\n return { content: textBlock.text as string, toolCalls };\n }\n return { toolCalls };\n }\n if (msgToolCalls.length > 0) {\n const toolCalls: ToolCall[] = msgToolCalls.map((tc) => {\n const fn = tc.function as Record<string, unknown> | undefined;\n return {\n name: String(tc.name ?? fn?.name ?? \"\"),\n arguments:\n typeof tc.parameters === \"string\"\n ? tc.parameters\n : typeof tc.parameters === \"object\"\n ? JSON.stringify(tc.parameters)\n : typeof fn?.arguments === \"string\"\n ? String(fn.arguments)\n : JSON.stringify(fn?.arguments),\n ...(tc.id ? { id: String(tc.id) } : {}),\n };\n });\n if (hasContent) {\n return { content: textBlock.text as string, toolCalls };\n }\n return { toolCalls };\n }\n if (hasContent) {\n return { content: textBlock.text as string };\n }\n }\n\n // Ollama: { message: { content: \"...\", tool_calls: [...] } }\n if (obj.message && typeof obj.message === \"object\") {\n const msg = obj.message as Record<string, unknown>;\n const hasOllamaToolCalls = Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0;\n const hasOllamaContent = typeof msg.content === \"string\" && msg.content.length > 0;\n\n if (hasOllamaToolCalls) {\n const toolCalls: ToolCall[] = (msg.tool_calls as Array<Record<string, unknown>>)\n .filter((tc) => tc.function != null)\n .map((tc) => {\n const fn = tc.function as Record<string, unknown>;\n return {\n name: String(fn.name ?? \"\"),\n arguments:\n typeof fn.arguments === \"string\" ? fn.arguments : JSON.stringify(fn.arguments),\n };\n });\n if (hasOllamaContent) {\n return { content: msg.content as string, toolCalls };\n }\n return { toolCalls };\n }\n if (hasOllamaContent) {\n return { content: msg.content as string };\n }\n // Ollama message with content array (like Cohere)\n if (Array.isArray(msg.content) && msg.content.length > 0) {\n const first = msg.content[0] as Record<string, unknown>;\n if (typeof first.text === \"string\") {\n return { content: first.text };\n }\n }\n }\n\n // Ollama /api/generate: { response: \"...\", done: true/false }\n if (typeof obj.response === \"string\" && \"done\" in obj) {\n return { content: obj.response };\n }\n\n // Fallback: unknown format — save as error\n return {\n error: {\n message: \"Could not detect response format from upstream\",\n type: \"proxy_error\",\n },\n status,\n };\n}\n\n/**\n * Derive fixture match criteria from the original request.\n */\ntype EndpointType = \"chat\" | \"image\" | \"speech\" | \"transcription\" | \"video\" | \"embedding\";\n\nfunction buildFixtureMatch(request: ChatCompletionRequest): {\n userMessage?: string;\n inputText?: string;\n endpoint?: EndpointType;\n} {\n const match: { userMessage?: string; inputText?: string; endpoint?: EndpointType } = {};\n\n // Include endpoint type for multimedia fixtures\n if (request._endpointType && request._endpointType !== \"chat\") {\n match.endpoint = request._endpointType as EndpointType;\n }\n\n // Embedding request\n if (request.embeddingInput) {\n match.inputText = request.embeddingInput;\n return match;\n }\n\n // Chat/multimedia request — match on the last user message\n const lastUser = getLastMessageByRole(request.messages ?? [], \"user\");\n if (lastUser) {\n const text = getTextContent(lastUser.content);\n if (text) {\n match.userMessage = text;\n }\n }\n\n return match;\n}\n"],"mappings":"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"}
|
package/dist/recorder.js
CHANGED
|
@@ -61,6 +61,7 @@ async function proxyAndRecord(req, res, request, providerKey, pathname, fixtures
|
|
|
61
61
|
let upstreamBody;
|
|
62
62
|
let rawBuffer;
|
|
63
63
|
let streamedToClient = false;
|
|
64
|
+
let clientDisconnected = false;
|
|
64
65
|
try {
|
|
65
66
|
const result = await makeUpstreamRequest(target, forwardHeaders, requestBody, res);
|
|
66
67
|
upstreamStatus = result.status;
|
|
@@ -68,14 +69,17 @@ async function proxyAndRecord(req, res, request, providerKey, pathname, fixtures
|
|
|
68
69
|
upstreamBody = result.body;
|
|
69
70
|
rawBuffer = result.rawBuffer;
|
|
70
71
|
streamedToClient = result.streamedToClient;
|
|
72
|
+
clientDisconnected = result.clientDisconnected;
|
|
71
73
|
} catch (err) {
|
|
72
74
|
const msg = err instanceof Error ? err.message : "Unknown proxy error";
|
|
73
75
|
defaults.logger.error(`Proxy request failed: ${msg}`);
|
|
74
|
-
res.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
if (!res.headersSent) {
|
|
77
|
+
res.writeHead(502, { "Content-Type": "application/json" });
|
|
78
|
+
res.end(JSON.stringify({ error: {
|
|
79
|
+
message: `Proxy to upstream failed: ${msg}`,
|
|
80
|
+
type: "proxy_error"
|
|
81
|
+
} }));
|
|
82
|
+
} else res.end();
|
|
79
83
|
return true;
|
|
80
84
|
}
|
|
81
85
|
const contentType = upstreamHeaders["content-type"];
|
|
@@ -94,10 +98,20 @@ async function proxyAndRecord(req, res, request, providerKey, pathname, fixtures
|
|
|
94
98
|
if (collapsed.truncated) defaults.logger.warn("Bedrock EventStream: CRC mismatch — response may be truncated");
|
|
95
99
|
if (collapsed.droppedChunks && collapsed.droppedChunks > 0) defaults.logger.warn(`${collapsed.droppedChunks} chunk(s) dropped during stream collapse`);
|
|
96
100
|
if (collapsed.content === "" && (!collapsed.toolCalls || collapsed.toolCalls.length === 0)) defaults.logger.warn("Stream collapse produced empty content — fixture may be incomplete");
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
+
const reasoningSpread = collapsed.reasoning ? { reasoning: collapsed.reasoning } : {};
|
|
102
|
+
if (collapsed.toolCalls && collapsed.toolCalls.length > 0) if (collapsed.content) fixtureResponse = {
|
|
103
|
+
content: collapsed.content,
|
|
104
|
+
toolCalls: collapsed.toolCalls,
|
|
105
|
+
...reasoningSpread
|
|
106
|
+
};
|
|
107
|
+
else fixtureResponse = {
|
|
108
|
+
toolCalls: collapsed.toolCalls,
|
|
109
|
+
...reasoningSpread
|
|
110
|
+
};
|
|
111
|
+
else fixtureResponse = {
|
|
112
|
+
content: collapsed.content ?? "",
|
|
113
|
+
...reasoningSpread
|
|
114
|
+
};
|
|
101
115
|
} else {
|
|
102
116
|
let parsedResponse = null;
|
|
103
117
|
try {
|
|
@@ -108,9 +122,15 @@ async function proxyAndRecord(req, res, request, providerKey, pathname, fixtures
|
|
|
108
122
|
let encodingFormat;
|
|
109
123
|
try {
|
|
110
124
|
encodingFormat = rawBody ? JSON.parse(rawBody).encoding_format : void 0;
|
|
111
|
-
} catch {
|
|
125
|
+
} catch (err) {
|
|
126
|
+
defaults.logger.debug(`Could not parse encoding_format from raw body: ${err instanceof Error ? err.message : "unknown error"}`);
|
|
127
|
+
}
|
|
112
128
|
fixtureResponse = buildFixtureResponse(parsedResponse, upstreamStatus, encodingFormat);
|
|
113
129
|
}
|
|
130
|
+
if (clientDisconnected) {
|
|
131
|
+
defaults.logger.warn("Client disconnected mid-stream — skipping fixture save to avoid truncated data");
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
114
134
|
const fixtureMatch = buildFixtureMatch(defaults.requestTransform ? defaults.requestTransform(request) : request);
|
|
115
135
|
const fixture = {
|
|
116
136
|
match: fixtureMatch,
|
|
@@ -135,7 +155,8 @@ async function proxyAndRecord(req, res, request, providerKey, pathname, fixtures
|
|
|
135
155
|
} catch (err) {
|
|
136
156
|
const msg = err instanceof Error ? err.message : "Unknown filesystem error";
|
|
137
157
|
defaults.logger.error(`Failed to save fixture to disk: ${msg}`);
|
|
138
|
-
res.setHeader("X-LLMock-Record-Error", msg);
|
|
158
|
+
if (!res.headersSent) res.setHeader("X-LLMock-Record-Error", msg);
|
|
159
|
+
else defaults.logger.warn(`Cannot set X-LLMock-Record-Error header — headers already sent`);
|
|
139
160
|
}
|
|
140
161
|
if (writtenToDisk) {
|
|
141
162
|
if (!isEmptyMatch) fixtures.push(fixture);
|
|
@@ -146,7 +167,8 @@ async function proxyAndRecord(req, res, request, providerKey, pathname, fixtures
|
|
|
146
167
|
const relayHeaders = {};
|
|
147
168
|
if (ctString) relayHeaders["Content-Type"] = ctString;
|
|
148
169
|
res.writeHead(upstreamStatus, relayHeaders);
|
|
149
|
-
|
|
170
|
+
const isAudioRelay = ctString.toLowerCase().startsWith("audio/");
|
|
171
|
+
res.end(isBinaryStream || isAudioRelay ? rawBuffer : upstreamBody);
|
|
150
172
|
}
|
|
151
173
|
return true;
|
|
152
174
|
}
|
|
@@ -170,28 +192,34 @@ function makeUpstreamRequest(target, headers, body, clientRes) {
|
|
|
170
192
|
const ctStr = Array.isArray(ct) ? ct.join(", ") : ct ?? "";
|
|
171
193
|
const isSSE = ctStr.toLowerCase().includes("text/event-stream");
|
|
172
194
|
let streamedToClient = false;
|
|
195
|
+
let clientDisconnected = false;
|
|
173
196
|
if (isSSE && clientRes && !clientRes.headersSent) {
|
|
174
197
|
const relayHeaders = {};
|
|
175
198
|
if (ctStr) relayHeaders["Content-Type"] = ctStr;
|
|
176
199
|
clientRes.writeHead(res.statusCode ?? 200, relayHeaders);
|
|
177
200
|
if (typeof clientRes.flushHeaders === "function") clientRes.flushHeaders();
|
|
178
201
|
streamedToClient = true;
|
|
202
|
+
clientRes.on("close", () => {
|
|
203
|
+
clientDisconnected = true;
|
|
204
|
+
req.destroy();
|
|
205
|
+
});
|
|
179
206
|
}
|
|
180
207
|
const chunks = [];
|
|
181
208
|
res.on("data", (chunk) => {
|
|
182
209
|
chunks.push(chunk);
|
|
183
|
-
if (streamedToClient) clientRes.write(chunk);
|
|
210
|
+
if (streamedToClient && clientRes && !clientDisconnected && !clientRes.destroyed && !clientRes.writableEnded) clientRes.write(chunk);
|
|
184
211
|
});
|
|
185
212
|
res.on("error", reject);
|
|
186
213
|
res.on("end", () => {
|
|
187
214
|
const rawBuffer = Buffer.concat(chunks);
|
|
188
|
-
if (streamedToClient) clientRes.end();
|
|
215
|
+
if (streamedToClient && clientRes && !clientDisconnected && !clientRes.destroyed && !clientRes.writableEnded) clientRes.end();
|
|
189
216
|
resolve({
|
|
190
217
|
status: res.statusCode ?? 500,
|
|
191
218
|
headers: res.headers,
|
|
192
219
|
body: rawBuffer.toString(),
|
|
193
220
|
rawBuffer,
|
|
194
|
-
streamedToClient
|
|
221
|
+
streamedToClient,
|
|
222
|
+
clientDisconnected
|
|
195
223
|
});
|
|
196
224
|
});
|
|
197
225
|
});
|
|
@@ -216,7 +244,7 @@ function buildFixtureResponse(parsed, status, encodingFormat) {
|
|
|
216
244
|
status
|
|
217
245
|
};
|
|
218
246
|
const obj = parsed;
|
|
219
|
-
if (obj.error) {
|
|
247
|
+
if (typeof obj.error === "object" && obj.error !== null && typeof obj.error.message === "string") {
|
|
220
248
|
const err = obj.error;
|
|
221
249
|
return {
|
|
222
250
|
error: {
|
|
@@ -230,11 +258,12 @@ function buildFixtureResponse(parsed, status, encodingFormat) {
|
|
|
230
258
|
if (Array.isArray(obj.data) && obj.data.length > 0) {
|
|
231
259
|
const first = obj.data[0];
|
|
232
260
|
if (Array.isArray(first.embedding)) return { embedding: first.embedding };
|
|
233
|
-
if (typeof first.embedding === "string" && encodingFormat === "base64")
|
|
261
|
+
if (typeof first.embedding === "string" && encodingFormat === "base64") {
|
|
234
262
|
const buf = Buffer.from(first.embedding, "base64");
|
|
235
|
-
const
|
|
263
|
+
const aligned = new Uint8Array(buf).buffer;
|
|
264
|
+
const floats = new Float32Array(aligned, 0, buf.byteLength / 4);
|
|
236
265
|
return { embedding: Array.from(floats) };
|
|
237
|
-
}
|
|
266
|
+
}
|
|
238
267
|
if (first.url || first.b64_json) {
|
|
239
268
|
const images = obj.data.map((item) => ({
|
|
240
269
|
...item.url ? { url: String(item.url) } : {},
|
|
@@ -260,7 +289,7 @@ function buildFixtureResponse(parsed, status, encodingFormat) {
|
|
|
260
289
|
...Array.isArray(obj.words) ? { words: obj.words } : {},
|
|
261
290
|
...Array.isArray(obj.segments) ? { segments: obj.segments } : {}
|
|
262
291
|
} };
|
|
263
|
-
if (typeof obj.id === "string" && typeof obj.status === "string" && (obj.status === "completed" || obj.status === "in_progress" || obj.status === "failed")) {
|
|
292
|
+
if (typeof obj.id === "string" && typeof obj.status === "string" && (obj.status === "completed" || obj.status === "in_progress" || obj.status === "failed") && !("choices" in obj) && !("content" in obj) && !("candidates" in obj) && !("message" in obj) && !("data" in obj) && !("object" in obj)) {
|
|
264
293
|
if (obj.status === "completed" && obj.url) return { video: {
|
|
265
294
|
id: String(obj.id),
|
|
266
295
|
status: "completed",
|
|
@@ -275,40 +304,109 @@ function buildFixtureResponse(parsed, status, encodingFormat) {
|
|
|
275
304
|
if (Array.isArray(obj.choices) && obj.choices.length > 0) {
|
|
276
305
|
const message = obj.choices[0].message;
|
|
277
306
|
if (message) {
|
|
278
|
-
|
|
279
|
-
|
|
307
|
+
const hasToolCalls = Array.isArray(message.tool_calls) && message.tool_calls.length > 0;
|
|
308
|
+
const hasContent = typeof message.content === "string" && message.content.length > 0;
|
|
309
|
+
const openaiReasoning = typeof message.reasoning_content === "string" && message.reasoning_content.length > 0 ? message.reasoning_content : void 0;
|
|
310
|
+
if (hasToolCalls) {
|
|
311
|
+
const toolCalls = message.tool_calls.map((tc) => {
|
|
312
|
+
const fn = tc.function;
|
|
313
|
+
return {
|
|
314
|
+
name: String(fn.name),
|
|
315
|
+
arguments: String(fn.arguments),
|
|
316
|
+
...tc.id ? { id: String(tc.id) } : {}
|
|
317
|
+
};
|
|
318
|
+
});
|
|
319
|
+
if (hasContent) return {
|
|
320
|
+
content: message.content,
|
|
321
|
+
toolCalls,
|
|
322
|
+
...openaiReasoning ? { reasoning: openaiReasoning } : {}
|
|
323
|
+
};
|
|
280
324
|
return {
|
|
281
|
-
|
|
282
|
-
|
|
325
|
+
toolCalls,
|
|
326
|
+
...openaiReasoning ? { reasoning: openaiReasoning } : {}
|
|
283
327
|
};
|
|
284
|
-
}
|
|
285
|
-
if (
|
|
328
|
+
}
|
|
329
|
+
if (hasContent) return {
|
|
330
|
+
content: message.content,
|
|
331
|
+
...openaiReasoning ? { reasoning: openaiReasoning } : {}
|
|
332
|
+
};
|
|
333
|
+
return {
|
|
334
|
+
content: "",
|
|
335
|
+
...openaiReasoning ? { reasoning: openaiReasoning } : {}
|
|
336
|
+
};
|
|
286
337
|
}
|
|
287
338
|
}
|
|
288
339
|
if (Array.isArray(obj.content) && obj.content.length > 0) {
|
|
289
340
|
const blocks = obj.content;
|
|
290
341
|
const toolUseBlocks = blocks.filter((b) => b.type === "tool_use");
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
const
|
|
296
|
-
|
|
342
|
+
const textBlocks = blocks.filter((b) => b.type === "text" && typeof b.text === "string");
|
|
343
|
+
const thinkingBlocks = blocks.filter((b) => b.type === "thinking");
|
|
344
|
+
const hasToolCalls = toolUseBlocks.length > 0;
|
|
345
|
+
const joinedText = textBlocks.map((b) => String(b.text ?? "")).join("");
|
|
346
|
+
const hasContent = joinedText.length > 0;
|
|
347
|
+
const anthropicReasoning = thinkingBlocks.length > 0 ? thinkingBlocks.map((b) => String(b.thinking ?? "")).join("") : void 0;
|
|
348
|
+
if (hasToolCalls) {
|
|
349
|
+
const toolCalls = toolUseBlocks.map((b) => ({
|
|
350
|
+
name: String(b.name),
|
|
351
|
+
arguments: typeof b.input === "string" ? b.input : JSON.stringify(b.input),
|
|
352
|
+
...b.id ? { id: String(b.id) } : {}
|
|
353
|
+
}));
|
|
354
|
+
if (hasContent) return {
|
|
355
|
+
content: joinedText,
|
|
356
|
+
toolCalls,
|
|
357
|
+
...anthropicReasoning ? { reasoning: anthropicReasoning } : {}
|
|
358
|
+
};
|
|
359
|
+
return {
|
|
360
|
+
toolCalls,
|
|
361
|
+
...anthropicReasoning ? { reasoning: anthropicReasoning } : {}
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
if (hasContent) return {
|
|
365
|
+
content: joinedText,
|
|
366
|
+
...anthropicReasoning ? { reasoning: anthropicReasoning } : {}
|
|
367
|
+
};
|
|
368
|
+
if (anthropicReasoning) return {
|
|
369
|
+
content: "",
|
|
370
|
+
reasoning: anthropicReasoning
|
|
371
|
+
};
|
|
297
372
|
}
|
|
298
373
|
if (Array.isArray(obj.candidates) && obj.candidates.length > 0) {
|
|
299
374
|
const content = obj.candidates[0].content;
|
|
300
375
|
if (content && Array.isArray(content.parts)) {
|
|
301
376
|
const parts = content.parts;
|
|
302
377
|
const fnCallParts = parts.filter((p) => p.functionCall);
|
|
303
|
-
|
|
304
|
-
|
|
378
|
+
const textParts = parts.filter((p) => typeof p.text === "string" && !p.thought);
|
|
379
|
+
const thoughtParts = parts.filter((p) => p.thought === true && typeof p.text === "string");
|
|
380
|
+
const hasToolCalls = fnCallParts.length > 0;
|
|
381
|
+
const joinedText = textParts.map((p) => String(p.text ?? "")).join("");
|
|
382
|
+
const hasContent = joinedText.length > 0;
|
|
383
|
+
const geminiReasoning = thoughtParts.length > 0 ? thoughtParts.map((p) => String(p.text ?? "")).join("") : void 0;
|
|
384
|
+
if (hasToolCalls) {
|
|
385
|
+
const toolCalls = fnCallParts.map((p) => {
|
|
386
|
+
const fc = p.functionCall;
|
|
387
|
+
return {
|
|
388
|
+
name: String(fc.name),
|
|
389
|
+
arguments: typeof fc.args === "string" ? fc.args : JSON.stringify(fc.args)
|
|
390
|
+
};
|
|
391
|
+
});
|
|
392
|
+
if (hasContent) return {
|
|
393
|
+
content: joinedText,
|
|
394
|
+
toolCalls,
|
|
395
|
+
...geminiReasoning ? { reasoning: geminiReasoning } : {}
|
|
396
|
+
};
|
|
305
397
|
return {
|
|
306
|
-
|
|
307
|
-
|
|
398
|
+
toolCalls,
|
|
399
|
+
...geminiReasoning ? { reasoning: geminiReasoning } : {}
|
|
308
400
|
};
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
|
|
401
|
+
}
|
|
402
|
+
if (hasContent) return {
|
|
403
|
+
content: joinedText,
|
|
404
|
+
...geminiReasoning ? { reasoning: geminiReasoning } : {}
|
|
405
|
+
};
|
|
406
|
+
return {
|
|
407
|
+
content: "",
|
|
408
|
+
...geminiReasoning ? { reasoning: geminiReasoning } : {}
|
|
409
|
+
};
|
|
312
410
|
}
|
|
313
411
|
}
|
|
314
412
|
if (obj.output && typeof obj.output === "object") {
|
|
@@ -316,32 +414,105 @@ function buildFixtureResponse(parsed, status, encodingFormat) {
|
|
|
316
414
|
if (msg && Array.isArray(msg.content)) {
|
|
317
415
|
const blocks = msg.content;
|
|
318
416
|
const toolUseBlocks = blocks.filter((b) => b.toolUse);
|
|
319
|
-
|
|
320
|
-
|
|
417
|
+
const textBlocks = blocks.filter((b) => typeof b.text === "string");
|
|
418
|
+
const reasoningBlocks = blocks.filter((b) => b.reasoningContent);
|
|
419
|
+
const hasToolCalls = toolUseBlocks.length > 0;
|
|
420
|
+
const joinedText = textBlocks.map((b) => String(b.text ?? "")).join("");
|
|
421
|
+
const hasContent = joinedText.length > 0;
|
|
422
|
+
const bedrockReasoning = reasoningBlocks.length > 0 ? reasoningBlocks.map((b) => {
|
|
423
|
+
const rt = b.reasoningContent?.reasoningText;
|
|
424
|
+
return String(rt?.text ?? "");
|
|
425
|
+
}).join("") : void 0;
|
|
426
|
+
if (hasToolCalls) {
|
|
427
|
+
const toolCalls = toolUseBlocks.map((b) => {
|
|
428
|
+
const tu = b.toolUse;
|
|
429
|
+
return {
|
|
430
|
+
name: String(tu.name ?? ""),
|
|
431
|
+
arguments: typeof tu.input === "string" ? tu.input : JSON.stringify(tu.input),
|
|
432
|
+
...tu.toolUseId ? { id: String(tu.toolUseId) } : {}
|
|
433
|
+
};
|
|
434
|
+
});
|
|
435
|
+
if (hasContent) return {
|
|
436
|
+
content: joinedText,
|
|
437
|
+
toolCalls,
|
|
438
|
+
...bedrockReasoning ? { reasoning: bedrockReasoning } : {}
|
|
439
|
+
};
|
|
321
440
|
return {
|
|
322
|
-
|
|
323
|
-
|
|
441
|
+
toolCalls,
|
|
442
|
+
...bedrockReasoning ? { reasoning: bedrockReasoning } : {}
|
|
324
443
|
};
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
|
|
444
|
+
}
|
|
445
|
+
if (hasContent) return {
|
|
446
|
+
content: joinedText,
|
|
447
|
+
...bedrockReasoning ? { reasoning: bedrockReasoning } : {}
|
|
448
|
+
};
|
|
449
|
+
return {
|
|
450
|
+
content: "",
|
|
451
|
+
...bedrockReasoning ? { reasoning: bedrockReasoning } : {}
|
|
452
|
+
};
|
|
328
453
|
}
|
|
329
454
|
}
|
|
455
|
+
if (typeof obj.finish_reason === "string" && obj.message && typeof obj.message === "object" && Array.isArray(obj.message.content)) {
|
|
456
|
+
const msg = obj.message;
|
|
457
|
+
const contentBlocks = msg.content;
|
|
458
|
+
const textBlock = contentBlocks.find((b) => b.type === "text" && typeof b.text === "string");
|
|
459
|
+
const hasContent = textBlock && typeof textBlock.text === "string" && textBlock.text.length > 0;
|
|
460
|
+
const toolCallBlocks = contentBlocks.filter((b) => b.type === "tool_call");
|
|
461
|
+
const msgToolCalls = Array.isArray(msg.tool_calls) ? msg.tool_calls : [];
|
|
462
|
+
if (toolCallBlocks.length > 0) {
|
|
463
|
+
const toolCalls = toolCallBlocks.map((b) => ({
|
|
464
|
+
name: String(b.name ?? b.function?.name ?? ""),
|
|
465
|
+
arguments: typeof b.parameters === "string" ? b.parameters : typeof b.parameters === "object" ? JSON.stringify(b.parameters) : typeof b.function?.arguments === "string" ? String(b.function.arguments) : JSON.stringify(b.function?.arguments),
|
|
466
|
+
...b.id ? { id: String(b.id) } : {}
|
|
467
|
+
}));
|
|
468
|
+
if (hasContent) return {
|
|
469
|
+
content: textBlock.text,
|
|
470
|
+
toolCalls
|
|
471
|
+
};
|
|
472
|
+
return { toolCalls };
|
|
473
|
+
}
|
|
474
|
+
if (msgToolCalls.length > 0) {
|
|
475
|
+
const toolCalls = msgToolCalls.map((tc) => {
|
|
476
|
+
const fn = tc.function;
|
|
477
|
+
return {
|
|
478
|
+
name: String(tc.name ?? fn?.name ?? ""),
|
|
479
|
+
arguments: typeof tc.parameters === "string" ? tc.parameters : typeof tc.parameters === "object" ? JSON.stringify(tc.parameters) : typeof fn?.arguments === "string" ? String(fn.arguments) : JSON.stringify(fn?.arguments),
|
|
480
|
+
...tc.id ? { id: String(tc.id) } : {}
|
|
481
|
+
};
|
|
482
|
+
});
|
|
483
|
+
if (hasContent) return {
|
|
484
|
+
content: textBlock.text,
|
|
485
|
+
toolCalls
|
|
486
|
+
};
|
|
487
|
+
return { toolCalls };
|
|
488
|
+
}
|
|
489
|
+
if (hasContent) return { content: textBlock.text };
|
|
490
|
+
}
|
|
330
491
|
if (obj.message && typeof obj.message === "object") {
|
|
331
492
|
const msg = obj.message;
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
493
|
+
const hasOllamaToolCalls = Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0;
|
|
494
|
+
const hasOllamaContent = typeof msg.content === "string" && msg.content.length > 0;
|
|
495
|
+
if (hasOllamaToolCalls) {
|
|
496
|
+
const toolCalls = msg.tool_calls.filter((tc) => tc.function != null).map((tc) => {
|
|
497
|
+
const fn = tc.function;
|
|
498
|
+
return {
|
|
499
|
+
name: String(fn.name ?? ""),
|
|
500
|
+
arguments: typeof fn.arguments === "string" ? fn.arguments : JSON.stringify(fn.arguments)
|
|
501
|
+
};
|
|
502
|
+
});
|
|
503
|
+
if (hasOllamaContent) return {
|
|
504
|
+
content: msg.content,
|
|
505
|
+
toolCalls
|
|
337
506
|
};
|
|
338
|
-
|
|
339
|
-
|
|
507
|
+
return { toolCalls };
|
|
508
|
+
}
|
|
509
|
+
if (hasOllamaContent) return { content: msg.content };
|
|
340
510
|
if (Array.isArray(msg.content) && msg.content.length > 0) {
|
|
341
511
|
const first = msg.content[0];
|
|
342
512
|
if (typeof first.text === "string") return { content: first.text };
|
|
343
513
|
}
|
|
344
514
|
}
|
|
515
|
+
if (typeof obj.response === "string" && "done" in obj) return { content: obj.response };
|
|
345
516
|
return {
|
|
346
517
|
error: {
|
|
347
518
|
message: "Could not detect response format from upstream",
|