@copilotkit/aimock 1.14.3 → 1.14.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/CHANGELOG.md +376 -0
  2. package/README.md +24 -14
  3. package/dist/_virtual/_rolldown/runtime.cjs +0 -2
  4. package/dist/config-loader.d.cts.map +1 -1
  5. package/dist/config-loader.d.ts.map +1 -1
  6. package/dist/recorder.cjs +28 -8
  7. package/dist/recorder.cjs.map +1 -1
  8. package/dist/recorder.js +28 -8
  9. package/dist/recorder.js.map +1 -1
  10. package/dist/vector-types.d.ts.map +1 -1
  11. package/dist/vitest.cjs +4 -4
  12. package/dist/vitest.cjs.map +1 -1
  13. package/dist/vitest.js +1 -1
  14. package/package.json +31 -12
  15. package/dist/_virtual/_rolldown/runtime.js +0 -29
  16. package/dist/node_modules/.pnpm/@vitest_pretty-format@3.2.4/node_modules/@vitest/pretty-format/dist/index.cjs +0 -934
  17. package/dist/node_modules/.pnpm/@vitest_pretty-format@3.2.4/node_modules/@vitest/pretty-format/dist/index.cjs.map +0 -1
  18. package/dist/node_modules/.pnpm/@vitest_pretty-format@3.2.4/node_modules/@vitest/pretty-format/dist/index.js +0 -934
  19. package/dist/node_modules/.pnpm/@vitest_pretty-format@3.2.4/node_modules/@vitest/pretty-format/dist/index.js.map +0 -1
  20. package/dist/node_modules/.pnpm/@vitest_runner@3.2.4/node_modules/@vitest/runner/dist/chunk-hooks.cjs +0 -1051
  21. package/dist/node_modules/.pnpm/@vitest_runner@3.2.4/node_modules/@vitest/runner/dist/chunk-hooks.cjs.map +0 -1
  22. package/dist/node_modules/.pnpm/@vitest_runner@3.2.4/node_modules/@vitest/runner/dist/chunk-hooks.js +0 -1042
  23. package/dist/node_modules/.pnpm/@vitest_runner@3.2.4/node_modules/@vitest/runner/dist/chunk-hooks.js.map +0 -1
  24. package/dist/node_modules/.pnpm/@vitest_runner@3.2.4/node_modules/@vitest/runner/dist/index.cjs +0 -1
  25. package/dist/node_modules/.pnpm/@vitest_runner@3.2.4/node_modules/@vitest/runner/dist/index.js +0 -3
  26. package/dist/node_modules/.pnpm/@vitest_utils@3.2.4/node_modules/@vitest/utils/dist/chunk-_commonjsHelpers.cjs +0 -96
  27. package/dist/node_modules/.pnpm/@vitest_utils@3.2.4/node_modules/@vitest/utils/dist/chunk-_commonjsHelpers.cjs.map +0 -1
  28. package/dist/node_modules/.pnpm/@vitest_utils@3.2.4/node_modules/@vitest/utils/dist/chunk-_commonjsHelpers.js +0 -93
  29. package/dist/node_modules/.pnpm/@vitest_utils@3.2.4/node_modules/@vitest/utils/dist/chunk-_commonjsHelpers.js.map +0 -1
  30. package/dist/node_modules/.pnpm/@vitest_utils@3.2.4/node_modules/@vitest/utils/dist/helpers.cjs +0 -49
  31. package/dist/node_modules/.pnpm/@vitest_utils@3.2.4/node_modules/@vitest/utils/dist/helpers.cjs.map +0 -1
  32. package/dist/node_modules/.pnpm/@vitest_utils@3.2.4/node_modules/@vitest/utils/dist/helpers.js +0 -43
  33. package/dist/node_modules/.pnpm/@vitest_utils@3.2.4/node_modules/@vitest/utils/dist/helpers.js.map +0 -1
  34. package/dist/node_modules/.pnpm/@vitest_utils@3.2.4/node_modules/@vitest/utils/dist/index.cjs +0 -456
  35. package/dist/node_modules/.pnpm/@vitest_utils@3.2.4/node_modules/@vitest/utils/dist/index.cjs.map +0 -1
  36. package/dist/node_modules/.pnpm/@vitest_utils@3.2.4/node_modules/@vitest/utils/dist/index.js +0 -456
  37. package/dist/node_modules/.pnpm/@vitest_utils@3.2.4/node_modules/@vitest/utils/dist/index.js.map +0 -1
  38. package/dist/node_modules/.pnpm/@vitest_utils@3.2.4/node_modules/@vitest/utils/dist/source-map.cjs +0 -170
  39. package/dist/node_modules/.pnpm/@vitest_utils@3.2.4/node_modules/@vitest/utils/dist/source-map.cjs.map +0 -1
  40. package/dist/node_modules/.pnpm/@vitest_utils@3.2.4/node_modules/@vitest/utils/dist/source-map.js +0 -169
  41. package/dist/node_modules/.pnpm/@vitest_utils@3.2.4/node_modules/@vitest/utils/dist/source-map.js.map +0 -1
  42. package/dist/node_modules/.pnpm/js-tokens@9.0.1/node_modules/js-tokens/index.cjs +0 -388
  43. package/dist/node_modules/.pnpm/js-tokens@9.0.1/node_modules/js-tokens/index.cjs.map +0 -1
  44. package/dist/node_modules/.pnpm/js-tokens@9.0.1/node_modules/js-tokens/index.js +0 -385
  45. package/dist/node_modules/.pnpm/js-tokens@9.0.1/node_modules/js-tokens/index.js.map +0 -1
  46. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/arguments.cjs +0 -12
  47. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/arguments.cjs.map +0 -1
  48. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/arguments.js +0 -12
  49. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/arguments.js.map +0 -1
  50. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/array.cjs +0 -17
  51. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/array.cjs.map +0 -1
  52. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/array.js +0 -17
  53. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/array.js.map +0 -1
  54. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/bigint.cjs +0 -12
  55. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/bigint.cjs.map +0 -1
  56. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/bigint.js +0 -12
  57. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/bigint.js.map +0 -1
  58. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/class.cjs +0 -16
  59. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/class.cjs.map +0 -1
  60. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/class.js +0 -16
  61. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/class.js.map +0 -1
  62. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/date.cjs +0 -14
  63. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/date.cjs.map +0 -1
  64. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/date.js +0 -14
  65. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/date.js.map +0 -1
  66. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/error.cjs +0 -35
  67. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/error.cjs.map +0 -1
  68. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/error.js +0 -35
  69. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/error.js.map +0 -1
  70. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/function.cjs +0 -13
  71. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/function.cjs.map +0 -1
  72. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/function.js +0 -13
  73. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/function.js.map +0 -1
  74. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/helpers.cjs +0 -128
  75. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/helpers.cjs.map +0 -1
  76. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/helpers.js +0 -123
  77. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/helpers.js.map +0 -1
  78. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/html.cjs +0 -41
  79. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/html.cjs.map +0 -1
  80. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/html.js +0 -40
  81. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/html.js.map +0 -1
  82. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/index.cjs +0 -100
  83. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/index.cjs.map +0 -1
  84. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/index.js +0 -100
  85. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/index.js.map +0 -1
  86. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/map.cjs +0 -26
  87. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/map.cjs.map +0 -1
  88. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/map.js +0 -26
  89. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/map.js.map +0 -1
  90. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/number.cjs +0 -15
  91. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/number.cjs.map +0 -1
  92. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/number.js +0 -15
  93. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/number.js.map +0 -1
  94. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/object.cjs +0 -22
  95. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/object.cjs.map +0 -1
  96. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/object.js +0 -22
  97. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/object.js.map +0 -1
  98. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/promise.cjs +0 -7
  99. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/promise.cjs.map +0 -1
  100. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/promise.js +0 -6
  101. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/promise.js.map +0 -1
  102. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/regexp.cjs +0 -13
  103. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/regexp.cjs.map +0 -1
  104. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/regexp.js +0 -13
  105. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/regexp.js.map +0 -1
  106. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/set.cjs +0 -19
  107. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/set.cjs.map +0 -1
  108. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/set.js +0 -19
  109. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/set.js.map +0 -1
  110. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/string.cjs +0 -26
  111. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/string.cjs.map +0 -1
  112. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/string.js +0 -26
  113. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/string.js.map +0 -1
  114. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/symbol.cjs +0 -10
  115. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/symbol.cjs.map +0 -1
  116. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/symbol.js +0 -9
  117. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/symbol.js.map +0 -1
  118. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/typedarray.cjs +0 -31
  119. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/typedarray.cjs.map +0 -1
  120. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/typedarray.js +0 -31
  121. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/typedarray.js.map +0 -1
  122. package/dist/node_modules/.pnpm/strip-literal@3.1.0/node_modules/strip-literal/dist/index.cjs +0 -52
  123. package/dist/node_modules/.pnpm/strip-literal@3.1.0/node_modules/strip-literal/dist/index.cjs.map +0 -1
  124. package/dist/node_modules/.pnpm/strip-literal@3.1.0/node_modules/strip-literal/dist/index.js +0 -52
  125. package/dist/node_modules/.pnpm/strip-literal@3.1.0/node_modules/strip-literal/dist/index.js.map +0 -1
  126. package/dist/node_modules/.pnpm/tinyrainbow@2.0.0/node_modules/tinyrainbow/dist/chunk-BVHSVHOK.cjs +0 -83
  127. package/dist/node_modules/.pnpm/tinyrainbow@2.0.0/node_modules/tinyrainbow/dist/chunk-BVHSVHOK.cjs.map +0 -1
  128. package/dist/node_modules/.pnpm/tinyrainbow@2.0.0/node_modules/tinyrainbow/dist/chunk-BVHSVHOK.js +0 -82
  129. package/dist/node_modules/.pnpm/tinyrainbow@2.0.0/node_modules/tinyrainbow/dist/chunk-BVHSVHOK.js.map +0 -1
  130. package/dist/node_modules/.pnpm/tinyrainbow@2.0.0/node_modules/tinyrainbow/dist/node.cjs +0 -10
  131. package/dist/node_modules/.pnpm/tinyrainbow@2.0.0/node_modules/tinyrainbow/dist/node.cjs.map +0 -1
  132. package/dist/node_modules/.pnpm/tinyrainbow@2.0.0/node_modules/tinyrainbow/dist/node.js +0 -10
  133. package/dist/node_modules/.pnpm/tinyrainbow@2.0.0/node_modules/tinyrainbow/dist/node.js.map +0 -1
@@ -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 try {\n const result = await makeUpstreamRequest(target, forwardHeaders, requestBody);\n upstreamStatus = result.status;\n upstreamHeaders = result.headers;\n upstreamBody = result.body;\n rawBuffer = result.rawBuffer;\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\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 return true;\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nfunction makeUpstreamRequest(\n target: URL,\n headers: Record<string, string>,\n body: string,\n): Promise<{ status: number; headers: http.IncomingHttpHeaders; body: string; rawBuffer: Buffer }> {\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 const chunks: Buffer[] = [];\n res.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n res.on(\"error\", reject);\n res.on(\"end\", () => {\n const rawBuffer = Buffer.concat(chunks);\n resolve({\n status: res.statusCode ?? 500,\n headers: res.headers,\n body: rawBuffer.toString(),\n rawBuffer,\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 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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"}
package/dist/recorder.js CHANGED
@@ -60,12 +60,14 @@ async function proxyAndRecord(req, res, request, providerKey, pathname, fixtures
60
60
  let upstreamHeaders;
61
61
  let upstreamBody;
62
62
  let rawBuffer;
63
+ let streamedToClient = false;
63
64
  try {
64
- const result = await makeUpstreamRequest(target, forwardHeaders, requestBody);
65
+ const result = await makeUpstreamRequest(target, forwardHeaders, requestBody, res);
65
66
  upstreamStatus = result.status;
66
67
  upstreamHeaders = result.headers;
67
68
  upstreamBody = result.body;
68
69
  rawBuffer = result.rawBuffer;
70
+ streamedToClient = result.streamedToClient;
69
71
  } catch (err) {
70
72
  const msg = err instanceof Error ? err.message : "Unknown proxy error";
71
73
  defaults.logger.error(`Proxy request failed: ${msg}`);
@@ -140,13 +142,15 @@ async function proxyAndRecord(req, res, request, providerKey, pathname, fixtures
140
142
  defaults.logger.warn(`Response recorded → ${filepath}`);
141
143
  } else defaults.logger.warn(`Response relayed but NOT saved to disk — see error above`);
142
144
  } else defaults.logger.info(`Proxied ${providerKey} request (proxy-only mode)`);
143
- const relayHeaders = {};
144
- if (ctString) relayHeaders["Content-Type"] = ctString;
145
- res.writeHead(upstreamStatus, relayHeaders);
146
- res.end(isBinaryStream ? rawBuffer : upstreamBody);
145
+ if (!streamedToClient) {
146
+ const relayHeaders = {};
147
+ if (ctString) relayHeaders["Content-Type"] = ctString;
148
+ res.writeHead(upstreamStatus, relayHeaders);
149
+ res.end(isBinaryStream ? rawBuffer : upstreamBody);
150
+ }
147
151
  return true;
148
152
  }
149
- function makeUpstreamRequest(target, headers, body) {
153
+ function makeUpstreamRequest(target, headers, body, clientRes) {
150
154
  return new Promise((resolve, reject) => {
151
155
  const transport = target.protocol === "https:" ? https : http;
152
156
  const UPSTREAM_TIMEOUT_MS = 3e4;
@@ -162,16 +166,32 @@ function makeUpstreamRequest(target, headers, body) {
162
166
  res.setTimeout(BODY_TIMEOUT_MS, () => {
163
167
  req.destroy(/* @__PURE__ */ new Error(`Upstream response timed out after ${BODY_TIMEOUT_MS / 1e3}s`));
164
168
  });
169
+ const ct = res.headers["content-type"];
170
+ const ctStr = Array.isArray(ct) ? ct.join(", ") : ct ?? "";
171
+ const isSSE = ctStr.toLowerCase().includes("text/event-stream");
172
+ let streamedToClient = false;
173
+ if (isSSE && clientRes && !clientRes.headersSent) {
174
+ const relayHeaders = {};
175
+ if (ctStr) relayHeaders["Content-Type"] = ctStr;
176
+ clientRes.writeHead(res.statusCode ?? 200, relayHeaders);
177
+ if (typeof clientRes.flushHeaders === "function") clientRes.flushHeaders();
178
+ streamedToClient = true;
179
+ }
165
180
  const chunks = [];
166
- res.on("data", (chunk) => chunks.push(chunk));
181
+ res.on("data", (chunk) => {
182
+ chunks.push(chunk);
183
+ if (streamedToClient) clientRes.write(chunk);
184
+ });
167
185
  res.on("error", reject);
168
186
  res.on("end", () => {
169
187
  const rawBuffer = Buffer.concat(chunks);
188
+ if (streamedToClient) clientRes.end();
170
189
  resolve({
171
190
  status: res.statusCode ?? 500,
172
191
  headers: res.headers,
173
192
  body: rawBuffer.toString(),
174
- rawBuffer
193
+ rawBuffer,
194
+ streamedToClient
175
195
  });
176
196
  });
177
197
  });
@@ -1 +1 @@
1
- {"version":3,"file":"recorder.js","names":[],"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 try {\n const result = await makeUpstreamRequest(target, forwardHeaders, requestBody);\n upstreamStatus = result.status;\n upstreamHeaders = result.headers;\n upstreamBody = result.body;\n rawBuffer = result.rawBuffer;\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\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 return true;\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nfunction makeUpstreamRequest(\n target: URL,\n headers: Record<string, string>,\n body: string,\n): Promise<{ status: number; headers: http.IncomingHttpHeaders; body: string; rawBuffer: Buffer }> {\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 const chunks: Buffer[] = [];\n res.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n res.on(\"error\", reject);\n res.on(\"end\", () => {\n const rawBuffer = Buffer.concat(chunks);\n resolve({\n status: res.statusCode ?? 500,\n headers: res.headers,\n body: rawBuffer.toString(),\n rawBuffer,\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.js","names":[],"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 +1 @@
1
- {"version":3,"file":"vector-types.d.ts","names":[],"sources":["../src/vector-types.ts"],"sourcesContent":[],"mappings":";UAAiB,iBAAA;EAAA,IAAA,CAAA,EAAA,MAAA;EAKA,IAAA,CAAA,EAAA,MAAA;;AAGM,UAHN,gBAAA,CAGM;MAAZ,EAAA,MAAA;EAAG,SAAA,EAAA,MAAA;EAGG,OAAA,EAHN,GAGM,CAAA,MAAW,EAHL,WAMV,CAAM;AAGnB;AAOiB,UAbA,WAAA,CAaW;EAOhB,EAAA,EAAA,MAAA;EAAY,MAAA,EAAA,MAAA,EAAA;UAAG,CAAA,EAjBd,MAiBc,CAAA,MAAA,EAAA,OAAA,CAAA;;AAAyC,UAdnD,WAAA,CAcmD;EAAW,EAAA,EAAA,MAAA;;aAXlE;;;UAII,WAAA;;;;;;KAOL,YAAA,GAAe,yBAAyB,gBAAgB"}
1
+ {"version":3,"file":"vector-types.d.ts","names":[],"sources":["../src/vector-types.ts"],"sourcesContent":[],"mappings":";UAAiB,iBAAA;EAAA,IAAA,CAAA,EAAA,MAAA;EAKA,IAAA,CAAA,EAAA,MAAA;;AAGM,UAHN,gBAAA,CAGM;MAAZ,EAAA,MAAA;EAAG,SAAA,EAAA,MAAA;EAGG,OAAA,EAHN,GAGM,CAAA,MAAW,EAHL,WAMJ,CAAA;AAGnB;AAOiB,UAbA,WAAA,CAaW;EAOhB,EAAA,EAAA,MAAA;EAAY,MAAA,EAAA,MAAA,EAAA;UAAG,CAAA,EAjBd,MAiBc,CAAA,MAAA,EAAA,OAAA,CAAA;;AAAyC,UAdnD,WAAA,CAcmD;EAAW,EAAA,EAAA,MAAA;;aAXlE;;;UAII,WAAA;;;;;;KAOL,YAAA,GAAe,yBAAyB,gBAAgB"}
package/dist/vitest.cjs CHANGED
@@ -2,9 +2,9 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
2
  const require_runtime = require('./_virtual/_rolldown/runtime.cjs');
3
3
  const require_fixture_loader = require('./fixture-loader.cjs');
4
4
  const require_llmock = require('./llmock.cjs');
5
- const require_chunk_hooks = require('./node_modules/.pnpm/@vitest_runner@3.2.4/node_modules/@vitest/runner/dist/chunk-hooks.cjs');
6
5
  let node_path = require("node:path");
7
6
  let node_fs = require("node:fs");
7
+ let vitest = require("vitest");
8
8
 
9
9
  //#region src/vitest.ts
10
10
  /**
@@ -30,7 +30,7 @@ let node_fs = require("node:fs");
30
30
  */
31
31
  function useAimock(options = {}) {
32
32
  let handle = null;
33
- require_chunk_hooks.beforeAll(async () => {
33
+ (0, vitest.beforeAll)(async () => {
34
34
  const { fixtures: fixturePath, patchEnv, ...serverOpts } = options;
35
35
  const llm = new require_llmock.LLMock(serverOpts);
36
36
  if (fixturePath) {
@@ -47,10 +47,10 @@ function useAimock(options = {}) {
47
47
  url
48
48
  };
49
49
  });
50
- require_chunk_hooks.beforeEach(() => {
50
+ (0, vitest.beforeEach)(() => {
51
51
  if (handle) handle.llm.resetMatchCounts();
52
52
  });
53
- require_chunk_hooks.afterAll(async () => {
53
+ (0, vitest.afterAll)(async () => {
54
54
  if (handle) {
55
55
  if (options.patchEnv !== false) {
56
56
  delete process.env.OPENAI_BASE_URL;
@@ -1 +1 @@
1
- {"version":3,"file":"vitest.cjs","names":["LLMock","loadFixturesFromDir","loadFixtureFile"],"sources":["../src/vitest.ts"],"sourcesContent":["/**\n * Vitest integration for aimock.\n *\n * Usage:\n * import { useAimock } from \"@copilotkit/aimock/vitest\";\n *\n * const mock = useAimock({ fixtures: \"./fixtures\" });\n *\n * it(\"responds\", async () => {\n * const res = await fetch(`${mock().url}/v1/chat/completions`, { ... });\n * });\n */\n\nimport { beforeAll, afterAll, beforeEach } from \"vitest\";\nimport { LLMock } from \"./llmock.js\";\nimport { loadFixtureFile, loadFixturesFromDir } from \"./fixture-loader.js\";\nimport type { Fixture, MockServerOptions } from \"./types.js\";\nimport { statSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\n\nexport interface UseAimockOptions extends MockServerOptions {\n /** Path to fixture file or directory. Loaded automatically on start. */\n fixtures?: string;\n /** If true, sets process.env.OPENAI_BASE_URL to the mock URL + /v1. */\n patchEnv?: boolean;\n}\n\nexport interface AimockHandle {\n /** The LLMock instance. */\n readonly llm: LLMock;\n /** The server URL (e.g., http://127.0.0.1:4010). */\n readonly url: string;\n}\n\n/**\n * Start an aimock server for the duration of the test suite.\n *\n * - `beforeAll`: starts the server and optionally loads fixtures\n * - `beforeEach`: resets fixture match counts (not fixtures themselves)\n * - `afterAll`: stops the server\n *\n * Returns a getter function — call it inside tests to access the handle.\n */\nexport function useAimock(options: UseAimockOptions = {}): () => AimockHandle {\n let handle: AimockHandle | null = null;\n\n beforeAll(async () => {\n const { fixtures: fixturePath, patchEnv, ...serverOpts } = options;\n const llm = new LLMock(serverOpts);\n\n if (fixturePath) {\n const resolved = resolve(fixturePath);\n const loadedFixtures = loadFixtures(resolved);\n for (const f of loadedFixtures) {\n llm.addFixture(f);\n }\n }\n\n const url = await llm.start();\n\n if (patchEnv !== false) {\n process.env.OPENAI_BASE_URL = `${url}/v1`;\n process.env.ANTHROPIC_BASE_URL = `${url}/v1`;\n }\n\n handle = { llm, url };\n });\n\n beforeEach(() => {\n if (handle) {\n handle.llm.resetMatchCounts();\n }\n });\n\n afterAll(async () => {\n if (handle) {\n if (options.patchEnv !== false) {\n delete process.env.OPENAI_BASE_URL;\n delete process.env.ANTHROPIC_BASE_URL;\n }\n await handle.llm.stop();\n handle = null;\n }\n });\n\n return () => {\n if (!handle) {\n throw new Error(\"useAimock(): server not started — are you calling this inside a test?\");\n }\n return handle;\n };\n}\n\nfunction loadFixtures(fixturePath: string): Fixture[] {\n try {\n const stat = statSync(fixturePath);\n if (stat.isDirectory()) {\n return loadFixturesFromDir(fixturePath);\n }\n return loadFixtureFile(fixturePath);\n } catch {\n return [];\n }\n}\n\nexport { LLMock } from \"./llmock.js\";\nexport type { MockServerOptions, Fixture } from \"./types.js\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2CA,SAAgB,UAAU,UAA4B,EAAE,EAAsB;CAC5E,IAAI,SAA8B;AAElC,+BAAU,YAAY;EACpB,MAAM,EAAE,UAAU,aAAa,UAAU,GAAG,eAAe;EAC3D,MAAM,MAAM,IAAIA,sBAAO,WAAW;AAElC,MAAI,aAAa;GAEf,MAAM,iBAAiB,oCADE,YAAY,CACQ;AAC7C,QAAK,MAAM,KAAK,eACd,KAAI,WAAW,EAAE;;EAIrB,MAAM,MAAM,MAAM,IAAI,OAAO;AAE7B,MAAI,aAAa,OAAO;AACtB,WAAQ,IAAI,kBAAkB,GAAG,IAAI;AACrC,WAAQ,IAAI,qBAAqB,GAAG,IAAI;;AAG1C,WAAS;GAAE;GAAK;GAAK;GACrB;AAEF,sCAAiB;AACf,MAAI,OACF,QAAO,IAAI,kBAAkB;GAE/B;AAEF,8BAAS,YAAY;AACnB,MAAI,QAAQ;AACV,OAAI,QAAQ,aAAa,OAAO;AAC9B,WAAO,QAAQ,IAAI;AACnB,WAAO,QAAQ,IAAI;;AAErB,SAAM,OAAO,IAAI,MAAM;AACvB,YAAS;;GAEX;AAEF,cAAa;AACX,MAAI,CAAC,OACH,OAAM,IAAI,MAAM,wEAAwE;AAE1F,SAAO;;;AAIX,SAAS,aAAa,aAAgC;AACpD,KAAI;AAEF,4BADsB,YAAY,CACzB,aAAa,CACpB,QAAOC,2CAAoB,YAAY;AAEzC,SAAOC,uCAAgB,YAAY;SAC7B;AACN,SAAO,EAAE"}
1
+ {"version":3,"file":"vitest.cjs","names":["LLMock","loadFixturesFromDir","loadFixtureFile"],"sources":["../src/vitest.ts"],"sourcesContent":["/**\n * Vitest integration for aimock.\n *\n * Usage:\n * import { useAimock } from \"@copilotkit/aimock/vitest\";\n *\n * const mock = useAimock({ fixtures: \"./fixtures\" });\n *\n * it(\"responds\", async () => {\n * const res = await fetch(`${mock().url}/v1/chat/completions`, { ... });\n * });\n */\n\nimport { beforeAll, afterAll, beforeEach } from \"vitest\";\nimport { LLMock } from \"./llmock.js\";\nimport { loadFixtureFile, loadFixturesFromDir } from \"./fixture-loader.js\";\nimport type { Fixture, MockServerOptions } from \"./types.js\";\nimport { statSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\n\nexport interface UseAimockOptions extends MockServerOptions {\n /** Path to fixture file or directory. Loaded automatically on start. */\n fixtures?: string;\n /** If true, sets process.env.OPENAI_BASE_URL to the mock URL + /v1. */\n patchEnv?: boolean;\n}\n\nexport interface AimockHandle {\n /** The LLMock instance. */\n readonly llm: LLMock;\n /** The server URL (e.g., http://127.0.0.1:4010). */\n readonly url: string;\n}\n\n/**\n * Start an aimock server for the duration of the test suite.\n *\n * - `beforeAll`: starts the server and optionally loads fixtures\n * - `beforeEach`: resets fixture match counts (not fixtures themselves)\n * - `afterAll`: stops the server\n *\n * Returns a getter function — call it inside tests to access the handle.\n */\nexport function useAimock(options: UseAimockOptions = {}): () => AimockHandle {\n let handle: AimockHandle | null = null;\n\n beforeAll(async () => {\n const { fixtures: fixturePath, patchEnv, ...serverOpts } = options;\n const llm = new LLMock(serverOpts);\n\n if (fixturePath) {\n const resolved = resolve(fixturePath);\n const loadedFixtures = loadFixtures(resolved);\n for (const f of loadedFixtures) {\n llm.addFixture(f);\n }\n }\n\n const url = await llm.start();\n\n if (patchEnv !== false) {\n process.env.OPENAI_BASE_URL = `${url}/v1`;\n process.env.ANTHROPIC_BASE_URL = `${url}/v1`;\n }\n\n handle = { llm, url };\n });\n\n beforeEach(() => {\n if (handle) {\n handle.llm.resetMatchCounts();\n }\n });\n\n afterAll(async () => {\n if (handle) {\n if (options.patchEnv !== false) {\n delete process.env.OPENAI_BASE_URL;\n delete process.env.ANTHROPIC_BASE_URL;\n }\n await handle.llm.stop();\n handle = null;\n }\n });\n\n return () => {\n if (!handle) {\n throw new Error(\"useAimock(): server not started — are you calling this inside a test?\");\n }\n return handle;\n };\n}\n\nfunction loadFixtures(fixturePath: string): Fixture[] {\n try {\n const stat = statSync(fixturePath);\n if (stat.isDirectory()) {\n return loadFixturesFromDir(fixturePath);\n }\n return loadFixtureFile(fixturePath);\n } catch {\n return [];\n }\n}\n\nexport { LLMock } from \"./llmock.js\";\nexport type { MockServerOptions, Fixture } from \"./types.js\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2CA,SAAgB,UAAU,UAA4B,EAAE,EAAsB;CAC5E,IAAI,SAA8B;AAElC,uBAAU,YAAY;EACpB,MAAM,EAAE,UAAU,aAAa,UAAU,GAAG,eAAe;EAC3D,MAAM,MAAM,IAAIA,sBAAO,WAAW;AAElC,MAAI,aAAa;GAEf,MAAM,iBAAiB,oCADE,YAAY,CACQ;AAC7C,QAAK,MAAM,KAAK,eACd,KAAI,WAAW,EAAE;;EAIrB,MAAM,MAAM,MAAM,IAAI,OAAO;AAE7B,MAAI,aAAa,OAAO;AACtB,WAAQ,IAAI,kBAAkB,GAAG,IAAI;AACrC,WAAQ,IAAI,qBAAqB,GAAG,IAAI;;AAG1C,WAAS;GAAE;GAAK;GAAK;GACrB;AAEF,8BAAiB;AACf,MAAI,OACF,QAAO,IAAI,kBAAkB;GAE/B;AAEF,sBAAS,YAAY;AACnB,MAAI,QAAQ;AACV,OAAI,QAAQ,aAAa,OAAO;AAC9B,WAAO,QAAQ,IAAI;AACnB,WAAO,QAAQ,IAAI;;AAErB,SAAM,OAAO,IAAI,MAAM;AACvB,YAAS;;GAEX;AAEF,cAAa;AACX,MAAI,CAAC,OACH,OAAM,IAAI,MAAM,wEAAwE;AAE1F,SAAO;;;AAIX,SAAS,aAAa,aAAgC;AACpD,KAAI;AAEF,4BADsB,YAAY,CACzB,aAAa,CACpB,QAAOC,2CAAoB,YAAY;AAEzC,SAAOC,uCAAgB,YAAY;SAC7B;AACN,SAAO,EAAE"}
package/dist/vitest.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { loadFixtureFile, loadFixturesFromDir } from "./fixture-loader.js";
2
2
  import { LLMock } from "./llmock.js";
3
- import { afterAll, beforeAll, beforeEach } from "./node_modules/.pnpm/@vitest_runner@3.2.4/node_modules/@vitest/runner/dist/chunk-hooks.js";
4
3
  import { resolve } from "node:path";
5
4
  import { statSync } from "node:fs";
5
+ import { afterAll, beforeAll, beforeEach } from "vitest";
6
6
 
7
7
  //#region src/vitest.ts
8
8
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@copilotkit/aimock",
3
- "version": "1.14.3",
3
+ "version": "1.14.6",
4
4
  "description": "Mock infrastructure for AI application testing — LLM APIs, image generation, text-to-speech, transcription, video generation, MCP tools, A2A agents, AG-UI event streams, vector databases, search, rerank, and moderation. One package, one port, zero dependencies.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -38,11 +38,11 @@
38
38
  "homepage": "https://aimock.copilotkit.dev",
39
39
  "repository": {
40
40
  "type": "git",
41
- "url": "https://github.com/CopilotKit/aimock"
41
+ "url": "git+https://github.com/CopilotKit/aimock.git"
42
42
  },
43
43
  "packageManager": "pnpm@10.28.2",
44
44
  "engines": {
45
- "node": ">=20.15.0"
45
+ "node": ">=24.0.0"
46
46
  },
47
47
  "type": "module",
48
48
  "exports": {
@@ -113,19 +113,24 @@
113
113
  "typesVersions": {
114
114
  "*": {
115
115
  "mcp": [
116
- "./dist/mcp-stub.d.ts"
116
+ "./dist/mcp-stub.d.ts",
117
+ "./dist/mcp-stub.d.cts"
117
118
  ],
118
119
  "a2a": [
119
- "./dist/a2a-stub.d.ts"
120
+ "./dist/a2a-stub.d.ts",
121
+ "./dist/a2a-stub.d.cts"
120
122
  ],
121
123
  "vector": [
122
- "./dist/vector-stub.d.ts"
124
+ "./dist/vector-stub.d.ts",
125
+ "./dist/vector-stub.d.cts"
123
126
  ],
124
127
  "vitest": [
125
- "./dist/vitest.d.ts"
128
+ "./dist/vitest.d.ts",
129
+ "./dist/vitest.d.cts"
126
130
  ],
127
131
  "jest": [
128
- "./dist/jest.d.ts"
132
+ "./dist/jest.d.ts",
133
+ "./dist/jest.d.cts"
129
134
  ]
130
135
  }
131
136
  },
@@ -137,32 +142,46 @@
137
142
  "dist",
138
143
  "fixtures",
139
144
  ".claude-plugin",
140
- "skills"
145
+ "skills",
146
+ "CHANGELOG.md"
141
147
  ],
142
148
  "publishConfig": {
143
149
  "access": "public"
144
150
  },
145
151
  "scripts": {
152
+ "preinstall": "npx only-allow pnpm",
146
153
  "build": "tsdown",
147
154
  "test": "vitest run",
148
155
  "test:drift": "vitest run --config vitest.config.drift.ts",
149
156
  "test:exports": "publint && attw --pack .",
150
157
  "lint": "eslint .",
151
158
  "format:check": "prettier --check .",
152
- "release": "pnpm build && npm publish",
153
- "prepare": "husky || true"
159
+ "release": "pnpm build && pnpm test && pnpm lint && npm publish",
160
+ "prepare": "husky"
154
161
  },
155
162
  "lint-staged": {
156
163
  "*.{ts,mts,js,mjs,cjs,json,html,css,md}": "prettier --write",
157
164
  "*.{ts,mts,js,mjs}": "eslint --fix"
158
165
  },
166
+ "peerDependencies": {
167
+ "jest": ">=29",
168
+ "vitest": ">=3"
169
+ },
170
+ "peerDependenciesMeta": {
171
+ "jest": {
172
+ "optional": true
173
+ },
174
+ "vitest": {
175
+ "optional": true
176
+ }
177
+ },
159
178
  "devDependencies": {
160
179
  "@anthropic-ai/sdk": "^0.78.0",
161
180
  "@arethetypeswrong/cli": "^0.17.3",
162
181
  "@commitlint/cli": "^19.8.1",
163
182
  "@commitlint/config-conventional": "^19.8.0",
164
183
  "@eslint/js": "^9.30.0",
165
- "@google/generative-ai": "^0.24.0",
184
+ "@google/genai": "^1.0.0",
166
185
  "@types/node": "^22.0.0",
167
186
  "@vitest/coverage-v8": "^3.2.4",
168
187
  "eslint": "^9.30.0",
@@ -1,29 +0,0 @@
1
- //#region \0rolldown/runtime.js
2
- var __create = Object.create;
3
- var __defProp = Object.defineProperty;
4
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
- var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
- var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __commonJSMin = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
9
- var __copyProps = (to, from, except, desc) => {
10
- if (from && typeof from === "object" || typeof from === "function") {
11
- for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
12
- key = keys[i];
13
- if (!__hasOwnProp.call(to, key) && key !== except) {
14
- __defProp(to, key, {
15
- get: ((k) => from[k]).bind(null, key),
16
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
17
- });
18
- }
19
- }
20
- }
21
- return to;
22
- };
23
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
24
- value: mod,
25
- enumerable: true
26
- }) : target, mod));
27
-
28
- //#endregion
29
- export { __commonJSMin, __toESM };