@copilotkit/aimock 1.29.0 → 1.31.0

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 (207) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +34 -0
  4. package/README.md +1 -1
  5. package/dist/agui-types.d.ts.map +1 -1
  6. package/dist/bedrock-converse.cjs +63 -31
  7. package/dist/bedrock-converse.cjs.map +1 -1
  8. package/dist/bedrock-converse.d.cts.map +1 -1
  9. package/dist/bedrock-converse.d.ts.map +1 -1
  10. package/dist/bedrock-converse.js +65 -33
  11. package/dist/bedrock-converse.js.map +1 -1
  12. package/dist/bedrock.cjs +95 -33
  13. package/dist/bedrock.cjs.map +1 -1
  14. package/dist/bedrock.d.cts.map +1 -1
  15. package/dist/bedrock.d.ts.map +1 -1
  16. package/dist/bedrock.js +97 -35
  17. package/dist/bedrock.js.map +1 -1
  18. package/dist/cli.cjs +18 -1
  19. package/dist/cli.cjs.map +1 -1
  20. package/dist/cli.js +18 -1
  21. package/dist/cli.js.map +1 -1
  22. package/dist/cohere.cjs +49 -15
  23. package/dist/cohere.cjs.map +1 -1
  24. package/dist/cohere.d.cts.map +1 -1
  25. package/dist/cohere.d.ts.map +1 -1
  26. package/dist/cohere.js +51 -17
  27. package/dist/cohere.js.map +1 -1
  28. package/dist/config-loader.d.ts.map +1 -1
  29. package/dist/elevenlabs-audio.cjs +8 -4
  30. package/dist/elevenlabs-audio.cjs.map +1 -1
  31. package/dist/elevenlabs-audio.d.cts.map +1 -1
  32. package/dist/elevenlabs-audio.d.ts.map +1 -1
  33. package/dist/elevenlabs-audio.js +10 -6
  34. package/dist/elevenlabs-audio.js.map +1 -1
  35. package/dist/embeddings.cjs +4 -3
  36. package/dist/embeddings.cjs.map +1 -1
  37. package/dist/embeddings.d.cts.map +1 -1
  38. package/dist/embeddings.d.ts.map +1 -1
  39. package/dist/embeddings.js +6 -5
  40. package/dist/embeddings.js.map +1 -1
  41. package/dist/fal-audio.cjs +18 -8
  42. package/dist/fal-audio.cjs.map +1 -1
  43. package/dist/fal-audio.d.cts.map +1 -1
  44. package/dist/fal-audio.d.ts.map +1 -1
  45. package/dist/fal-audio.js +22 -12
  46. package/dist/fal-audio.js.map +1 -1
  47. package/dist/fal.cjs +55 -46
  48. package/dist/fal.cjs.map +1 -1
  49. package/dist/fal.d.cts +8 -0
  50. package/dist/fal.d.cts.map +1 -1
  51. package/dist/fal.d.ts +8 -0
  52. package/dist/fal.d.ts.map +1 -1
  53. package/dist/fal.js +58 -49
  54. package/dist/fal.js.map +1 -1
  55. package/dist/gemini-embeddings.cjs +4 -3
  56. package/dist/gemini-embeddings.cjs.map +1 -1
  57. package/dist/gemini-embeddings.js +6 -5
  58. package/dist/gemini-embeddings.js.map +1 -1
  59. package/dist/gemini-interactions.cjs +3 -3
  60. package/dist/gemini-interactions.cjs.map +1 -1
  61. package/dist/gemini-interactions.d.cts.map +1 -1
  62. package/dist/gemini-interactions.d.ts.map +1 -1
  63. package/dist/gemini-interactions.js +5 -5
  64. package/dist/gemini-interactions.js.map +1 -1
  65. package/dist/gemini.cjs +55 -24
  66. package/dist/gemini.cjs.map +1 -1
  67. package/dist/gemini.d.cts.map +1 -1
  68. package/dist/gemini.d.ts.map +1 -1
  69. package/dist/gemini.js +57 -26
  70. package/dist/gemini.js.map +1 -1
  71. package/dist/helpers.cjs +120 -2
  72. package/dist/helpers.cjs.map +1 -1
  73. package/dist/helpers.d.cts +43 -3
  74. package/dist/helpers.d.cts.map +1 -1
  75. package/dist/helpers.d.ts +43 -3
  76. package/dist/helpers.d.ts.map +1 -1
  77. package/dist/helpers.js +117 -3
  78. package/dist/helpers.js.map +1 -1
  79. package/dist/images.cjs +12 -6
  80. package/dist/images.cjs.map +1 -1
  81. package/dist/images.d.cts.map +1 -1
  82. package/dist/images.d.ts.map +1 -1
  83. package/dist/images.js +14 -8
  84. package/dist/images.js.map +1 -1
  85. package/dist/index.cjs +13 -2
  86. package/dist/index.d.cts +4 -3
  87. package/dist/index.d.ts +4 -3
  88. package/dist/index.js +6 -5
  89. package/dist/llmock.cjs +2 -1
  90. package/dist/llmock.cjs.map +1 -1
  91. package/dist/llmock.d.cts.map +1 -1
  92. package/dist/llmock.d.ts.map +1 -1
  93. package/dist/llmock.js +2 -1
  94. package/dist/llmock.js.map +1 -1
  95. package/dist/messages.cjs +325 -85
  96. package/dist/messages.cjs.map +1 -1
  97. package/dist/messages.d.cts.map +1 -1
  98. package/dist/messages.d.ts.map +1 -1
  99. package/dist/messages.js +327 -87
  100. package/dist/messages.js.map +1 -1
  101. package/dist/metrics.cjs +9 -0
  102. package/dist/metrics.cjs.map +1 -1
  103. package/dist/metrics.d.cts.map +1 -1
  104. package/dist/metrics.d.ts.map +1 -1
  105. package/dist/metrics.js +7 -1
  106. package/dist/metrics.js.map +1 -1
  107. package/dist/model-utils.cjs +68 -0
  108. package/dist/model-utils.cjs.map +1 -1
  109. package/dist/model-utils.js +68 -1
  110. package/dist/model-utils.js.map +1 -1
  111. package/dist/ollama.cjs +58 -21
  112. package/dist/ollama.cjs.map +1 -1
  113. package/dist/ollama.d.cts.map +1 -1
  114. package/dist/ollama.d.ts.map +1 -1
  115. package/dist/ollama.js +60 -23
  116. package/dist/ollama.js.map +1 -1
  117. package/dist/openrouter-video.cjs +1609 -0
  118. package/dist/openrouter-video.cjs.map +1 -0
  119. package/dist/openrouter-video.d.cts +154 -0
  120. package/dist/openrouter-video.d.cts.map +1 -0
  121. package/dist/openrouter-video.d.ts +154 -0
  122. package/dist/openrouter-video.d.ts.map +1 -0
  123. package/dist/openrouter-video.js +1601 -0
  124. package/dist/openrouter-video.js.map +1 -0
  125. package/dist/recorder.cjs +97 -15
  126. package/dist/recorder.cjs.map +1 -1
  127. package/dist/recorder.d.cts.map +1 -1
  128. package/dist/recorder.d.ts.map +1 -1
  129. package/dist/recorder.js +96 -17
  130. package/dist/recorder.js.map +1 -1
  131. package/dist/responses.cjs +26 -12
  132. package/dist/responses.cjs.map +1 -1
  133. package/dist/responses.d.cts +1 -1
  134. package/dist/responses.d.cts.map +1 -1
  135. package/dist/responses.d.ts +1 -1
  136. package/dist/responses.d.ts.map +1 -1
  137. package/dist/responses.js +28 -14
  138. package/dist/responses.js.map +1 -1
  139. package/dist/router.cjs +37 -8
  140. package/dist/router.cjs.map +1 -1
  141. package/dist/router.d.cts +30 -1
  142. package/dist/router.d.cts.map +1 -1
  143. package/dist/router.d.ts +30 -1
  144. package/dist/router.d.ts.map +1 -1
  145. package/dist/router.js +37 -9
  146. package/dist/router.js.map +1 -1
  147. package/dist/server.cjs +173 -19
  148. package/dist/server.cjs.map +1 -1
  149. package/dist/server.d.cts +2 -0
  150. package/dist/server.d.cts.map +1 -1
  151. package/dist/server.d.ts +2 -0
  152. package/dist/server.d.ts.map +1 -1
  153. package/dist/server.js +176 -22
  154. package/dist/server.js.map +1 -1
  155. package/dist/speech.cjs +4 -2
  156. package/dist/speech.cjs.map +1 -1
  157. package/dist/speech.d.cts.map +1 -1
  158. package/dist/speech.d.ts.map +1 -1
  159. package/dist/speech.js +6 -4
  160. package/dist/speech.js.map +1 -1
  161. package/dist/stream-collapse.cjs +44 -1
  162. package/dist/stream-collapse.cjs.map +1 -1
  163. package/dist/stream-collapse.d.cts +28 -0
  164. package/dist/stream-collapse.d.cts.map +1 -1
  165. package/dist/stream-collapse.d.ts +28 -0
  166. package/dist/stream-collapse.d.ts.map +1 -1
  167. package/dist/stream-collapse.js +44 -2
  168. package/dist/stream-collapse.js.map +1 -1
  169. package/dist/transcription.cjs +4 -2
  170. package/dist/transcription.cjs.map +1 -1
  171. package/dist/transcription.d.cts.map +1 -1
  172. package/dist/transcription.d.ts.map +1 -1
  173. package/dist/transcription.js +6 -4
  174. package/dist/transcription.js.map +1 -1
  175. package/dist/types.d.cts +123 -8
  176. package/dist/types.d.cts.map +1 -1
  177. package/dist/types.d.ts +123 -8
  178. package/dist/types.d.ts.map +1 -1
  179. package/dist/vector-types.d.cts.map +1 -1
  180. package/dist/vector-types.d.ts.map +1 -1
  181. package/dist/video.cjs +4 -2
  182. package/dist/video.cjs.map +1 -1
  183. package/dist/video.d.cts.map +1 -1
  184. package/dist/video.d.ts.map +1 -1
  185. package/dist/video.js +6 -4
  186. package/dist/video.js.map +1 -1
  187. package/dist/ws-gemini-live.cjs +4 -3
  188. package/dist/ws-gemini-live.cjs.map +1 -1
  189. package/dist/ws-gemini-live.d.cts +2 -2
  190. package/dist/ws-gemini-live.d.cts.map +1 -1
  191. package/dist/ws-gemini-live.d.ts.map +1 -1
  192. package/dist/ws-gemini-live.js +6 -5
  193. package/dist/ws-gemini-live.js.map +1 -1
  194. package/dist/ws-realtime.cjs +4 -3
  195. package/dist/ws-realtime.cjs.map +1 -1
  196. package/dist/ws-realtime.d.cts +2 -2
  197. package/dist/ws-realtime.d.cts.map +1 -1
  198. package/dist/ws-realtime.d.ts.map +1 -1
  199. package/dist/ws-realtime.js +6 -5
  200. package/dist/ws-realtime.js.map +1 -1
  201. package/dist/ws-responses.cjs +8 -6
  202. package/dist/ws-responses.cjs.map +1 -1
  203. package/dist/ws-responses.d.cts.map +1 -1
  204. package/dist/ws-responses.d.ts.map +1 -1
  205. package/dist/ws-responses.js +10 -8
  206. package/dist/ws-responses.js.map +1 -1
  207. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"router.js","names":[],"sources":["../src/router.ts"],"sourcesContent":["import type { ChatCompletionRequest, ChatMessage, ContentPart, Fixture } from \"./types.js\";\nimport {\n isImageResponse,\n isAudioResponse,\n isTranscriptionResponse,\n isVideoResponse,\n isJSONResponse,\n isErrorResponse,\n} from \"./helpers.js\";\n\nexport function getLastMessageByRole(messages: ChatMessage[], role: string): ChatMessage | null {\n for (let i = messages.length - 1; i >= 0; i--) {\n if (messages[i].role === role) return messages[i];\n }\n return null;\n}\n\n/**\n * Concatenate the text content of every `system` role message in order.\n * Hosts that build a system context from multiple sources (persona, agent\n * context entries, tool guidance) often emit several system messages in one\n * request; this joins them with newlines so a substring matcher sees the\n * whole context as one body.\n */\nexport function getSystemText(messages: ChatMessage[]): string {\n const parts: string[] = [];\n for (const m of messages) {\n if (m.role !== \"system\") continue;\n const text = getTextContent(m.content);\n if (text) parts.push(text);\n }\n return parts.join(\"\\n\");\n}\n\n/**\n * Extract the text content from a message's content field.\n * Handles both plain string content and array-of-parts content\n * (e.g. `[{type: \"text\", text: \"...\"}]` as sent by some SDKs).\n */\nexport function getTextContent(content: string | ContentPart[] | null): string | null {\n if (typeof content === \"string\") return content;\n if (Array.isArray(content)) {\n const texts = content\n .filter((p) => p.type === \"text\" && typeof p.text === \"string\" && p.text !== \"\")\n .map((p) => p.text as string);\n return texts.length > 0 ? texts.join(\"\") : null;\n }\n return null;\n}\n\nexport function matchFixture(\n fixtures: Fixture[],\n req: ChatCompletionRequest,\n matchCounts?: Map<Fixture, number>,\n requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest,\n): Fixture | null {\n // Apply transform once before matching — used for stripping dynamic data\n const effective = requestTransform ? requestTransform(req) : req;\n const useExactMatch = !!requestTransform;\n\n for (const fixture of fixtures) {\n const { match } = fixture;\n\n // predicate — if present, must return true (receives original request)\n if (match.predicate !== undefined) {\n if (!match.predicate(req)) continue;\n }\n\n // endpoint — bidirectional filtering:\n // 1. If fixture has endpoint set, only match requests of that type\n // 2. If request has _endpointType but fixture doesn't, skip fixtures\n // whose response type is incompatible (prevents generic chat fixtures\n // from matching image/speech/video requests and causing 500s)\n const reqEndpoint = effective._endpointType as string | undefined;\n if (match.endpoint !== undefined) {\n if (match.endpoint !== reqEndpoint) continue;\n } else if (\n reqEndpoint &&\n reqEndpoint !== \"chat\" &&\n reqEndpoint !== \"embedding\" &&\n !reqEndpoint.startsWith(\"realtime\")\n ) {\n // Fixture has no endpoint restriction but request is multimedia —\n // only match if the response type is compatible.\n // Function responses cannot be checked statically, so treat them as compatible.\n const r = fixture.response;\n if (typeof r !== \"function\") {\n const compatible =\n (reqEndpoint === \"image\" && isImageResponse(r)) ||\n (reqEndpoint === \"speech\" && isAudioResponse(r)) ||\n (reqEndpoint === \"elevenlabs-tts\" && isAudioResponse(r)) ||\n (reqEndpoint === \"audio-gen\" && isAudioResponse(r)) ||\n (reqEndpoint === \"fal-audio\" && isAudioResponse(r)) ||\n (reqEndpoint === \"fal\" && (isJSONResponse(r) || isErrorResponse(r))) ||\n (reqEndpoint === \"transcription\" && isTranscriptionResponse(r)) ||\n (reqEndpoint === \"translation\" && isTranscriptionResponse(r)) ||\n (reqEndpoint === \"video\" && isVideoResponse(r));\n if (!compatible) continue;\n }\n }\n\n // context — opt-in exact match against the request's _context field.\n // If fixture specifies a context, only match requests with that exact context.\n // If fixture omits context, match any request regardless of _context.\n if (match.context !== undefined) {\n if (effective._context !== match.context) continue;\n }\n\n // userMessage — case-sensitive match against the last user message content.\n // String matching is intentionally case-sensitive so fixture authors can\n // rely on exact string values. This differs from the case-insensitive\n // matchesPattern() in helpers.ts, which is used for search/rerank/moderation\n // where exact casing rarely matters.\n if (match.userMessage !== undefined) {\n const msg = getLastMessageByRole(effective.messages, \"user\");\n const text = msg ? getTextContent(msg.content) : null;\n if (!text) continue;\n if (typeof match.userMessage === \"string\") {\n if (useExactMatch) {\n if (text !== match.userMessage) continue;\n } else {\n if (!text.includes(match.userMessage)) continue;\n }\n } else {\n match.userMessage.lastIndex = 0;\n if (!match.userMessage.test(text)) continue;\n }\n }\n\n // systemMessage — case-sensitive substring, regexp, or array-of-substrings\n // match against the joined text of every system message in the request.\n // Use to gate a fixture on host-supplied context (e.g. agent-context\n // entries) so that when the calling app changes that context the fixture\n // stops matching and the request falls through to the next fixture or\n // upstream proxy.\n //\n // Array form (string[]) requires ALL substrings to be present — useful\n // when the gate must combine multiple non-adjacent tokens (e.g. a default\n // name AND a default activity list whose positions in the serialised\n // context JSON aren't stable).\n if (match.systemMessage !== undefined) {\n const text = getSystemText(effective.messages);\n if (!text) continue;\n const sm = match.systemMessage;\n if (Array.isArray(sm)) {\n // Empty array is treated as \"no constraint\" → effectively matches\n // unconditionally. Validation rejects this at load time for JSON\n // fixtures; programmatic callers that pass [] get the same\n // permissive behaviour as not setting systemMessage at all.\n let allPresent = true;\n for (const needle of sm) {\n if (!text.includes(needle)) {\n allPresent = false;\n break;\n }\n }\n if (!allPresent) continue;\n } else if (typeof sm === \"string\") {\n if (useExactMatch) {\n if (text !== sm) continue;\n } else {\n if (!text.includes(sm)) continue;\n }\n } else {\n sm.lastIndex = 0;\n if (!sm.test(text)) continue;\n }\n }\n\n // toolCallId — a toolCallId fixture answers the model's response to a tool\n // result, which by API contract only happens when the conversation's LAST\n // message is a tool result. If a newer user (or other) turn follows the\n // tool message, the stale tool_call_id must not shadow userMessage matchers.\n if (match.toolCallId !== undefined) {\n const last = effective.messages[effective.messages.length - 1];\n if (!last || last.role !== \"tool\" || last.tool_call_id !== match.toolCallId) continue;\n }\n\n // toolName — match against any tool definition by function.name\n if (match.toolName !== undefined) {\n const tools = effective.tools ?? [];\n const found = tools.some((t) => t.function.name === match.toolName);\n if (!found) continue;\n }\n\n // inputText — case-sensitive match against the embedding input text.\n // Same rationale as userMessage above: fixture authors specify exact strings.\n if (match.inputText !== undefined) {\n const embeddingInput = effective.embeddingInput;\n if (!embeddingInput) continue;\n if (typeof match.inputText === \"string\") {\n if (useExactMatch) {\n if (embeddingInput !== match.inputText) continue;\n } else {\n if (!embeddingInput.includes(match.inputText)) continue;\n }\n } else {\n match.inputText.lastIndex = 0;\n if (!match.inputText.test(embeddingInput)) continue;\n }\n }\n\n // responseFormat — exact string match against request response_format.type\n if (match.responseFormat !== undefined) {\n const reqType = effective.response_format?.type;\n if (reqType !== match.responseFormat) continue;\n }\n\n // model — exact match or prefix + dash-digit boundary for strings (so that\n // \"claude-opus-4\" matches \"claude-opus-4-20250514\" but \"gpt-4\" does NOT\n // match \"gpt-4o\" and \"gpt-4o\" does NOT match \"gpt-4o-mini\"), regexp unchanged\n if (match.model !== undefined) {\n if (typeof match.model === \"string\") {\n if (effective.model !== match.model) {\n if (!effective.model?.startsWith(match.model)) continue;\n const rest = effective.model.slice(match.model.length);\n if (!/^-\\d/.test(rest)) continue;\n }\n } else {\n match.model.lastIndex = 0;\n if (!match.model.test(effective.model ?? \"\")) continue;\n }\n }\n\n // sequenceIndex — check against the fixture's match count\n if (match.sequenceIndex !== undefined && matchCounts !== undefined) {\n const count = matchCounts.get(fixture) ?? 0;\n if (count !== match.sequenceIndex) continue;\n }\n\n if (match.turnIndex !== undefined) {\n const assistantCount = effective.messages.filter((m) => m.role === \"assistant\").length;\n if (assistantCount !== match.turnIndex) continue;\n }\n\n if (match.hasToolResult !== undefined) {\n const hasTool = effective.messages.some((m) => m.role === \"tool\");\n if (hasTool !== match.hasToolResult) continue;\n }\n\n return fixture;\n }\n\n return null;\n}\n"],"mappings":";;;AAUA,SAAgB,qBAAqB,UAAyB,MAAkC;AAC9F,MAAK,IAAI,IAAI,SAAS,SAAS,GAAG,KAAK,GAAG,IACxC,KAAI,SAAS,GAAG,SAAS,KAAM,QAAO,SAAS;AAEjD,QAAO;;;;;;;;;AAUT,SAAgB,cAAc,UAAiC;CAC7D,MAAM,QAAkB,EAAE;AAC1B,MAAK,MAAM,KAAK,UAAU;AACxB,MAAI,EAAE,SAAS,SAAU;EACzB,MAAM,OAAO,eAAe,EAAE,QAAQ;AACtC,MAAI,KAAM,OAAM,KAAK,KAAK;;AAE5B,QAAO,MAAM,KAAK,KAAK;;;;;;;AAQzB,SAAgB,eAAe,SAAuD;AACpF,KAAI,OAAO,YAAY,SAAU,QAAO;AACxC,KAAI,MAAM,QAAQ,QAAQ,EAAE;EAC1B,MAAM,QAAQ,QACX,QAAQ,MAAM,EAAE,SAAS,UAAU,OAAO,EAAE,SAAS,YAAY,EAAE,SAAS,GAAG,CAC/E,KAAK,MAAM,EAAE,KAAe;AAC/B,SAAO,MAAM,SAAS,IAAI,MAAM,KAAK,GAAG,GAAG;;AAE7C,QAAO;;AAGT,SAAgB,aACd,UACA,KACA,aACA,kBACgB;CAEhB,MAAM,YAAY,mBAAmB,iBAAiB,IAAI,GAAG;CAC7D,MAAM,gBAAgB,CAAC,CAAC;AAExB,MAAK,MAAM,WAAW,UAAU;EAC9B,MAAM,EAAE,UAAU;AAGlB,MAAI,MAAM,cAAc,QACtB;OAAI,CAAC,MAAM,UAAU,IAAI,CAAE;;EAQ7B,MAAM,cAAc,UAAU;AAC9B,MAAI,MAAM,aAAa,QACrB;OAAI,MAAM,aAAa,YAAa;aAEpC,eACA,gBAAgB,UAChB,gBAAgB,eAChB,CAAC,YAAY,WAAW,WAAW,EACnC;GAIA,MAAM,IAAI,QAAQ;AAClB,OAAI,OAAO,MAAM,YAWf;QAAI,EATD,gBAAgB,WAAW,gBAAgB,EAAE,IAC7C,gBAAgB,YAAY,gBAAgB,EAAE,IAC9C,gBAAgB,oBAAoB,gBAAgB,EAAE,IACtD,gBAAgB,eAAe,gBAAgB,EAAE,IACjD,gBAAgB,eAAe,gBAAgB,EAAE,IACjD,gBAAgB,UAAU,eAAe,EAAE,IAAI,gBAAgB,EAAE,KACjE,gBAAgB,mBAAmB,wBAAwB,EAAE,IAC7D,gBAAgB,iBAAiB,wBAAwB,EAAE,IAC3D,gBAAgB,WAAW,gBAAgB,EAAE,EAC/B;;;AAOrB,MAAI,MAAM,YAAY,QACpB;OAAI,UAAU,aAAa,MAAM,QAAS;;AAQ5C,MAAI,MAAM,gBAAgB,QAAW;GACnC,MAAM,MAAM,qBAAqB,UAAU,UAAU,OAAO;GAC5D,MAAM,OAAO,MAAM,eAAe,IAAI,QAAQ,GAAG;AACjD,OAAI,CAAC,KAAM;AACX,OAAI,OAAO,MAAM,gBAAgB,UAC/B;QAAI,eACF;SAAI,SAAS,MAAM,YAAa;eAE5B,CAAC,KAAK,SAAS,MAAM,YAAY,CAAE;UAEpC;AACL,UAAM,YAAY,YAAY;AAC9B,QAAI,CAAC,MAAM,YAAY,KAAK,KAAK,CAAE;;;AAevC,MAAI,MAAM,kBAAkB,QAAW;GACrC,MAAM,OAAO,cAAc,UAAU,SAAS;AAC9C,OAAI,CAAC,KAAM;GACX,MAAM,KAAK,MAAM;AACjB,OAAI,MAAM,QAAQ,GAAG,EAAE;IAKrB,IAAI,aAAa;AACjB,SAAK,MAAM,UAAU,GACnB,KAAI,CAAC,KAAK,SAAS,OAAO,EAAE;AAC1B,kBAAa;AACb;;AAGJ,QAAI,CAAC,WAAY;cACR,OAAO,OAAO,UACvB;QAAI,eACF;SAAI,SAAS,GAAI;eAEb,CAAC,KAAK,SAAS,GAAG,CAAE;UAErB;AACL,OAAG,YAAY;AACf,QAAI,CAAC,GAAG,KAAK,KAAK,CAAE;;;AAQxB,MAAI,MAAM,eAAe,QAAW;GAClC,MAAM,OAAO,UAAU,SAAS,UAAU,SAAS,SAAS;AAC5D,OAAI,CAAC,QAAQ,KAAK,SAAS,UAAU,KAAK,iBAAiB,MAAM,WAAY;;AAI/E,MAAI,MAAM,aAAa,QAGrB;OAAI,EAFU,UAAU,SAAS,EAAE,EACf,MAAM,MAAM,EAAE,SAAS,SAAS,MAAM,SAAS,CACvD;;AAKd,MAAI,MAAM,cAAc,QAAW;GACjC,MAAM,iBAAiB,UAAU;AACjC,OAAI,CAAC,eAAgB;AACrB,OAAI,OAAO,MAAM,cAAc,UAC7B;QAAI,eACF;SAAI,mBAAmB,MAAM,UAAW;eAEpC,CAAC,eAAe,SAAS,MAAM,UAAU,CAAE;UAE5C;AACL,UAAM,UAAU,YAAY;AAC5B,QAAI,CAAC,MAAM,UAAU,KAAK,eAAe,CAAE;;;AAK/C,MAAI,MAAM,mBAAmB,QAE3B;OADgB,UAAU,iBAAiB,SAC3B,MAAM,eAAgB;;AAMxC,MAAI,MAAM,UAAU,OAClB,KAAI,OAAO,MAAM,UAAU,UACzB;OAAI,UAAU,UAAU,MAAM,OAAO;AACnC,QAAI,CAAC,UAAU,OAAO,WAAW,MAAM,MAAM,CAAE;IAC/C,MAAM,OAAO,UAAU,MAAM,MAAM,MAAM,MAAM,OAAO;AACtD,QAAI,CAAC,OAAO,KAAK,KAAK,CAAE;;SAErB;AACL,SAAM,MAAM,YAAY;AACxB,OAAI,CAAC,MAAM,MAAM,KAAK,UAAU,SAAS,GAAG,CAAE;;AAKlD,MAAI,MAAM,kBAAkB,UAAa,gBAAgB,QAEvD;QADc,YAAY,IAAI,QAAQ,IAAI,OAC5B,MAAM,cAAe;;AAGrC,MAAI,MAAM,cAAc,QAEtB;OADuB,UAAU,SAAS,QAAQ,MAAM,EAAE,SAAS,YAAY,CAAC,WACzD,MAAM,UAAW;;AAG1C,MAAI,MAAM,kBAAkB,QAE1B;OADgB,UAAU,SAAS,MAAM,MAAM,EAAE,SAAS,OAAO,KACjD,MAAM,cAAe;;AAGvC,SAAO;;AAGT,QAAO"}
1
+ {"version":3,"file":"router.js","names":[],"sources":["../src/router.ts"],"sourcesContent":["import type { ChatCompletionRequest, ChatMessage, ContentPart, Fixture } from \"./types.js\";\nimport {\n isImageResponse,\n isAudioResponse,\n isTranscriptionResponse,\n isVideoResponse,\n isJSONResponse,\n isErrorResponse,\n} from \"./helpers.js\";\n\nexport function getLastMessageByRole(messages: ChatMessage[], role: string): ChatMessage | null {\n for (let i = messages.length - 1; i >= 0; i--) {\n if (messages[i].role === role) return messages[i];\n }\n return null;\n}\n\n/**\n * Concatenate the text content of every `system` role message in order.\n * Hosts that build a system context from multiple sources (persona, agent\n * context entries, tool guidance) often emit several system messages in one\n * request; this joins them with newlines so a substring matcher sees the\n * whole context as one body.\n */\nexport function getSystemText(messages: ChatMessage[]): string {\n const parts: string[] = [];\n for (const m of messages) {\n if (m.role !== \"system\") continue;\n const text = getTextContent(m.content);\n if (text) parts.push(text);\n }\n return parts.join(\"\\n\");\n}\n\n/**\n * Extract the text content from a message's content field.\n * Handles both plain string content and array-of-parts content\n * (e.g. `[{type: \"text\", text: \"...\"}]` as sent by some SDKs).\n */\nexport function getTextContent(content: string | ContentPart[] | null): string | null {\n if (typeof content === \"string\") return content;\n if (Array.isArray(content)) {\n const texts = content\n .filter((p) => p.type === \"text\" && typeof p.text === \"string\" && p.text !== \"\")\n .map((p) => p.text as string);\n return texts.length > 0 ? texts.join(\"\") : null;\n }\n return null;\n}\n\n/**\n * Result of {@link matchFixtureDiagnostic}: the matched fixture (or `null`) plus\n * the number of fixtures that matched the request SHAPE (every predicate above\n * the sequenceIndex/turnIndex gates) but were rejected ONLY by the\n * sequenceIndex/turnIndex count state.\n *\n * `skippedBySequenceOrTurn > 0` with `fixture === null` distinguishes a\n * \"sequence/turn exhausted\" miss (candidate fixtures existed but their count\n * gate had moved on) from a true \"no fixture had a matching shape\" miss — used\n * to disambiguate the strict-mode 503 message.\n */\nexport interface MatchFixtureDiagnostic {\n fixture: Fixture | null;\n skippedBySequenceOrTurn: number;\n}\n\n/**\n * Match a fixture against a request, additionally reporting WHY a `null` result\n * occurred. Shares the exact predicate loop with {@link matchFixture}; a fixture\n * that passes every shape predicate but fails ONLY the sequenceIndex or\n * turnIndex gate increments `skippedBySequenceOrTurn`. {@link matchFixture} is a\n * thin wrapper that returns the `.fixture` field, so existing callers are\n * unaffected.\n */\nexport function matchFixtureDiagnostic(\n fixtures: Fixture[],\n req: ChatCompletionRequest,\n matchCounts?: Map<Fixture, number>,\n requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest,\n): MatchFixtureDiagnostic {\n // Apply transform once before matching — used for stripping dynamic data\n const effective = requestTransform ? requestTransform(req) : req;\n const useExactMatch = !!requestTransform;\n\n let skippedBySequenceOrTurn = 0;\n\n for (const fixture of fixtures) {\n const { match } = fixture;\n\n // predicate — if present, must return true (receives original request)\n if (match.predicate !== undefined) {\n if (!match.predicate(req)) continue;\n }\n\n // endpoint — bidirectional filtering:\n // 1. If fixture has endpoint set, only match requests of that type\n // 2. If request has _endpointType but fixture doesn't, skip fixtures\n // whose response type is incompatible (prevents generic chat fixtures\n // from matching image/speech/video requests and causing 500s)\n const reqEndpoint = effective._endpointType as string | undefined;\n if (match.endpoint !== undefined) {\n if (match.endpoint !== reqEndpoint) continue;\n } else if (\n reqEndpoint &&\n reqEndpoint !== \"chat\" &&\n reqEndpoint !== \"embedding\" &&\n !reqEndpoint.startsWith(\"realtime\")\n ) {\n // Fixture has no endpoint restriction but request is multimedia —\n // only match if the response type is compatible.\n // Function responses cannot be checked statically, so treat them as compatible.\n const r = fixture.response;\n if (typeof r !== \"function\") {\n const compatible =\n (reqEndpoint === \"image\" && isImageResponse(r)) ||\n (reqEndpoint === \"speech\" && isAudioResponse(r)) ||\n (reqEndpoint === \"elevenlabs-tts\" && isAudioResponse(r)) ||\n (reqEndpoint === \"audio-gen\" && isAudioResponse(r)) ||\n (reqEndpoint === \"fal-audio\" && isAudioResponse(r)) ||\n (reqEndpoint === \"fal\" && (isJSONResponse(r) || isErrorResponse(r))) ||\n (reqEndpoint === \"transcription\" && isTranscriptionResponse(r)) ||\n (reqEndpoint === \"translation\" && isTranscriptionResponse(r)) ||\n (reqEndpoint === \"video\" && isVideoResponse(r));\n if (!compatible) continue;\n }\n }\n\n // context — opt-in exact match against the request's _context field.\n // If fixture specifies a context, only match requests with that exact context.\n // If fixture omits context, match any request regardless of _context.\n if (match.context !== undefined) {\n if (effective._context !== match.context) continue;\n }\n\n // userMessage — case-sensitive match against the last user message content.\n // String matching is intentionally case-sensitive so fixture authors can\n // rely on exact string values. This differs from the case-insensitive\n // matchesPattern() in helpers.ts, which is used for search/rerank/moderation\n // where exact casing rarely matters.\n if (match.userMessage !== undefined) {\n const msg = getLastMessageByRole(effective.messages, \"user\");\n const text = msg ? getTextContent(msg.content) : null;\n if (!text) continue;\n if (typeof match.userMessage === \"string\") {\n if (useExactMatch) {\n if (text !== match.userMessage) continue;\n } else {\n if (!text.includes(match.userMessage)) continue;\n }\n } else {\n match.userMessage.lastIndex = 0;\n if (!match.userMessage.test(text)) continue;\n }\n }\n\n // systemMessage — case-sensitive substring, regexp, or array-of-substrings\n // match against the joined text of every system message in the request.\n // Use to gate a fixture on host-supplied context (e.g. agent-context\n // entries) so that when the calling app changes that context the fixture\n // stops matching and the request falls through to the next fixture or\n // upstream proxy.\n //\n // Array form (string[]) requires ALL substrings to be present — useful\n // when the gate must combine multiple non-adjacent tokens (e.g. a default\n // name AND a default activity list whose positions in the serialised\n // context JSON aren't stable).\n if (match.systemMessage !== undefined) {\n const text = getSystemText(effective.messages);\n if (!text) continue;\n const sm = match.systemMessage;\n if (Array.isArray(sm)) {\n // Empty array is treated as \"no constraint\" → effectively matches\n // unconditionally. Validation rejects this at load time for JSON\n // fixtures; programmatic callers that pass [] get the same\n // permissive behaviour as not setting systemMessage at all.\n let allPresent = true;\n for (const needle of sm) {\n if (!text.includes(needle)) {\n allPresent = false;\n break;\n }\n }\n if (!allPresent) continue;\n } else if (typeof sm === \"string\") {\n if (useExactMatch) {\n if (text !== sm) continue;\n } else {\n if (!text.includes(sm)) continue;\n }\n } else {\n sm.lastIndex = 0;\n if (!sm.test(text)) continue;\n }\n }\n\n // toolCallId — a toolCallId fixture answers the model's response to a tool\n // result, which by API contract only happens when the conversation's LAST\n // message is a tool result. If a newer user (or other) turn follows the\n // tool message, the stale tool_call_id must not shadow userMessage matchers.\n if (match.toolCallId !== undefined) {\n const last = effective.messages[effective.messages.length - 1];\n if (!last || last.role !== \"tool\" || last.tool_call_id !== match.toolCallId) continue;\n }\n\n // toolName — match against any tool definition by function.name\n if (match.toolName !== undefined) {\n const tools = effective.tools ?? [];\n const found = tools.some((t) => t.function.name === match.toolName);\n if (!found) continue;\n }\n\n // inputText — case-sensitive match against the embedding input text.\n // Same rationale as userMessage above: fixture authors specify exact strings.\n if (match.inputText !== undefined) {\n const embeddingInput = effective.embeddingInput;\n if (!embeddingInput) continue;\n if (typeof match.inputText === \"string\") {\n if (useExactMatch) {\n if (embeddingInput !== match.inputText) continue;\n } else {\n if (!embeddingInput.includes(match.inputText)) continue;\n }\n } else {\n match.inputText.lastIndex = 0;\n if (!match.inputText.test(embeddingInput)) continue;\n }\n }\n\n // responseFormat — exact string match against request response_format.type\n if (match.responseFormat !== undefined) {\n const reqType = effective.response_format?.type;\n if (reqType !== match.responseFormat) continue;\n }\n\n // model — exact match or prefix + dash-digit boundary for strings (so that\n // \"claude-opus-4\" matches \"claude-opus-4-20250514\" but \"gpt-4\" does NOT\n // match \"gpt-4o\" and \"gpt-4o\" does NOT match \"gpt-4o-mini\"), regexp unchanged\n if (match.model !== undefined) {\n if (typeof match.model === \"string\") {\n if (effective.model !== match.model) {\n if (!effective.model?.startsWith(match.model)) continue;\n const rest = effective.model.slice(match.model.length);\n if (!/^-\\d/.test(rest)) continue;\n }\n } else {\n match.model.lastIndex = 0;\n if (!match.model.test(effective.model ?? \"\")) continue;\n }\n }\n\n // hasToolResult — request-SHAPE predicate: does the conversation contain a\n // tool result message? Must be evaluated with the other shape predicates\n // ABOVE the sequence/turn state gates so that a fixture whose shape never\n // matched is not miscounted as \"skipped by sequence/turn state\".\n if (match.hasToolResult !== undefined) {\n const hasTool = effective.messages.some((m) => m.role === \"tool\");\n if (hasTool !== match.hasToolResult) continue;\n }\n\n // At this point every SHAPE predicate above has passed. The sequenceIndex\n // and turnIndex gates below reject based on per-test count / turn STATE,\n // not request shape — a fixture that fails only these is a \"candidate that\n // was skipped by sequence/turn state\", which we count separately so callers\n // can disambiguate the strict-mode 503 message.\n let skippedByState = false;\n\n // sequenceIndex — check against the fixture's match count\n if (match.sequenceIndex !== undefined && matchCounts !== undefined) {\n const count = matchCounts.get(fixture) ?? 0;\n if (count !== match.sequenceIndex) skippedByState = true;\n }\n\n if (!skippedByState && match.turnIndex !== undefined) {\n const assistantCount = effective.messages.filter((m) => m.role === \"assistant\").length;\n if (assistantCount !== match.turnIndex) skippedByState = true;\n }\n\n if (skippedByState) {\n skippedBySequenceOrTurn++;\n continue;\n }\n\n return { fixture, skippedBySequenceOrTurn };\n }\n\n return { fixture: null, skippedBySequenceOrTurn };\n}\n\n/**\n * Match a fixture against a request, returning the fixture or `null`. Thin\n * wrapper over {@link matchFixtureDiagnostic} that discards the skip diagnostic\n * — preserves the historical signature for all existing callers.\n */\nexport function matchFixture(\n fixtures: Fixture[],\n req: ChatCompletionRequest,\n matchCounts?: Map<Fixture, number>,\n requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest,\n): Fixture | null {\n return matchFixtureDiagnostic(fixtures, req, matchCounts, requestTransform).fixture;\n}\n"],"mappings":"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"}
package/dist/server.cjs CHANGED
@@ -20,9 +20,10 @@ const require_transcription = require('./transcription.cjs');
20
20
  const require_images = require('./images.cjs');
21
21
  const require_speech = require('./speech.cjs');
22
22
  const require_video = require('./video.cjs');
23
- const require_elevenlabs_audio = require('./elevenlabs-audio.cjs');
24
- const require_fal = require('./fal.cjs');
25
23
  const require_fal_audio = require('./fal-audio.cjs');
24
+ const require_fal = require('./fal.cjs');
25
+ const require_openrouter_video = require('./openrouter-video.cjs');
26
+ const require_elevenlabs_audio = require('./elevenlabs-audio.cjs');
26
27
  const require_ollama = require('./ollama.cjs');
27
28
  const require_cohere = require('./cohere.cjs');
28
29
  const require_search = require('./search.cjs');
@@ -55,7 +56,6 @@ const SPEECH_PATH = "/v1/audio/speech";
55
56
  const TRANSCRIPTIONS_PATH = "/v1/audio/transcriptions";
56
57
  const TRANSLATIONS_PATH = "/v1/audio/translations";
57
58
  const VIDEOS_PATH = "/v1/videos";
58
- const VIDEOS_STATUS_RE = /^\/v1\/videos\/([^/]+)$/;
59
59
  const GEMINI_PREDICT_RE = /^\/v1beta\/models\/([^:]+):predict$/;
60
60
  const ELEVENLABS_SOUND_GENERATION_PATH = "/v1/sound-generation";
61
61
  const ELEVENLABS_TTS_RE = /^\/v1\/text-to-speech\/([^/]+)$/;
@@ -106,6 +106,8 @@ const OLLAMA_GENERATE_PATH = "/api/generate";
106
106
  const OLLAMA_EMBEDDINGS_PATH = "/api/embeddings";
107
107
  const OLLAMA_EMBED_PATH = "/api/embed";
108
108
  const OLLAMA_TAGS_PATH = "/api/tags";
109
+ const OPENROUTER_VIDEOS_PATH = "/api/v1/videos";
110
+ const OPENROUTER_VIDEO_MODELS_PATH = "/api/v1/videos/models";
109
111
  const HEALTH_PATH = "/health";
110
112
  const READY_PATH = "/ready";
111
113
  const MODELS_PATH = "/v1/models";
@@ -144,10 +146,11 @@ const CONTROL_PREFIX = "/__aimock";
144
146
  * `aimock_fixtures_loaded` gauge. Shared by `/reset/fixtures` and the
145
147
  * deprecated `/reset` alias.
146
148
  */
147
- function performFixturesReset(fixtures, journal, videoStates, defaults) {
149
+ function performFixturesReset(fixtures, journal, videoStates, openRouterVideoJobs, defaults) {
148
150
  fixtures.length = 0;
149
151
  journal.clear();
150
152
  videoStates.clear();
153
+ openRouterVideoJobs.clear();
151
154
  require_fal_audio.falJobs.clear();
152
155
  require_fal.falQueueStates.clear();
153
156
  require_gemini_interactions.resetInteractionCounter();
@@ -158,7 +161,7 @@ function performFixturesReset(fixtures, journal, videoStates, defaults) {
158
161
  * Handle requests under `/__aimock/`. Returns `true` if the request was
159
162
  * handled, `false` if the path doesn't match the control prefix.
160
163
  */
161
- async function handleControlAPI(req, res, pathname, fixtures, journal, videoStates, defaults) {
164
+ async function handleControlAPI(req, res, pathname, fixtures, journal, videoStates, openRouterVideoJobs, defaults) {
162
165
  if (!pathname.startsWith(CONTROL_PREFIX)) return false;
163
166
  const subPath = pathname.slice(9);
164
167
  setCorsHeaders(res);
@@ -222,7 +225,7 @@ async function handleControlAPI(req, res, pathname, fixtures, journal, videoStat
222
225
  return true;
223
226
  }
224
227
  if (subPath === "/reset/fixtures" && req.method === "POST") {
225
- performFixturesReset(fixtures, journal, videoStates, defaults);
228
+ performFixturesReset(fixtures, journal, videoStates, openRouterVideoJobs, defaults);
226
229
  res.writeHead(200, { "Content-Type": "application/json" });
227
230
  res.end(JSON.stringify({ reset: true }));
228
231
  return true;
@@ -234,7 +237,7 @@ async function handleControlAPI(req, res, pathname, fixtures, journal, videoStat
234
237
  return true;
235
238
  }
236
239
  if (subPath === "/reset" && req.method === "POST") {
237
- performFixturesReset(fixtures, journal, videoStates, defaults);
240
+ performFixturesReset(fixtures, journal, videoStates, openRouterVideoJobs, defaults);
238
241
  const deprecation = "POST /__aimock/reset is deprecated; use POST /__aimock/reset/fixtures (full reset) or POST /__aimock/reset/journal (journal only)";
239
242
  defaults.logger.warn("POST /__aimock/reset is deprecated; use /__aimock/reset/fixtures or /__aimock/reset/journal");
240
243
  res.writeHead(200, {
@@ -376,7 +379,7 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
376
379
  body._endpointType = "chat";
377
380
  body._context = require_helpers.getContext(req);
378
381
  const testId = require_helpers.getTestId(req);
379
- const fixture = require_router.matchFixture(fixtures, body, journal.getFixtureMatchCountsForTest(testId), defaults.requestTransform);
382
+ const { fixture, skippedBySequenceOrTurn } = require_router.matchFixtureDiagnostic(fixtures, body, journal.getFixtureMatchCountsForTest(testId), defaults.requestTransform);
380
383
  if (fixture) {
381
384
  journal.incrementFixtureMatchCount(fixture, fixtures, testId);
382
385
  defaults.logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);
@@ -403,8 +406,8 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
403
406
  if (!fixture) {
404
407
  if (require_helpers.resolveStrictMode(defaults.strict, req.headers)) {
405
408
  const strictStatus = 503;
406
- const strictMessage = "Strict mode: no fixture matched";
407
- defaults.logger.error(`STRICT: No fixture matched for ${req.method ?? "POST"} ${req.url ?? COMPLETIONS_PATH}`);
409
+ const strictMessage = require_helpers.strictNoMatchMessage(skippedBySequenceOrTurn);
410
+ defaults.logger.error(require_helpers.strictNoMatchLogLine(req.method ?? "POST", req.url ?? COMPLETIONS_PATH, skippedBySequenceOrTurn));
408
411
  journal.add({
409
412
  method: req.method ?? "POST",
410
413
  path: req.url ?? COMPLETIONS_PATH,
@@ -514,6 +517,8 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
514
517
  if (require_helpers.isContentWithToolCallsResponse(response)) {
515
518
  if (response.webSearches?.length) defaults.logger.warn("webSearches in fixture response are not supported for Chat Completions API — ignoring");
516
519
  const overrides = require_helpers.extractOverrides(response);
520
+ const effectiveStrict = require_helpers.resolveStrictMode(defaults.strict, req.headers);
521
+ const effReasoning = require_helpers.resolveReasoningForModel(response.reasoning, body.model, effectiveStrict, defaults.logger);
517
522
  const journalEntry = journal.add({
518
523
  method: req.method ?? "POST",
519
524
  path: req.url ?? COMPLETIONS_PATH,
@@ -525,11 +530,11 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
525
530
  }
526
531
  });
527
532
  if (body.stream !== true) {
528
- const completion = require_helpers.buildContentWithToolCallsCompletion(response.content, response.toolCalls, body.model, response.reasoning, overrides, body.messages);
533
+ const completion = require_helpers.buildContentWithToolCallsCompletion(response.content, response.toolCalls, body.model, effReasoning, overrides, body.messages);
529
534
  res.writeHead(200, { "Content-Type": "application/json" });
530
535
  res.end(JSON.stringify(completion));
531
536
  } else {
532
- const chunks = require_helpers.buildContentWithToolCallsChunks(response.content, response.toolCalls, body.model, chunkSize, response.reasoning, overrides);
537
+ const chunks = require_helpers.buildContentWithToolCallsChunks(response.content, response.toolCalls, body.model, chunkSize, effReasoning, overrides);
533
538
  const completionText = response.content + response.toolCalls.map((tc) => tc.name + tc.arguments).join("");
534
539
  const usageChunk = includeUsage ? require_helpers.buildUsageChunk(chunks[0]?.id ?? "chatcmpl-unknown", overrides?.model ?? body.model, chunks[0]?.created ?? Math.floor(Date.now() / 1e3), overrides?.usage ? {
535
540
  prompt_tokens: overrides.usage.prompt_tokens ?? 0,
@@ -561,6 +566,8 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
561
566
  if (require_helpers.isTextResponse(response)) {
562
567
  if (response.webSearches?.length) defaults.logger.warn("webSearches in fixture response are not supported for Chat Completions API — ignoring");
563
568
  const overrides = require_helpers.extractOverrides(response);
569
+ const effectiveStrict = require_helpers.resolveStrictMode(defaults.strict, req.headers);
570
+ const effReasoning = require_helpers.resolveReasoningForModel(response.reasoning, body.model, effectiveStrict, defaults.logger);
564
571
  const journalEntry = journal.add({
565
572
  method: req.method ?? "POST",
566
573
  path: req.url ?? COMPLETIONS_PATH,
@@ -572,11 +579,11 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
572
579
  }
573
580
  });
574
581
  if (body.stream !== true) {
575
- const completion = require_helpers.buildTextCompletion(response.content, body.model, response.reasoning, overrides, body.messages);
582
+ const completion = require_helpers.buildTextCompletion(response.content, body.model, effReasoning, overrides, body.messages);
576
583
  res.writeHead(200, { "Content-Type": "application/json" });
577
584
  res.end(JSON.stringify(completion));
578
585
  } else {
579
- const chunks = require_helpers.buildTextChunks(response.content, body.model, chunkSize, response.reasoning, overrides);
586
+ const chunks = require_helpers.buildTextChunks(response.content, body.model, chunkSize, effReasoning, overrides);
580
587
  const usageChunk = includeUsage ? require_helpers.buildUsageChunk(chunks[0]?.id ?? "chatcmpl-unknown", overrides?.model ?? body.model, chunks[0]?.created ?? Math.floor(Date.now() / 1e3), overrides?.usage ? {
581
588
  prompt_tokens: overrides.usage.prompt_tokens ?? 0,
582
589
  completion_tokens: overrides.usage.completion_tokens ?? 0,
@@ -607,6 +614,8 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
607
614
  if (require_helpers.isToolCallResponse(response)) {
608
615
  if (response.webSearches?.length) defaults.logger.warn("webSearches in fixture response are not supported for Chat Completions API — ignoring");
609
616
  const overrides = require_helpers.extractOverrides(response);
617
+ const effectiveStrict = require_helpers.resolveStrictMode(defaults.strict, req.headers);
618
+ const effReasoning = require_helpers.resolveReasoningForModel(response.reasoning, body.model, effectiveStrict, defaults.logger);
610
619
  const journalEntry = journal.add({
611
620
  method: req.method ?? "POST",
612
621
  path: req.url ?? COMPLETIONS_PATH,
@@ -618,11 +627,11 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
618
627
  }
619
628
  });
620
629
  if (body.stream !== true) {
621
- const completion = require_helpers.buildToolCallCompletion(response.toolCalls, body.model, overrides, body.messages);
630
+ const completion = require_helpers.buildToolCallCompletion(response.toolCalls, body.model, effReasoning, overrides, body.messages);
622
631
  res.writeHead(200, { "Content-Type": "application/json" });
623
632
  res.end(JSON.stringify(completion));
624
633
  } else {
625
- const chunks = require_helpers.buildToolCallChunks(response.toolCalls, body.model, chunkSize, overrides);
634
+ const chunks = require_helpers.buildToolCallChunks(response.toolCalls, body.model, chunkSize, effReasoning, overrides);
626
635
  const completionText = response.toolCalls.map((tc) => tc.name + tc.arguments).join("");
627
636
  const usageChunk = includeUsage ? require_helpers.buildUsageChunk(chunks[0]?.id ?? "chatcmpl-unknown", overrides?.model ?? body.model, chunks[0]?.created ?? Math.floor(Date.now() / 1e3), overrides?.usage ? {
628
637
  prompt_tokens: overrides.usage.prompt_tokens ?? 0,
@@ -692,6 +701,9 @@ async function createServer(fixtures, options, mounts, serviceFixtures) {
692
701
  },
693
702
  get falQueue() {
694
703
  return serverOptions.falQueue;
704
+ },
705
+ get openRouterVideo() {
706
+ return serverOptions.openRouterVideo;
695
707
  }
696
708
  };
697
709
  if (options?.chaos) {
@@ -711,11 +723,31 @@ async function createServer(fixtures, options, mounts, serviceFixtures) {
711
723
  ];
712
724
  for (const { name, value } of chaosRates) if (value !== void 0 && (value < 0 || value > 1)) logger.warn(`Chaos ${name} (${value}) is outside 0-1 range — will be clamped at runtime`);
713
725
  }
726
+ for (const { name, config } of [{
727
+ name: "falQueue",
728
+ config: options?.falQueue
729
+ }, {
730
+ name: "openRouterVideo",
731
+ config: options?.openRouterVideo
732
+ }]) {
733
+ if (!config) continue;
734
+ for (const field of ["pollsBeforeInProgress", "pollsBeforeCompleted"]) {
735
+ const value = config[field];
736
+ if (value === void 0) continue;
737
+ if (!Number.isFinite(value)) logger.warn(`${name}.${field} (${value}) is not a finite number — treating as unset`);
738
+ else if (!Number.isInteger(value) || value < 0) logger.warn(`${name}.${field} (${value}) is not a non-negative integer — flooring/clamping to a non-negative integer`);
739
+ }
740
+ }
741
+ {
742
+ const cap = options?.record?.openRouterVideo?.maxContentBytes;
743
+ if (cap !== void 0 && (!Number.isInteger(cap) || cap < 0)) logger.warn(`record.openRouterVideo.maxContentBytes (${cap}) is not a non-negative integer — using the default cap (${require_openrouter_video.OPENROUTER_VIDEO_DEFAULT_MAX_CONTENT_BYTES})`);
744
+ }
714
745
  const journal = new require_journal.Journal({
715
746
  maxEntries: options?.journalMaxEntries ?? 1e3,
716
747
  fixtureCountsMaxTestIds: options?.fixtureCountsMaxTestIds ?? 500
717
748
  });
718
749
  const videoStates = new require_video.VideoStateMap();
750
+ const openRouterVideoJobs = new require_openrouter_video.OpenRouterVideoJobMap();
719
751
  if (mounts) for (const { handler } of mounts) {
720
752
  if (handler.setJournal) handler.setJournal(journal);
721
753
  if (registry && handler.setRegistry) handler.setRegistry(registry);
@@ -762,7 +794,7 @@ async function createServer(fixtures, options, mounts, serviceFixtures) {
762
794
  }
763
795
  });
764
796
  if (pathname.startsWith(CONTROL_PREFIX)) {
765
- await handleControlAPI(req, res, pathname, fixtures, journal, videoStates, defaults);
797
+ await handleControlAPI(req, res, pathname, fixtures, journal, videoStates, openRouterVideoJobs, defaults);
766
798
  return;
767
799
  }
768
800
  if (mounts) {
@@ -826,6 +858,125 @@ async function createServer(fixtures, options, mounts, serviceFixtures) {
826
858
  res.end(JSON.stringify({ models }));
827
859
  return;
828
860
  }
861
+ const openRouterVideoContentMatch = pathname.match(require_metrics.OPENROUTER_VIDEO_CONTENT_RE);
862
+ if (openRouterVideoContentMatch && req.method === "GET") {
863
+ try {
864
+ await require_openrouter_video.handleOpenRouterVideoContent(req, res, openRouterVideoContentMatch[1], journal, defaults, setCorsHeaders, openRouterVideoJobs);
865
+ } catch (err) {
866
+ const msg = err instanceof Error ? err.message : "Internal error";
867
+ defaults.logger.error(`openrouter-video content: ${msg}`);
868
+ if (!res.headersSent) {
869
+ try {
870
+ journal.add({
871
+ method: req.method ?? "GET",
872
+ path: req.url ?? pathname,
873
+ headers: require_helpers.flattenHeaders(req.headers),
874
+ body: null,
875
+ response: {
876
+ status: 500,
877
+ fixture: null
878
+ }
879
+ });
880
+ } catch (jErr) {
881
+ defaults.logger.warn(`openrouter-video content: journal write failed after handler error: ${jErr instanceof Error ? jErr.message : String(jErr)}`);
882
+ }
883
+ require_sse_writer.writeErrorResponse(res, 500, JSON.stringify({ error: {
884
+ message: msg,
885
+ type: "server_error"
886
+ } }));
887
+ } else if (!res.writableEnded) res.destroy();
888
+ }
889
+ return;
890
+ }
891
+ if (pathname === OPENROUTER_VIDEO_MODELS_PATH && req.method === "GET") {
892
+ try {
893
+ await require_openrouter_video.handleOpenRouterVideoModels(req, res, fixtures, journal, defaults, setCorsHeaders);
894
+ } catch (err) {
895
+ const msg = err instanceof Error ? err.message : "Internal error";
896
+ defaults.logger.error(`openrouter-video models: ${msg}`);
897
+ if (!res.headersSent) {
898
+ try {
899
+ journal.add({
900
+ method: req.method ?? "GET",
901
+ path: req.url ?? pathname,
902
+ headers: require_helpers.flattenHeaders(req.headers),
903
+ body: null,
904
+ response: {
905
+ status: 500,
906
+ fixture: null
907
+ }
908
+ });
909
+ } catch (jErr) {
910
+ defaults.logger.warn(`openrouter-video models: journal write failed after handler error: ${jErr instanceof Error ? jErr.message : String(jErr)}`);
911
+ }
912
+ require_sse_writer.writeErrorResponse(res, 500, JSON.stringify({ error: {
913
+ message: msg,
914
+ type: "server_error"
915
+ } }));
916
+ } else if (!res.writableEnded) res.destroy();
917
+ }
918
+ return;
919
+ }
920
+ const openRouterVideoStatusMatch = pathname.match(require_metrics.OPENROUTER_VIDEO_STATUS_RE);
921
+ if (openRouterVideoStatusMatch && req.method === "GET") {
922
+ try {
923
+ await require_openrouter_video.handleOpenRouterVideoStatus(req, res, openRouterVideoStatusMatch[1], fixtures, journal, defaults, setCorsHeaders, openRouterVideoJobs);
924
+ } catch (err) {
925
+ const msg = err instanceof Error ? err.message : "Internal error";
926
+ defaults.logger.error(`openrouter-video status: ${msg}`);
927
+ if (!res.headersSent) {
928
+ try {
929
+ journal.add({
930
+ method: req.method ?? "GET",
931
+ path: req.url ?? pathname,
932
+ headers: require_helpers.flattenHeaders(req.headers),
933
+ body: null,
934
+ response: {
935
+ status: 500,
936
+ fixture: null
937
+ }
938
+ });
939
+ } catch (jErr) {
940
+ defaults.logger.warn(`openrouter-video status: journal write failed after handler error: ${jErr instanceof Error ? jErr.message : String(jErr)}`);
941
+ }
942
+ require_sse_writer.writeErrorResponse(res, 500, JSON.stringify({ error: {
943
+ message: msg,
944
+ type: "server_error"
945
+ } }));
946
+ } else if (!res.writableEnded) res.destroy();
947
+ }
948
+ return;
949
+ }
950
+ if (pathname === OPENROUTER_VIDEOS_PATH && req.method === "POST") {
951
+ setCorsHeaders(res);
952
+ try {
953
+ await require_openrouter_video.handleOpenRouterVideoCreate(req, res, await require_helpers.readBody(req), fixtures, journal, defaults, setCorsHeaders, openRouterVideoJobs);
954
+ } catch (err) {
955
+ const msg = err instanceof Error ? err.message : "Internal error";
956
+ defaults.logger.error(`openrouter-video submit: ${msg}`);
957
+ if (!res.headersSent) {
958
+ try {
959
+ journal.add({
960
+ method: req.method ?? "POST",
961
+ path: req.url ?? pathname,
962
+ headers: require_helpers.flattenHeaders(req.headers),
963
+ body: null,
964
+ response: {
965
+ status: 500,
966
+ fixture: null
967
+ }
968
+ });
969
+ } catch (jErr) {
970
+ defaults.logger.warn(`openrouter-video submit: journal write failed after handler error: ${jErr instanceof Error ? jErr.message : String(jErr)}`);
971
+ }
972
+ require_sse_writer.writeErrorResponse(res, 500, JSON.stringify({ error: {
973
+ message: msg,
974
+ type: "server_error"
975
+ } }));
976
+ } else if (!res.writableEnded) res.destroy();
977
+ }
978
+ return;
979
+ }
829
980
  let azureDeploymentId;
830
981
  const azureMatch = pathname.match(AZURE_DEPLOYMENT_RE);
831
982
  if (azureMatch && req.method === "POST") {
@@ -1101,7 +1252,7 @@ async function createServer(fixtures, options, mounts, serviceFixtures) {
1101
1252
  }
1102
1253
  return;
1103
1254
  }
1104
- const videoStatusMatch = pathname.match(VIDEOS_STATUS_RE);
1255
+ const videoStatusMatch = pathname.match(require_metrics.OPENAI_VIDEO_STATUS_RE);
1105
1256
  if (videoStatusMatch && req.method === "GET") {
1106
1257
  const videoId = videoStatusMatch[1];
1107
1258
  require_video.handleVideoStatus(req, res, videoId, journal, defaults, setCorsHeaders, videoStates);
@@ -1523,6 +1674,8 @@ async function createServer(fixtures, options, mounts, serviceFixtures) {
1523
1674
  server.close = function(callback) {
1524
1675
  for (const ws of activeConnections) ws.close(1001, "Server shutting down");
1525
1676
  activeConnections.clear();
1677
+ videoStates.clear();
1678
+ openRouterVideoJobs.clear();
1526
1679
  originalClose(callback);
1527
1680
  return this;
1528
1681
  };
@@ -1543,7 +1696,8 @@ async function createServer(fixtures, options, mounts, serviceFixtures) {
1543
1696
  journal,
1544
1697
  url,
1545
1698
  defaults,
1546
- videoStates
1699
+ videoStates,
1700
+ openRouterVideoJobs
1547
1701
  });
1548
1702
  });
1549
1703
  });