@copilotkit/aimock 1.45.1 → 1.46.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 (425) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +13 -0
  4. package/README.md +7 -1
  5. package/dist/agui-mock.cjs +25 -25
  6. package/dist/agui-mock.cjs.map +1 -1
  7. package/dist/agui-mock.d.cts.map +1 -1
  8. package/dist/agui-mock.d.ts.map +1 -1
  9. package/dist/agui-mock.js +25 -25
  10. package/dist/agui-mock.js.map +1 -1
  11. package/dist/agui-types.d.cts.map +1 -1
  12. package/dist/aimock-cli.cjs +15 -1
  13. package/dist/aimock-cli.cjs.map +1 -1
  14. package/dist/aimock-cli.d.cts.map +1 -1
  15. package/dist/aimock-cli.d.ts.map +1 -1
  16. package/dist/aimock-cli.js +16 -2
  17. package/dist/aimock-cli.js.map +1 -1
  18. package/dist/api-key-auth.cjs +10 -9
  19. package/dist/api-key-auth.cjs.map +1 -1
  20. package/dist/api-key-auth.d.cts.map +1 -1
  21. package/dist/api-key-auth.d.ts.map +1 -1
  22. package/dist/api-key-auth.js +11 -10
  23. package/dist/api-key-auth.js.map +1 -1
  24. package/dist/batches.cjs +2 -4
  25. package/dist/batches.cjs.map +1 -1
  26. package/dist/batches.d.cts.map +1 -1
  27. package/dist/batches.d.ts.map +1 -1
  28. package/dist/batches.js +2 -4
  29. package/dist/batches.js.map +1 -1
  30. package/dist/bedrock-converse.cjs +61 -80
  31. package/dist/bedrock-converse.cjs.map +1 -1
  32. package/dist/bedrock-converse.d.cts.map +1 -1
  33. package/dist/bedrock-converse.d.ts.map +1 -1
  34. package/dist/bedrock-converse.js +58 -77
  35. package/dist/bedrock-converse.js.map +1 -1
  36. package/dist/bedrock.cjs +65 -97
  37. package/dist/bedrock.cjs.map +1 -1
  38. package/dist/bedrock.d.cts.map +1 -1
  39. package/dist/bedrock.d.ts.map +1 -1
  40. package/dist/bedrock.js +62 -94
  41. package/dist/bedrock.js.map +1 -1
  42. package/dist/byteplus-video.cjs +11 -10
  43. package/dist/byteplus-video.cjs.map +1 -1
  44. package/dist/byteplus-video.d.cts.map +1 -1
  45. package/dist/byteplus-video.d.ts.map +1 -1
  46. package/dist/byteplus-video.js +5 -4
  47. package/dist/byteplus-video.js.map +1 -1
  48. package/dist/chaos.cjs +5 -4
  49. package/dist/chaos.cjs.map +1 -1
  50. package/dist/chaos.d.cts.map +1 -1
  51. package/dist/chaos.d.ts.map +1 -1
  52. package/dist/chaos.js +5 -4
  53. package/dist/chaos.js.map +1 -1
  54. package/dist/cli.cjs +15 -0
  55. package/dist/cli.cjs.map +1 -1
  56. package/dist/cli.js +15 -0
  57. package/dist/cli.js.map +1 -1
  58. package/dist/cohere.cjs +49 -74
  59. package/dist/cohere.cjs.map +1 -1
  60. package/dist/cohere.d.cts.map +1 -1
  61. package/dist/cohere.d.ts.map +1 -1
  62. package/dist/cohere.js +47 -72
  63. package/dist/cohere.js.map +1 -1
  64. package/dist/config-loader.cjs +29 -1
  65. package/dist/config-loader.cjs.map +1 -1
  66. package/dist/config-loader.d.cts +7 -1
  67. package/dist/config-loader.d.cts.map +1 -1
  68. package/dist/config-loader.d.ts +7 -1
  69. package/dist/config-loader.d.ts.map +1 -1
  70. package/dist/config-loader.js +29 -2
  71. package/dist/config-loader.js.map +1 -1
  72. package/dist/count-tokens.cjs +146 -0
  73. package/dist/count-tokens.cjs.map +1 -0
  74. package/dist/count-tokens.js +146 -0
  75. package/dist/count-tokens.js.map +1 -0
  76. package/dist/elevenlabs-audio.cjs +36 -34
  77. package/dist/elevenlabs-audio.cjs.map +1 -1
  78. package/dist/elevenlabs-audio.d.cts.map +1 -1
  79. package/dist/elevenlabs-audio.d.ts.map +1 -1
  80. package/dist/elevenlabs-audio.js +33 -31
  81. package/dist/elevenlabs-audio.js.map +1 -1
  82. package/dist/elevenlabs-voice.cjs +38 -37
  83. package/dist/elevenlabs-voice.cjs.map +1 -1
  84. package/dist/elevenlabs-voice.d.cts.map +1 -1
  85. package/dist/elevenlabs-voice.d.ts.map +1 -1
  86. package/dist/elevenlabs-voice.js +33 -32
  87. package/dist/elevenlabs-voice.js.map +1 -1
  88. package/dist/embedding-request.cjs +16 -0
  89. package/dist/embedding-request.cjs.map +1 -0
  90. package/dist/embedding-request.js +14 -0
  91. package/dist/embedding-request.js.map +1 -0
  92. package/dist/embeddings.cjs +30 -20
  93. package/dist/embeddings.cjs.map +1 -1
  94. package/dist/embeddings.d.cts.map +1 -1
  95. package/dist/embeddings.d.ts.map +1 -1
  96. package/dist/embeddings.js +30 -20
  97. package/dist/embeddings.js.map +1 -1
  98. package/dist/fal-audio.cjs +45 -43
  99. package/dist/fal-audio.cjs.map +1 -1
  100. package/dist/fal-audio.d.cts.map +1 -1
  101. package/dist/fal-audio.d.ts.map +1 -1
  102. package/dist/fal-audio.js +42 -40
  103. package/dist/fal-audio.js.map +1 -1
  104. package/dist/fal.cjs +17 -15
  105. package/dist/fal.cjs.map +1 -1
  106. package/dist/fal.js +16 -14
  107. package/dist/fal.js.map +1 -1
  108. package/dist/files.cjs +2 -4
  109. package/dist/files.cjs.map +1 -1
  110. package/dist/files.d.cts.map +1 -1
  111. package/dist/files.d.ts.map +1 -1
  112. package/dist/files.js +2 -4
  113. package/dist/files.js.map +1 -1
  114. package/dist/fine-tuning.cjs +2 -4
  115. package/dist/fine-tuning.cjs.map +1 -1
  116. package/dist/fine-tuning.d.cts.map +1 -1
  117. package/dist/fine-tuning.d.ts.map +1 -1
  118. package/dist/fine-tuning.js +2 -4
  119. package/dist/fine-tuning.js.map +1 -1
  120. package/dist/fixture-loader.cjs +37 -42
  121. package/dist/fixture-loader.cjs.map +1 -1
  122. package/dist/fixture-loader.d.cts.map +1 -1
  123. package/dist/fixture-loader.d.ts.map +1 -1
  124. package/dist/fixture-loader.js +37 -42
  125. package/dist/fixture-loader.js.map +1 -1
  126. package/dist/gemini-embeddings.cjs +26 -24
  127. package/dist/gemini-embeddings.cjs.map +1 -1
  128. package/dist/gemini-embeddings.js +26 -24
  129. package/dist/gemini-embeddings.js.map +1 -1
  130. package/dist/gemini-interactions.cjs +50 -35
  131. package/dist/gemini-interactions.cjs.map +1 -1
  132. package/dist/gemini-interactions.d.cts +0 -1
  133. package/dist/gemini-interactions.d.cts.map +1 -1
  134. package/dist/gemini-interactions.d.ts +0 -1
  135. package/dist/gemini-interactions.d.ts.map +1 -1
  136. package/dist/gemini-interactions.js +49 -34
  137. package/dist/gemini-interactions.js.map +1 -1
  138. package/dist/gemini.cjs +57 -67
  139. package/dist/gemini.cjs.map +1 -1
  140. package/dist/gemini.d.cts.map +1 -1
  141. package/dist/gemini.d.ts.map +1 -1
  142. package/dist/gemini.js +56 -67
  143. package/dist/gemini.js.map +1 -1
  144. package/dist/grok-video.cjs +14 -12
  145. package/dist/grok-video.cjs.map +1 -1
  146. package/dist/grok-video.d.cts.map +1 -1
  147. package/dist/grok-video.d.ts.map +1 -1
  148. package/dist/grok-video.js +8 -6
  149. package/dist/grok-video.js.map +1 -1
  150. package/dist/helpers.cjs +11 -68
  151. package/dist/helpers.cjs.map +1 -1
  152. package/dist/helpers.d.cts.map +1 -1
  153. package/dist/helpers.d.ts.map +1 -1
  154. package/dist/helpers.js +12 -67
  155. package/dist/helpers.js.map +1 -1
  156. package/dist/images.cjs +30 -28
  157. package/dist/images.cjs.map +1 -1
  158. package/dist/images.d.cts.map +1 -1
  159. package/dist/images.d.ts.map +1 -1
  160. package/dist/images.js +27 -25
  161. package/dist/images.js.map +1 -1
  162. package/dist/index.d.cts +2 -2
  163. package/dist/index.d.ts +2 -2
  164. package/dist/journal.cjs +3 -1
  165. package/dist/journal.cjs.map +1 -1
  166. package/dist/journal.d.cts.map +1 -1
  167. package/dist/journal.d.ts.map +1 -1
  168. package/dist/journal.js +2 -1
  169. package/dist/journal.js.map +1 -1
  170. package/dist/llmock.cjs +3 -0
  171. package/dist/llmock.cjs.map +1 -1
  172. package/dist/llmock.d.cts.map +1 -1
  173. package/dist/llmock.d.ts.map +1 -1
  174. package/dist/llmock.js +3 -0
  175. package/dist/llmock.js.map +1 -1
  176. package/dist/mcp-handler.cjs +7 -3
  177. package/dist/mcp-handler.cjs.map +1 -1
  178. package/dist/mcp-handler.js +7 -3
  179. package/dist/mcp-handler.js.map +1 -1
  180. package/dist/mcp-recorder.cjs +2 -1
  181. package/dist/mcp-recorder.cjs.map +1 -1
  182. package/dist/mcp-recorder.js +3 -2
  183. package/dist/mcp-recorder.js.map +1 -1
  184. package/dist/messages.cjs +111 -96
  185. package/dist/messages.cjs.map +1 -1
  186. package/dist/messages.d.cts.map +1 -1
  187. package/dist/messages.d.ts.map +1 -1
  188. package/dist/messages.js +109 -95
  189. package/dist/messages.js.map +1 -1
  190. package/dist/metrics.cjs +5 -2
  191. package/dist/metrics.cjs.map +1 -1
  192. package/dist/metrics.d.cts.map +1 -1
  193. package/dist/metrics.d.ts.map +1 -1
  194. package/dist/metrics.js +6 -3
  195. package/dist/metrics.js.map +1 -1
  196. package/dist/model-utils.cjs +1 -0
  197. package/dist/model-utils.cjs.map +1 -1
  198. package/dist/model-utils.js +1 -1
  199. package/dist/model-utils.js.map +1 -1
  200. package/dist/moderation.cjs +15 -12
  201. package/dist/moderation.cjs.map +1 -1
  202. package/dist/moderation.js +14 -11
  203. package/dist/moderation.js.map +1 -1
  204. package/dist/ollama.cjs +128 -68
  205. package/dist/ollama.cjs.map +1 -1
  206. package/dist/ollama.d.cts.map +1 -1
  207. package/dist/ollama.d.ts.map +1 -1
  208. package/dist/ollama.js +124 -64
  209. package/dist/ollama.js.map +1 -1
  210. package/dist/openapi.cjs +323 -3
  211. package/dist/openapi.cjs.map +1 -1
  212. package/dist/openapi.d.cts.map +1 -1
  213. package/dist/openapi.d.ts.map +1 -1
  214. package/dist/openapi.js +324 -4
  215. package/dist/openapi.js.map +1 -1
  216. package/dist/openrouter-chat.cjs +11 -155
  217. package/dist/openrouter-chat.cjs.map +1 -1
  218. package/dist/openrouter-chat.js +11 -155
  219. package/dist/openrouter-chat.js.map +1 -1
  220. package/dist/openrouter-video.cjs +51 -74
  221. package/dist/openrouter-video.cjs.map +1 -1
  222. package/dist/openrouter-video.d.cts.map +1 -1
  223. package/dist/openrouter-video.d.ts.map +1 -1
  224. package/dist/openrouter-video.js +37 -60
  225. package/dist/openrouter-video.js.map +1 -1
  226. package/dist/recorder.cjs +176 -95
  227. package/dist/recorder.cjs.map +1 -1
  228. package/dist/recorder.d.cts.map +1 -1
  229. package/dist/recorder.d.ts.map +1 -1
  230. package/dist/recorder.js +176 -95
  231. package/dist/recorder.js.map +1 -1
  232. package/dist/rerank.cjs +13 -11
  233. package/dist/rerank.cjs.map +1 -1
  234. package/dist/rerank.js +12 -10
  235. package/dist/rerank.js.map +1 -1
  236. package/dist/response/catalog/anthropic-page.cjs +39 -0
  237. package/dist/response/catalog/anthropic-page.cjs.map +1 -0
  238. package/dist/response/catalog/anthropic-page.js +38 -0
  239. package/dist/response/catalog/anthropic-page.js.map +1 -0
  240. package/dist/response/catalog/collect.cjs +61 -0
  241. package/dist/response/catalog/collect.cjs.map +1 -0
  242. package/dist/response/catalog/collect.js +59 -0
  243. package/dist/response/catalog/collect.js.map +1 -0
  244. package/dist/response/catalog/gemini-page.cjs +25 -0
  245. package/dist/response/catalog/gemini-page.cjs.map +1 -0
  246. package/dist/response/catalog/gemini-page.js +24 -0
  247. package/dist/response/catalog/gemini-page.js.map +1 -0
  248. package/dist/response/catalog/render.cjs +182 -0
  249. package/dist/response/catalog/render.cjs.map +1 -0
  250. package/dist/response/catalog/render.js +171 -0
  251. package/dist/response/catalog/render.js.map +1 -0
  252. package/dist/response/errors.cjs +225 -0
  253. package/dist/response/errors.cjs.map +1 -0
  254. package/dist/response/errors.js +224 -0
  255. package/dist/response/errors.js.map +1 -0
  256. package/dist/response/finish.cjs +39 -0
  257. package/dist/response/finish.cjs.map +1 -0
  258. package/dist/response/finish.js +35 -0
  259. package/dist/response/finish.js.map +1 -0
  260. package/dist/response/policy.cjs +126 -0
  261. package/dist/response/policy.cjs.map +1 -0
  262. package/dist/response/policy.js +120 -0
  263. package/dist/response/policy.js.map +1 -0
  264. package/dist/response/profiles.cjs +69 -0
  265. package/dist/response/profiles.cjs.map +1 -0
  266. package/dist/response/profiles.js +65 -0
  267. package/dist/response/profiles.js.map +1 -0
  268. package/dist/response/usage/codecs/anthropic.cjs +52 -0
  269. package/dist/response/usage/codecs/anthropic.cjs.map +1 -0
  270. package/dist/response/usage/codecs/anthropic.js +50 -0
  271. package/dist/response/usage/codecs/anthropic.js.map +1 -0
  272. package/dist/response/usage/codecs/cohere.cjs +36 -0
  273. package/dist/response/usage/codecs/cohere.cjs.map +1 -0
  274. package/dist/response/usage/codecs/cohere.js +35 -0
  275. package/dist/response/usage/codecs/cohere.js.map +1 -0
  276. package/dist/response/usage/codecs/converse.cjs +16 -0
  277. package/dist/response/usage/codecs/converse.cjs.map +1 -0
  278. package/dist/response/usage/codecs/converse.js +15 -0
  279. package/dist/response/usage/codecs/converse.js.map +1 -0
  280. package/dist/response/usage/codecs/gemini.cjs +31 -0
  281. package/dist/response/usage/codecs/gemini.cjs.map +1 -0
  282. package/dist/response/usage/codecs/gemini.js +31 -0
  283. package/dist/response/usage/codecs/gemini.js.map +1 -0
  284. package/dist/response/usage/codecs/interactions.cjs +22 -0
  285. package/dist/response/usage/codecs/interactions.cjs.map +1 -0
  286. package/dist/response/usage/codecs/interactions.js +21 -0
  287. package/dist/response/usage/codecs/interactions.js.map +1 -0
  288. package/dist/response/usage/codecs/ollama.cjs +17 -0
  289. package/dist/response/usage/codecs/ollama.cjs.map +1 -0
  290. package/dist/response/usage/codecs/ollama.js +17 -0
  291. package/dist/response/usage/codecs/ollama.js.map +1 -0
  292. package/dist/response/usage/codecs/openai.cjs +52 -0
  293. package/dist/response/usage/codecs/openai.cjs.map +1 -0
  294. package/dist/response/usage/codecs/openai.js +50 -0
  295. package/dist/response/usage/codecs/openai.js.map +1 -0
  296. package/dist/response/usage/codecs/openrouter.cjs +68 -0
  297. package/dist/response/usage/codecs/openrouter.cjs.map +1 -0
  298. package/dist/response/usage/codecs/openrouter.js +67 -0
  299. package/dist/response/usage/codecs/openrouter.js.map +1 -0
  300. package/dist/response/usage/codecs/realtime.cjs +19 -0
  301. package/dist/response/usage/codecs/realtime.cjs.map +1 -0
  302. package/dist/response/usage/codecs/realtime.js +18 -0
  303. package/dist/response/usage/codecs/realtime.js.map +1 -0
  304. package/dist/response/usage/codecs/responses.cjs +21 -0
  305. package/dist/response/usage/codecs/responses.cjs.map +1 -0
  306. package/dist/response/usage/codecs/responses.js +20 -0
  307. package/dist/response/usage/codecs/responses.js.map +1 -0
  308. package/dist/response/usage/estimate.cjs +23 -0
  309. package/dist/response/usage/estimate.cjs.map +1 -0
  310. package/dist/response/usage/estimate.js +21 -0
  311. package/dist/response/usage/estimate.js.map +1 -0
  312. package/dist/response/usage/fields.cjs +72 -0
  313. package/dist/response/usage/fields.cjs.map +1 -0
  314. package/dist/response/usage/fields.js +70 -0
  315. package/dist/response/usage/fields.js.map +1 -0
  316. package/dist/response/usage/recording.cjs +52 -0
  317. package/dist/response/usage/recording.cjs.map +1 -0
  318. package/dist/response/usage/recording.js +52 -0
  319. package/dist/response/usage/recording.js.map +1 -0
  320. package/dist/responses.cjs +45 -58
  321. package/dist/responses.cjs.map +1 -1
  322. package/dist/responses.d.cts.map +1 -1
  323. package/dist/responses.d.ts.map +1 -1
  324. package/dist/responses.js +44 -57
  325. package/dist/responses.js.map +1 -1
  326. package/dist/route-registry.cjs +45 -0
  327. package/dist/route-registry.cjs.map +1 -1
  328. package/dist/route-registry.d.cts.map +1 -1
  329. package/dist/route-registry.d.ts.map +1 -1
  330. package/dist/route-registry.js +41 -1
  331. package/dist/route-registry.js.map +1 -1
  332. package/dist/router.cjs +4 -0
  333. package/dist/router.cjs.map +1 -1
  334. package/dist/router.d.cts.map +1 -1
  335. package/dist/router.d.ts.map +1 -1
  336. package/dist/router.js +3 -0
  337. package/dist/router.js.map +1 -1
  338. package/dist/search.cjs +13 -11
  339. package/dist/search.cjs.map +1 -1
  340. package/dist/search.js +12 -10
  341. package/dist/search.js.map +1 -1
  342. package/dist/server.cjs +222 -146
  343. package/dist/server.cjs.map +1 -1
  344. package/dist/server.d.cts.map +1 -1
  345. package/dist/server.d.ts.map +1 -1
  346. package/dist/server.js +223 -147
  347. package/dist/server.js.map +1 -1
  348. package/dist/speech.cjs +20 -18
  349. package/dist/speech.cjs.map +1 -1
  350. package/dist/speech.d.cts.map +1 -1
  351. package/dist/speech.d.ts.map +1 -1
  352. package/dist/speech.js +19 -17
  353. package/dist/speech.js.map +1 -1
  354. package/dist/sse-writer.cjs +7 -0
  355. package/dist/sse-writer.cjs.map +1 -1
  356. package/dist/sse-writer.d.cts.map +1 -1
  357. package/dist/sse-writer.d.ts.map +1 -1
  358. package/dist/sse-writer.js +7 -1
  359. package/dist/sse-writer.js.map +1 -1
  360. package/dist/stream-collapse.cjs +118 -11
  361. package/dist/stream-collapse.cjs.map +1 -1
  362. package/dist/stream-collapse.d.cts +10 -9
  363. package/dist/stream-collapse.d.cts.map +1 -1
  364. package/dist/stream-collapse.d.ts +10 -9
  365. package/dist/stream-collapse.d.ts.map +1 -1
  366. package/dist/stream-collapse.js +118 -11
  367. package/dist/stream-collapse.js.map +1 -1
  368. package/dist/transcription.cjs +12 -10
  369. package/dist/transcription.cjs.map +1 -1
  370. package/dist/transcription.d.cts.map +1 -1
  371. package/dist/transcription.d.ts.map +1 -1
  372. package/dist/transcription.js +11 -9
  373. package/dist/transcription.js.map +1 -1
  374. package/dist/types.cjs.map +1 -1
  375. package/dist/types.d.cts +62 -3
  376. package/dist/types.d.cts.map +1 -1
  377. package/dist/types.d.ts +62 -3
  378. package/dist/types.d.ts.map +1 -1
  379. package/dist/types.js.map +1 -1
  380. package/dist/vector-mock.cjs +3 -8
  381. package/dist/vector-mock.cjs.map +1 -1
  382. package/dist/vector-mock.d.cts.map +1 -1
  383. package/dist/vector-mock.d.ts.map +1 -1
  384. package/dist/vector-mock.js +3 -8
  385. package/dist/vector-mock.js.map +1 -1
  386. package/dist/vector-stores.cjs +2 -4
  387. package/dist/vector-stores.cjs.map +1 -1
  388. package/dist/vector-stores.d.cts.map +1 -1
  389. package/dist/vector-stores.d.ts.map +1 -1
  390. package/dist/vector-stores.js +2 -4
  391. package/dist/vector-stores.js.map +1 -1
  392. package/dist/veo-video.cjs +16 -14
  393. package/dist/veo-video.cjs.map +1 -1
  394. package/dist/veo-video.js +10 -8
  395. package/dist/veo-video.js.map +1 -1
  396. package/dist/video.cjs +21 -19
  397. package/dist/video.cjs.map +1 -1
  398. package/dist/video.d.cts.map +1 -1
  399. package/dist/video.d.ts.map +1 -1
  400. package/dist/video.js +20 -18
  401. package/dist/video.js.map +1 -1
  402. package/dist/ws-gemini-live.cjs +11 -3
  403. package/dist/ws-gemini-live.cjs.map +1 -1
  404. package/dist/ws-gemini-live.d.cts +0 -1
  405. package/dist/ws-gemini-live.d.cts.map +1 -1
  406. package/dist/ws-gemini-live.d.ts +0 -1
  407. package/dist/ws-gemini-live.d.ts.map +1 -1
  408. package/dist/ws-gemini-live.js +10 -2
  409. package/dist/ws-gemini-live.js.map +1 -1
  410. package/dist/ws-live.cjs +6 -5
  411. package/dist/ws-live.cjs.map +1 -1
  412. package/dist/ws-live.js +5 -4
  413. package/dist/ws-live.js.map +1 -1
  414. package/dist/ws-realtime.cjs +28 -54
  415. package/dist/ws-realtime.cjs.map +1 -1
  416. package/dist/ws-realtime.d.cts.map +1 -1
  417. package/dist/ws-realtime.d.ts.map +1 -1
  418. package/dist/ws-realtime.js +25 -51
  419. package/dist/ws-realtime.js.map +1 -1
  420. package/dist/ws-responses.cjs +8 -6
  421. package/dist/ws-responses.cjs.map +1 -1
  422. package/dist/ws-responses.js +7 -5
  423. package/dist/ws-responses.js.map +1 -1
  424. package/package.json +4 -1
  425. package/skills/write-fixtures/SKILL.md +25 -16
@@ -1,4 +1,8 @@
1
+ const require_errors = require('./response/errors.cjs');
1
2
  const require_helpers = require('./helpers.cjs');
3
+ const require_collect = require('./response/catalog/collect.cjs');
4
+ const require_render = require('./response/catalog/render.cjs');
5
+ const require_openrouter = require('./response/usage/codecs/openrouter.cjs');
2
6
 
3
7
  //#region src/openrouter-chat.ts
4
8
  /**
@@ -63,41 +67,6 @@ function toGenId(id) {
63
67
  return id.startsWith(OPENAI_ID_PREFIX) ? OPENROUTER_ID_PREFIX + id.slice(9) : id;
64
68
  }
65
69
  /**
66
- * `usage` keys that are already accounted for elsewhere and must therefore NOT
67
- * be re-emitted through the forward-compat passthrough below: the canonical
68
- * token counts (and their per-provider aliases), which `resolveUsage` folds into
69
- * `prompt_tokens` / `completion_tokens` / `total_tokens` on the base usage
70
- * object, plus every field this function shapes explicitly.
71
- */
72
- const SHAPED_OR_CANONICAL_USAGE_KEYS = new Set([
73
- "prompt_tokens",
74
- "completion_tokens",
75
- "total_tokens",
76
- "input_tokens",
77
- "output_tokens",
78
- "promptTokenCount",
79
- "candidatesTokenCount",
80
- "totalTokenCount",
81
- "cost",
82
- "cost_details",
83
- "prompt_tokens_details",
84
- "completion_tokens_details",
85
- "is_byok"
86
- ]);
87
- /**
88
- * Keys that must never flow through the passthrough assignment below: a
89
- * JSON-parsed usage frame (fixtures are attacker-adjacent — they mirror upstream
90
- * provider responses) can carry a real own `__proto__`/`constructor`/`prototype`
91
- * key, and a plain `usageExtras[key] = value` for one of those hits the
92
- * prototype setter, corrupting the emitted object's prototype chain instead of
93
- * emitting a data field. Real providers never send these, so skipping is safe.
94
- */
95
- const UNSAFE_PROTO_KEYS = new Set([
96
- "__proto__",
97
- "constructor",
98
- "prototype"
99
- ]);
100
- /**
101
70
  * Resolve the shaping context from a fixture's response overrides and the
102
71
  * winning model slug. `provider` defaults to the winning slug's author;
103
72
  * `native_finish_reason` mirrors `finish_reason` unless overridden. `cost` and
@@ -109,33 +78,7 @@ const UNSAFE_PROTO_KEYS = new Set([
109
78
  * `reasoning_tokens`).
110
79
  */
111
80
  function resolveOpenRouterShaping(overrides, winningModel) {
112
- const u = overrides?.usage;
113
- const usageExtras = {};
114
- if (u?.cost !== void 0) {
115
- usageExtras.cost = u.cost;
116
- const cd = u.cost_details;
117
- usageExtras.cost_details = {
118
- ...cd ?? {},
119
- upstream_inference_cost: cd?.upstream_inference_cost ?? 0,
120
- upstream_inference_prompt_cost: cd?.upstream_inference_prompt_cost ?? 0,
121
- upstream_inference_completions_cost: cd?.upstream_inference_completions_cost ?? 0
122
- };
123
- }
124
- if (u?.prompt_tokens_details !== void 0) usageExtras.prompt_tokens_details = {
125
- ...u.prompt_tokens_details,
126
- cached_tokens: u.prompt_tokens_details.cached_tokens ?? 0
127
- };
128
- if (u?.completion_tokens_details !== void 0) usageExtras.completion_tokens_details = {
129
- ...u.completion_tokens_details,
130
- reasoning_tokens: u.completion_tokens_details.reasoning_tokens ?? 0
131
- };
132
- if (u?.is_byok !== void 0) usageExtras.is_byok = u.is_byok;
133
- for (const [key, value] of Object.entries(u ?? {})) {
134
- if (value === void 0) continue;
135
- if (SHAPED_OR_CANONICAL_USAGE_KEYS.has(key)) continue;
136
- if (UNSAFE_PROTO_KEYS.has(key)) continue;
137
- usageExtras[key] = value;
138
- }
81
+ const usageExtras = require_openrouter.resolve(overrides);
139
82
  return {
140
83
  provider: overrides?.provider ?? deriveOpenRouterProvider(winningModel),
141
84
  ...overrides?.nativeFinishReason !== void 0 && { nativeFinishReason: overrides.nativeFinishReason },
@@ -144,10 +87,7 @@ function resolveOpenRouterShaping(overrides, winningModel) {
144
87
  }
145
88
  /** Merge the OpenRouter usage extras onto an existing (OpenAI-shape) usage object. */
146
89
  function augmentUsage(usage, shaping) {
147
- return {
148
- ...usage,
149
- ...shaping.usageExtras
150
- };
90
+ return require_openrouter.render(usage, shaping.usageExtras);
151
91
  }
152
92
  /** Resolve a choice's native_finish_reason (mirrors finish_reason unless overridden). */
153
93
  function nativeFinish(finishReason, shaping) {
@@ -208,95 +148,11 @@ function shapeOpenRouterChunks(chunks, shaping, idOverridden) {
208
148
  * routing failures.
209
149
  */
210
150
  function serializeOpenRouterError(status, message, metadata) {
211
- return JSON.stringify({ error: {
151
+ return JSON.stringify(require_errors.buildOpenAIError("openrouter-error", {
212
152
  code: status,
213
153
  message,
214
- ...metadata !== void 0 && { metadata }
215
- } });
216
- }
217
- /** Default model slug set for `GET /api/v1/models` when no chat fixtures name one. */
218
- const DEFAULT_OPENROUTER_MODELS = [
219
- "openai/gpt-4o",
220
- "anthropic/claude-3.5-sonnet",
221
- "google/gemini-2.0-flash-001"
222
- ];
223
- /**
224
- * Build one OpenRouter model-catalog object (deterministic static filler).
225
- * Field set mirrors real OpenRouter bytes (or-capture/models-first.json):
226
- * pricing values are STRINGS; the object also carries hugging_face_id,
227
- * knowledge_cutoff, supported_voices, links, default_parameters,
228
- * expiration_date, and a `reasoning` descriptor (SDKs tolerate extras).
229
- */
230
- function openRouterModelObject(id) {
231
- return {
232
- id,
233
- canonical_slug: id,
234
- hugging_face_id: null,
235
- name: id,
236
- created: 0,
237
- description: `Mock OpenRouter model ${id} served by aimock.`,
238
- context_length: 128e3,
239
- architecture: {
240
- modality: "text->text",
241
- input_modalities: ["text"],
242
- output_modalities: ["text"],
243
- tokenizer: "Other",
244
- instruct_type: null
245
- },
246
- pricing: {
247
- prompt: "0",
248
- completion: "0",
249
- request: "0",
250
- image: "0",
251
- web_search: "0",
252
- internal_reasoning: "0",
253
- input_cache_read: "0",
254
- input_cache_write: "0"
255
- },
256
- top_provider: {
257
- context_length: 128e3,
258
- max_completion_tokens: 16384,
259
- is_moderated: false
260
- },
261
- per_request_limits: null,
262
- supported_parameters: [
263
- "tools",
264
- "tool_choice",
265
- "max_tokens",
266
- "temperature",
267
- "top_p",
268
- "stop",
269
- "reasoning"
270
- ],
271
- default_parameters: null,
272
- supported_voices: null,
273
- knowledge_cutoff: null,
274
- expiration_date: null,
275
- links: { details: `/api/v1/models/${id}/endpoints` },
276
- reasoning: {
277
- mandatory: false,
278
- default_enabled: false
279
- }
280
- };
281
- }
282
- /**
283
- * Collect the model ids advertised by `GET /api/v1/models`: every loaded chat
284
- * (or endpoint-less) fixture with a string `match.model`, in first-seen order;
285
- * the default set when none contribute one. Mirrors the Ollama `/api/tags` and
286
- * OpenRouter video `/models` synthesis idioms.
287
- */
288
- function collectModelIds(fixtures) {
289
- const ids = [];
290
- const seen = /* @__PURE__ */ new Set();
291
- for (const f of fixtures) {
292
- if (f.match.endpoint !== void 0 && f.match.endpoint !== "chat") continue;
293
- const m = f.match.model;
294
- if (typeof m === "string" && m.length > 0 && !seen.has(m)) {
295
- seen.add(m);
296
- ids.push(m);
297
- }
298
- }
299
- return ids.length > 0 ? ids : DEFAULT_OPENROUTER_MODELS;
154
+ metadata
155
+ }));
300
156
  }
301
157
  function journalGet(req, journal, path, status) {
302
158
  journal.add({
@@ -314,10 +170,10 @@ function journalGet(req, journal, path, status) {
314
170
  function handleOpenRouterModels(req, res, fixtures, journal, _defaults, setCorsHeaders) {
315
171
  setCorsHeaders(res);
316
172
  const path = req.url ?? "/api/v1/models";
317
- const data = collectModelIds(fixtures).map(openRouterModelObject);
173
+ const body = require_render.renderOpenRouterChatModels(require_collect.collect(fixtures, "chat").ids);
318
174
  journalGet(req, journal, path, 200);
319
175
  res.writeHead(200, { "Content-Type": "application/json" });
320
- res.end(JSON.stringify({ data }));
176
+ res.end(JSON.stringify(body));
321
177
  }
322
178
  /** GET /api/v1/key — the caller's key metadata/limits/usage (static defaults). */
323
179
  function handleOpenRouterKey(req, res, journal, setCorsHeaders) {
@@ -1 +1 @@
1
- {"version":3,"file":"openrouter-chat.cjs","names":["flattenHeaders"],"sources":["../src/openrouter-chat.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport type {\n ChatCompletion,\n ChatCompletionRequest,\n Fixture,\n HandlerDefaults,\n OpenRouterUsageExtras,\n ResponseOverrides,\n SSEChunk,\n} from \"./types.js\";\nimport type { Journal } from \"./journal.js\";\nimport { flattenHeaders } from \"./helpers.js\";\n\n/**\n * OpenRouter chat/LLM router shaping. aimock detects an OpenRouter request by\n * the ORIGINAL request path (before `normalizeCompatPath` erases it) starting\n * with the OpenRouter canonical base `/api/v1/` — mirroring a real client that\n * points an OpenAI SDK at `baseURL: https://openrouter.ai/api/v1`. When a\n * request is OpenRouter, the same OpenAI-compatible chat response is shaped\n * with OpenRouter's distinguishing fields (a `gen-` id prefix, a top-level\n * `provider`, `native_finish_reason`, and a rich cost-bearing `usage`) and the\n * `models[]` fallback array is simulated deterministically against fixtures.\n * OpenAI (`/v1/...`) callers are left byte-for-byte unchanged.\n *\n * The shaping is applied as a post-pass over the objects the existing OpenAI\n * builders produce (never a fork of those builders), so every OpenAI code path\n * is untouched.\n */\n\n/** OpenRouter's canonical API base — the detection prefix. */\nexport const OPENROUTER_BASE_PREFIX = \"/api/v1/\";\n\nconst OPENAI_ID_PREFIX = \"chatcmpl-\";\nconst OPENROUTER_ID_PREFIX = \"gen-\";\n\n/**\n * Return true when the ORIGINAL request path marks an OpenRouter request. Must\n * be evaluated on the pre-normalization pathname: `normalizeCompatPath`\n * rewrites `/api/v1/chat/completions` → `/v1/chat/completions`, erasing the\n * `/api/v1/` signal.\n */\nexport function isOpenRouterPath(originalPathname: string): boolean {\n return originalPathname.startsWith(OPENROUTER_BASE_PREFIX);\n}\n\n/**\n * Derive the default OpenRouter `provider` from a model slug: the author\n * segment before the first `/` (`openai/gpt-4o` → `\"openai\"`). A slug without\n * a `/` yields the whole slug. An absent/empty slug — a request that never set\n * `model` yet matched a model-less fixture — yields `\"\"` rather than throwing\n * (guards against a `TypeError` becoming a generic 500).\n */\nexport function deriveOpenRouterProvider(model: string | undefined): string {\n if (!model) return \"\";\n const slash = model.indexOf(\"/\");\n return slash === -1 ? model : model.slice(0, slash);\n}\n\n/**\n * Build the OpenRouter fallback candidate list `[body.model, ...body.models]`,\n * de-duplicated with insertion order preserved. A request without `models`\n * yields just `[body.model]` — identical to today's single-model behavior.\n */\nexport function buildOpenRouterCandidates(body: ChatCompletionRequest): string[] {\n const raw = [body.model, ...(Array.isArray(body.models) ? body.models : [])];\n const seen = new Set<string>();\n const out: string[] = [];\n for (const m of raw) {\n if (typeof m !== \"string\" || m.length === 0) continue;\n if (seen.has(m)) continue;\n seen.add(m);\n out.push(m);\n }\n return out;\n}\n\n/** Rewrite an auto-generated `chatcmpl-` id to OpenRouter's `gen-` prefix. */\nfunction toGenId(id: string): string {\n return id.startsWith(OPENAI_ID_PREFIX)\n ? OPENROUTER_ID_PREFIX + id.slice(OPENAI_ID_PREFIX.length)\n : id;\n}\n\n/**\n * The resolved OpenRouter shaping context for one response. `provider` and the\n * always-present `cost`/`cost_details` are computed here; the detail\n * breakdowns and `is_byok` are carried only when a fixture overrode them.\n */\nexport interface OpenRouterShaping {\n provider: string;\n nativeFinishReason?: string;\n usageExtras: OpenRouterUsageExtras;\n}\n\n/**\n * `usage` keys that are already accounted for elsewhere and must therefore NOT\n * be re-emitted through the forward-compat passthrough below: the canonical\n * token counts (and their per-provider aliases), which `resolveUsage` folds into\n * `prompt_tokens` / `completion_tokens` / `total_tokens` on the base usage\n * object, plus every field this function shapes explicitly.\n */\nconst SHAPED_OR_CANONICAL_USAGE_KEYS = new Set([\n \"prompt_tokens\",\n \"completion_tokens\",\n \"total_tokens\",\n \"input_tokens\",\n \"output_tokens\",\n \"promptTokenCount\",\n \"candidatesTokenCount\",\n \"totalTokenCount\",\n \"cost\",\n \"cost_details\",\n \"prompt_tokens_details\",\n \"completion_tokens_details\",\n \"is_byok\",\n]);\n\n/**\n * Keys that must never flow through the passthrough assignment below: a\n * JSON-parsed usage frame (fixtures are attacker-adjacent — they mirror upstream\n * provider responses) can carry a real own `__proto__`/`constructor`/`prototype`\n * key, and a plain `usageExtras[key] = value` for one of those hits the\n * prototype setter, corrupting the emitted object's prototype chain instead of\n * emitting a data field. Real providers never send these, so skipping is safe.\n */\nconst UNSAFE_PROTO_KEYS = new Set([\"__proto__\", \"constructor\", \"prototype\"]);\n\n/**\n * Resolve the shaping context from a fixture's response overrides and the\n * winning model slug. `provider` defaults to the winning slug's author;\n * `native_finish_reason` mirrors `finish_reason` unless overridden. `cost` and\n * `cost_details` are NULLABLE — emitted only when the fixture supplies a `cost`\n * (never defaulted to 0); when emitted, `cost_details` carries all three\n * upstream fields (a partial object breaks the canonical SDK). The token-detail\n * breakdowns and `is_byok` are included only when the fixture set them, and an\n * emitted breakdown always carries its required member (`cached_tokens` /\n * `reasoning_tokens`).\n */\nexport function resolveOpenRouterShaping(\n overrides: ResponseOverrides | undefined,\n winningModel: string | undefined,\n): OpenRouterShaping {\n const u = overrides?.usage;\n const usageExtras: OpenRouterUsageExtras = {};\n if (u?.cost !== undefined) {\n usageExtras.cost = u.cost;\n const cd = u.cost_details;\n usageExtras.cost_details = {\n ...(cd ?? {}),\n upstream_inference_cost: cd?.upstream_inference_cost ?? 0,\n upstream_inference_prompt_cost: cd?.upstream_inference_prompt_cost ?? 0,\n upstream_inference_completions_cost: cd?.upstream_inference_completions_cost ?? 0,\n };\n }\n if (u?.prompt_tokens_details !== undefined) {\n usageExtras.prompt_tokens_details = {\n ...u.prompt_tokens_details,\n cached_tokens: u.prompt_tokens_details.cached_tokens ?? 0,\n };\n }\n if (u?.completion_tokens_details !== undefined) {\n usageExtras.completion_tokens_details = {\n ...u.completion_tokens_details,\n reasoning_tokens: u.completion_tokens_details.reasoning_tokens ?? 0,\n };\n }\n if (u?.is_byok !== undefined) {\n usageExtras.is_byok = u.is_byok;\n }\n // Forward-compat passthrough (#368): a fixture's `usage` may carry provider\n // fields aimock does not model — typically written by the recorder straight\n // from a real upstream usage frame (OpenRouter `native_tokens_prompt`,\n // `native_tokens_completion`, …). Emit them verbatim rather than silently\n // dropping them, so a recorded usage object round-trips on replay. The\n // canonical token counts and every field shaped above are excluded: those are\n // already resolved onto the base usage object (see helpers.ts `resolveUsage`)\n // or handled with their own required-member defaults.\n for (const [key, value] of Object.entries(u ?? {})) {\n if (value === undefined) continue;\n if (SHAPED_OR_CANONICAL_USAGE_KEYS.has(key)) continue;\n if (UNSAFE_PROTO_KEYS.has(key)) continue;\n usageExtras[key] = value;\n }\n return {\n provider: overrides?.provider ?? deriveOpenRouterProvider(winningModel),\n ...(overrides?.nativeFinishReason !== undefined && {\n nativeFinishReason: overrides.nativeFinishReason,\n }),\n usageExtras,\n };\n}\n\n/** Merge the OpenRouter usage extras onto an existing (OpenAI-shape) usage object. */\nfunction augmentUsage<T extends object>(\n usage: T,\n shaping: OpenRouterShaping,\n): T & OpenRouterUsageExtras {\n return { ...usage, ...shaping.usageExtras };\n}\n\n/** Resolve a choice's native_finish_reason (mirrors finish_reason unless overridden). */\nfunction nativeFinish(finishReason: string | null, shaping: OpenRouterShaping): string | null {\n if (finishReason == null) return null;\n return shaping.nativeFinishReason ?? finishReason;\n}\n\n/**\n * Shape a non-streaming OpenAI completion into OpenRouter form, in place, to\n * match real OpenRouter bytes (or-capture/chat-nonstream.json): `gen-` id\n * (unless the fixture pinned `id`), top-level `provider`, `service_tier: null`,\n * an always-present `system_fingerprint` (null when unset — the canonical\n * `@openrouter/sdk` requires it), per-choice `native_finish_reason`, a\n * `message.reasoning` field (mirrors `reasoning_content`, else null), and\n * cost-bearing `usage`.\n */\nexport function shapeOpenRouterCompletion(\n completion: ChatCompletion,\n shaping: OpenRouterShaping,\n idOverridden: boolean,\n): ChatCompletion {\n if (!idOverridden) completion.id = toGenId(completion.id);\n completion.provider = shaping.provider;\n if (completion.system_fingerprint === undefined) completion.system_fingerprint = null;\n if (completion.service_tier === undefined) completion.service_tier = null;\n for (const choice of completion.choices) {\n // Non-streaming finish_reason is always a string, so native mirrors it.\n choice.native_finish_reason = shaping.nativeFinishReason ?? choice.finish_reason;\n if (choice.message.reasoning === undefined) {\n choice.message.reasoning = choice.message.reasoning_content ?? null;\n }\n }\n completion.usage = augmentUsage(completion.usage, shaping);\n return completion;\n}\n\n/**\n * Shape streaming OpenAI chunks into OpenRouter form, in place, to match real\n * OpenRouter bytes (or-capture/chat-stream.sse): EVERY chunk carries the\n * `gen-` id (unless pinned), top-level `provider`, and `system_fingerprint`\n * (null when unset); EVERY delta choice carries `native_finish_reason` (null\n * until the finish chunk, where it mirrors finish_reason); real OR repeats\n * `role: \"assistant\"` on every content delta (mirrored here); and the final\n * usage-bearing chunk gets cost-bearing usage plus `service_tier: null`.\n */\nexport function shapeOpenRouterChunks(\n chunks: SSEChunk[],\n shaping: OpenRouterShaping,\n idOverridden: boolean,\n): SSEChunk[] {\n for (const chunk of chunks) {\n if (!idOverridden) chunk.id = toGenId(chunk.id);\n chunk.provider = shaping.provider;\n if (chunk.system_fingerprint === undefined) chunk.system_fingerprint = null;\n for (const choice of chunk.choices) {\n choice.native_finish_reason = nativeFinish(choice.finish_reason, shaping);\n // Real OR stamps role:\"assistant\" on every content-bearing delta. A\n // `content: null` delta is NOT content-bearing, so require string content.\n if (typeof choice.delta.content === \"string\" && choice.delta.role === undefined) {\n choice.delta.role = \"assistant\";\n }\n }\n if (chunk.usage) {\n chunk.usage = augmentUsage(chunk.usage, shaping);\n chunk.service_tier = null;\n }\n }\n return chunks;\n}\n\n/**\n * Serialize an OpenRouter error envelope: `{ error: { code, message,\n * metadata? } }` where `code` equals the HTTP status (contrast the OpenAI\n * shape `{ error: { message, type, param, code } }`). Used for every error\n * emitted on an OpenRouter request — fixture errors, no-match, strict, and\n * routing failures.\n */\nexport function serializeOpenRouterError(\n status: number,\n message: string,\n metadata?: Record<string, unknown>,\n): string {\n return JSON.stringify({\n error: {\n code: status,\n message,\n ...(metadata !== undefined && { metadata }),\n },\n });\n}\n\n// ─── Discovery endpoints ─────────────────────────────────────────────────────\n\n/** Default model slug set for `GET /api/v1/models` when no chat fixtures name one. */\nconst DEFAULT_OPENROUTER_MODELS = [\n \"openai/gpt-4o\",\n \"anthropic/claude-3.5-sonnet\",\n \"google/gemini-2.0-flash-001\",\n];\n\n/**\n * Build one OpenRouter model-catalog object (deterministic static filler).\n * Field set mirrors real OpenRouter bytes (or-capture/models-first.json):\n * pricing values are STRINGS; the object also carries hugging_face_id,\n * knowledge_cutoff, supported_voices, links, default_parameters,\n * expiration_date, and a `reasoning` descriptor (SDKs tolerate extras).\n */\nfunction openRouterModelObject(id: string): Record<string, unknown> {\n return {\n id,\n canonical_slug: id,\n hugging_face_id: null,\n name: id,\n created: 0,\n description: `Mock OpenRouter model ${id} served by aimock.`,\n context_length: 128000,\n architecture: {\n modality: \"text->text\",\n input_modalities: [\"text\"],\n output_modalities: [\"text\"],\n tokenizer: \"Other\",\n instruct_type: null,\n },\n pricing: {\n prompt: \"0\",\n completion: \"0\",\n request: \"0\",\n image: \"0\",\n web_search: \"0\",\n internal_reasoning: \"0\",\n input_cache_read: \"0\",\n input_cache_write: \"0\",\n },\n top_provider: {\n context_length: 128000,\n max_completion_tokens: 16384,\n is_moderated: false,\n },\n per_request_limits: null,\n supported_parameters: [\n \"tools\",\n \"tool_choice\",\n \"max_tokens\",\n \"temperature\",\n \"top_p\",\n \"stop\",\n \"reasoning\",\n ],\n default_parameters: null,\n supported_voices: null,\n knowledge_cutoff: null,\n expiration_date: null,\n links: { details: `/api/v1/models/${id}/endpoints` },\n reasoning: { mandatory: false, default_enabled: false },\n };\n}\n\n/**\n * Collect the model ids advertised by `GET /api/v1/models`: every loaded chat\n * (or endpoint-less) fixture with a string `match.model`, in first-seen order;\n * the default set when none contribute one. Mirrors the Ollama `/api/tags` and\n * OpenRouter video `/models` synthesis idioms.\n */\nfunction collectModelIds(fixtures: Fixture[]): string[] {\n const ids: string[] = [];\n const seen = new Set<string>();\n for (const f of fixtures) {\n if (f.match.endpoint !== undefined && f.match.endpoint !== \"chat\") continue;\n const m = f.match.model;\n if (typeof m === \"string\" && m.length > 0 && !seen.has(m)) {\n seen.add(m);\n ids.push(m);\n }\n }\n return ids.length > 0 ? ids : DEFAULT_OPENROUTER_MODELS;\n}\n\nfunction journalGet(\n req: http.IncomingMessage,\n journal: Journal,\n path: string,\n status: number,\n): void {\n journal.add({\n method: req.method ?? \"GET\",\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status, fixture: null },\n });\n}\n\n/** GET /api/v1/models — the OpenRouter model catalog synthesized from fixtures. */\nexport function handleOpenRouterModels(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n fixtures: Fixture[],\n journal: Journal,\n _defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): void {\n setCorsHeaders(res);\n const path = req.url ?? \"/api/v1/models\";\n const data = collectModelIds(fixtures).map(openRouterModelObject);\n journalGet(req, journal, path, 200);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ data }));\n}\n\n/** GET /api/v1/key — the caller's key metadata/limits/usage (static defaults). */\nexport function handleOpenRouterKey(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n journal: Journal,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): void {\n setCorsHeaders(res);\n const path = req.url ?? \"/api/v1/key\";\n journalGet(req, journal, path, 200);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n // Field set + order mirror real OpenRouter bytes (or-capture/key.json).\n // null limit / limit_remaining = unlimited. `creator_user_id` is an account\n // echo — null for the mock (no account). rate_limit is deprecated upstream.\n res.end(\n JSON.stringify({\n data: {\n label: \"sk-or-v1-aimock\",\n is_management_key: false,\n is_provisioning_key: false,\n limit: null,\n limit_reset: null,\n limit_remaining: null,\n include_byok_in_limit: false,\n usage: 0,\n usage_daily: 0,\n usage_weekly: 0,\n usage_monthly: 0,\n byok_usage: 0,\n byok_usage_daily: 0,\n byok_usage_weekly: 0,\n byok_usage_monthly: 0,\n is_free_tier: false,\n expires_at: null,\n creator_user_id: null,\n rate_limit: {\n requests: -1,\n interval: \"10s\",\n note: \"This field is deprecated and safe to ignore.\",\n },\n },\n }),\n );\n}\n\n/** GET /api/v1/credits — account credit totals (static defaults). */\nexport function handleOpenRouterCredits(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n journal: Journal,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): void {\n setCorsHeaders(res);\n const path = req.url ?? \"/api/v1/credits\";\n journalGet(req, journal, path, 200);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ data: { total_credits: 0, total_usage: 0 } }));\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"openrouter-chat.cjs","names":["resolveOpenRouterUsage","renderOpenRouterUsage","buildOpenAIError","flattenHeaders","renderOpenRouterChatModels","collect"],"sources":["../src/openrouter-chat.ts"],"sourcesContent":["import {\n resolve as resolveOpenRouterUsage,\n render as renderOpenRouterUsage,\n} from \"./response/usage/codecs/openrouter.js\";\nimport type * as http from \"node:http\";\nimport type {\n ChatCompletion,\n ChatCompletionRequest,\n Fixture,\n HandlerDefaults,\n OpenRouterUsageExtras,\n ResponseOverrides,\n SSEChunk,\n} from \"./types.js\";\nimport type { Journal } from \"./journal.js\";\nimport { flattenHeaders } from \"./helpers.js\";\nimport { collect } from \"./response/catalog/collect.js\";\nimport { renderOpenRouterChatModels } from \"./response/catalog/render.js\";\nimport { buildOpenAIError } from \"./response/errors.js\";\n\n/**\n * OpenRouter chat/LLM router shaping. aimock detects an OpenRouter request by\n * the ORIGINAL request path (before `normalizeCompatPath` erases it) starting\n * with the OpenRouter canonical base `/api/v1/` — mirroring a real client that\n * points an OpenAI SDK at `baseURL: https://openrouter.ai/api/v1`. When a\n * request is OpenRouter, the same OpenAI-compatible chat response is shaped\n * with OpenRouter's distinguishing fields (a `gen-` id prefix, a top-level\n * `provider`, `native_finish_reason`, and a rich cost-bearing `usage`) and the\n * `models[]` fallback array is simulated deterministically against fixtures.\n * OpenAI (`/v1/...`) callers are left byte-for-byte unchanged.\n *\n * The shaping is applied as a post-pass over the objects the existing OpenAI\n * builders produce (never a fork of those builders), so every OpenAI code path\n * is untouched.\n */\n\n/** OpenRouter's canonical API base — the detection prefix. */\nexport const OPENROUTER_BASE_PREFIX = \"/api/v1/\";\n\nconst OPENAI_ID_PREFIX = \"chatcmpl-\";\nconst OPENROUTER_ID_PREFIX = \"gen-\";\n\n/**\n * Return true when the ORIGINAL request path marks an OpenRouter request. Must\n * be evaluated on the pre-normalization pathname: `normalizeCompatPath`\n * rewrites `/api/v1/chat/completions` → `/v1/chat/completions`, erasing the\n * `/api/v1/` signal.\n */\nexport function isOpenRouterPath(originalPathname: string): boolean {\n return originalPathname.startsWith(OPENROUTER_BASE_PREFIX);\n}\n\n/**\n * Derive the default OpenRouter `provider` from a model slug: the author\n * segment before the first `/` (`openai/gpt-4o` → `\"openai\"`). A slug without\n * a `/` yields the whole slug. An absent/empty slug — a request that never set\n * `model` yet matched a model-less fixture — yields `\"\"` rather than throwing\n * (guards against a `TypeError` becoming a generic 500).\n */\nexport function deriveOpenRouterProvider(model: string | undefined): string {\n if (!model) return \"\";\n const slash = model.indexOf(\"/\");\n return slash === -1 ? model : model.slice(0, slash);\n}\n\n/**\n * Build the OpenRouter fallback candidate list `[body.model, ...body.models]`,\n * de-duplicated with insertion order preserved. A request without `models`\n * yields just `[body.model]` — identical to today's single-model behavior.\n */\nexport function buildOpenRouterCandidates(body: ChatCompletionRequest): string[] {\n const raw = [body.model, ...(Array.isArray(body.models) ? body.models : [])];\n const seen = new Set<string>();\n const out: string[] = [];\n for (const m of raw) {\n if (typeof m !== \"string\" || m.length === 0) continue;\n if (seen.has(m)) continue;\n seen.add(m);\n out.push(m);\n }\n return out;\n}\n\n/** Rewrite an auto-generated `chatcmpl-` id to OpenRouter's `gen-` prefix. */\nfunction toGenId(id: string): string {\n return id.startsWith(OPENAI_ID_PREFIX)\n ? OPENROUTER_ID_PREFIX + id.slice(OPENAI_ID_PREFIX.length)\n : id;\n}\n\n/**\n * The resolved OpenRouter shaping context for one response. `provider` and the\n * always-present `cost`/`cost_details` are computed here; the detail\n * breakdowns and `is_byok` are carried only when a fixture overrode them.\n */\nexport interface OpenRouterShaping {\n provider: string;\n nativeFinishReason?: string;\n usageExtras: OpenRouterUsageExtras;\n}\n\n/**\n * Resolve the shaping context from a fixture's response overrides and the\n * winning model slug. `provider` defaults to the winning slug's author;\n * `native_finish_reason` mirrors `finish_reason` unless overridden. `cost` and\n * `cost_details` are NULLABLE — emitted only when the fixture supplies a `cost`\n * (never defaulted to 0); when emitted, `cost_details` carries all three\n * upstream fields (a partial object breaks the canonical SDK). The token-detail\n * breakdowns and `is_byok` are included only when the fixture set them, and an\n * emitted breakdown always carries its required member (`cached_tokens` /\n * `reasoning_tokens`).\n */\nexport function resolveOpenRouterShaping(\n overrides: ResponseOverrides | undefined,\n winningModel: string | undefined,\n): OpenRouterShaping {\n const usageExtras = resolveOpenRouterUsage(overrides);\n return {\n provider: overrides?.provider ?? deriveOpenRouterProvider(winningModel),\n ...(overrides?.nativeFinishReason !== undefined && {\n nativeFinishReason: overrides.nativeFinishReason,\n }),\n usageExtras,\n };\n}\n\n/** Merge the OpenRouter usage extras onto an existing (OpenAI-shape) usage object. */\nfunction augmentUsage<T extends object>(\n usage: T,\n shaping: OpenRouterShaping,\n): T & OpenRouterUsageExtras {\n return renderOpenRouterUsage(usage, shaping.usageExtras);\n}\n\n/** Resolve a choice's native_finish_reason (mirrors finish_reason unless overridden). */\nfunction nativeFinish(finishReason: string | null, shaping: OpenRouterShaping): string | null {\n if (finishReason == null) return null;\n return shaping.nativeFinishReason ?? finishReason;\n}\n\n/**\n * Shape a non-streaming OpenAI completion into OpenRouter form, in place, to\n * match real OpenRouter bytes (or-capture/chat-nonstream.json): `gen-` id\n * (unless the fixture pinned `id`), top-level `provider`, `service_tier: null`,\n * an always-present `system_fingerprint` (null when unset — the canonical\n * `@openrouter/sdk` requires it), per-choice `native_finish_reason`, a\n * `message.reasoning` field (mirrors `reasoning_content`, else null), and\n * cost-bearing `usage`.\n */\nexport function shapeOpenRouterCompletion(\n completion: ChatCompletion,\n shaping: OpenRouterShaping,\n idOverridden: boolean,\n): ChatCompletion {\n if (!idOverridden) completion.id = toGenId(completion.id);\n completion.provider = shaping.provider;\n if (completion.system_fingerprint === undefined) completion.system_fingerprint = null;\n if (completion.service_tier === undefined) completion.service_tier = null;\n for (const choice of completion.choices) {\n // Non-streaming finish_reason is always a string, so native mirrors it.\n choice.native_finish_reason = shaping.nativeFinishReason ?? choice.finish_reason;\n if (choice.message.reasoning === undefined) {\n choice.message.reasoning = choice.message.reasoning_content ?? null;\n }\n }\n completion.usage = augmentUsage(completion.usage, shaping);\n return completion;\n}\n\n/**\n * Shape streaming OpenAI chunks into OpenRouter form, in place, to match real\n * OpenRouter bytes (or-capture/chat-stream.sse): EVERY chunk carries the\n * `gen-` id (unless pinned), top-level `provider`, and `system_fingerprint`\n * (null when unset); EVERY delta choice carries `native_finish_reason` (null\n * until the finish chunk, where it mirrors finish_reason); real OR repeats\n * `role: \"assistant\"` on every content delta (mirrored here); and the final\n * usage-bearing chunk gets cost-bearing usage plus `service_tier: null`.\n */\nexport function shapeOpenRouterChunks(\n chunks: SSEChunk[],\n shaping: OpenRouterShaping,\n idOverridden: boolean,\n): SSEChunk[] {\n for (const chunk of chunks) {\n if (!idOverridden) chunk.id = toGenId(chunk.id);\n chunk.provider = shaping.provider;\n if (chunk.system_fingerprint === undefined) chunk.system_fingerprint = null;\n for (const choice of chunk.choices) {\n choice.native_finish_reason = nativeFinish(choice.finish_reason, shaping);\n // Real OR stamps role:\"assistant\" on every content-bearing delta. A\n // `content: null` delta is NOT content-bearing, so require string content.\n if (typeof choice.delta.content === \"string\" && choice.delta.role === undefined) {\n choice.delta.role = \"assistant\";\n }\n }\n if (chunk.usage) {\n chunk.usage = augmentUsage(chunk.usage, shaping);\n chunk.service_tier = null;\n }\n }\n return chunks;\n}\n\n/**\n * Serialize an OpenRouter error envelope: `{ error: { code, message,\n * metadata? } }` where `code` equals the HTTP status (contrast the OpenAI\n * shape `{ error: { message, type, param, code } }`). Used for every error\n * emitted on an OpenRouter request — fixture errors, no-match, strict, and\n * routing failures.\n */\nexport function serializeOpenRouterError(\n status: number,\n message: string,\n metadata?: Record<string, unknown>,\n): string {\n return JSON.stringify(buildOpenAIError(\"openrouter-error\", { code: status, message, metadata }));\n}\n\n// ─── Discovery endpoints ─────────────────────────────────────────────────────\n\nfunction journalGet(\n req: http.IncomingMessage,\n journal: Journal,\n path: string,\n status: number,\n): void {\n journal.add({\n method: req.method ?? \"GET\",\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status, fixture: null },\n });\n}\n\n/** GET /api/v1/models — the OpenRouter model catalog synthesized from fixtures. */\nexport function handleOpenRouterModels(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n fixtures: Fixture[],\n journal: Journal,\n _defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): void {\n setCorsHeaders(res);\n const path = req.url ?? \"/api/v1/models\";\n const body = renderOpenRouterChatModels(collect(fixtures, \"chat\").ids);\n journalGet(req, journal, path, 200);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(body));\n}\n\n/** GET /api/v1/key — the caller's key metadata/limits/usage (static defaults). */\nexport function handleOpenRouterKey(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n journal: Journal,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): void {\n setCorsHeaders(res);\n const path = req.url ?? \"/api/v1/key\";\n journalGet(req, journal, path, 200);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n // Field set + order mirror real OpenRouter bytes (or-capture/key.json).\n // null limit / limit_remaining = unlimited. `creator_user_id` is an account\n // echo — null for the mock (no account). rate_limit is deprecated upstream.\n res.end(\n JSON.stringify({\n data: {\n label: \"sk-or-v1-aimock\",\n is_management_key: false,\n is_provisioning_key: false,\n limit: null,\n limit_reset: null,\n limit_remaining: null,\n include_byok_in_limit: false,\n usage: 0,\n usage_daily: 0,\n usage_weekly: 0,\n usage_monthly: 0,\n byok_usage: 0,\n byok_usage_daily: 0,\n byok_usage_weekly: 0,\n byok_usage_monthly: 0,\n is_free_tier: false,\n expires_at: null,\n creator_user_id: null,\n rate_limit: {\n requests: -1,\n interval: \"10s\",\n note: \"This field is deprecated and safe to ignore.\",\n },\n },\n }),\n );\n}\n\n/** GET /api/v1/credits — account credit totals (static defaults). */\nexport function handleOpenRouterCredits(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n journal: Journal,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): void {\n setCorsHeaders(res);\n const path = req.url ?? \"/api/v1/credits\";\n journalGet(req, journal, path, 200);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ data: { total_credits: 0, total_usage: 0 } }));\n}\n"],"mappings":"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"}
@@ -1,4 +1,8 @@
1
+ import { buildOpenAIError } from "./response/errors.js";
1
2
  import { flattenHeaders } from "./helpers.js";
3
+ import { collect } from "./response/catalog/collect.js";
4
+ import { renderOpenRouterChatModels } from "./response/catalog/render.js";
5
+ import { render, resolve } from "./response/usage/codecs/openrouter.js";
2
6
 
3
7
  //#region src/openrouter-chat.ts
4
8
  /**
@@ -63,41 +67,6 @@ function toGenId(id) {
63
67
  return id.startsWith(OPENAI_ID_PREFIX) ? OPENROUTER_ID_PREFIX + id.slice(9) : id;
64
68
  }
65
69
  /**
66
- * `usage` keys that are already accounted for elsewhere and must therefore NOT
67
- * be re-emitted through the forward-compat passthrough below: the canonical
68
- * token counts (and their per-provider aliases), which `resolveUsage` folds into
69
- * `prompt_tokens` / `completion_tokens` / `total_tokens` on the base usage
70
- * object, plus every field this function shapes explicitly.
71
- */
72
- const SHAPED_OR_CANONICAL_USAGE_KEYS = new Set([
73
- "prompt_tokens",
74
- "completion_tokens",
75
- "total_tokens",
76
- "input_tokens",
77
- "output_tokens",
78
- "promptTokenCount",
79
- "candidatesTokenCount",
80
- "totalTokenCount",
81
- "cost",
82
- "cost_details",
83
- "prompt_tokens_details",
84
- "completion_tokens_details",
85
- "is_byok"
86
- ]);
87
- /**
88
- * Keys that must never flow through the passthrough assignment below: a
89
- * JSON-parsed usage frame (fixtures are attacker-adjacent — they mirror upstream
90
- * provider responses) can carry a real own `__proto__`/`constructor`/`prototype`
91
- * key, and a plain `usageExtras[key] = value` for one of those hits the
92
- * prototype setter, corrupting the emitted object's prototype chain instead of
93
- * emitting a data field. Real providers never send these, so skipping is safe.
94
- */
95
- const UNSAFE_PROTO_KEYS = new Set([
96
- "__proto__",
97
- "constructor",
98
- "prototype"
99
- ]);
100
- /**
101
70
  * Resolve the shaping context from a fixture's response overrides and the
102
71
  * winning model slug. `provider` defaults to the winning slug's author;
103
72
  * `native_finish_reason` mirrors `finish_reason` unless overridden. `cost` and
@@ -109,33 +78,7 @@ const UNSAFE_PROTO_KEYS = new Set([
109
78
  * `reasoning_tokens`).
110
79
  */
111
80
  function resolveOpenRouterShaping(overrides, winningModel) {
112
- const u = overrides?.usage;
113
- const usageExtras = {};
114
- if (u?.cost !== void 0) {
115
- usageExtras.cost = u.cost;
116
- const cd = u.cost_details;
117
- usageExtras.cost_details = {
118
- ...cd ?? {},
119
- upstream_inference_cost: cd?.upstream_inference_cost ?? 0,
120
- upstream_inference_prompt_cost: cd?.upstream_inference_prompt_cost ?? 0,
121
- upstream_inference_completions_cost: cd?.upstream_inference_completions_cost ?? 0
122
- };
123
- }
124
- if (u?.prompt_tokens_details !== void 0) usageExtras.prompt_tokens_details = {
125
- ...u.prompt_tokens_details,
126
- cached_tokens: u.prompt_tokens_details.cached_tokens ?? 0
127
- };
128
- if (u?.completion_tokens_details !== void 0) usageExtras.completion_tokens_details = {
129
- ...u.completion_tokens_details,
130
- reasoning_tokens: u.completion_tokens_details.reasoning_tokens ?? 0
131
- };
132
- if (u?.is_byok !== void 0) usageExtras.is_byok = u.is_byok;
133
- for (const [key, value] of Object.entries(u ?? {})) {
134
- if (value === void 0) continue;
135
- if (SHAPED_OR_CANONICAL_USAGE_KEYS.has(key)) continue;
136
- if (UNSAFE_PROTO_KEYS.has(key)) continue;
137
- usageExtras[key] = value;
138
- }
81
+ const usageExtras = resolve(overrides);
139
82
  return {
140
83
  provider: overrides?.provider ?? deriveOpenRouterProvider(winningModel),
141
84
  ...overrides?.nativeFinishReason !== void 0 && { nativeFinishReason: overrides.nativeFinishReason },
@@ -144,10 +87,7 @@ function resolveOpenRouterShaping(overrides, winningModel) {
144
87
  }
145
88
  /** Merge the OpenRouter usage extras onto an existing (OpenAI-shape) usage object. */
146
89
  function augmentUsage(usage, shaping) {
147
- return {
148
- ...usage,
149
- ...shaping.usageExtras
150
- };
90
+ return render(usage, shaping.usageExtras);
151
91
  }
152
92
  /** Resolve a choice's native_finish_reason (mirrors finish_reason unless overridden). */
153
93
  function nativeFinish(finishReason, shaping) {
@@ -208,95 +148,11 @@ function shapeOpenRouterChunks(chunks, shaping, idOverridden) {
208
148
  * routing failures.
209
149
  */
210
150
  function serializeOpenRouterError(status, message, metadata) {
211
- return JSON.stringify({ error: {
151
+ return JSON.stringify(buildOpenAIError("openrouter-error", {
212
152
  code: status,
213
153
  message,
214
- ...metadata !== void 0 && { metadata }
215
- } });
216
- }
217
- /** Default model slug set for `GET /api/v1/models` when no chat fixtures name one. */
218
- const DEFAULT_OPENROUTER_MODELS = [
219
- "openai/gpt-4o",
220
- "anthropic/claude-3.5-sonnet",
221
- "google/gemini-2.0-flash-001"
222
- ];
223
- /**
224
- * Build one OpenRouter model-catalog object (deterministic static filler).
225
- * Field set mirrors real OpenRouter bytes (or-capture/models-first.json):
226
- * pricing values are STRINGS; the object also carries hugging_face_id,
227
- * knowledge_cutoff, supported_voices, links, default_parameters,
228
- * expiration_date, and a `reasoning` descriptor (SDKs tolerate extras).
229
- */
230
- function openRouterModelObject(id) {
231
- return {
232
- id,
233
- canonical_slug: id,
234
- hugging_face_id: null,
235
- name: id,
236
- created: 0,
237
- description: `Mock OpenRouter model ${id} served by aimock.`,
238
- context_length: 128e3,
239
- architecture: {
240
- modality: "text->text",
241
- input_modalities: ["text"],
242
- output_modalities: ["text"],
243
- tokenizer: "Other",
244
- instruct_type: null
245
- },
246
- pricing: {
247
- prompt: "0",
248
- completion: "0",
249
- request: "0",
250
- image: "0",
251
- web_search: "0",
252
- internal_reasoning: "0",
253
- input_cache_read: "0",
254
- input_cache_write: "0"
255
- },
256
- top_provider: {
257
- context_length: 128e3,
258
- max_completion_tokens: 16384,
259
- is_moderated: false
260
- },
261
- per_request_limits: null,
262
- supported_parameters: [
263
- "tools",
264
- "tool_choice",
265
- "max_tokens",
266
- "temperature",
267
- "top_p",
268
- "stop",
269
- "reasoning"
270
- ],
271
- default_parameters: null,
272
- supported_voices: null,
273
- knowledge_cutoff: null,
274
- expiration_date: null,
275
- links: { details: `/api/v1/models/${id}/endpoints` },
276
- reasoning: {
277
- mandatory: false,
278
- default_enabled: false
279
- }
280
- };
281
- }
282
- /**
283
- * Collect the model ids advertised by `GET /api/v1/models`: every loaded chat
284
- * (or endpoint-less) fixture with a string `match.model`, in first-seen order;
285
- * the default set when none contribute one. Mirrors the Ollama `/api/tags` and
286
- * OpenRouter video `/models` synthesis idioms.
287
- */
288
- function collectModelIds(fixtures) {
289
- const ids = [];
290
- const seen = /* @__PURE__ */ new Set();
291
- for (const f of fixtures) {
292
- if (f.match.endpoint !== void 0 && f.match.endpoint !== "chat") continue;
293
- const m = f.match.model;
294
- if (typeof m === "string" && m.length > 0 && !seen.has(m)) {
295
- seen.add(m);
296
- ids.push(m);
297
- }
298
- }
299
- return ids.length > 0 ? ids : DEFAULT_OPENROUTER_MODELS;
154
+ metadata
155
+ }));
300
156
  }
301
157
  function journalGet(req, journal, path, status) {
302
158
  journal.add({
@@ -314,10 +170,10 @@ function journalGet(req, journal, path, status) {
314
170
  function handleOpenRouterModels(req, res, fixtures, journal, _defaults, setCorsHeaders) {
315
171
  setCorsHeaders(res);
316
172
  const path = req.url ?? "/api/v1/models";
317
- const data = collectModelIds(fixtures).map(openRouterModelObject);
173
+ const body = renderOpenRouterChatModels(collect(fixtures, "chat").ids);
318
174
  journalGet(req, journal, path, 200);
319
175
  res.writeHead(200, { "Content-Type": "application/json" });
320
- res.end(JSON.stringify({ data }));
176
+ res.end(JSON.stringify(body));
321
177
  }
322
178
  /** GET /api/v1/key — the caller's key metadata/limits/usage (static defaults). */
323
179
  function handleOpenRouterKey(req, res, journal, setCorsHeaders) {
@@ -1 +1 @@
1
- {"version":3,"file":"openrouter-chat.js","names":[],"sources":["../src/openrouter-chat.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport type {\n ChatCompletion,\n ChatCompletionRequest,\n Fixture,\n HandlerDefaults,\n OpenRouterUsageExtras,\n ResponseOverrides,\n SSEChunk,\n} from \"./types.js\";\nimport type { Journal } from \"./journal.js\";\nimport { flattenHeaders } from \"./helpers.js\";\n\n/**\n * OpenRouter chat/LLM router shaping. aimock detects an OpenRouter request by\n * the ORIGINAL request path (before `normalizeCompatPath` erases it) starting\n * with the OpenRouter canonical base `/api/v1/` — mirroring a real client that\n * points an OpenAI SDK at `baseURL: https://openrouter.ai/api/v1`. When a\n * request is OpenRouter, the same OpenAI-compatible chat response is shaped\n * with OpenRouter's distinguishing fields (a `gen-` id prefix, a top-level\n * `provider`, `native_finish_reason`, and a rich cost-bearing `usage`) and the\n * `models[]` fallback array is simulated deterministically against fixtures.\n * OpenAI (`/v1/...`) callers are left byte-for-byte unchanged.\n *\n * The shaping is applied as a post-pass over the objects the existing OpenAI\n * builders produce (never a fork of those builders), so every OpenAI code path\n * is untouched.\n */\n\n/** OpenRouter's canonical API base — the detection prefix. */\nexport const OPENROUTER_BASE_PREFIX = \"/api/v1/\";\n\nconst OPENAI_ID_PREFIX = \"chatcmpl-\";\nconst OPENROUTER_ID_PREFIX = \"gen-\";\n\n/**\n * Return true when the ORIGINAL request path marks an OpenRouter request. Must\n * be evaluated on the pre-normalization pathname: `normalizeCompatPath`\n * rewrites `/api/v1/chat/completions` → `/v1/chat/completions`, erasing the\n * `/api/v1/` signal.\n */\nexport function isOpenRouterPath(originalPathname: string): boolean {\n return originalPathname.startsWith(OPENROUTER_BASE_PREFIX);\n}\n\n/**\n * Derive the default OpenRouter `provider` from a model slug: the author\n * segment before the first `/` (`openai/gpt-4o` → `\"openai\"`). A slug without\n * a `/` yields the whole slug. An absent/empty slug — a request that never set\n * `model` yet matched a model-less fixture — yields `\"\"` rather than throwing\n * (guards against a `TypeError` becoming a generic 500).\n */\nexport function deriveOpenRouterProvider(model: string | undefined): string {\n if (!model) return \"\";\n const slash = model.indexOf(\"/\");\n return slash === -1 ? model : model.slice(0, slash);\n}\n\n/**\n * Build the OpenRouter fallback candidate list `[body.model, ...body.models]`,\n * de-duplicated with insertion order preserved. A request without `models`\n * yields just `[body.model]` — identical to today's single-model behavior.\n */\nexport function buildOpenRouterCandidates(body: ChatCompletionRequest): string[] {\n const raw = [body.model, ...(Array.isArray(body.models) ? body.models : [])];\n const seen = new Set<string>();\n const out: string[] = [];\n for (const m of raw) {\n if (typeof m !== \"string\" || m.length === 0) continue;\n if (seen.has(m)) continue;\n seen.add(m);\n out.push(m);\n }\n return out;\n}\n\n/** Rewrite an auto-generated `chatcmpl-` id to OpenRouter's `gen-` prefix. */\nfunction toGenId(id: string): string {\n return id.startsWith(OPENAI_ID_PREFIX)\n ? OPENROUTER_ID_PREFIX + id.slice(OPENAI_ID_PREFIX.length)\n : id;\n}\n\n/**\n * The resolved OpenRouter shaping context for one response. `provider` and the\n * always-present `cost`/`cost_details` are computed here; the detail\n * breakdowns and `is_byok` are carried only when a fixture overrode them.\n */\nexport interface OpenRouterShaping {\n provider: string;\n nativeFinishReason?: string;\n usageExtras: OpenRouterUsageExtras;\n}\n\n/**\n * `usage` keys that are already accounted for elsewhere and must therefore NOT\n * be re-emitted through the forward-compat passthrough below: the canonical\n * token counts (and their per-provider aliases), which `resolveUsage` folds into\n * `prompt_tokens` / `completion_tokens` / `total_tokens` on the base usage\n * object, plus every field this function shapes explicitly.\n */\nconst SHAPED_OR_CANONICAL_USAGE_KEYS = new Set([\n \"prompt_tokens\",\n \"completion_tokens\",\n \"total_tokens\",\n \"input_tokens\",\n \"output_tokens\",\n \"promptTokenCount\",\n \"candidatesTokenCount\",\n \"totalTokenCount\",\n \"cost\",\n \"cost_details\",\n \"prompt_tokens_details\",\n \"completion_tokens_details\",\n \"is_byok\",\n]);\n\n/**\n * Keys that must never flow through the passthrough assignment below: a\n * JSON-parsed usage frame (fixtures are attacker-adjacent — they mirror upstream\n * provider responses) can carry a real own `__proto__`/`constructor`/`prototype`\n * key, and a plain `usageExtras[key] = value` for one of those hits the\n * prototype setter, corrupting the emitted object's prototype chain instead of\n * emitting a data field. Real providers never send these, so skipping is safe.\n */\nconst UNSAFE_PROTO_KEYS = new Set([\"__proto__\", \"constructor\", \"prototype\"]);\n\n/**\n * Resolve the shaping context from a fixture's response overrides and the\n * winning model slug. `provider` defaults to the winning slug's author;\n * `native_finish_reason` mirrors `finish_reason` unless overridden. `cost` and\n * `cost_details` are NULLABLE — emitted only when the fixture supplies a `cost`\n * (never defaulted to 0); when emitted, `cost_details` carries all three\n * upstream fields (a partial object breaks the canonical SDK). The token-detail\n * breakdowns and `is_byok` are included only when the fixture set them, and an\n * emitted breakdown always carries its required member (`cached_tokens` /\n * `reasoning_tokens`).\n */\nexport function resolveOpenRouterShaping(\n overrides: ResponseOverrides | undefined,\n winningModel: string | undefined,\n): OpenRouterShaping {\n const u = overrides?.usage;\n const usageExtras: OpenRouterUsageExtras = {};\n if (u?.cost !== undefined) {\n usageExtras.cost = u.cost;\n const cd = u.cost_details;\n usageExtras.cost_details = {\n ...(cd ?? {}),\n upstream_inference_cost: cd?.upstream_inference_cost ?? 0,\n upstream_inference_prompt_cost: cd?.upstream_inference_prompt_cost ?? 0,\n upstream_inference_completions_cost: cd?.upstream_inference_completions_cost ?? 0,\n };\n }\n if (u?.prompt_tokens_details !== undefined) {\n usageExtras.prompt_tokens_details = {\n ...u.prompt_tokens_details,\n cached_tokens: u.prompt_tokens_details.cached_tokens ?? 0,\n };\n }\n if (u?.completion_tokens_details !== undefined) {\n usageExtras.completion_tokens_details = {\n ...u.completion_tokens_details,\n reasoning_tokens: u.completion_tokens_details.reasoning_tokens ?? 0,\n };\n }\n if (u?.is_byok !== undefined) {\n usageExtras.is_byok = u.is_byok;\n }\n // Forward-compat passthrough (#368): a fixture's `usage` may carry provider\n // fields aimock does not model — typically written by the recorder straight\n // from a real upstream usage frame (OpenRouter `native_tokens_prompt`,\n // `native_tokens_completion`, …). Emit them verbatim rather than silently\n // dropping them, so a recorded usage object round-trips on replay. The\n // canonical token counts and every field shaped above are excluded: those are\n // already resolved onto the base usage object (see helpers.ts `resolveUsage`)\n // or handled with their own required-member defaults.\n for (const [key, value] of Object.entries(u ?? {})) {\n if (value === undefined) continue;\n if (SHAPED_OR_CANONICAL_USAGE_KEYS.has(key)) continue;\n if (UNSAFE_PROTO_KEYS.has(key)) continue;\n usageExtras[key] = value;\n }\n return {\n provider: overrides?.provider ?? deriveOpenRouterProvider(winningModel),\n ...(overrides?.nativeFinishReason !== undefined && {\n nativeFinishReason: overrides.nativeFinishReason,\n }),\n usageExtras,\n };\n}\n\n/** Merge the OpenRouter usage extras onto an existing (OpenAI-shape) usage object. */\nfunction augmentUsage<T extends object>(\n usage: T,\n shaping: OpenRouterShaping,\n): T & OpenRouterUsageExtras {\n return { ...usage, ...shaping.usageExtras };\n}\n\n/** Resolve a choice's native_finish_reason (mirrors finish_reason unless overridden). */\nfunction nativeFinish(finishReason: string | null, shaping: OpenRouterShaping): string | null {\n if (finishReason == null) return null;\n return shaping.nativeFinishReason ?? finishReason;\n}\n\n/**\n * Shape a non-streaming OpenAI completion into OpenRouter form, in place, to\n * match real OpenRouter bytes (or-capture/chat-nonstream.json): `gen-` id\n * (unless the fixture pinned `id`), top-level `provider`, `service_tier: null`,\n * an always-present `system_fingerprint` (null when unset — the canonical\n * `@openrouter/sdk` requires it), per-choice `native_finish_reason`, a\n * `message.reasoning` field (mirrors `reasoning_content`, else null), and\n * cost-bearing `usage`.\n */\nexport function shapeOpenRouterCompletion(\n completion: ChatCompletion,\n shaping: OpenRouterShaping,\n idOverridden: boolean,\n): ChatCompletion {\n if (!idOverridden) completion.id = toGenId(completion.id);\n completion.provider = shaping.provider;\n if (completion.system_fingerprint === undefined) completion.system_fingerprint = null;\n if (completion.service_tier === undefined) completion.service_tier = null;\n for (const choice of completion.choices) {\n // Non-streaming finish_reason is always a string, so native mirrors it.\n choice.native_finish_reason = shaping.nativeFinishReason ?? choice.finish_reason;\n if (choice.message.reasoning === undefined) {\n choice.message.reasoning = choice.message.reasoning_content ?? null;\n }\n }\n completion.usage = augmentUsage(completion.usage, shaping);\n return completion;\n}\n\n/**\n * Shape streaming OpenAI chunks into OpenRouter form, in place, to match real\n * OpenRouter bytes (or-capture/chat-stream.sse): EVERY chunk carries the\n * `gen-` id (unless pinned), top-level `provider`, and `system_fingerprint`\n * (null when unset); EVERY delta choice carries `native_finish_reason` (null\n * until the finish chunk, where it mirrors finish_reason); real OR repeats\n * `role: \"assistant\"` on every content delta (mirrored here); and the final\n * usage-bearing chunk gets cost-bearing usage plus `service_tier: null`.\n */\nexport function shapeOpenRouterChunks(\n chunks: SSEChunk[],\n shaping: OpenRouterShaping,\n idOverridden: boolean,\n): SSEChunk[] {\n for (const chunk of chunks) {\n if (!idOverridden) chunk.id = toGenId(chunk.id);\n chunk.provider = shaping.provider;\n if (chunk.system_fingerprint === undefined) chunk.system_fingerprint = null;\n for (const choice of chunk.choices) {\n choice.native_finish_reason = nativeFinish(choice.finish_reason, shaping);\n // Real OR stamps role:\"assistant\" on every content-bearing delta. A\n // `content: null` delta is NOT content-bearing, so require string content.\n if (typeof choice.delta.content === \"string\" && choice.delta.role === undefined) {\n choice.delta.role = \"assistant\";\n }\n }\n if (chunk.usage) {\n chunk.usage = augmentUsage(chunk.usage, shaping);\n chunk.service_tier = null;\n }\n }\n return chunks;\n}\n\n/**\n * Serialize an OpenRouter error envelope: `{ error: { code, message,\n * metadata? } }` where `code` equals the HTTP status (contrast the OpenAI\n * shape `{ error: { message, type, param, code } }`). Used for every error\n * emitted on an OpenRouter request — fixture errors, no-match, strict, and\n * routing failures.\n */\nexport function serializeOpenRouterError(\n status: number,\n message: string,\n metadata?: Record<string, unknown>,\n): string {\n return JSON.stringify({\n error: {\n code: status,\n message,\n ...(metadata !== undefined && { metadata }),\n },\n });\n}\n\n// ─── Discovery endpoints ─────────────────────────────────────────────────────\n\n/** Default model slug set for `GET /api/v1/models` when no chat fixtures name one. */\nconst DEFAULT_OPENROUTER_MODELS = [\n \"openai/gpt-4o\",\n \"anthropic/claude-3.5-sonnet\",\n \"google/gemini-2.0-flash-001\",\n];\n\n/**\n * Build one OpenRouter model-catalog object (deterministic static filler).\n * Field set mirrors real OpenRouter bytes (or-capture/models-first.json):\n * pricing values are STRINGS; the object also carries hugging_face_id,\n * knowledge_cutoff, supported_voices, links, default_parameters,\n * expiration_date, and a `reasoning` descriptor (SDKs tolerate extras).\n */\nfunction openRouterModelObject(id: string): Record<string, unknown> {\n return {\n id,\n canonical_slug: id,\n hugging_face_id: null,\n name: id,\n created: 0,\n description: `Mock OpenRouter model ${id} served by aimock.`,\n context_length: 128000,\n architecture: {\n modality: \"text->text\",\n input_modalities: [\"text\"],\n output_modalities: [\"text\"],\n tokenizer: \"Other\",\n instruct_type: null,\n },\n pricing: {\n prompt: \"0\",\n completion: \"0\",\n request: \"0\",\n image: \"0\",\n web_search: \"0\",\n internal_reasoning: \"0\",\n input_cache_read: \"0\",\n input_cache_write: \"0\",\n },\n top_provider: {\n context_length: 128000,\n max_completion_tokens: 16384,\n is_moderated: false,\n },\n per_request_limits: null,\n supported_parameters: [\n \"tools\",\n \"tool_choice\",\n \"max_tokens\",\n \"temperature\",\n \"top_p\",\n \"stop\",\n \"reasoning\",\n ],\n default_parameters: null,\n supported_voices: null,\n knowledge_cutoff: null,\n expiration_date: null,\n links: { details: `/api/v1/models/${id}/endpoints` },\n reasoning: { mandatory: false, default_enabled: false },\n };\n}\n\n/**\n * Collect the model ids advertised by `GET /api/v1/models`: every loaded chat\n * (or endpoint-less) fixture with a string `match.model`, in first-seen order;\n * the default set when none contribute one. Mirrors the Ollama `/api/tags` and\n * OpenRouter video `/models` synthesis idioms.\n */\nfunction collectModelIds(fixtures: Fixture[]): string[] {\n const ids: string[] = [];\n const seen = new Set<string>();\n for (const f of fixtures) {\n if (f.match.endpoint !== undefined && f.match.endpoint !== \"chat\") continue;\n const m = f.match.model;\n if (typeof m === \"string\" && m.length > 0 && !seen.has(m)) {\n seen.add(m);\n ids.push(m);\n }\n }\n return ids.length > 0 ? ids : DEFAULT_OPENROUTER_MODELS;\n}\n\nfunction journalGet(\n req: http.IncomingMessage,\n journal: Journal,\n path: string,\n status: number,\n): void {\n journal.add({\n method: req.method ?? \"GET\",\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status, fixture: null },\n });\n}\n\n/** GET /api/v1/models — the OpenRouter model catalog synthesized from fixtures. */\nexport function handleOpenRouterModels(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n fixtures: Fixture[],\n journal: Journal,\n _defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): void {\n setCorsHeaders(res);\n const path = req.url ?? \"/api/v1/models\";\n const data = collectModelIds(fixtures).map(openRouterModelObject);\n journalGet(req, journal, path, 200);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ data }));\n}\n\n/** GET /api/v1/key — the caller's key metadata/limits/usage (static defaults). */\nexport function handleOpenRouterKey(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n journal: Journal,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): void {\n setCorsHeaders(res);\n const path = req.url ?? \"/api/v1/key\";\n journalGet(req, journal, path, 200);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n // Field set + order mirror real OpenRouter bytes (or-capture/key.json).\n // null limit / limit_remaining = unlimited. `creator_user_id` is an account\n // echo — null for the mock (no account). rate_limit is deprecated upstream.\n res.end(\n JSON.stringify({\n data: {\n label: \"sk-or-v1-aimock\",\n is_management_key: false,\n is_provisioning_key: false,\n limit: null,\n limit_reset: null,\n limit_remaining: null,\n include_byok_in_limit: false,\n usage: 0,\n usage_daily: 0,\n usage_weekly: 0,\n usage_monthly: 0,\n byok_usage: 0,\n byok_usage_daily: 0,\n byok_usage_weekly: 0,\n byok_usage_monthly: 0,\n is_free_tier: false,\n expires_at: null,\n creator_user_id: null,\n rate_limit: {\n requests: -1,\n interval: \"10s\",\n note: \"This field is deprecated and safe to ignore.\",\n },\n },\n }),\n );\n}\n\n/** GET /api/v1/credits — account credit totals (static defaults). */\nexport function handleOpenRouterCredits(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n journal: Journal,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): void {\n setCorsHeaders(res);\n const path = req.url ?? \"/api/v1/credits\";\n journalGet(req, journal, path, 200);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ data: { total_credits: 0, total_usage: 0 } }));\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"openrouter-chat.js","names":["resolveOpenRouterUsage","renderOpenRouterUsage"],"sources":["../src/openrouter-chat.ts"],"sourcesContent":["import {\n resolve as resolveOpenRouterUsage,\n render as renderOpenRouterUsage,\n} from \"./response/usage/codecs/openrouter.js\";\nimport type * as http from \"node:http\";\nimport type {\n ChatCompletion,\n ChatCompletionRequest,\n Fixture,\n HandlerDefaults,\n OpenRouterUsageExtras,\n ResponseOverrides,\n SSEChunk,\n} from \"./types.js\";\nimport type { Journal } from \"./journal.js\";\nimport { flattenHeaders } from \"./helpers.js\";\nimport { collect } from \"./response/catalog/collect.js\";\nimport { renderOpenRouterChatModels } from \"./response/catalog/render.js\";\nimport { buildOpenAIError } from \"./response/errors.js\";\n\n/**\n * OpenRouter chat/LLM router shaping. aimock detects an OpenRouter request by\n * the ORIGINAL request path (before `normalizeCompatPath` erases it) starting\n * with the OpenRouter canonical base `/api/v1/` — mirroring a real client that\n * points an OpenAI SDK at `baseURL: https://openrouter.ai/api/v1`. When a\n * request is OpenRouter, the same OpenAI-compatible chat response is shaped\n * with OpenRouter's distinguishing fields (a `gen-` id prefix, a top-level\n * `provider`, `native_finish_reason`, and a rich cost-bearing `usage`) and the\n * `models[]` fallback array is simulated deterministically against fixtures.\n * OpenAI (`/v1/...`) callers are left byte-for-byte unchanged.\n *\n * The shaping is applied as a post-pass over the objects the existing OpenAI\n * builders produce (never a fork of those builders), so every OpenAI code path\n * is untouched.\n */\n\n/** OpenRouter's canonical API base — the detection prefix. */\nexport const OPENROUTER_BASE_PREFIX = \"/api/v1/\";\n\nconst OPENAI_ID_PREFIX = \"chatcmpl-\";\nconst OPENROUTER_ID_PREFIX = \"gen-\";\n\n/**\n * Return true when the ORIGINAL request path marks an OpenRouter request. Must\n * be evaluated on the pre-normalization pathname: `normalizeCompatPath`\n * rewrites `/api/v1/chat/completions` → `/v1/chat/completions`, erasing the\n * `/api/v1/` signal.\n */\nexport function isOpenRouterPath(originalPathname: string): boolean {\n return originalPathname.startsWith(OPENROUTER_BASE_PREFIX);\n}\n\n/**\n * Derive the default OpenRouter `provider` from a model slug: the author\n * segment before the first `/` (`openai/gpt-4o` → `\"openai\"`). A slug without\n * a `/` yields the whole slug. An absent/empty slug — a request that never set\n * `model` yet matched a model-less fixture — yields `\"\"` rather than throwing\n * (guards against a `TypeError` becoming a generic 500).\n */\nexport function deriveOpenRouterProvider(model: string | undefined): string {\n if (!model) return \"\";\n const slash = model.indexOf(\"/\");\n return slash === -1 ? model : model.slice(0, slash);\n}\n\n/**\n * Build the OpenRouter fallback candidate list `[body.model, ...body.models]`,\n * de-duplicated with insertion order preserved. A request without `models`\n * yields just `[body.model]` — identical to today's single-model behavior.\n */\nexport function buildOpenRouterCandidates(body: ChatCompletionRequest): string[] {\n const raw = [body.model, ...(Array.isArray(body.models) ? body.models : [])];\n const seen = new Set<string>();\n const out: string[] = [];\n for (const m of raw) {\n if (typeof m !== \"string\" || m.length === 0) continue;\n if (seen.has(m)) continue;\n seen.add(m);\n out.push(m);\n }\n return out;\n}\n\n/** Rewrite an auto-generated `chatcmpl-` id to OpenRouter's `gen-` prefix. */\nfunction toGenId(id: string): string {\n return id.startsWith(OPENAI_ID_PREFIX)\n ? OPENROUTER_ID_PREFIX + id.slice(OPENAI_ID_PREFIX.length)\n : id;\n}\n\n/**\n * The resolved OpenRouter shaping context for one response. `provider` and the\n * always-present `cost`/`cost_details` are computed here; the detail\n * breakdowns and `is_byok` are carried only when a fixture overrode them.\n */\nexport interface OpenRouterShaping {\n provider: string;\n nativeFinishReason?: string;\n usageExtras: OpenRouterUsageExtras;\n}\n\n/**\n * Resolve the shaping context from a fixture's response overrides and the\n * winning model slug. `provider` defaults to the winning slug's author;\n * `native_finish_reason` mirrors `finish_reason` unless overridden. `cost` and\n * `cost_details` are NULLABLE — emitted only when the fixture supplies a `cost`\n * (never defaulted to 0); when emitted, `cost_details` carries all three\n * upstream fields (a partial object breaks the canonical SDK). The token-detail\n * breakdowns and `is_byok` are included only when the fixture set them, and an\n * emitted breakdown always carries its required member (`cached_tokens` /\n * `reasoning_tokens`).\n */\nexport function resolveOpenRouterShaping(\n overrides: ResponseOverrides | undefined,\n winningModel: string | undefined,\n): OpenRouterShaping {\n const usageExtras = resolveOpenRouterUsage(overrides);\n return {\n provider: overrides?.provider ?? deriveOpenRouterProvider(winningModel),\n ...(overrides?.nativeFinishReason !== undefined && {\n nativeFinishReason: overrides.nativeFinishReason,\n }),\n usageExtras,\n };\n}\n\n/** Merge the OpenRouter usage extras onto an existing (OpenAI-shape) usage object. */\nfunction augmentUsage<T extends object>(\n usage: T,\n shaping: OpenRouterShaping,\n): T & OpenRouterUsageExtras {\n return renderOpenRouterUsage(usage, shaping.usageExtras);\n}\n\n/** Resolve a choice's native_finish_reason (mirrors finish_reason unless overridden). */\nfunction nativeFinish(finishReason: string | null, shaping: OpenRouterShaping): string | null {\n if (finishReason == null) return null;\n return shaping.nativeFinishReason ?? finishReason;\n}\n\n/**\n * Shape a non-streaming OpenAI completion into OpenRouter form, in place, to\n * match real OpenRouter bytes (or-capture/chat-nonstream.json): `gen-` id\n * (unless the fixture pinned `id`), top-level `provider`, `service_tier: null`,\n * an always-present `system_fingerprint` (null when unset — the canonical\n * `@openrouter/sdk` requires it), per-choice `native_finish_reason`, a\n * `message.reasoning` field (mirrors `reasoning_content`, else null), and\n * cost-bearing `usage`.\n */\nexport function shapeOpenRouterCompletion(\n completion: ChatCompletion,\n shaping: OpenRouterShaping,\n idOverridden: boolean,\n): ChatCompletion {\n if (!idOverridden) completion.id = toGenId(completion.id);\n completion.provider = shaping.provider;\n if (completion.system_fingerprint === undefined) completion.system_fingerprint = null;\n if (completion.service_tier === undefined) completion.service_tier = null;\n for (const choice of completion.choices) {\n // Non-streaming finish_reason is always a string, so native mirrors it.\n choice.native_finish_reason = shaping.nativeFinishReason ?? choice.finish_reason;\n if (choice.message.reasoning === undefined) {\n choice.message.reasoning = choice.message.reasoning_content ?? null;\n }\n }\n completion.usage = augmentUsage(completion.usage, shaping);\n return completion;\n}\n\n/**\n * Shape streaming OpenAI chunks into OpenRouter form, in place, to match real\n * OpenRouter bytes (or-capture/chat-stream.sse): EVERY chunk carries the\n * `gen-` id (unless pinned), top-level `provider`, and `system_fingerprint`\n * (null when unset); EVERY delta choice carries `native_finish_reason` (null\n * until the finish chunk, where it mirrors finish_reason); real OR repeats\n * `role: \"assistant\"` on every content delta (mirrored here); and the final\n * usage-bearing chunk gets cost-bearing usage plus `service_tier: null`.\n */\nexport function shapeOpenRouterChunks(\n chunks: SSEChunk[],\n shaping: OpenRouterShaping,\n idOverridden: boolean,\n): SSEChunk[] {\n for (const chunk of chunks) {\n if (!idOverridden) chunk.id = toGenId(chunk.id);\n chunk.provider = shaping.provider;\n if (chunk.system_fingerprint === undefined) chunk.system_fingerprint = null;\n for (const choice of chunk.choices) {\n choice.native_finish_reason = nativeFinish(choice.finish_reason, shaping);\n // Real OR stamps role:\"assistant\" on every content-bearing delta. A\n // `content: null` delta is NOT content-bearing, so require string content.\n if (typeof choice.delta.content === \"string\" && choice.delta.role === undefined) {\n choice.delta.role = \"assistant\";\n }\n }\n if (chunk.usage) {\n chunk.usage = augmentUsage(chunk.usage, shaping);\n chunk.service_tier = null;\n }\n }\n return chunks;\n}\n\n/**\n * Serialize an OpenRouter error envelope: `{ error: { code, message,\n * metadata? } }` where `code` equals the HTTP status (contrast the OpenAI\n * shape `{ error: { message, type, param, code } }`). Used for every error\n * emitted on an OpenRouter request — fixture errors, no-match, strict, and\n * routing failures.\n */\nexport function serializeOpenRouterError(\n status: number,\n message: string,\n metadata?: Record<string, unknown>,\n): string {\n return JSON.stringify(buildOpenAIError(\"openrouter-error\", { code: status, message, metadata }));\n}\n\n// ─── Discovery endpoints ─────────────────────────────────────────────────────\n\nfunction journalGet(\n req: http.IncomingMessage,\n journal: Journal,\n path: string,\n status: number,\n): void {\n journal.add({\n method: req.method ?? \"GET\",\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status, fixture: null },\n });\n}\n\n/** GET /api/v1/models — the OpenRouter model catalog synthesized from fixtures. */\nexport function handleOpenRouterModels(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n fixtures: Fixture[],\n journal: Journal,\n _defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): void {\n setCorsHeaders(res);\n const path = req.url ?? \"/api/v1/models\";\n const body = renderOpenRouterChatModels(collect(fixtures, \"chat\").ids);\n journalGet(req, journal, path, 200);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(body));\n}\n\n/** GET /api/v1/key — the caller's key metadata/limits/usage (static defaults). */\nexport function handleOpenRouterKey(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n journal: Journal,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): void {\n setCorsHeaders(res);\n const path = req.url ?? \"/api/v1/key\";\n journalGet(req, journal, path, 200);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n // Field set + order mirror real OpenRouter bytes (or-capture/key.json).\n // null limit / limit_remaining = unlimited. `creator_user_id` is an account\n // echo — null for the mock (no account). rate_limit is deprecated upstream.\n res.end(\n JSON.stringify({\n data: {\n label: \"sk-or-v1-aimock\",\n is_management_key: false,\n is_provisioning_key: false,\n limit: null,\n limit_reset: null,\n limit_remaining: null,\n include_byok_in_limit: false,\n usage: 0,\n usage_daily: 0,\n usage_weekly: 0,\n usage_monthly: 0,\n byok_usage: 0,\n byok_usage_daily: 0,\n byok_usage_weekly: 0,\n byok_usage_monthly: 0,\n is_free_tier: false,\n expires_at: null,\n creator_user_id: null,\n rate_limit: {\n requests: -1,\n interval: \"10s\",\n note: \"This field is deprecated and safe to ignore.\",\n },\n },\n }),\n );\n}\n\n/** GET /api/v1/credits — account credit totals (static defaults). */\nexport function handleOpenRouterCredits(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n journal: Journal,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): void {\n setCorsHeaders(res);\n const path = req.url ?? \"/api/v1/credits\";\n journalGet(req, journal, path, 200);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ data: { total_credits: 0, total_usage: 0 } }));\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAqCA,MAAa,yBAAyB;AAEtC,MAAM,mBAAmB;AACzB,MAAM,uBAAuB;;;;;;;AAQ7B,SAAgB,iBAAiB,kBAAmC;AAClE,QAAO,iBAAiB,WAAW,uBAAuB;;;;;;;;;AAU5D,SAAgB,yBAAyB,OAAmC;AAC1E,KAAI,CAAC,MAAO,QAAO;CACnB,MAAM,QAAQ,MAAM,QAAQ,IAAI;AAChC,QAAO,UAAU,KAAK,QAAQ,MAAM,MAAM,GAAG,MAAM;;;;;;;AAQrD,SAAgB,0BAA0B,MAAuC;CAC/E,MAAM,MAAM,CAAC,KAAK,OAAO,GAAI,MAAM,QAAQ,KAAK,OAAO,GAAG,KAAK,SAAS,EAAE,CAAE;CAC5E,MAAM,uBAAO,IAAI,KAAa;CAC9B,MAAM,MAAgB,EAAE;AACxB,MAAK,MAAM,KAAK,KAAK;AACnB,MAAI,OAAO,MAAM,YAAY,EAAE,WAAW,EAAG;AAC7C,MAAI,KAAK,IAAI,EAAE,CAAE;AACjB,OAAK,IAAI,EAAE;AACX,MAAI,KAAK,EAAE;;AAEb,QAAO;;;AAIT,SAAS,QAAQ,IAAoB;AACnC,QAAO,GAAG,WAAW,iBAAiB,GAClC,uBAAuB,GAAG,MAAM,EAAwB,GACxD;;;;;;;;;;;;;AAyBN,SAAgB,yBACd,WACA,cACmB;CACnB,MAAM,cAAcA,QAAuB,UAAU;AACrD,QAAO;EACL,UAAU,WAAW,YAAY,yBAAyB,aAAa;EACvE,GAAI,WAAW,uBAAuB,UAAa,EACjD,oBAAoB,UAAU,oBAC/B;EACD;EACD;;;AAIH,SAAS,aACP,OACA,SAC2B;AAC3B,QAAOC,OAAsB,OAAO,QAAQ,YAAY;;;AAI1D,SAAS,aAAa,cAA6B,SAA2C;AAC5F,KAAI,gBAAgB,KAAM,QAAO;AACjC,QAAO,QAAQ,sBAAsB;;;;;;;;;;;AAYvC,SAAgB,0BACd,YACA,SACA,cACgB;AAChB,KAAI,CAAC,aAAc,YAAW,KAAK,QAAQ,WAAW,GAAG;AACzD,YAAW,WAAW,QAAQ;AAC9B,KAAI,WAAW,uBAAuB,OAAW,YAAW,qBAAqB;AACjF,KAAI,WAAW,iBAAiB,OAAW,YAAW,eAAe;AACrE,MAAK,MAAM,UAAU,WAAW,SAAS;AAEvC,SAAO,uBAAuB,QAAQ,sBAAsB,OAAO;AACnE,MAAI,OAAO,QAAQ,cAAc,OAC/B,QAAO,QAAQ,YAAY,OAAO,QAAQ,qBAAqB;;AAGnE,YAAW,QAAQ,aAAa,WAAW,OAAO,QAAQ;AAC1D,QAAO;;;;;;;;;;;AAYT,SAAgB,sBACd,QACA,SACA,cACY;AACZ,MAAK,MAAM,SAAS,QAAQ;AAC1B,MAAI,CAAC,aAAc,OAAM,KAAK,QAAQ,MAAM,GAAG;AAC/C,QAAM,WAAW,QAAQ;AACzB,MAAI,MAAM,uBAAuB,OAAW,OAAM,qBAAqB;AACvE,OAAK,MAAM,UAAU,MAAM,SAAS;AAClC,UAAO,uBAAuB,aAAa,OAAO,eAAe,QAAQ;AAGzE,OAAI,OAAO,OAAO,MAAM,YAAY,YAAY,OAAO,MAAM,SAAS,OACpE,QAAO,MAAM,OAAO;;AAGxB,MAAI,MAAM,OAAO;AACf,SAAM,QAAQ,aAAa,MAAM,OAAO,QAAQ;AAChD,SAAM,eAAe;;;AAGzB,QAAO;;;;;;;;;AAUT,SAAgB,yBACd,QACA,SACA,UACQ;AACR,QAAO,KAAK,UAAU,iBAAiB,oBAAoB;EAAE,MAAM;EAAQ;EAAS;EAAU,CAAC,CAAC;;AAKlG,SAAS,WACP,KACA,SACA,MACA,QACM;AACN,SAAQ,IAAI;EACV,QAAQ,IAAI,UAAU;EACtB;EACA,SAAS,eAAe,IAAI,QAAQ;EACpC,MAAM;EACN,UAAU;GAAE;GAAQ,SAAS;GAAM;EACpC,CAAC;;;AAIJ,SAAgB,uBACd,KACA,KACA,UACA,SACA,WACA,gBACM;AACN,gBAAe,IAAI;CACnB,MAAM,OAAO,IAAI,OAAO;CACxB,MAAM,OAAO,2BAA2B,QAAQ,UAAU,OAAO,CAAC,IAAI;AACtE,YAAW,KAAK,SAAS,MAAM,IAAI;AACnC,KAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,KAAI,IAAI,KAAK,UAAU,KAAK,CAAC;;;AAI/B,SAAgB,oBACd,KACA,KACA,SACA,gBACM;AACN,gBAAe,IAAI;AAEnB,YAAW,KAAK,SADH,IAAI,OAAO,eACO,IAAI;AACnC,KAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAI1D,KAAI,IACF,KAAK,UAAU,EACb,MAAM;EACJ,OAAO;EACP,mBAAmB;EACnB,qBAAqB;EACrB,OAAO;EACP,aAAa;EACb,iBAAiB;EACjB,uBAAuB;EACvB,OAAO;EACP,aAAa;EACb,cAAc;EACd,eAAe;EACf,YAAY;EACZ,kBAAkB;EAClB,mBAAmB;EACnB,oBAAoB;EACpB,cAAc;EACd,YAAY;EACZ,iBAAiB;EACjB,YAAY;GACV,UAAU;GACV,UAAU;GACV,MAAM;GACP;EACF,EACF,CAAC,CACH;;;AAIH,SAAgB,wBACd,KACA,KACA,SACA,gBACM;AACN,gBAAe,IAAI;AAEnB,YAAW,KAAK,SADH,IAAI,OAAO,mBACO,IAAI;AACnC,KAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,KAAI,IAAI,KAAK,UAAU,EAAE,MAAM;EAAE,eAAe;EAAG,aAAa;EAAG,EAAE,CAAC,CAAC"}