@srouterhq/server 0.0.0-stage → 0.9.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 (1027) hide show
  1. package/LICENSE +21 -0
  2. package/client/dist/assets/am-TQ7Jmdqe.js +1 -0
  3. package/client/dist/assets/ar-Bf5a7yfC.js +1 -0
  4. package/client/dist/assets/az-CrosE1xH.js +1 -0
  5. package/client/dist/assets/bg-BvWnmcUz.js +1 -0
  6. package/client/dist/assets/bn-ams5TsJp.js +1 -0
  7. package/client/dist/assets/cs-Dh67CaJ_.js +1 -0
  8. package/client/dist/assets/da-9WlT8Iq7.js +1 -0
  9. package/client/dist/assets/de-lWhJJzz7.js +1 -0
  10. package/client/dist/assets/el-Cu7_GhNq.js +1 -0
  11. package/client/dist/assets/es-BZuzsBcP.js +1 -0
  12. package/client/dist/assets/fa-Dwdn-jKS.js +1 -0
  13. package/client/dist/assets/fi-BFrFyOTy.js +1 -0
  14. package/client/dist/assets/fr-XhbtmPpj.js +1 -0
  15. package/client/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  16. package/client/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  17. package/client/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  18. package/client/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  19. package/client/dist/assets/geist-mono-cyrillic-ext-wght-normal-I4S5GZfc.woff2 +0 -0
  20. package/client/dist/assets/geist-mono-cyrillic-wght-normal-BmXc_FBt.woff2 +0 -0
  21. package/client/dist/assets/geist-mono-latin-ext-wght-normal-DrnZ1wKl.woff2 +0 -0
  22. package/client/dist/assets/geist-mono-latin-wght-normal-B_7UjwxQ.woff2 +0 -0
  23. package/client/dist/assets/geist-mono-symbols2-wght-normal-GZpp1pK2.woff2 +0 -0
  24. package/client/dist/assets/geist-mono-vietnamese-wght-normal-D8KDMBhC.woff2 +0 -0
  25. package/client/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  26. package/client/dist/assets/gu-D0uhLUB8.js +1 -0
  27. package/client/dist/assets/ha-CVlm9pqp.js +1 -0
  28. package/client/dist/assets/he-CMIFeoWr.js +1 -0
  29. package/client/dist/assets/hi-ENTwhBpn.js +1 -0
  30. package/client/dist/assets/hr-fc03vMy_.js +1 -0
  31. package/client/dist/assets/hu-YBo2BIYt.js +1 -0
  32. package/client/dist/assets/id-BEWXYX69.js +1 -0
  33. package/client/dist/assets/ig-BKLZFKka.js +1 -0
  34. package/client/dist/assets/index-D9I4KtvR.js +145 -0
  35. package/client/dist/assets/index-txhEmCdb.css +2 -0
  36. package/client/dist/assets/it-Bzmei1Y2.js +1 -0
  37. package/client/dist/assets/ja-BfcpuqEl.js +1 -0
  38. package/client/dist/assets/ka-CnWjRgLo.js +1 -0
  39. package/client/dist/assets/km-BAjPR3Qj.js +1 -0
  40. package/client/dist/assets/kn-B15WKU5m.js +1 -0
  41. package/client/dist/assets/ko-C61fAB-w.js +1 -0
  42. package/client/dist/assets/lt-CYTr4_YW.js +1 -0
  43. package/client/dist/assets/ml-CtqxgyH3.js +1 -0
  44. package/client/dist/assets/mr-X5TMY7aR.js +1 -0
  45. package/client/dist/assets/ms-DSg8RqPS.js +1 -0
  46. package/client/dist/assets/my-D7RqR8uE.js +1 -0
  47. package/client/dist/assets/ne-DaflxkDj.js +1 -0
  48. package/client/dist/assets/nl-BOqqtsC8.js +1 -0
  49. package/client/dist/assets/no-BSHp_StB.js +1 -0
  50. package/client/dist/assets/or-BkLR9Vub.js +1 -0
  51. package/client/dist/assets/pa-CarJ2XgI.js +1 -0
  52. package/client/dist/assets/pl-C2n0V-0k.js +1 -0
  53. package/client/dist/assets/pt-BR-CqJdNhg2.js +1 -0
  54. package/client/dist/assets/pt-PT-C_HpMyKz.js +1 -0
  55. package/client/dist/assets/ro-DVRZqSAS.js +1 -0
  56. package/client/dist/assets/ru-DwAJG7VC.js +1 -0
  57. package/client/dist/assets/si-BxUezXm0.js +1 -0
  58. package/client/dist/assets/sk-eI1VcIfY.js +1 -0
  59. package/client/dist/assets/sr-CJF3auCr.js +1 -0
  60. package/client/dist/assets/srouter-logo-C6ZfjGIi.svg +14 -0
  61. package/client/dist/assets/sv-nqvwHAq_.js +1 -0
  62. package/client/dist/assets/sw-DWynE2pW.js +1 -0
  63. package/client/dist/assets/ta-BVM12knC.js +1 -0
  64. package/client/dist/assets/te-oyzEkF7_.js +1 -0
  65. package/client/dist/assets/th-COVeE0ZQ.js +1 -0
  66. package/client/dist/assets/tl-DsJANnQz.js +1 -0
  67. package/client/dist/assets/tr-B-Fl-urO.js +1 -0
  68. package/client/dist/assets/uk-C5qketAO.js +1 -0
  69. package/client/dist/assets/ur-QU9KugfV.js +1 -0
  70. package/client/dist/assets/uz-DL9cG_mY.js +1 -0
  71. package/client/dist/assets/vi-CQiVy3Pu.js +1 -0
  72. package/client/dist/assets/yo-DqazMdrZ.js +1 -0
  73. package/client/dist/assets/zh-CN-DnYL_Kio.js +1 -0
  74. package/client/dist/assets/zh-TW-5bdbosKj.js +1 -0
  75. package/client/dist/favicon.svg +14 -0
  76. package/client/dist/icons.svg +24 -0
  77. package/client/dist/index.html +37 -0
  78. package/dist/app.d.ts +4 -0
  79. package/dist/app.d.ts.map +1 -0
  80. package/dist/app.js +372 -0
  81. package/dist/app.js.map +1 -0
  82. package/dist/db/index.d.ts +49 -0
  83. package/dist/db/index.d.ts.map +1 -0
  84. package/dist/db/index.js +203 -0
  85. package/dist/db/index.js.map +1 -0
  86. package/dist/db/migrate/TEMPLATE.d.ts +4 -0
  87. package/dist/db/migrate/TEMPLATE.d.ts.map +1 -0
  88. package/dist/db/migrate/TEMPLATE.js +18 -0
  89. package/dist/db/migrate/TEMPLATE.js.map +1 -0
  90. package/dist/db/migrate/cli.d.ts +2 -0
  91. package/dist/db/migrate/cli.d.ts.map +1 -0
  92. package/dist/db/migrate/cli.js +177 -0
  93. package/dist/db/migrate/cli.js.map +1 -0
  94. package/dist/db/migrate/defaults.d.ts +52 -0
  95. package/dist/db/migrate/defaults.d.ts.map +1 -0
  96. package/dist/db/migrate/defaults.js +126 -0
  97. package/dist/db/migrate/defaults.js.map +1 -0
  98. package/dist/db/migrate/runner.d.ts +16 -0
  99. package/dist/db/migrate/runner.d.ts.map +1 -0
  100. package/dist/db/migrate/runner.js +178 -0
  101. package/dist/db/migrate/runner.js.map +1 -0
  102. package/dist/db/migrations/20260101_000000_legacy_baseline.d.ts +4 -0
  103. package/dist/db/migrations/20260101_000000_legacy_baseline.d.ts.map +1 -0
  104. package/dist/db/migrations/20260101_000000_legacy_baseline.js +2240 -0
  105. package/dist/db/migrations/20260101_000000_legacy_baseline.js.map +1 -0
  106. package/dist/db/migrations/20260627_000001_custom_provider_modalities.d.ts +4 -0
  107. package/dist/db/migrations/20260627_000001_custom_provider_modalities.d.ts.map +1 -0
  108. package/dist/db/migrations/20260627_000001_custom_provider_modalities.js +27 -0
  109. package/dist/db/migrations/20260627_000001_custom_provider_modalities.js.map +1 -0
  110. package/dist/db/migrations/20260627_000002_catalog_model_state.d.ts +4 -0
  111. package/dist/db/migrations/20260627_000002_catalog_model_state.d.ts.map +1 -0
  112. package/dist/db/migrations/20260627_000002_catalog_model_state.js +30 -0
  113. package/dist/db/migrations/20260627_000002_catalog_model_state.js.map +1 -0
  114. package/dist/db/migrations/20260628_120000_request_aggregates.d.ts +6 -0
  115. package/dist/db/migrations/20260628_120000_request_aggregates.d.ts.map +1 -0
  116. package/dist/db/migrations/20260628_120000_request_aggregates.js +104 -0
  117. package/dist/db/migrations/20260628_120000_request_aggregates.js.map +1 -0
  118. package/dist/db/migrations/20260630_000001_github_gpt41_context.d.ts +9 -0
  119. package/dist/db/migrations/20260630_000001_github_gpt41_context.d.ts.map +1 -0
  120. package/dist/db/migrations/20260630_000001_github_gpt41_context.js +22 -0
  121. package/dist/db/migrations/20260630_000001_github_gpt41_context.js.map +1 -0
  122. package/dist/db/migrations/20260706_000001_request_client_info.d.ts +4 -0
  123. package/dist/db/migrations/20260706_000001_request_client_info.d.ts.map +1 -0
  124. package/dist/db/migrations/20260706_000001_request_client_info.js +30 -0
  125. package/dist/db/migrations/20260706_000001_request_client_info.js.map +1 -0
  126. package/dist/db/migrations/20260706_000002_custom_model_tool_support.d.ts +17 -0
  127. package/dist/db/migrations/20260706_000002_custom_model_tool_support.d.ts.map +1 -0
  128. package/dist/db/migrations/20260706_000002_custom_model_tool_support.js +20 -0
  129. package/dist/db/migrations/20260706_000002_custom_model_tool_support.js.map +1 -0
  130. package/dist/db/migrations/20260714_000001_profile_chain_backfill.d.ts +12 -0
  131. package/dist/db/migrations/20260714_000001_profile_chain_backfill.d.ts.map +1 -0
  132. package/dist/db/migrations/20260714_000001_profile_chain_backfill.js +45 -0
  133. package/dist/db/migrations/20260714_000001_profile_chain_backfill.js.map +1 -0
  134. package/dist/db/migrations/20260720_000001_key_health_error.d.ts +5 -0
  135. package/dist/db/migrations/20260720_000001_key_health_error.d.ts.map +1 -0
  136. package/dist/db/migrations/20260720_000001_key_health_error.js +16 -0
  137. package/dist/db/migrations/20260720_000001_key_health_error.js.map +1 -0
  138. package/dist/db/migrations/20260726_000001_cooldown_probe_provenance.d.ts +4 -0
  139. package/dist/db/migrations/20260726_000001_cooldown_probe_provenance.d.ts.map +1 -0
  140. package/dist/db/migrations/20260726_000001_cooldown_probe_provenance.js +38 -0
  141. package/dist/db/migrations/20260726_000001_cooldown_probe_provenance.js.map +1 -0
  142. package/dist/db/migrations/20260726_000002_request_attempts.d.ts +4 -0
  143. package/dist/db/migrations/20260726_000002_request_attempts.d.ts.map +1 -0
  144. package/dist/db/migrations/20260726_000002_request_attempts.js +43 -0
  145. package/dist/db/migrations/20260726_000002_request_attempts.js.map +1 -0
  146. package/dist/db/migrations/20260726_000003_model_source_provenance.d.ts +4 -0
  147. package/dist/db/migrations/20260726_000003_model_source_provenance.d.ts.map +1 -0
  148. package/dist/db/migrations/20260726_000003_model_source_provenance.js +80 -0
  149. package/dist/db/migrations/20260726_000003_model_source_provenance.js.map +1 -0
  150. package/dist/db/migrations/20260726_000004_media_model_meta.d.ts +4 -0
  151. package/dist/db/migrations/20260726_000004_media_model_meta.d.ts.map +1 -0
  152. package/dist/db/migrations/20260726_000004_media_model_meta.js +34 -0
  153. package/dist/db/migrations/20260726_000004_media_model_meta.js.map +1 -0
  154. package/dist/db/migrations/20260726_000005_request_served_model.d.ts +4 -0
  155. package/dist/db/migrations/20260726_000005_request_served_model.d.ts.map +1 -0
  156. package/dist/db/migrations/20260726_000005_request_served_model.js +31 -0
  157. package/dist/db/migrations/20260726_000005_request_served_model.js.map +1 -0
  158. package/dist/db/migrations/20260726_000006_attempt_error_summary.d.ts +4 -0
  159. package/dist/db/migrations/20260726_000006_attempt_error_summary.d.ts.map +1 -0
  160. package/dist/db/migrations/20260726_000006_attempt_error_summary.js +29 -0
  161. package/dist/db/migrations/20260726_000006_attempt_error_summary.js.map +1 -0
  162. package/dist/db/migrations/20260727_000001_agent_compatibility.d.ts +4 -0
  163. package/dist/db/migrations/20260727_000001_agent_compatibility.d.ts.map +1 -0
  164. package/dist/db/migrations/20260727_000001_agent_compatibility.js +41 -0
  165. package/dist/db/migrations/20260727_000001_agent_compatibility.js.map +1 -0
  166. package/dist/db/migrations/20260728_000001_tombstone_provenance.d.ts +12 -0
  167. package/dist/db/migrations/20260728_000001_tombstone_provenance.d.ts.map +1 -0
  168. package/dist/db/migrations/20260728_000001_tombstone_provenance.js +37 -0
  169. package/dist/db/migrations/20260728_000001_tombstone_provenance.js.map +1 -0
  170. package/dist/db/migrations/20260729_000001_custom_model_endpoint_identity.d.ts +4 -0
  171. package/dist/db/migrations/20260729_000001_custom_model_endpoint_identity.d.ts.map +1 -0
  172. package/dist/db/migrations/20260729_000001_custom_model_endpoint_identity.js +145 -0
  173. package/dist/db/migrations/20260729_000001_custom_model_endpoint_identity.js.map +1 -0
  174. package/dist/db/migrations/20260802_000001_custom_endpoint_host_labels.d.ts +6 -0
  175. package/dist/db/migrations/20260802_000001_custom_endpoint_host_labels.d.ts.map +1 -0
  176. package/dist/db/migrations/20260802_000001_custom_endpoint_host_labels.js +52 -0
  177. package/dist/db/migrations/20260802_000001_custom_endpoint_host_labels.js.map +1 -0
  178. package/dist/db/migrations/20260805_000001_key_model_scope.d.ts +6 -0
  179. package/dist/db/migrations/20260805_000001_key_model_scope.d.ts.map +1 -0
  180. package/dist/db/migrations/20260805_000001_key_model_scope.js +17 -0
  181. package/dist/db/migrations/20260805_000001_key_model_scope.js.map +1 -0
  182. package/dist/db/migrations/20260805_000002_client_profiles.d.ts +4 -0
  183. package/dist/db/migrations/20260805_000002_client_profiles.d.ts.map +1 -0
  184. package/dist/db/migrations/20260805_000002_client_profiles.js +33 -0
  185. package/dist/db/migrations/20260805_000002_client_profiles.js.map +1 -0
  186. package/dist/db/migrations/20260810_000001_api_key_proxy.d.ts +4 -0
  187. package/dist/db/migrations/20260810_000001_api_key_proxy.d.ts.map +1 -0
  188. package/dist/db/migrations/20260810_000001_api_key_proxy.js +33 -0
  189. package/dist/db/migrations/20260810_000001_api_key_proxy.js.map +1 -0
  190. package/dist/db/migrations/20260819_000001_custom_model_tombstones.d.ts +4 -0
  191. package/dist/db/migrations/20260819_000001_custom_model_tombstones.d.ts.map +1 -0
  192. package/dist/db/migrations/20260819_000001_custom_model_tombstones.js +19 -0
  193. package/dist/db/migrations/20260819_000001_custom_model_tombstones.js.map +1 -0
  194. package/dist/db/migrations/20260820_000001_playground_conversations.d.ts +4 -0
  195. package/dist/db/migrations/20260820_000001_playground_conversations.d.ts.map +1 -0
  196. package/dist/db/migrations/20260820_000001_playground_conversations.js +47 -0
  197. package/dist/db/migrations/20260820_000001_playground_conversations.js.map +1 -0
  198. package/dist/db/migrations/20260823_000001_server_logs.d.ts +4 -0
  199. package/dist/db/migrations/20260823_000001_server_logs.d.ts.map +1 -0
  200. package/dist/db/migrations/20260823_000001_server_logs.js +53 -0
  201. package/dist/db/migrations/20260823_000001_server_logs.js.map +1 -0
  202. package/dist/db/migrations/20260823_000002_backups_table.d.ts +8 -0
  203. package/dist/db/migrations/20260823_000002_backups_table.d.ts.map +1 -0
  204. package/dist/db/migrations/20260823_000002_backups_table.js +22 -0
  205. package/dist/db/migrations/20260823_000002_backups_table.js.map +1 -0
  206. package/dist/db/migrations/20260823_000003_attempt_key_label.d.ts +19 -0
  207. package/dist/db/migrations/20260823_000003_attempt_key_label.d.ts.map +1 -0
  208. package/dist/db/migrations/20260823_000003_attempt_key_label.js +28 -0
  209. package/dist/db/migrations/20260823_000003_attempt_key_label.js.map +1 -0
  210. package/dist/db/migrations/20260823_000004_profile_auto_include.d.ts +14 -0
  211. package/dist/db/migrations/20260823_000004_profile_auto_include.d.ts.map +1 -0
  212. package/dist/db/migrations/20260823_000004_profile_auto_include.js +25 -0
  213. package/dist/db/migrations/20260823_000004_profile_auto_include.js.map +1 -0
  214. package/dist/db/migrations/20260901_000001_idempotency_claims.d.ts +4 -0
  215. package/dist/db/migrations/20260901_000001_idempotency_claims.d.ts.map +1 -0
  216. package/dist/db/migrations/20260901_000001_idempotency_claims.js +46 -0
  217. package/dist/db/migrations/20260901_000001_idempotency_claims.js.map +1 -0
  218. package/dist/db/migrations/20260901_000002_quota_observation_lookup.d.ts +4 -0
  219. package/dist/db/migrations/20260901_000002_quota_observation_lookup.d.ts.map +1 -0
  220. package/dist/db/migrations/20260901_000002_quota_observation_lookup.js +37 -0
  221. package/dist/db/migrations/20260901_000002_quota_observation_lookup.js.map +1 -0
  222. package/dist/db/migrations/20260901_000003_request_caller.d.ts +4 -0
  223. package/dist/db/migrations/20260901_000003_request_caller.d.ts.map +1 -0
  224. package/dist/db/migrations/20260901_000003_request_caller.js +27 -0
  225. package/dist/db/migrations/20260901_000003_request_caller.js.map +1 -0
  226. package/dist/db/migrations/20260902_000001_analytics_latency_percentile_index.d.ts +4 -0
  227. package/dist/db/migrations/20260902_000001_analytics_latency_percentile_index.d.ts.map +1 -0
  228. package/dist/db/migrations/20260902_000001_analytics_latency_percentile_index.js +35 -0
  229. package/dist/db/migrations/20260902_000001_analytics_latency_percentile_index.js.map +1 -0
  230. package/dist/db/migrations/20260903_000001_mcp_enabled_default.d.ts +4 -0
  231. package/dist/db/migrations/20260903_000001_mcp_enabled_default.d.ts.map +1 -0
  232. package/dist/db/migrations/20260903_000001_mcp_enabled_default.js +37 -0
  233. package/dist/db/migrations/20260903_000001_mcp_enabled_default.js.map +1 -0
  234. package/dist/db/migrations/20260903_000002_response_cache.d.ts +4 -0
  235. package/dist/db/migrations/20260903_000002_response_cache.d.ts.map +1 -0
  236. package/dist/db/migrations/20260903_000002_response_cache.js +53 -0
  237. package/dist/db/migrations/20260903_000002_response_cache.js.map +1 -0
  238. package/dist/db/migrations/20260904_000001_key_monthly_budget.d.ts +13 -0
  239. package/dist/db/migrations/20260904_000001_key_monthly_budget.d.ts.map +1 -0
  240. package/dist/db/migrations/20260904_000001_key_monthly_budget.js +35 -0
  241. package/dist/db/migrations/20260904_000001_key_monthly_budget.js.map +1 -0
  242. package/dist/db/migrations/20260913_000001_request_model_attribution.d.ts +4 -0
  243. package/dist/db/migrations/20260913_000001_request_model_attribution.d.ts.map +1 -0
  244. package/dist/db/migrations/20260913_000001_request_model_attribution.js +68 -0
  245. package/dist/db/migrations/20260913_000001_request_model_attribution.js.map +1 -0
  246. package/dist/db/migrations/20260914_000001_key_monthly_usage.d.ts +5 -0
  247. package/dist/db/migrations/20260914_000001_key_monthly_usage.d.ts.map +1 -0
  248. package/dist/db/migrations/20260914_000001_key_monthly_usage.js +37 -0
  249. package/dist/db/migrations/20260914_000001_key_monthly_usage.js.map +1 -0
  250. package/dist/db/migrations/20260915_000001_quota_snapshot_freshness.d.ts +4 -0
  251. package/dist/db/migrations/20260915_000001_quota_snapshot_freshness.d.ts.map +1 -0
  252. package/dist/db/migrations/20260915_000001_quota_snapshot_freshness.js +28 -0
  253. package/dist/db/migrations/20260915_000001_quota_snapshot_freshness.js.map +1 -0
  254. package/dist/db/migrations/20261004_000001_unified_key_sr_prefix.d.ts +4 -0
  255. package/dist/db/migrations/20261004_000001_unified_key_sr_prefix.d.ts.map +1 -0
  256. package/dist/db/migrations/20261004_000001_unified_key_sr_prefix.js +25 -0
  257. package/dist/db/migrations/20261004_000001_unified_key_sr_prefix.js.map +1 -0
  258. package/dist/db/migrations/20261005_000001_key_budget_period.d.ts +10 -0
  259. package/dist/db/migrations/20261005_000001_key_budget_period.d.ts.map +1 -0
  260. package/dist/db/migrations/20261005_000001_key_budget_period.js +78 -0
  261. package/dist/db/migrations/20261005_000001_key_budget_period.js.map +1 -0
  262. package/dist/db/migrations/20261005_000002_key_budget_period_auto.d.ts +21 -0
  263. package/dist/db/migrations/20261005_000002_key_budget_period_auto.d.ts.map +1 -0
  264. package/dist/db/migrations/20261005_000002_key_budget_period_auto.js +41 -0
  265. package/dist/db/migrations/20261005_000002_key_budget_period_auto.js.map +1 -0
  266. package/dist/db/model-pricing.d.ts +39 -0
  267. package/dist/db/model-pricing.d.ts.map +1 -0
  268. package/dist/db/model-pricing.js +274 -0
  269. package/dist/db/model-pricing.js.map +1 -0
  270. package/dist/db/node-sqlite.d.ts +9 -0
  271. package/dist/db/node-sqlite.d.ts.map +1 -0
  272. package/dist/db/node-sqlite.js +87 -0
  273. package/dist/db/node-sqlite.js.map +1 -0
  274. package/dist/db/types.d.ts +22 -0
  275. package/dist/db/types.d.ts.map +1 -0
  276. package/dist/db/types.js +2 -0
  277. package/dist/db/types.js.map +1 -0
  278. package/dist/docs/docs-page.d.ts +2 -0
  279. package/dist/docs/docs-page.d.ts.map +1 -0
  280. package/dist/docs/docs-page.js +319 -0
  281. package/dist/docs/docs-page.js.map +1 -0
  282. package/dist/docs/openapi.d.ts +1896 -0
  283. package/dist/docs/openapi.d.ts.map +1 -0
  284. package/dist/docs/openapi.js +1096 -0
  285. package/dist/docs/openapi.js.map +1 -0
  286. package/dist/env.d.ts +2 -0
  287. package/dist/env.d.ts.map +1 -0
  288. package/dist/env.js +9 -0
  289. package/dist/env.js.map +1 -0
  290. package/dist/index.d.ts +2 -0
  291. package/dist/index.d.ts.map +1 -0
  292. package/dist/index.js +144 -0
  293. package/dist/index.js.map +1 -0
  294. package/dist/lib/anthropic-documents.d.ts +41 -0
  295. package/dist/lib/anthropic-documents.d.ts.map +1 -0
  296. package/dist/lib/anthropic-documents.js +132 -0
  297. package/dist/lib/anthropic-documents.js.map +1 -0
  298. package/dist/lib/app-version.d.ts +5 -0
  299. package/dist/lib/app-version.d.ts.map +1 -0
  300. package/dist/lib/app-version.js +61 -0
  301. package/dist/lib/app-version.js.map +1 -0
  302. package/dist/lib/attempt-trace.d.ts +23 -0
  303. package/dist/lib/attempt-trace.d.ts.map +1 -0
  304. package/dist/lib/attempt-trace.js +32 -0
  305. package/dist/lib/attempt-trace.js.map +1 -0
  306. package/dist/lib/budget.d.ts +6 -0
  307. package/dist/lib/budget.d.ts.map +1 -0
  308. package/dist/lib/budget.js +40 -0
  309. package/dist/lib/budget.js.map +1 -0
  310. package/dist/lib/client-classifier.d.ts +10 -0
  311. package/dist/lib/client-classifier.d.ts.map +1 -0
  312. package/dist/lib/client-classifier.js +126 -0
  313. package/dist/lib/client-classifier.js.map +1 -0
  314. package/dist/lib/client-context.d.ts +10 -0
  315. package/dist/lib/client-context.d.ts.map +1 -0
  316. package/dist/lib/client-context.js +39 -0
  317. package/dist/lib/client-context.js.map +1 -0
  318. package/dist/lib/config.d.ts +33 -0
  319. package/dist/lib/config.d.ts.map +1 -0
  320. package/dist/lib/config.js +94 -0
  321. package/dist/lib/config.js.map +1 -0
  322. package/dist/lib/content.d.ts +33 -0
  323. package/dist/lib/content.d.ts.map +1 -0
  324. package/dist/lib/content.js +201 -0
  325. package/dist/lib/content.js.map +1 -0
  326. package/dist/lib/credential.d.ts +11 -0
  327. package/dist/lib/credential.d.ts.map +1 -0
  328. package/dist/lib/credential.js +18 -0
  329. package/dist/lib/credential.js.map +1 -0
  330. package/dist/lib/crypto.d.ts +31 -0
  331. package/dist/lib/crypto.d.ts.map +1 -0
  332. package/dist/lib/crypto.js +206 -0
  333. package/dist/lib/crypto.js.map +1 -0
  334. package/dist/lib/custom-provider-cleanup.d.ts +13 -0
  335. package/dist/lib/custom-provider-cleanup.d.ts.map +1 -0
  336. package/dist/lib/custom-provider-cleanup.js +43 -0
  337. package/dist/lib/custom-provider-cleanup.js.map +1 -0
  338. package/dist/lib/db-backup.d.ts +31 -0
  339. package/dist/lib/db-backup.d.ts.map +1 -0
  340. package/dist/lib/db-backup.js +255 -0
  341. package/dist/lib/db-backup.js.map +1 -0
  342. package/dist/lib/endpoint-scope.d.ts +42 -0
  343. package/dist/lib/endpoint-scope.d.ts.map +1 -0
  344. package/dist/lib/endpoint-scope.js +95 -0
  345. package/dist/lib/endpoint-scope.js.map +1 -0
  346. package/dist/lib/env-drift.d.ts +17 -0
  347. package/dist/lib/env-drift.d.ts.map +1 -0
  348. package/dist/lib/env-drift.js +108 -0
  349. package/dist/lib/env-drift.js.map +1 -0
  350. package/dist/lib/error-classify.d.ts +40 -0
  351. package/dist/lib/error-classify.d.ts.map +1 -0
  352. package/dist/lib/error-classify.js +687 -0
  353. package/dist/lib/error-classify.js.map +1 -0
  354. package/dist/lib/error-redaction.d.ts +8 -0
  355. package/dist/lib/error-redaction.d.ts.map +1 -0
  356. package/dist/lib/error-redaction.js +45 -0
  357. package/dist/lib/error-redaction.js.map +1 -0
  358. package/dist/lib/fallback-loop.d.ts +306 -0
  359. package/dist/lib/fallback-loop.d.ts.map +1 -0
  360. package/dist/lib/fallback-loop.js +1336 -0
  361. package/dist/lib/fallback-loop.js.map +1 -0
  362. package/dist/lib/file-permissions.d.ts +98 -0
  363. package/dist/lib/file-permissions.d.ts.map +1 -0
  364. package/dist/lib/file-permissions.js +159 -0
  365. package/dist/lib/file-permissions.js.map +1 -0
  366. package/dist/lib/gemini-wire.d.ts +76 -0
  367. package/dist/lib/gemini-wire.d.ts.map +1 -0
  368. package/dist/lib/gemini-wire.js +392 -0
  369. package/dist/lib/gemini-wire.js.map +1 -0
  370. package/dist/lib/guardrails.d.ts +37 -0
  371. package/dist/lib/guardrails.d.ts.map +1 -0
  372. package/dist/lib/guardrails.js +106 -0
  373. package/dist/lib/guardrails.js.map +1 -0
  374. package/dist/lib/header-value.d.ts +8 -0
  375. package/dist/lib/header-value.d.ts.map +1 -0
  376. package/dist/lib/header-value.js +55 -0
  377. package/dist/lib/header-value.js.map +1 -0
  378. package/dist/lib/image-normalize.d.ts +21 -0
  379. package/dist/lib/image-normalize.d.ts.map +1 -0
  380. package/dist/lib/image-normalize.js +218 -0
  381. package/dist/lib/image-normalize.js.map +1 -0
  382. package/dist/lib/inbound-chat.d.ts +48 -0
  383. package/dist/lib/inbound-chat.d.ts.map +1 -0
  384. package/dist/lib/inbound-chat.js +406 -0
  385. package/dist/lib/inbound-chat.js.map +1 -0
  386. package/dist/lib/key-parser.d.ts +79 -0
  387. package/dist/lib/key-parser.d.ts.map +1 -0
  388. package/dist/lib/key-parser.js +701 -0
  389. package/dist/lib/key-parser.js.map +1 -0
  390. package/dist/lib/key-proxy.d.ts +49 -0
  391. package/dist/lib/key-proxy.d.ts.map +1 -0
  392. package/dist/lib/key-proxy.js +92 -0
  393. package/dist/lib/key-proxy.js.map +1 -0
  394. package/dist/lib/log-redaction.d.ts +28 -0
  395. package/dist/lib/log-redaction.d.ts.map +1 -0
  396. package/dist/lib/log-redaction.js +166 -0
  397. package/dist/lib/log-redaction.js.map +1 -0
  398. package/dist/lib/model-scope.d.ts +9 -0
  399. package/dist/lib/model-scope.d.ts.map +1 -0
  400. package/dist/lib/model-scope.js +29 -0
  401. package/dist/lib/model-scope.js.map +1 -0
  402. package/dist/lib/one-time-code.d.ts +15 -0
  403. package/dist/lib/one-time-code.d.ts.map +1 -0
  404. package/dist/lib/one-time-code.js +41 -0
  405. package/dist/lib/one-time-code.js.map +1 -0
  406. package/dist/lib/output-cap.d.ts +24 -0
  407. package/dist/lib/output-cap.d.ts.map +1 -0
  408. package/dist/lib/output-cap.js +80 -0
  409. package/dist/lib/output-cap.js.map +1 -0
  410. package/dist/lib/password.d.ts +3 -0
  411. package/dist/lib/password.d.ts.map +1 -0
  412. package/dist/lib/password.js +27 -0
  413. package/dist/lib/password.js.map +1 -0
  414. package/dist/lib/process-safety-net.d.ts +32 -0
  415. package/dist/lib/process-safety-net.d.ts.map +1 -0
  416. package/dist/lib/process-safety-net.js +123 -0
  417. package/dist/lib/process-safety-net.js.map +1 -0
  418. package/dist/lib/provider-identity.d.ts +37 -0
  419. package/dist/lib/provider-identity.d.ts.map +1 -0
  420. package/dist/lib/provider-identity.js +110 -0
  421. package/dist/lib/provider-identity.js.map +1 -0
  422. package/dist/lib/provider-size-parser.d.ts +6 -0
  423. package/dist/lib/provider-size-parser.d.ts.map +1 -0
  424. package/dist/lib/provider-size-parser.js +72 -0
  425. package/dist/lib/provider-size-parser.js.map +1 -0
  426. package/dist/lib/provider-timeout.d.ts +17 -0
  427. package/dist/lib/provider-timeout.d.ts.map +1 -0
  428. package/dist/lib/provider-timeout.js +70 -0
  429. package/dist/lib/provider-timeout.js.map +1 -0
  430. package/dist/lib/proxy.d.ts +180 -0
  431. package/dist/lib/proxy.d.ts.map +1 -0
  432. package/dist/lib/proxy.js +1002 -0
  433. package/dist/lib/proxy.js.map +1 -0
  434. package/dist/lib/request-log.d.ts +4 -0
  435. package/dist/lib/request-log.d.ts.map +1 -0
  436. package/dist/lib/request-log.js +161 -0
  437. package/dist/lib/request-log.js.map +1 -0
  438. package/dist/lib/reset-code.d.ts +5 -0
  439. package/dist/lib/reset-code.d.ts.map +1 -0
  440. package/dist/lib/reset-code.js +39 -0
  441. package/dist/lib/reset-code.js.map +1 -0
  442. package/dist/lib/retry-hint.d.ts +14 -0
  443. package/dist/lib/retry-hint.d.ts.map +1 -0
  444. package/dist/lib/retry-hint.js +36 -0
  445. package/dist/lib/retry-hint.js.map +1 -0
  446. package/dist/lib/route-live.d.ts +29 -0
  447. package/dist/lib/route-live.d.ts.map +1 -0
  448. package/dist/lib/route-live.js +97 -0
  449. package/dist/lib/route-live.js.map +1 -0
  450. package/dist/lib/sampling-params.d.ts +183 -0
  451. package/dist/lib/sampling-params.d.ts.map +1 -0
  452. package/dist/lib/sampling-params.js +386 -0
  453. package/dist/lib/sampling-params.js.map +1 -0
  454. package/dist/lib/scheduler.d.ts +13 -0
  455. package/dist/lib/scheduler.d.ts.map +1 -0
  456. package/dist/lib/scheduler.js +11 -0
  457. package/dist/lib/scheduler.js.map +1 -0
  458. package/dist/lib/served-model.d.ts +16 -0
  459. package/dist/lib/served-model.d.ts.map +1 -0
  460. package/dist/lib/served-model.js +0 -0
  461. package/dist/lib/served-model.js.map +1 -0
  462. package/dist/lib/server-logs.d.ts +132 -0
  463. package/dist/lib/server-logs.d.ts.map +1 -0
  464. package/dist/lib/server-logs.js +473 -0
  465. package/dist/lib/server-logs.js.map +1 -0
  466. package/dist/lib/setup-code.d.ts +5 -0
  467. package/dist/lib/setup-code.d.ts.map +1 -0
  468. package/dist/lib/setup-code.js +35 -0
  469. package/dist/lib/setup-code.js.map +1 -0
  470. package/dist/lib/structured-output.d.ts +9 -0
  471. package/dist/lib/structured-output.d.ts.map +1 -0
  472. package/dist/lib/structured-output.js +72 -0
  473. package/dist/lib/structured-output.js.map +1 -0
  474. package/dist/lib/system-prompt.d.ts +30 -0
  475. package/dist/lib/system-prompt.d.ts.map +1 -0
  476. package/dist/lib/system-prompt.js +66 -0
  477. package/dist/lib/system-prompt.js.map +1 -0
  478. package/dist/lib/task-type.d.ts +18 -0
  479. package/dist/lib/task-type.d.ts.map +1 -0
  480. package/dist/lib/task-type.js +92 -0
  481. package/dist/lib/task-type.js.map +1 -0
  482. package/dist/lib/think-tags.d.ts +37 -0
  483. package/dist/lib/think-tags.d.ts.map +1 -0
  484. package/dist/lib/think-tags.js +203 -0
  485. package/dist/lib/think-tags.js.map +1 -0
  486. package/dist/lib/tool-args.d.ts +36 -0
  487. package/dist/lib/tool-args.d.ts.map +1 -0
  488. package/dist/lib/tool-args.js +190 -0
  489. package/dist/lib/tool-args.js.map +1 -0
  490. package/dist/lib/tool-call-rescue.d.ts +62 -0
  491. package/dist/lib/tool-call-rescue.d.ts.map +1 -0
  492. package/dist/lib/tool-call-rescue.js +258 -0
  493. package/dist/lib/tool-call-rescue.js.map +1 -0
  494. package/dist/lib/tool-capability.d.ts +13 -0
  495. package/dist/lib/tool-capability.d.ts.map +1 -0
  496. package/dist/lib/tool-capability.js +70 -0
  497. package/dist/lib/tool-capability.js.map +1 -0
  498. package/dist/lib/tool-validate.d.ts +44 -0
  499. package/dist/lib/tool-validate.d.ts.map +1 -0
  500. package/dist/lib/tool-validate.js +165 -0
  501. package/dist/lib/tool-validate.js.map +1 -0
  502. package/dist/lib/ttfb-budget.d.ts +13 -0
  503. package/dist/lib/ttfb-budget.d.ts.map +1 -0
  504. package/dist/lib/ttfb-budget.js +153 -0
  505. package/dist/lib/ttfb-budget.js.map +1 -0
  506. package/dist/lib/url-guard.d.ts +47 -0
  507. package/dist/lib/url-guard.d.ts.map +1 -0
  508. package/dist/lib/url-guard.js +229 -0
  509. package/dist/lib/url-guard.js.map +1 -0
  510. package/dist/lib/wake-detect.d.ts +12 -0
  511. package/dist/lib/wake-detect.d.ts.map +1 -0
  512. package/dist/lib/wake-detect.js +99 -0
  513. package/dist/lib/wake-detect.js.map +1 -0
  514. package/dist/middleware/errorHandler.d.ts +3 -0
  515. package/dist/middleware/errorHandler.d.ts.map +1 -0
  516. package/dist/middleware/errorHandler.js +45 -0
  517. package/dist/middleware/errorHandler.js.map +1 -0
  518. package/dist/middleware/rateLimit.d.ts +4 -0
  519. package/dist/middleware/rateLimit.d.ts.map +1 -0
  520. package/dist/middleware/rateLimit.js +125 -0
  521. package/dist/middleware/rateLimit.js.map +1 -0
  522. package/dist/middleware/requireAuth.d.ts +3 -0
  523. package/dist/middleware/requireAuth.d.ts.map +1 -0
  524. package/dist/middleware/requireAuth.js +17 -0
  525. package/dist/middleware/requireAuth.js.map +1 -0
  526. package/dist/providers/aclide.d.ts +18 -0
  527. package/dist/providers/aclide.d.ts.map +1 -0
  528. package/dist/providers/aclide.js +174 -0
  529. package/dist/providers/aclide.js.map +1 -0
  530. package/dist/providers/aihorde.d.ts +42 -0
  531. package/dist/providers/aihorde.d.ts.map +1 -0
  532. package/dist/providers/aihorde.js +192 -0
  533. package/dist/providers/aihorde.js.map +1 -0
  534. package/dist/providers/airforce.d.ts +11 -0
  535. package/dist/providers/airforce.d.ts.map +1 -0
  536. package/dist/providers/airforce.js +32 -0
  537. package/dist/providers/airforce.js.map +1 -0
  538. package/dist/providers/base.d.ts +177 -0
  539. package/dist/providers/base.d.ts.map +1 -0
  540. package/dist/providers/base.js +354 -0
  541. package/dist/providers/base.js.map +1 -0
  542. package/dist/providers/blaze.d.ts +11 -0
  543. package/dist/providers/blaze.d.ts.map +1 -0
  544. package/dist/providers/blaze.js +33 -0
  545. package/dist/providers/blaze.js.map +1 -0
  546. package/dist/providers/blockrun.d.ts +14 -0
  547. package/dist/providers/blockrun.d.ts.map +1 -0
  548. package/dist/providers/blockrun.js +36 -0
  549. package/dist/providers/blockrun.js.map +1 -0
  550. package/dist/providers/clod.d.ts +11 -0
  551. package/dist/providers/clod.d.ts.map +1 -0
  552. package/dist/providers/clod.js +39 -0
  553. package/dist/providers/clod.js.map +1 -0
  554. package/dist/providers/cloudflare.d.ts +15 -0
  555. package/dist/providers/cloudflare.d.ts.map +1 -0
  556. package/dist/providers/cloudflare.js +175 -0
  557. package/dist/providers/cloudflare.js.map +1 -0
  558. package/dist/providers/cohere.d.ts +11 -0
  559. package/dist/providers/cohere.d.ts.map +1 -0
  560. package/dist/providers/cohere.js +117 -0
  561. package/dist/providers/cohere.js.map +1 -0
  562. package/dist/providers/dreamprompting.d.ts +11 -0
  563. package/dist/providers/dreamprompting.d.ts.map +1 -0
  564. package/dist/providers/dreamprompting.js +41 -0
  565. package/dist/providers/dreamprompting.js.map +1 -0
  566. package/dist/providers/electronhub.d.ts +10 -0
  567. package/dist/providers/electronhub.d.ts.map +1 -0
  568. package/dist/providers/electronhub.js +56 -0
  569. package/dist/providers/electronhub.js.map +1 -0
  570. package/dist/providers/experiential.d.ts +10 -0
  571. package/dist/providers/experiential.d.ts.map +1 -0
  572. package/dist/providers/experiential.js +33 -0
  573. package/dist/providers/experiential.js.map +1 -0
  574. package/dist/providers/gizmo.d.ts +15 -0
  575. package/dist/providers/gizmo.d.ts.map +1 -0
  576. package/dist/providers/gizmo.js +54 -0
  577. package/dist/providers/gizmo.js.map +1 -0
  578. package/dist/providers/google.d.ts +30 -0
  579. package/dist/providers/google.d.ts.map +1 -0
  580. package/dist/providers/google.js +766 -0
  581. package/dist/providers/google.js.map +1 -0
  582. package/dist/providers/index.d.ts +13 -0
  583. package/dist/providers/index.d.ts.map +1 -0
  584. package/dist/providers/index.js +561 -0
  585. package/dist/providers/index.js.map +1 -0
  586. package/dist/providers/llmtr.d.ts +15 -0
  587. package/dist/providers/llmtr.d.ts.map +1 -0
  588. package/dist/providers/llmtr.js +51 -0
  589. package/dist/providers/llmtr.js.map +1 -0
  590. package/dist/providers/logfare.d.ts +11 -0
  591. package/dist/providers/logfare.d.ts.map +1 -0
  592. package/dist/providers/logfare.js +33 -0
  593. package/dist/providers/logfare.js.map +1 -0
  594. package/dist/providers/lucidity.d.ts +11 -0
  595. package/dist/providers/lucidity.d.ts.map +1 -0
  596. package/dist/providers/lucidity.js +37 -0
  597. package/dist/providers/lucidity.js.map +1 -0
  598. package/dist/providers/modelscope.d.ts +50 -0
  599. package/dist/providers/modelscope.d.ts.map +1 -0
  600. package/dist/providers/modelscope.js +143 -0
  601. package/dist/providers/modelscope.js.map +1 -0
  602. package/dist/providers/moondream.d.ts +17 -0
  603. package/dist/providers/moondream.d.ts.map +1 -0
  604. package/dist/providers/moondream.js +140 -0
  605. package/dist/providers/moondream.js.map +1 -0
  606. package/dist/providers/openai-compat.d.ts +140 -0
  607. package/dist/providers/openai-compat.d.ts.map +1 -0
  608. package/dist/providers/openai-compat.js +537 -0
  609. package/dist/providers/openai-compat.js.map +1 -0
  610. package/dist/providers/opencode-free.d.ts +24 -0
  611. package/dist/providers/opencode-free.d.ts.map +1 -0
  612. package/dist/providers/opencode-free.js +262 -0
  613. package/dist/providers/opencode-free.js.map +1 -0
  614. package/dist/providers/pollinations.d.ts +32 -0
  615. package/dist/providers/pollinations.d.ts.map +1 -0
  616. package/dist/providers/pollinations.js +65 -0
  617. package/dist/providers/pollinations.js.map +1 -0
  618. package/dist/providers/router9.d.ts +16 -0
  619. package/dist/providers/router9.d.ts.map +1 -0
  620. package/dist/providers/router9.js +42 -0
  621. package/dist/providers/router9.js.map +1 -0
  622. package/dist/providers/sail.d.ts +44 -0
  623. package/dist/providers/sail.d.ts.map +1 -0
  624. package/dist/providers/sail.js +302 -0
  625. package/dist/providers/sail.js.map +1 -0
  626. package/dist/providers/septor.d.ts +10 -0
  627. package/dist/providers/septor.d.ts.map +1 -0
  628. package/dist/providers/septor.js +36 -0
  629. package/dist/providers/septor.js.map +1 -0
  630. package/dist/providers/speechify.d.ts +14 -0
  631. package/dist/providers/speechify.d.ts.map +1 -0
  632. package/dist/providers/speechify.js +28 -0
  633. package/dist/providers/speechify.js.map +1 -0
  634. package/dist/providers/speka.d.ts +12 -0
  635. package/dist/providers/speka.d.ts.map +1 -0
  636. package/dist/providers/speka.js +37 -0
  637. package/dist/providers/speka.js.map +1 -0
  638. package/dist/providers/waterfall.d.ts +11 -0
  639. package/dist/providers/waterfall.d.ts.map +1 -0
  640. package/dist/providers/waterfall.js +33 -0
  641. package/dist/providers/waterfall.js.map +1 -0
  642. package/dist/providers/xkiro.d.ts +71 -0
  643. package/dist/providers/xkiro.d.ts.map +1 -0
  644. package/dist/providers/xkiro.js +119 -0
  645. package/dist/providers/xkiro.js.map +1 -0
  646. package/dist/providers/zhipu.d.ts +43 -0
  647. package/dist/providers/zhipu.d.ts.map +1 -0
  648. package/dist/providers/zhipu.js +95 -0
  649. package/dist/providers/zhipu.js.map +1 -0
  650. package/dist/routes/analytics.d.ts +2 -0
  651. package/dist/routes/analytics.d.ts.map +1 -0
  652. package/dist/routes/analytics.js +738 -0
  653. package/dist/routes/analytics.js.map +1 -0
  654. package/dist/routes/anthropic.d.ts +7 -0
  655. package/dist/routes/anthropic.d.ts.map +1 -0
  656. package/dist/routes/anthropic.js +1076 -0
  657. package/dist/routes/anthropic.js.map +1 -0
  658. package/dist/routes/auth.d.ts +2 -0
  659. package/dist/routes/auth.d.ts.map +1 -0
  660. package/dist/routes/auth.js +310 -0
  661. package/dist/routes/auth.js.map +1 -0
  662. package/dist/routes/backups.d.ts +2 -0
  663. package/dist/routes/backups.d.ts.map +1 -0
  664. package/dist/routes/backups.js +127 -0
  665. package/dist/routes/backups.js.map +1 -0
  666. package/dist/routes/cache.d.ts +2 -0
  667. package/dist/routes/cache.d.ts.map +1 -0
  668. package/dist/routes/cache.js +40 -0
  669. package/dist/routes/cache.js.map +1 -0
  670. package/dist/routes/client-profiles.d.ts +2 -0
  671. package/dist/routes/client-profiles.d.ts.map +1 -0
  672. package/dist/routes/client-profiles.js +128 -0
  673. package/dist/routes/client-profiles.js.map +1 -0
  674. package/dist/routes/compression.d.ts +2 -0
  675. package/dist/routes/compression.d.ts.map +1 -0
  676. package/dist/routes/compression.js +82 -0
  677. package/dist/routes/compression.js.map +1 -0
  678. package/dist/routes/conversations.d.ts +3 -0
  679. package/dist/routes/conversations.d.ts.map +1 -0
  680. package/dist/routes/conversations.js +215 -0
  681. package/dist/routes/conversations.js.map +1 -0
  682. package/dist/routes/docs.d.ts +2 -0
  683. package/dist/routes/docs.d.ts.map +1 -0
  684. package/dist/routes/docs.js +21 -0
  685. package/dist/routes/docs.js.map +1 -0
  686. package/dist/routes/embeddings.d.ts +2 -0
  687. package/dist/routes/embeddings.d.ts.map +1 -0
  688. package/dist/routes/embeddings.js +255 -0
  689. package/dist/routes/embeddings.js.map +1 -0
  690. package/dist/routes/fallback.d.ts +2 -0
  691. package/dist/routes/fallback.d.ts.map +1 -0
  692. package/dist/routes/fallback.js +631 -0
  693. package/dist/routes/fallback.js.map +1 -0
  694. package/dist/routes/free-tier.d.ts +22 -0
  695. package/dist/routes/free-tier.d.ts.map +1 -0
  696. package/dist/routes/free-tier.js +175 -0
  697. package/dist/routes/free-tier.js.map +1 -0
  698. package/dist/routes/gemini.d.ts +2 -0
  699. package/dist/routes/gemini.d.ts.map +1 -0
  700. package/dist/routes/gemini.js +218 -0
  701. package/dist/routes/gemini.js.map +1 -0
  702. package/dist/routes/health.d.ts +2 -0
  703. package/dist/routes/health.d.ts.map +1 -0
  704. package/dist/routes/health.js +72 -0
  705. package/dist/routes/health.js.map +1 -0
  706. package/dist/routes/keys.d.ts +17 -0
  707. package/dist/routes/keys.d.ts.map +1 -0
  708. package/dist/routes/keys.js +1787 -0
  709. package/dist/routes/keys.js.map +1 -0
  710. package/dist/routes/logs.d.ts +2 -0
  711. package/dist/routes/logs.d.ts.map +1 -0
  712. package/dist/routes/logs.js +88 -0
  713. package/dist/routes/logs.js.map +1 -0
  714. package/dist/routes/mcp.d.ts +4 -0
  715. package/dist/routes/mcp.d.ts.map +1 -0
  716. package/dist/routes/mcp.js +563 -0
  717. package/dist/routes/mcp.js.map +1 -0
  718. package/dist/routes/media.d.ts +2 -0
  719. package/dist/routes/media.d.ts.map +1 -0
  720. package/dist/routes/media.js +180 -0
  721. package/dist/routes/media.js.map +1 -0
  722. package/dist/routes/models.d.ts +2 -0
  723. package/dist/routes/models.d.ts.map +1 -0
  724. package/dist/routes/models.js +439 -0
  725. package/dist/routes/models.js.map +1 -0
  726. package/dist/routes/ollama.d.ts +7 -0
  727. package/dist/routes/ollama.d.ts.map +1 -0
  728. package/dist/routes/ollama.js +595 -0
  729. package/dist/routes/ollama.js.map +1 -0
  730. package/dist/routes/profiles.d.ts +3 -0
  731. package/dist/routes/profiles.d.ts.map +1 -0
  732. package/dist/routes/profiles.js +431 -0
  733. package/dist/routes/profiles.js.map +1 -0
  734. package/dist/routes/proxy.d.ts +32 -0
  735. package/dist/routes/proxy.d.ts.map +1 -0
  736. package/dist/routes/proxy.js +2793 -0
  737. package/dist/routes/proxy.js.map +1 -0
  738. package/dist/routes/responses.d.ts +1161 -0
  739. package/dist/routes/responses.d.ts.map +1 -0
  740. package/dist/routes/responses.js +1521 -0
  741. package/dist/routes/responses.js.map +1 -0
  742. package/dist/routes/settings.d.ts +2 -0
  743. package/dist/routes/settings.d.ts.map +1 -0
  744. package/dist/routes/settings.js +549 -0
  745. package/dist/routes/settings.js.map +1 -0
  746. package/dist/routes/status.d.ts +3 -0
  747. package/dist/routes/status.d.ts.map +1 -0
  748. package/dist/routes/status.js +214 -0
  749. package/dist/routes/status.js.map +1 -0
  750. package/dist/routes/update.d.ts +31 -0
  751. package/dist/routes/update.d.ts.map +1 -0
  752. package/dist/routes/update.js +536 -0
  753. package/dist/routes/update.js.map +1 -0
  754. package/dist/routes/url-tokens.d.ts +2 -0
  755. package/dist/routes/url-tokens.d.ts.map +1 -0
  756. package/dist/routes/url-tokens.js +31 -0
  757. package/dist/routes/url-tokens.js.map +1 -0
  758. package/dist/scripts/export-catalog.d.ts +2 -0
  759. package/dist/scripts/export-catalog.d.ts.map +1 -0
  760. package/dist/scripts/export-catalog.js +114 -0
  761. package/dist/scripts/export-catalog.js.map +1 -0
  762. package/dist/scripts/rotate-encryption-key.d.ts +81 -0
  763. package/dist/scripts/rotate-encryption-key.d.ts.map +1 -0
  764. package/dist/scripts/rotate-encryption-key.js +232 -0
  765. package/dist/scripts/rotate-encryption-key.js.map +1 -0
  766. package/dist/scripts/routing-sim.d.ts +2 -0
  767. package/dist/scripts/routing-sim.d.ts.map +1 -0
  768. package/dist/scripts/routing-sim.js +130 -0
  769. package/dist/scripts/routing-sim.js.map +1 -0
  770. package/dist/scripts/test-all-models.d.ts +2 -0
  771. package/dist/scripts/test-all-models.d.ts.map +1 -0
  772. package/dist/scripts/test-all-models.js +56 -0
  773. package/dist/scripts/test-all-models.js.map +1 -0
  774. package/dist/services/anthropic-map.d.ts +39 -0
  775. package/dist/services/anthropic-map.d.ts.map +1 -0
  776. package/dist/services/anthropic-map.js +138 -0
  777. package/dist/services/anthropic-map.js.map +1 -0
  778. package/dist/services/auth.d.ts +30 -0
  779. package/dist/services/auth.d.ts.map +1 -0
  780. package/dist/services/auth.js +124 -0
  781. package/dist/services/auth.js.map +1 -0
  782. package/dist/services/auto-discover.d.ts +77 -0
  783. package/dist/services/auto-discover.d.ts.map +1 -0
  784. package/dist/services/auto-discover.js +1047 -0
  785. package/dist/services/auto-discover.js.map +1 -0
  786. package/dist/services/backups.d.ts +67 -0
  787. package/dist/services/backups.d.ts.map +1 -0
  788. package/dist/services/backups.js +444 -0
  789. package/dist/services/backups.js.map +1 -0
  790. package/dist/services/builtin-model-discovery.d.ts +107 -0
  791. package/dist/services/builtin-model-discovery.d.ts.map +1 -0
  792. package/dist/services/builtin-model-discovery.js +296 -0
  793. package/dist/services/builtin-model-discovery.js.map +1 -0
  794. package/dist/services/cache.d.ts +190 -0
  795. package/dist/services/cache.d.ts.map +1 -0
  796. package/dist/services/cache.js +617 -0
  797. package/dist/services/cache.js.map +1 -0
  798. package/dist/services/catalog-sync.d.ts +257 -0
  799. package/dist/services/catalog-sync.d.ts.map +1 -0
  800. package/dist/services/catalog-sync.js +1041 -0
  801. package/dist/services/catalog-sync.js.map +1 -0
  802. package/dist/services/compression/config.d.ts +33 -0
  803. package/dist/services/compression/config.d.ts.map +1 -0
  804. package/dist/services/compression/config.js +158 -0
  805. package/dist/services/compression/config.js.map +1 -0
  806. package/dist/services/compression/engines/aging.d.ts +2 -0
  807. package/dist/services/compression/engines/aging.d.ts.map +1 -0
  808. package/dist/services/compression/engines/aging.js +53 -0
  809. package/dist/services/compression/engines/aging.js.map +1 -0
  810. package/dist/services/compression/engines/custom-filters.d.ts +4 -0
  811. package/dist/services/compression/engines/custom-filters.d.ts.map +1 -0
  812. package/dist/services/compression/engines/custom-filters.js +49 -0
  813. package/dist/services/compression/engines/custom-filters.js.map +1 -0
  814. package/dist/services/compression/engines/dedup.d.ts +2 -0
  815. package/dist/services/compression/engines/dedup.d.ts.map +1 -0
  816. package/dist/services/compression/engines/dedup.js +61 -0
  817. package/dist/services/compression/engines/dedup.js.map +1 -0
  818. package/dist/services/compression/engines/filter-definitions.d.ts +75 -0
  819. package/dist/services/compression/engines/filter-definitions.d.ts.map +1 -0
  820. package/dist/services/compression/engines/filter-definitions.js +45 -0
  821. package/dist/services/compression/engines/filter-definitions.js.map +1 -0
  822. package/dist/services/compression/engines/hard-budget.d.ts +2 -0
  823. package/dist/services/compression/engines/hard-budget.d.ts.map +1 -0
  824. package/dist/services/compression/engines/hard-budget.js +52 -0
  825. package/dist/services/compression/engines/hard-budget.js.map +1 -0
  826. package/dist/services/compression/engines/index.d.ts +9 -0
  827. package/dist/services/compression/engines/index.d.ts.map +1 -0
  828. package/dist/services/compression/engines/index.js +9 -0
  829. package/dist/services/compression/engines/index.js.map +1 -0
  830. package/dist/services/compression/engines/jsoncompact.d.ts +3 -0
  831. package/dist/services/compression/engines/jsoncompact.d.ts.map +1 -0
  832. package/dist/services/compression/engines/jsoncompact.js +145 -0
  833. package/dist/services/compression/engines/jsoncompact.js.map +1 -0
  834. package/dist/services/compression/engines/lite.d.ts +2 -0
  835. package/dist/services/compression/engines/lite.d.ts.map +1 -0
  836. package/dist/services/compression/engines/lite.js +34 -0
  837. package/dist/services/compression/engines/lite.js.map +1 -0
  838. package/dist/services/compression/engines/read-lifecycle.d.ts +2 -0
  839. package/dist/services/compression/engines/read-lifecycle.d.ts.map +1 -0
  840. package/dist/services/compression/engines/read-lifecycle.js +70 -0
  841. package/dist/services/compression/engines/read-lifecycle.js.map +1 -0
  842. package/dist/services/compression/engines/relevance.d.ts +2 -0
  843. package/dist/services/compression/engines/relevance.d.ts.map +1 -0
  844. package/dist/services/compression/engines/relevance.js +74 -0
  845. package/dist/services/compression/engines/relevance.js.map +1 -0
  846. package/dist/services/compression/engines/toolfilter.d.ts +2 -0
  847. package/dist/services/compression/engines/toolfilter.d.ts.map +1 -0
  848. package/dist/services/compression/engines/toolfilter.js +195 -0
  849. package/dist/services/compression/engines/toolfilter.js.map +1 -0
  850. package/dist/services/compression/fidelity-gate.d.ts +12 -0
  851. package/dist/services/compression/fidelity-gate.d.ts.map +1 -0
  852. package/dist/services/compression/fidelity-gate.js +90 -0
  853. package/dist/services/compression/fidelity-gate.js.map +1 -0
  854. package/dist/services/compression/helpers.d.ts +9 -0
  855. package/dist/services/compression/helpers.d.ts.map +1 -0
  856. package/dist/services/compression/helpers.js +52 -0
  857. package/dist/services/compression/helpers.js.map +1 -0
  858. package/dist/services/compression/pipeline.d.ts +7 -0
  859. package/dist/services/compression/pipeline.d.ts.map +1 -0
  860. package/dist/services/compression/pipeline.js +245 -0
  861. package/dist/services/compression/pipeline.js.map +1 -0
  862. package/dist/services/compression/preservation.d.ts +27 -0
  863. package/dist/services/compression/preservation.d.ts.map +1 -0
  864. package/dist/services/compression/preservation.js +109 -0
  865. package/dist/services/compression/preservation.js.map +1 -0
  866. package/dist/services/compression/registry.d.ts +6 -0
  867. package/dist/services/compression/registry.d.ts.map +1 -0
  868. package/dist/services/compression/registry.js +16 -0
  869. package/dist/services/compression/registry.js.map +1 -0
  870. package/dist/services/compression/stats.d.ts +29 -0
  871. package/dist/services/compression/stats.d.ts.map +1 -0
  872. package/dist/services/compression/stats.js +55 -0
  873. package/dist/services/compression/stats.js.map +1 -0
  874. package/dist/services/compression/types.d.ts +90 -0
  875. package/dist/services/compression/types.d.ts.map +1 -0
  876. package/dist/services/compression/types.js +2 -0
  877. package/dist/services/compression/types.js.map +1 -0
  878. package/dist/services/context-handoff.d.ts +22 -0
  879. package/dist/services/context-handoff.d.ts.map +1 -0
  880. package/dist/services/context-handoff.js +164 -0
  881. package/dist/services/context-handoff.js.map +1 -0
  882. package/dist/services/cooldown-probe.d.ts +24 -0
  883. package/dist/services/cooldown-probe.d.ts.map +1 -0
  884. package/dist/services/cooldown-probe.js +181 -0
  885. package/dist/services/cooldown-probe.js.map +1 -0
  886. package/dist/services/custom-endpoint.d.ts +58 -0
  887. package/dist/services/custom-endpoint.d.ts.map +1 -0
  888. package/dist/services/custom-endpoint.js +167 -0
  889. package/dist/services/custom-endpoint.js.map +1 -0
  890. package/dist/services/custom-media-register.d.ts +22 -0
  891. package/dist/services/custom-media-register.d.ts.map +1 -0
  892. package/dist/services/custom-media-register.js +45 -0
  893. package/dist/services/custom-media-register.js.map +1 -0
  894. package/dist/services/custom-model-register.d.ts +50 -0
  895. package/dist/services/custom-model-register.d.ts.map +1 -0
  896. package/dist/services/custom-model-register.js +103 -0
  897. package/dist/services/custom-model-register.js.map +1 -0
  898. package/dist/services/custom-model-seed.d.ts +18 -0
  899. package/dist/services/custom-model-seed.d.ts.map +1 -0
  900. package/dist/services/custom-model-seed.js +42 -0
  901. package/dist/services/custom-model-seed.js.map +1 -0
  902. package/dist/services/custom-model-sync.d.ts +37 -0
  903. package/dist/services/custom-model-sync.d.ts.map +1 -0
  904. package/dist/services/custom-model-sync.js +118 -0
  905. package/dist/services/custom-model-sync.js.map +1 -0
  906. package/dist/services/custom-model-tombstone.d.ts +5 -0
  907. package/dist/services/custom-model-tombstone.d.ts.map +1 -0
  908. package/dist/services/custom-model-tombstone.js +30 -0
  909. package/dist/services/custom-model-tombstone.js.map +1 -0
  910. package/dist/services/declarative-config.d.ts +356 -0
  911. package/dist/services/declarative-config.d.ts.map +1 -0
  912. package/dist/services/declarative-config.js +427 -0
  913. package/dist/services/declarative-config.js.map +1 -0
  914. package/dist/services/degradation.d.ts +40 -0
  915. package/dist/services/degradation.d.ts.map +1 -0
  916. package/dist/services/degradation.js +119 -0
  917. package/dist/services/degradation.js.map +1 -0
  918. package/dist/services/embeddings.d.ts +76 -0
  919. package/dist/services/embeddings.d.ts.map +1 -0
  920. package/dist/services/embeddings.js +319 -0
  921. package/dist/services/embeddings.js.map +1 -0
  922. package/dist/services/fusion.d.ts +162 -0
  923. package/dist/services/fusion.d.ts.map +1 -0
  924. package/dist/services/fusion.js +789 -0
  925. package/dist/services/fusion.js.map +1 -0
  926. package/dist/services/gemini-map.d.ts +14 -0
  927. package/dist/services/gemini-map.d.ts.map +1 -0
  928. package/dist/services/gemini-map.js +78 -0
  929. package/dist/services/gemini-map.js.map +1 -0
  930. package/dist/services/health.d.ts +52 -0
  931. package/dist/services/health.d.ts.map +1 -0
  932. package/dist/services/health.js +352 -0
  933. package/dist/services/health.js.map +1 -0
  934. package/dist/services/idempotency.d.ts +49 -0
  935. package/dist/services/idempotency.d.ts.map +1 -0
  936. package/dist/services/idempotency.js +140 -0
  937. package/dist/services/idempotency.js.map +1 -0
  938. package/dist/services/key-budget.d.ts +74 -0
  939. package/dist/services/key-budget.d.ts.map +1 -0
  940. package/dist/services/key-budget.js +200 -0
  941. package/dist/services/key-budget.js.map +1 -0
  942. package/dist/services/media.d.ts +123 -0
  943. package/dist/services/media.d.ts.map +1 -0
  944. package/dist/services/media.js +954 -0
  945. package/dist/services/media.js.map +1 -0
  946. package/dist/services/model-discovery.d.ts +126 -0
  947. package/dist/services/model-discovery.d.ts.map +1 -0
  948. package/dist/services/model-discovery.js +662 -0
  949. package/dist/services/model-discovery.js.map +1 -0
  950. package/dist/services/model-groups.d.ts +149 -0
  951. package/dist/services/model-groups.d.ts.map +1 -0
  952. package/dist/services/model-groups.js +314 -0
  953. package/dist/services/model-groups.js.map +1 -0
  954. package/dist/services/model-listing.d.ts +18 -0
  955. package/dist/services/model-listing.d.ts.map +1 -0
  956. package/dist/services/model-listing.js +94 -0
  957. package/dist/services/model-listing.js.map +1 -0
  958. package/dist/services/model-retirement.d.ts +29 -0
  959. package/dist/services/model-retirement.d.ts.map +1 -0
  960. package/dist/services/model-retirement.js +86 -0
  961. package/dist/services/model-retirement.js.map +1 -0
  962. package/dist/services/model-state.d.ts +90 -0
  963. package/dist/services/model-state.d.ts.map +1 -0
  964. package/dist/services/model-state.js +342 -0
  965. package/dist/services/model-state.js.map +1 -0
  966. package/dist/services/model-weight-overrides.d.ts +48 -0
  967. package/dist/services/model-weight-overrides.d.ts.map +1 -0
  968. package/dist/services/model-weight-overrides.js +127 -0
  969. package/dist/services/model-weight-overrides.js.map +1 -0
  970. package/dist/services/notification-emitter.d.ts +50 -0
  971. package/dist/services/notification-emitter.d.ts.map +1 -0
  972. package/dist/services/notification-emitter.js +177 -0
  973. package/dist/services/notification-emitter.js.map +1 -0
  974. package/dist/services/opencode-free-sync.d.ts +29 -0
  975. package/dist/services/opencode-free-sync.d.ts.map +1 -0
  976. package/dist/services/opencode-free-sync.js +206 -0
  977. package/dist/services/opencode-free-sync.js.map +1 -0
  978. package/dist/services/penalty-inspector.d.ts +56 -0
  979. package/dist/services/penalty-inspector.d.ts.map +1 -0
  980. package/dist/services/penalty-inspector.js +167 -0
  981. package/dist/services/penalty-inspector.js.map +1 -0
  982. package/dist/services/profile-models.d.ts +5 -0
  983. package/dist/services/profile-models.d.ts.map +1 -0
  984. package/dist/services/profile-models.js +62 -0
  985. package/dist/services/profile-models.js.map +1 -0
  986. package/dist/services/provider-credential.d.ts +19 -0
  987. package/dist/services/provider-credential.d.ts.map +1 -0
  988. package/dist/services/provider-credential.js +51 -0
  989. package/dist/services/provider-credential.js.map +1 -0
  990. package/dist/services/provider-quota.d.ts +64 -0
  991. package/dist/services/provider-quota.d.ts.map +1 -0
  992. package/dist/services/provider-quota.js +563 -0
  993. package/dist/services/provider-quota.js.map +1 -0
  994. package/dist/services/quirks.d.ts +0 -0
  995. package/dist/services/quirks.d.ts.map +1 -0
  996. package/dist/services/quirks.js +0 -0
  997. package/dist/services/quirks.js.map +1 -0
  998. package/dist/services/quota-forecast.d.ts +50 -0
  999. package/dist/services/quota-forecast.d.ts.map +1 -0
  1000. package/dist/services/quota-forecast.js +150 -0
  1001. package/dist/services/quota-forecast.js.map +1 -0
  1002. package/dist/services/quota-outlook.d.ts +4 -0
  1003. package/dist/services/quota-outlook.d.ts.map +1 -0
  1004. package/dist/services/quota-outlook.js +92 -0
  1005. package/dist/services/quota-outlook.js.map +1 -0
  1006. package/dist/services/ratelimit.d.ts +249 -0
  1007. package/dist/services/ratelimit.d.ts.map +1 -0
  1008. package/dist/services/ratelimit.js +1182 -0
  1009. package/dist/services/ratelimit.js.map +1 -0
  1010. package/dist/services/request-retention.d.ts +60 -0
  1011. package/dist/services/request-retention.d.ts.map +1 -0
  1012. package/dist/services/request-retention.js +229 -0
  1013. package/dist/services/request-retention.js.map +1 -0
  1014. package/dist/services/router.d.ts +384 -0
  1015. package/dist/services/router.d.ts.map +1 -0
  1016. package/dist/services/router.js +1986 -0
  1017. package/dist/services/router.js.map +1 -0
  1018. package/dist/services/scoring.d.ts +156 -0
  1019. package/dist/services/scoring.d.ts.map +1 -0
  1020. package/dist/services/scoring.js +435 -0
  1021. package/dist/services/scoring.js.map +1 -0
  1022. package/dist/services/url-tokens.d.ts +15 -0
  1023. package/dist/services/url-tokens.d.ts.map +1 -0
  1024. package/dist/services/url-tokens.js +55 -0
  1025. package/dist/services/url-tokens.js.map +1 -0
  1026. package/package.json +77 -4
  1027. package/README.md +0 -3
@@ -0,0 +1,1182 @@
1
+ // Sliding window rate limit tracker with SQLite persistence.
2
+ import { getDb, getSetting, setSetting } from '../db/index.js';
3
+ import { isLoopbackOrPrivateUrl } from '../lib/url-guard.js';
4
+ import { parseModelScope, scopeAllows } from '../lib/model-scope.js';
5
+ // Key format: "platform:modelId:keyId:type" where type is rpm|rpd|tpm|tpd
6
+ const windows = new Map();
7
+ function getWindow(key) {
8
+ let w = windows.get(key);
9
+ if (!w) {
10
+ w = { timestamps: [], tokenCount: 0, tokenTimestamps: [] };
11
+ windows.set(key, w);
12
+ }
13
+ return w;
14
+ }
15
+ function pruneTimestamps(timestamps, windowMs, now) {
16
+ const cutoff = now - windowMs;
17
+ return timestamps.filter(ts => ts > cutoff);
18
+ }
19
+ const MINUTE = 60 * 1000;
20
+ const DAY = 24 * 60 * MINUTE;
21
+ /** Milliseconds elapsed since the most recent midnight UTC. Used as the window
22
+ * width for provider-account daily caps, which reset on a day boundary rather
23
+ * than sliding. Never exceeds DAY, so it stays inside the usage-table retention. */
24
+ function msSinceUtcMidnight(now) {
25
+ return now - new Date(now).setUTCHours(0, 0, 0, 0);
26
+ }
27
+ function withDb(fn) {
28
+ try {
29
+ return fn(getDb());
30
+ }
31
+ catch {
32
+ return undefined;
33
+ }
34
+ }
35
+ const leases = new Map();
36
+ let nextLeaseId = 1;
37
+ // A lease should always be released explicitly by the fallback loop's finally
38
+ // block. This bound is the backstop for a path that somehow doesn't: without it
39
+ // a single leaked lease would count against a key's concurrency budget forever.
40
+ // Comfortably longer than the per-attempt provider timeout.
41
+ const LEASE_MAX_AGE_MS = 2 * MINUTE;
42
+ function pruneLeases(now) {
43
+ if (leases.size === 0)
44
+ return;
45
+ for (const [id, lease] of leases) {
46
+ if (now - lease.createdAt > LEASE_MAX_AGE_MS)
47
+ leases.delete(id);
48
+ }
49
+ }
50
+ /**
51
+ * Concurrent in-flight requests allowed per key, or null for unlimited.
52
+ *
53
+ * Some free tiers meter concurrency per credential rather than per minute, so
54
+ * parallel streams on one key mostly 429 each other. But most do not, and
55
+ * capping by default would serialise every provider and cost real throughput —
56
+ * so this is opt-in. There is deliberately no built-in per-platform table: the
57
+ * providers that behave this way are not documented well enough to assert a
58
+ * number for them here, and a wrong default is worse than none.
59
+ *
60
+ * `MAX_CONCURRENT_REQUESTS_PER_KEY_<PLATFORM>` sets it for one platform;
61
+ * `MAX_CONCURRENT_REQUESTS_PER_KEY` sets a fallback for all of them.
62
+ */
63
+ export function getKeyConcurrencyLimit(platform) {
64
+ const raw = process.env[`MAX_CONCURRENT_REQUESTS_PER_KEY_${platform.toUpperCase()}`]
65
+ ?? process.env.MAX_CONCURRENT_REQUESTS_PER_KEY;
66
+ if (raw !== undefined && raw.trim() !== '') {
67
+ const n = Number(raw);
68
+ if (Number.isInteger(n) && n > 0)
69
+ return n;
70
+ }
71
+ return null;
72
+ }
73
+ /** Requests this process currently has in flight against one platform+key. */
74
+ export function inFlightForKey(platform, keyId, now = Date.now()) {
75
+ pruneLeases(now);
76
+ let count = 0;
77
+ for (const lease of leases.values()) {
78
+ if (lease.platform === platform && lease.keyId === keyId)
79
+ count++;
80
+ }
81
+ return count;
82
+ }
83
+ /** False when this key already has its allowed number of requests in the air.
84
+ * Always true when no cap is configured, which is the default. */
85
+ export function canUseKeyConcurrency(platform, keyId, now = Date.now()) {
86
+ const limit = getKeyConcurrencyLimit(platform);
87
+ if (limit === null)
88
+ return true;
89
+ return inFlightForKey(platform, keyId, now) < limit;
90
+ }
91
+ /** Take a lease for an attempt about to be dispatched. Returns the id to release. */
92
+ export function acquireLease(platform, modelId, keyId, tokens, now = Date.now()) {
93
+ pruneLeases(now);
94
+ const id = nextLeaseId++;
95
+ leases.set(id, { platform, modelId, keyId, tokens, createdAt: now });
96
+ return id;
97
+ }
98
+ /** Release a lease. Idempotent, so a double release from overlapping cleanup
99
+ * paths is harmless. */
100
+ export function releaseLease(leaseId) {
101
+ leases.delete(leaseId);
102
+ }
103
+ /** Test seam: drop all leases. */
104
+ export function resetLeases() {
105
+ leases.clear();
106
+ }
107
+ // ── Provisional usage ─────────────────────────────────────────────────────────
108
+ // Every gate below adds in-flight leases on top of the recorded counters. This is
109
+ // what closes the check-then-act race: routeRequest is synchronous but usage is
110
+ // only written after the awaited provider call, so N concurrent requests all read
111
+ // the same pre-check and every one of them passes — then they collectively blow
112
+ // through the limit and collect real 429s. Counting leases makes the second
113
+ // caller see the first one's request.
114
+ //
115
+ // A lease is released just after the success write, so for a brief instant a
116
+ // completed request is counted twice. That errs toward under-dispatching by one,
117
+ // which is the safe direction for a free tier, and lasts microseconds.
118
+ function provisionalRequests(platform, modelId, keyId, now) {
119
+ pruneLeases(now);
120
+ let count = 0;
121
+ for (const lease of leases.values()) {
122
+ if (lease.platform === platform && lease.modelId === modelId && lease.keyId === keyId)
123
+ count++;
124
+ }
125
+ return count;
126
+ }
127
+ function provisionalTokens(platform, modelId, keyId, now) {
128
+ pruneLeases(now);
129
+ let total = 0;
130
+ for (const lease of leases.values()) {
131
+ if (lease.platform === platform && lease.modelId === modelId && lease.keyId === keyId)
132
+ total += lease.tokens;
133
+ }
134
+ return total;
135
+ }
136
+ /** In-flight requests for a provider account+key across every model — the
137
+ * provider-wide analogue, for the account-level gates. */
138
+ function provisionalProviderRequests(platform, keyId, now) {
139
+ return inFlightForKey(platform, keyId, now);
140
+ }
141
+ function provisionalProviderTokens(platform, keyId, now) {
142
+ pruneLeases(now);
143
+ let total = 0;
144
+ for (const lease of leases.values()) {
145
+ if (lease.platform === platform && lease.keyId === keyId)
146
+ total += lease.tokens;
147
+ }
148
+ return total;
149
+ }
150
+ // Retention sweep for rate_limit_usage. The only index on the table is
151
+ // idx_rate_limit_usage_lookup(platform, model_id, key_id, kind, created_at_ms),
152
+ // which a bare `created_at_ms <= ?` predicate cannot use — the prune is a full
153
+ // table scan. Running it inside every insert put that scan on the hot request
154
+ // path, so it is throttled: rows are kept for a DAY and every counter query is
155
+ // bounded by its own `created_at_ms > ?` filter, so letting expired rows linger
156
+ // for up to a minute costs nothing but a few stale rows.
157
+ const USAGE_PRUNE_INTERVAL_MS = MINUTE;
158
+ let lastUsagePruneMs = 0;
159
+ /** True when the row actually reached the DB. Callers use this to decide
160
+ * whether the in-memory windows have to carry the count instead. */
161
+ function recordUsage(platform, modelId, keyId, kind, tokens, now) {
162
+ return withDb(db => {
163
+ db.prepare(`
164
+ INSERT INTO rate_limit_usage (platform, model_id, key_id, kind, tokens, created_at_ms)
165
+ VALUES (?, ?, ?, ?, ?, ?)
166
+ `).run(platform, modelId, keyId, kind, tokens, now);
167
+ // `now < lastUsagePruneMs` covers a clock step backwards (NTP correction,
168
+ // a suspended host resuming) — without it a jump forward would wedge the
169
+ // prune off until real time caught up again.
170
+ if (now - lastUsagePruneMs >= USAGE_PRUNE_INTERVAL_MS || now < lastUsagePruneMs) {
171
+ lastUsagePruneMs = now;
172
+ db.prepare('DELETE FROM rate_limit_usage WHERE created_at_ms <= ?').run(now - DAY);
173
+ }
174
+ return true;
175
+ }) ?? false;
176
+ }
177
+ function countPersistedRequests(platform, modelId, keyId, windowMs, now) {
178
+ return withDb(db => {
179
+ const row = db.prepare(`
180
+ SELECT COUNT(*) AS used
181
+ FROM rate_limit_usage
182
+ WHERE platform = ?
183
+ AND model_id = ?
184
+ AND key_id = ?
185
+ AND kind = 'request'
186
+ AND created_at_ms > ?
187
+ `).get(platform, modelId, keyId, now - windowMs);
188
+ return row.used;
189
+ });
190
+ }
191
+ function sumPersistedTokens(platform, modelId, keyId, windowMs, now) {
192
+ return withDb(db => {
193
+ const row = db.prepare(`
194
+ SELECT COALESCE(SUM(tokens), 0) AS used
195
+ FROM rate_limit_usage
196
+ WHERE platform = ?
197
+ AND model_id = ?
198
+ AND key_id = ?
199
+ AND kind = 'tokens'
200
+ AND created_at_ms > ?
201
+ `).get(platform, modelId, keyId, now - windowMs);
202
+ return row.used;
203
+ });
204
+ }
205
+ function memoryRequestCount(key, windowMs, now) {
206
+ const w = getWindow(key);
207
+ w.timestamps = pruneTimestamps(w.timestamps, windowMs, now);
208
+ return w.timestamps.length;
209
+ }
210
+ function memoryTokenCount(key, windowMs, now) {
211
+ const w = getWindow(key);
212
+ w.tokenTimestamps = w.tokenTimestamps.filter(t => t.ts > now - windowMs);
213
+ return w.tokenTimestamps.reduce((sum, t) => sum + t.tokens, 0);
214
+ }
215
+ function requestCount(platform, modelId, keyId, windowMs, now) {
216
+ const persisted = countPersistedRequests(platform, modelId, keyId, windowMs, now);
217
+ if (persisted !== undefined)
218
+ return persisted;
219
+ const type = windowMs === MINUTE ? 'rpm' : 'rpd';
220
+ return memoryRequestCount(`${platform}:${modelId}:${keyId}:${type}`, windowMs, now);
221
+ }
222
+ function tokenCount(platform, modelId, keyId, windowMs, now) {
223
+ const persisted = sumPersistedTokens(platform, modelId, keyId, windowMs, now);
224
+ if (persisted !== undefined)
225
+ return persisted;
226
+ const type = windowMs === MINUTE ? 'tpm' : 'tpd';
227
+ return memoryTokenCount(`${platform}:${modelId}:${keyId}:${type}`, windowMs, now);
228
+ }
229
+ export function canMakeRequest(platform, modelId, keyId, limits) {
230
+ const now = Date.now();
231
+ // In-flight requests count against both windows: a lease means the request is
232
+ // happening now, so it belongs to this minute and to today.
233
+ const inFlight = provisionalRequests(platform, modelId, keyId, now);
234
+ if (limits.rpm !== null) {
235
+ if (requestCount(platform, modelId, keyId, MINUTE, now) + inFlight >= limits.rpm)
236
+ return false;
237
+ }
238
+ if (limits.rpd !== null) {
239
+ if (requestCount(platform, modelId, keyId, DAY, now) + inFlight >= limits.rpd)
240
+ return false;
241
+ }
242
+ return true;
243
+ }
244
+ export function canUseTokens(platform, modelId, keyId, estimatedTokens, limits) {
245
+ const now = Date.now();
246
+ // Tokens already promised to in-flight attempts on this model+key.
247
+ const inFlight = provisionalTokens(platform, modelId, keyId, now);
248
+ if (limits.tpm !== null) {
249
+ const used = tokenCount(platform, modelId, keyId, MINUTE, now);
250
+ if (used + inFlight + estimatedTokens > limits.tpm)
251
+ return false;
252
+ }
253
+ if (limits.tpd !== null) {
254
+ const used = tokenCount(platform, modelId, keyId, DAY, now);
255
+ if (used + inFlight + estimatedTokens > limits.tpd)
256
+ return false;
257
+ }
258
+ return true;
259
+ }
260
+ const NO_WINDOW_USAGE = { rpm: 0, rpd: 0, tpm: 0, tpd: 0 };
261
+ /** Utilization moves on the timescale of a rate-limit window, not a request, so
262
+ * a few seconds of staleness is invisible to a guardrail while the query count
263
+ * drops to ~one per burst. The hard gates above are unaffected — they still
264
+ * read live counts, so nothing here can let a request through a real limit. */
265
+ const WINDOW_USAGE_TTL_MS = 5_000;
266
+ // The Db handle is part of the cache identity, so reconnecting (tests, a
267
+ // restore) invalidates the snapshot rather than serving another database's
268
+ // numbers.
269
+ let windowUsageSnapshot = null;
270
+ function usageCacheKey(platform, modelId, keyId) {
271
+ return `${platform}\u0000${modelId}\u0000${keyId}`;
272
+ }
273
+ function buildWindowUsageSnapshot(now) {
274
+ return withDb(db => {
275
+ // One grouped scan replaces four counts per model × key. Same windows the
276
+ // gates above enforce: a sliding minute for rpm/tpm, a sliding day for
277
+ // rpd/tpd. Rows older than a day are pruned on write, but the WHERE keeps
278
+ // this correct even when a prune has not run yet.
279
+ const rows = db.prepare(`
280
+ SELECT platform, model_id, key_id,
281
+ SUM(CASE WHEN kind = 'request' AND created_at_ms > ? THEN 1 ELSE 0 END) AS rpm_used,
282
+ SUM(CASE WHEN kind = 'request' THEN 1 ELSE 0 END) AS rpd_used,
283
+ SUM(CASE WHEN kind = 'tokens' AND created_at_ms > ? THEN tokens ELSE 0 END) AS tpm_used,
284
+ SUM(CASE WHEN kind = 'tokens' THEN tokens ELSE 0 END) AS tpd_used
285
+ FROM rate_limit_usage
286
+ WHERE created_at_ms > ?
287
+ GROUP BY platform, model_id, key_id
288
+ `).all(now - MINUTE, now - MINUTE, now - DAY);
289
+ const usage = new Map();
290
+ for (const r of rows) {
291
+ usage.set(usageCacheKey(r.platform, r.model_id, r.key_id), {
292
+ rpm: r.rpm_used, rpd: r.rpd_used, tpm: r.tpm_used, tpd: r.tpd_used,
293
+ });
294
+ }
295
+ // Mirror the router's key eligibility (selectKeyForModel): enabled +
296
+ // healthy/unknown, and the model scope must admit the model. The custom
297
+ // endpoint check needs base_url for every row, disabled ones included, so
298
+ // that map is built from the unfiltered read.
299
+ const keyRows = db.prepare('SELECT id, platform, base_url, model_scope_json, enabled, status FROM api_keys').all();
300
+ const baseUrlByKeyId = new Map();
301
+ const keysByPlatform = new Map();
302
+ for (const k of keyRows) {
303
+ baseUrlByKeyId.set(k.id, k.base_url);
304
+ if (k.enabled !== 1 || (k.status !== 'healthy' && k.status !== 'unknown'))
305
+ continue;
306
+ const list = keysByPlatform.get(k.platform) ?? [];
307
+ list.push({ id: k.id, baseUrl: k.base_url, scope: parseModelScope(k.model_scope_json) });
308
+ keysByPlatform.set(k.platform, list);
309
+ }
310
+ return { db, at: now, usage, keysByPlatform, baseUrlByKeyId };
311
+ }) ?? null;
312
+ }
313
+ function getWindowUsageSnapshot(now) {
314
+ let db;
315
+ try {
316
+ db = getDb();
317
+ }
318
+ catch {
319
+ return null;
320
+ }
321
+ const cached = windowUsageSnapshot;
322
+ if (cached && cached.db === db && now - cached.at < WINDOW_USAGE_TTL_MS && now >= cached.at) {
323
+ return cached;
324
+ }
325
+ const built = buildWindowUsageSnapshot(now);
326
+ windowUsageSnapshot = built;
327
+ return built;
328
+ }
329
+ /** Drop the memoised window snapshot. Tests use it to make a write visible
330
+ * immediately; production relies on the TTL. */
331
+ export function invalidateWindowUsage() {
332
+ windowUsageSnapshot = null;
333
+ }
334
+ /** The busiest window of one key: the ratio that would reject its next request
335
+ * first. A key metered on several windows takes the WORST of them, because the
336
+ * binding constraint is what 429s. */
337
+ function keyWindowPressure(usage, limits) {
338
+ let worst = 0;
339
+ const ratios = [
340
+ [usage.rpm, limits.rpm],
341
+ [usage.rpd, limits.rpd],
342
+ [usage.tpm, limits.tpm],
343
+ [usage.tpd, limits.tpd],
344
+ ];
345
+ for (const [used, limit] of ratios) {
346
+ if (limit == null || limit <= 0)
347
+ continue;
348
+ const r = used / limit;
349
+ if (r > worst)
350
+ worst = r;
351
+ }
352
+ return worst;
353
+ }
354
+ /**
355
+ * Share of its binding rate-limit window a model has already consumed, as a
356
+ * 0..1 fraction (0 idle, 1 exhausted). null = no opinion: the model declares no
357
+ * window limits, has no routable key to measure, or the database is unreachable.
358
+ *
359
+ * Which key's numbers to report matters, and the answer is the same one the
360
+ * dashboard's usage badge settled on (#921): the guardrail asks "how close is
361
+ * this model to being unroutable", so it must follow the key the router would
362
+ * pick NEXT, not the worst key on the account. A platform with one exhausted
363
+ * key and one idle key routes perfectly well, so we take the ELIGIBLE key with
364
+ * the MOST headroom.
365
+ *
366
+ * In-flight leases are deliberately not counted. They exist to close the
367
+ * check-then-act race on the hard gates; this is a steering signal averaged over
368
+ * a whole window, and folding a per-request quantity into a cached snapshot
369
+ * would buy noise, not accuracy.
370
+ */
371
+ export function modelWindowUsedFraction(model, limits, now = Date.now()) {
372
+ if (limits.rpm == null && limits.rpd == null && limits.tpm == null && limits.tpd == null)
373
+ return null;
374
+ const snap = getWindowUsageSnapshot(now);
375
+ if (!snap)
376
+ return null;
377
+ const pool = snap.keysByPlatform.get(model.platform);
378
+ if (!pool || pool.length === 0)
379
+ return null; // nothing routable → a number would be fiction
380
+ // A custom model belongs to one endpoint; only that endpoint's credentials can
381
+ // serve it. Legacy rows (key_id NULL) keep the any-key match.
382
+ const endpointBaseUrl = model.platform === 'custom' && model.keyId != null
383
+ ? snap.baseUrlByKeyId.get(model.keyId) ?? null
384
+ : null;
385
+ let best = null;
386
+ for (const k of pool) {
387
+ if (!scopeAllows(k.scope, model.modelId))
388
+ continue;
389
+ if (model.platform === 'custom' && model.keyId != null) {
390
+ if (endpointBaseUrl == null ? k.id !== model.keyId : k.baseUrl !== endpointBaseUrl)
391
+ continue;
392
+ }
393
+ const usage = snap.usage.get(usageCacheKey(model.platform, model.modelId, k.id)) ?? NO_WINDOW_USAGE;
394
+ const pressure = keyWindowPressure(usage, limits);
395
+ if (best === null || pressure < best)
396
+ best = pressure;
397
+ if (best === 0)
398
+ break; // an untouched key is as good as it gets
399
+ }
400
+ if (best === null)
401
+ return null; // every key scoped away from this model
402
+ return Math.min(1, best);
403
+ }
404
+ // ── Provider-wide daily request caps (#162) ──
405
+ // Some providers enforce one daily REQUEST quota across the WHOLE account,
406
+ // shared by every model — not per model. OpenRouter's free tier is the classic
407
+ // case: ~1000 requests/day total (50/day if you've bought <10 credits) no
408
+ // matter how many different free models you spread them across. The
409
+ // per-(platform,model,key) rpd ledger can't see that, so without a provider-wide
410
+ // gate the router happily fires (models × rpd) requests and earns surprise 429s.
411
+ //
412
+ // Defaults below; override per provider with an env var, e.g.
413
+ // PROVIDER_DAILY_REQUEST_CAP_OPENROUTER=50 (set 0 to disable the cap)
414
+ const DEFAULT_PROVIDER_DAILY_REQUEST_CAPS = {
415
+ openrouter: 1000,
416
+ // ModelScope's free tier is 2000 requests/day across the whole account (all
417
+ // models share it). 1800 leaves margin for validation probes and for drift
418
+ // between our ledger and the provider's own daily boundary (#581).
419
+ modelscope: 1800,
420
+ // Requesty's free tier is 200 requests/day per account, shared by every free
421
+ // model (requesty.ai/free-models, no card). The catalog rows each carry
422
+ // rpd 200, so without this gate the router could spend 200 per model. 180
423
+ // leaves the same margin as ModelScope for cooldown probes and drift from
424
+ // Requesty's own daily reset.
425
+ requesty: 180,
426
+ };
427
+ const DEFAULT_PROVIDER_DAILY_TOKEN_CAPS = {
428
+ // NavyAI's free plan is one shared 150K-token/day pool per key. Individual
429
+ // models carry token_multiplier values, so the provider-visible drain can be
430
+ // higher than the OpenAI usage.total_tokens returned by /chat/completions.
431
+ navy: 150_000,
432
+ };
433
+ // Per-minute caps that apply to the whole provider account rather than one model.
434
+ // The per-model rpm_limit cannot express these: NVIDIA NIM meters ~40 requests a
435
+ // minute across the entire account, so glm-4.7, minimax-m3 and deepseek all draw
436
+ // from one bucket. Without an account-level gate the router sees each model's own
437
+ // rpm as unspent — and a model row whose rpm_limit is NULL escapes pre-throttling
438
+ // entirely — so it keeps dispatching and eats real 429s.
439
+ const DEFAULT_PROVIDER_MINUTE_REQUEST_CAPS = {
440
+ nvidia: 40,
441
+ };
442
+ export function getProviderDailyRequestCap(platform) {
443
+ const raw = process.env[`PROVIDER_DAILY_REQUEST_CAP_${platform.toUpperCase()}`];
444
+ if (raw !== undefined && raw.trim() !== '') {
445
+ const n = Number(raw);
446
+ if (Number.isFinite(n) && n >= 0)
447
+ return n === 0 ? null : n;
448
+ }
449
+ return DEFAULT_PROVIDER_DAILY_REQUEST_CAPS[platform] ?? null;
450
+ }
451
+ /** Account-wide requests-per-minute cap, or null when the provider has none.
452
+ * `PROVIDER_MINUTE_REQUEST_CAP_<PLATFORM>=0` disables the gate for that platform. */
453
+ export function getProviderMinuteRequestCap(platform) {
454
+ const raw = process.env[`PROVIDER_MINUTE_REQUEST_CAP_${platform.toUpperCase()}`];
455
+ if (raw !== undefined && raw.trim() !== '') {
456
+ const n = Number(raw);
457
+ if (Number.isFinite(n) && n >= 0)
458
+ return n === 0 ? null : n;
459
+ }
460
+ return DEFAULT_PROVIDER_MINUTE_REQUEST_CAPS[platform] ?? null;
461
+ }
462
+ export function getProviderDailyTokenCap(platform) {
463
+ const raw = process.env[`PROVIDER_DAILY_TOKEN_CAP_${platform.toUpperCase()}`];
464
+ if (raw !== undefined && raw.trim() !== '') {
465
+ const n = Number(raw);
466
+ if (Number.isFinite(n) && n >= 0)
467
+ return n === 0 ? null : n;
468
+ }
469
+ return DEFAULT_PROVIDER_DAILY_TOKEN_CAPS[platform] ?? null;
470
+ }
471
+ function countPersistedProviderRequests(platform, keyId, windowMs, now) {
472
+ return withDb(db => {
473
+ const row = db.prepare(`
474
+ SELECT COUNT(*) AS used
475
+ FROM rate_limit_usage
476
+ WHERE platform = ?
477
+ AND key_id = ?
478
+ AND kind = 'request'
479
+ AND created_at_ms > ?
480
+ `).get(platform, keyId, now - windowMs);
481
+ return row.used;
482
+ });
483
+ }
484
+ // Total requests today for a provider account+key, summed across every model.
485
+ // "Today" is the window since midnight UTC, not a sliding 24h: real provider
486
+ // account caps (OpenRouter, NVIDIA) reset on a wall-clock day boundary, so a
487
+ // sliding window keeps a provider benched well past its actual reset — a burst
488
+ // at 23:00 UTC would still be counted against the account at 22:00 the next day.
489
+ export function providerDailyRequestCount(platform, keyId, now = Date.now()) {
490
+ const windowMs = msSinceUtcMidnight(now);
491
+ const persisted = countPersistedProviderRequests(platform, keyId, windowMs, now);
492
+ if (persisted !== undefined)
493
+ return persisted;
494
+ // DB-unavailable fallback: sum the per-model rpd windows for this platform+key.
495
+ // Window key format is "platform:modelId:keyId:rpd" (modelId may contain ':').
496
+ let total = 0;
497
+ for (const [key, w] of windows) {
498
+ if (key.startsWith(`${platform}:`) && key.endsWith(`:${keyId}:rpd`)) {
499
+ total += pruneTimestamps(w.timestamps, windowMs, now).length;
500
+ }
501
+ }
502
+ return total;
503
+ }
504
+ // False when this provider account+key has hit its shared daily request cap, so
505
+ // the router skips every model on that provider for this key until UTC-ish reset.
506
+ export function canUseProvider(platform, keyId, now = Date.now()) {
507
+ const cap = getProviderDailyRequestCap(platform);
508
+ if (cap === null)
509
+ return true;
510
+ const used = providerDailyRequestCount(platform, keyId, now) + provisionalProviderRequests(platform, keyId, now);
511
+ return used < cap;
512
+ }
513
+ /** Requests in the last minute for a provider account+key, across every model. */
514
+ export function providerMinuteRequestCount(platform, keyId, now = Date.now()) {
515
+ const persisted = countPersistedProviderRequests(platform, keyId, MINUTE, now);
516
+ if (persisted !== undefined)
517
+ return persisted;
518
+ // DB-unavailable fallback: sum the per-model rpm windows for this platform+key.
519
+ let total = 0;
520
+ for (const [key, w] of windows) {
521
+ if (key.startsWith(`${platform}:`) && key.endsWith(`:${keyId}:rpm`)) {
522
+ total += pruneTimestamps(w.timestamps, MINUTE, now).length;
523
+ }
524
+ }
525
+ return total;
526
+ }
527
+ // False when this provider account+key has spent its shared per-minute request
528
+ // budget, so the router skips every model on that provider rather than learning
529
+ // the limit again from a 429 on each one in turn.
530
+ export function canUseProviderMinute(platform, keyId, now = Date.now()) {
531
+ const cap = getProviderMinuteRequestCap(platform);
532
+ if (cap === null)
533
+ return true;
534
+ const used = providerMinuteRequestCount(platform, keyId, now) + provisionalProviderRequests(platform, keyId, now);
535
+ return used < cap;
536
+ }
537
+ function multiplierFromQuotaRow(row, dailyCap) {
538
+ if (!row)
539
+ return 1;
540
+ const fromLabel = row.monthly_token_budget?.match(/(?:^|[\u00b7(\s])(\d+(?:\.\d+)?)x\b/i);
541
+ if (fromLabel) {
542
+ const n = Number(fromLabel[1]);
543
+ if (Number.isFinite(n) && n > 0)
544
+ return n;
545
+ }
546
+ const tpd = row.tpd_limit;
547
+ if (tpd != null && tpd > 0 && tpd < dailyCap)
548
+ return dailyCap / tpd;
549
+ return 1;
550
+ }
551
+ function getProviderTokenMultiplier(platform, modelId, dailyCap) {
552
+ if (platform !== 'navy')
553
+ return 1;
554
+ return withDb(db => {
555
+ const row = db.prepare(`
556
+ SELECT tpd_limit, monthly_token_budget
557
+ FROM models
558
+ WHERE platform = ? AND model_id = ?
559
+ LIMIT 1
560
+ `).get(platform, modelId);
561
+ return multiplierFromQuotaRow(row, dailyCap);
562
+ }) ?? 1;
563
+ }
564
+ function providerBilledTokens(platform, modelId, rawTokens) {
565
+ if (rawTokens <= 0)
566
+ return 0;
567
+ const dailyCap = getProviderDailyTokenCap(platform);
568
+ if (dailyCap === null)
569
+ return rawTokens;
570
+ const multiplier = getProviderTokenMultiplier(platform, modelId, dailyCap);
571
+ return Math.ceil(rawTokens * multiplier);
572
+ }
573
+ function sumPersistedProviderTokens(platform, keyId, windowMs, now) {
574
+ const dailyCap = getProviderDailyTokenCap(platform);
575
+ if (dailyCap === null)
576
+ return 0;
577
+ return withDb(db => {
578
+ const rows = db.prepare(`
579
+ SELECT rlu.model_id, COALESCE(SUM(rlu.tokens), 0) AS used,
580
+ m.tpd_limit, m.monthly_token_budget
581
+ FROM rate_limit_usage rlu
582
+ LEFT JOIN models m ON m.platform = rlu.platform AND m.model_id = rlu.model_id
583
+ WHERE rlu.platform = ?
584
+ AND rlu.key_id = ?
585
+ AND rlu.kind = 'tokens'
586
+ AND rlu.created_at_ms > ?
587
+ GROUP BY rlu.model_id
588
+ `).all(platform, keyId, now - windowMs);
589
+ return rows.reduce((sum, row) => {
590
+ const multiplier = platform === 'navy' ? multiplierFromQuotaRow(row, dailyCap) : 1;
591
+ return sum + Math.ceil(row.used * multiplier);
592
+ }, 0);
593
+ });
594
+ }
595
+ // Midnight-UTC boundary, for the same reason as providerDailyRequestCount.
596
+ export function providerDailyTokenCount(platform, keyId, now = Date.now()) {
597
+ const windowMs = msSinceUtcMidnight(now);
598
+ const persisted = sumPersistedProviderTokens(platform, keyId, windowMs, now);
599
+ if (persisted !== undefined)
600
+ return persisted;
601
+ let total = 0;
602
+ const suffix = `:${keyId}:tpd`;
603
+ for (const [key, w] of windows) {
604
+ if (!key.startsWith(`${platform}:`) || !key.endsWith(suffix))
605
+ continue;
606
+ const modelId = key.slice(platform.length + 1, -suffix.length);
607
+ // Read-only: the tpd window is shared with the per-model 24h token check, so
608
+ // narrowing it to the midnight boundary here must not prune the stored entries.
609
+ const raw = w.tokenTimestamps
610
+ .filter(t => t.ts > now - windowMs)
611
+ .reduce((sum, t) => sum + t.tokens, 0);
612
+ total += providerBilledTokens(platform, modelId, raw);
613
+ }
614
+ return total;
615
+ }
616
+ export function canUseProviderTokens(platform, keyId, modelId, estimatedTokens, now = Date.now()) {
617
+ const cap = getProviderDailyTokenCap(platform);
618
+ if (cap === null)
619
+ return true;
620
+ const used = providerDailyTokenCount(platform, keyId, now)
621
+ + providerBilledTokens(platform, modelId, provisionalProviderTokens(platform, keyId, now));
622
+ return used + providerBilledTokens(platform, modelId, estimatedTokens) <= cap;
623
+ }
624
+ // ── Degraded-mode memory windows ─────────────────────────────────────────────
625
+ // The in-memory windows are a FALLBACK for the persisted counters: requestCount
626
+ // / tokenCount / providerDailyRequestCount all prefer the DB and only consult
627
+ // memory when the DB refuses to answer. So on a healthy server nothing ever
628
+ // reads these arrays — and pruning happens only on the read path, which means
629
+ // nothing ever trimmed them either: every recorded request leaked one timestamp
630
+ // (and one {ts, tokens} object) for the lifetime of the process.
631
+ //
632
+ // Record in memory only when the persisted write actually failed, and prune the
633
+ // window as we push so a long DB outage cannot grow it past its own width
634
+ // either. Degraded-mode behavior is unchanged: the same timestamps land in the
635
+ // same windows whenever the DB is the one that could not record them.
636
+ function pushMemoryRequest(key, windowMs, now) {
637
+ const w = getWindow(key);
638
+ w.timestamps = pruneTimestamps(w.timestamps, windowMs, now);
639
+ w.timestamps.push(now);
640
+ }
641
+ function pushMemoryTokens(key, windowMs, now, tokens) {
642
+ const w = getWindow(key);
643
+ w.tokenTimestamps = w.tokenTimestamps.filter(t => t.ts > now - windowMs);
644
+ w.tokenTimestamps.push({ ts: now, tokens });
645
+ }
646
+ export function recordRequest(platform, modelId, keyId) {
647
+ const now = Date.now();
648
+ if (!recordUsage(platform, modelId, keyId, 'request', 0, now)) {
649
+ pushMemoryRequest(`${platform}:${modelId}:${keyId}:rpm`, MINUTE, now);
650
+ pushMemoryRequest(`${platform}:${modelId}:${keyId}:rpd`, DAY, now);
651
+ }
652
+ clearNullLimitHits(platform, modelId, keyId);
653
+ clearCooldownHits(platform, modelId, keyId);
654
+ }
655
+ export function recordTokens(platform, modelId, keyId, tokens) {
656
+ const now = Date.now();
657
+ if (!recordUsage(platform, modelId, keyId, 'tokens', tokens, now)) {
658
+ pushMemoryTokens(`${platform}:${modelId}:${keyId}:tpm`, MINUTE, now, tokens);
659
+ pushMemoryTokens(`${platform}:${modelId}:${keyId}:tpd`, DAY, now, tokens);
660
+ }
661
+ }
662
+ // Cooldown: when a provider returns 429, block that model+key for a period
663
+ const cooldowns = new Map(); // key -> expiry timestamp
664
+ // Escalating cooldown: track hits per key over a rolling 24h window so a
665
+ // daily-quota exhaustion (OpenRouter free: 50/day, Cohere free: 33/day, etc.)
666
+ // quarantines the key for the rest of the day instead of looping through
667
+ // the 2-minute cooldown 20 times per request and consuming every fallback slot.
668
+ // In-memory only — state resets on restart, which is fine (a clean restart
669
+ // will re-escalate on the next 429 if the quota is genuinely exhausted).
670
+ // A successful request clears the counter (recordRequest → clearCooldownHits),
671
+ // same reversibility contract as nullLimitHits: a served request proves the
672
+ // quota is NOT exhausted right now, so the next failure starts the ladder over
673
+ // instead of inheriting up-to-24h steps from stale hits. While a daily quota is
674
+ // genuinely spent no successes can occur (the counters/cooldowns gate routing),
675
+ // so the intended escalation-across-the-day behavior is untouched.
676
+ const cooldownHits = new Map(); // key -> timestamps of recent cooldown set events
677
+ const HOUR = 60 * MINUTE;
678
+ const COOLDOWN_DURATIONS = [
679
+ 2 * MINUTE, // 1st hit in 24h
680
+ 10 * MINUTE, // 2nd
681
+ HOUR, // 3rd
682
+ DAY, // 4th and beyond
683
+ ];
684
+ // ── Operator ceiling on automatic cooldowns (#952, #1049) ────────────────────
685
+ // The ladder above and the day-long 402/403 benches below assume a provider
686
+ // whose failures mean what they say. Relay endpoints often don't: a "busy" or
687
+ // "insufficient balance" blip clears in minutes, yet a handful of them per key
688
+ // walked every key+model pair of a pool onto a 24h bench with no way back but
689
+ // the per-key reset button. `routing_cooldown_ceiling_ms` caps how long OUR
690
+ // guesses may bench a route. It never shortens a provider-stated retry time
691
+ // (Retry-After, daily-quota reset): those are facts, and re-hammering a
692
+ // provider that told us when to come back would only burn quota. Unset =
693
+ // unchanged behaviour (the ladder tops out at a day).
694
+ export const COOLDOWN_CEILING_KEY = 'routing_cooldown_ceiling_ms';
695
+ export const MIN_COOLDOWN_CEILING_MS = MINUTE;
696
+ export const MAX_COOLDOWN_CEILING_MS = DAY;
697
+ /** The configured ceiling in ms, or null when unset/invalid (no cap). Read
698
+ * through a try/catch like every other DB touch in this module so a failure
699
+ * before the DB is ready degrades to "no cap" instead of throwing mid-route. */
700
+ export function getCooldownCeilingMs() {
701
+ let raw;
702
+ try {
703
+ raw = getSetting(COOLDOWN_CEILING_KEY);
704
+ }
705
+ catch {
706
+ return null;
707
+ }
708
+ if (raw === undefined || raw.trim() === '')
709
+ return null;
710
+ const n = Number(raw);
711
+ if (!Number.isInteger(n) || n < MIN_COOLDOWN_CEILING_MS || n > MAX_COOLDOWN_CEILING_MS)
712
+ return null;
713
+ return n;
714
+ }
715
+ /** null clears the ceiling (back to the uncapped ladder). */
716
+ export function setCooldownCeilingMs(value) {
717
+ if (value === null) {
718
+ getDb().prepare('DELETE FROM settings WHERE key = ?').run(COOLDOWN_CEILING_KEY);
719
+ return;
720
+ }
721
+ if (!Number.isInteger(value) || value < MIN_COOLDOWN_CEILING_MS || value > MAX_COOLDOWN_CEILING_MS) {
722
+ throw new Error(`cooldown ceiling must be an integer between ${MIN_COOLDOWN_CEILING_MS} and ${MAX_COOLDOWN_CEILING_MS} ms`);
723
+ }
724
+ setSetting(COOLDOWN_CEILING_KEY, String(value));
725
+ }
726
+ /** Apply the operator ceiling to one of OUR heuristic bench durations. */
727
+ export function capCooldownMs(durationMs) {
728
+ const ceiling = getCooldownCeilingMs();
729
+ return ceiling === null ? durationMs : Math.min(durationMs, ceiling);
730
+ }
731
+ export function getNextCooldownDuration(platform, modelId, keyId) {
732
+ const key = `${platform}:${modelId}:${keyId}`;
733
+ const now = Date.now();
734
+ const hits = (cooldownHits.get(key) ?? []).filter(t => t > now - DAY);
735
+ hits.push(now);
736
+ cooldownHits.set(key, hits);
737
+ const idx = Math.min(hits.length - 1, COOLDOWN_DURATIONS.length - 1);
738
+ return capCooldownMs(COOLDOWN_DURATIONS[idx]);
739
+ }
740
+ function clearCooldownHits(platform, modelId, keyId) {
741
+ cooldownHits.delete(`${platform}:${modelId}:${keyId}`);
742
+ }
743
+ // Short cooldown for a transient (per-minute) 429 — recovers within ~one window.
744
+ const TRANSIENT_COOLDOWN_MS = 90 * 1000;
745
+ // Ceiling for the null-limits escalation path. A provider that publishes no
746
+ // RPD/TPD gives us no counter to check, so "daily-exhausted" there is inferred
747
+ // from repeated 429s alone — and per-minute jitter under load looks exactly the
748
+ // same as a spent daily quota. Letting that guess climb the full ladder means a
749
+ // short burst of RPM 429s can quarantine a model+key for 24h with nothing able
750
+ // to contradict it (a benched route serves no request, so no success arrives to
751
+ // clear the counter). Cap the guess at the ladder's 10-minute step; a genuinely
752
+ // dead route just re-benches every 10 minutes instead of hammering the 90s loop.
753
+ // Real RPD/TPD exhaustion is measured, not guessed, and keeps the full ladder.
754
+ const UNKNOWN_LIMIT_MAX_COOLDOWN_MS = 10 * MINUTE;
755
+ // Long cooldown for a 402 Payment Required (provider/key out of credits). Unlike
756
+ // a 429, this won't clear on the next minute/day window — it needs a top-up or
757
+ // billing reset. Bench the model+key for a full day so the router fails over to
758
+ // other providers instead of re-hammering a dead key every retry. Re-escalates
759
+ // on the next 402 after expiry if still unpaid; a restart re-benches on first hit.
760
+ export const PAYMENT_REQUIRED_COOLDOWN_MS = DAY;
761
+ /** The 402 bench with the operator ceiling applied (#952). */
762
+ export function getPaymentRequiredCooldownMs() {
763
+ return capCooldownMs(PAYMENT_REQUIRED_COOLDOWN_MS);
764
+ }
765
+ // Long cooldown for a 403 Forbidden on a key that already passed validateKey
766
+ // (so it is not a dead key — the health checker disables those). A request-time
767
+ // 403 means this key's tier can't reach this specific model (e.g. gpt-4o on
768
+ // GitHub Models' free tier, subscription-only models on Cloudflare). That won't
769
+ // change within a minute window, so bench this model+key for a full day and let
770
+ // the router fail over to a model the key can actually serve. See issue #256.
771
+ export const MODEL_FORBIDDEN_COOLDOWN_MS = DAY;
772
+ /** The 403 tier bench with the operator ceiling applied (#952). */
773
+ export function getModelForbiddenCooldownMs() {
774
+ return capCooldownMs(MODEL_FORBIDDEN_COOLDOWN_MS);
775
+ }
776
+ // When RPD/TPD limits are NULL (provider's published daily quota is unknown or
777
+ // not yet seeded — common for ollama, cloudflare, nvidia, huggingface, mistral,
778
+ // kilo, llm7, pollinations), we cannot check a counter against a cap. Fall back
779
+ // to a hit-count heuristic: after 2+ 429s within this rolling window, treat as
780
+ // "effectively daily-exhausted" and escalate — but only as far as
781
+ // UNKNOWN_LIMIT_MAX_COOLDOWN_MS, since the verdict is a guess. Without this,
782
+ // these providers
783
+ // stay stuck at TRANSIENT_COOLDOWN_MS forever even when every request is a
784
+ // 429 (observed in production: ollama 130× 429s in 1h with all 90s cooldowns
785
+ // expired before the next request). Cheaper than waiting for the operator to
786
+ // seed per-provider limits (Option A), still reversible — a successful call
787
+ // clears the hit window via the normal path.
788
+ //
789
+ // Separate counter from `cooldownHits` (used by getNextCooldownDuration's
790
+ // escalation ladder). The shared Map would make this state path-coupled to
791
+ // the ladder index, which would over-skip steps because the ladder also
792
+ // pushes a hit on each call.
793
+ const NULL_LIMIT_HIT_THRESHOLD = 2;
794
+ const NULL_LIMIT_HIT_WINDOW_MS = HOUR;
795
+ const nullLimitHits = new Map(); // key -> timestamps
796
+ function recordNullLimitHit(platform, modelId, keyId, now) {
797
+ const key = `${platform}:${modelId}:${keyId}`;
798
+ const hits = nullLimitHits.get(key) ?? [];
799
+ hits.push(now);
800
+ nullLimitHits.set(key, hits);
801
+ }
802
+ function clearNullLimitHits(platform, modelId, keyId) {
803
+ nullLimitHits.delete(`${platform}:${modelId}:${keyId}`);
804
+ }
805
+ export function recentHitCount(platform, modelId, keyId, now, windowMs = NULL_LIMIT_HIT_WINDOW_MS) {
806
+ const key = `${platform}:${modelId}:${keyId}`;
807
+ const hits = nullLimitHits.get(key) ?? [];
808
+ return hits.filter(t => t > now - windowMs).length;
809
+ }
810
+ // ── Local-endpoint exemption (#592) ──────────────────────────────────────────
811
+ // A key whose base_url points at this machine or the LAN (custom-platform
812
+ // Ollama / llama.cpp / LM Studio) has no provider quota to protect: every
813
+ // cooldown longer than a few seconds just strands what is usually the user's
814
+ // ONLY route to that model, turning a slow local generation (timeout) into an
815
+ // "All models exhausted" 429. Such keys are capped at this short bench and
816
+ // never enter the escalation ladder. Source stays 'heuristic' so the
817
+ // cooldown-probe recovery job may clear even that early.
818
+ export const LOCAL_ENDPOINT_COOLDOWN_MS = 5_000;
819
+ // base_url is immutable per key id (routes/keys.ts matches custom rows ON
820
+ // base_url — a new endpoint gets a new row, #212), so the verdict can be
821
+ // cached for the life of the process. Built-in platforms have base_url NULL
822
+ // → non-local. A DB that isn't ready yet is NOT cached, so a later call can
823
+ // still decide properly.
824
+ const keyLocality = new Map(); // keyId -> is local endpoint
825
+ export function isLocalEndpointKey(keyId) {
826
+ const cached = keyLocality.get(keyId);
827
+ if (cached !== undefined)
828
+ return cached;
829
+ const baseUrl = withDb(db => {
830
+ const row = db.prepare('SELECT base_url FROM api_keys WHERE id = ?')
831
+ .get(keyId);
832
+ return row?.base_url ?? '';
833
+ });
834
+ if (baseUrl === undefined)
835
+ return false; // DB not ready — don't cache
836
+ const local = isLoopbackOrPrivateUrl(baseUrl || null);
837
+ keyLocality.set(keyId, local);
838
+ return local;
839
+ }
840
+ /** Test hook: the locality verdict is cached per key id for the process
841
+ * lifetime (see above); tests that rewrite api_keys rows need a reset. */
842
+ export function resetKeyLocalityCache() {
843
+ keyLocality.clear();
844
+ }
845
+ export function getCooldownDurationForLimit(platform, modelId, keyId, limits, retryAfterMs, opts) {
846
+ return getCooldownDecisionForLimit(platform, modelId, keyId, limits, retryAfterMs, opts).durationMs;
847
+ }
848
+ /**
849
+ * Same verdict as getCooldownDurationForLimit, plus the provenance the
850
+ * cooldown-probe recovery job needs: 'authoritative' when an explicit provider
851
+ * Retry-After actually determined the expiry (a fact — never probed early),
852
+ * 'heuristic' when the duration is our own transient/escalation guess (probe-
853
+ * eligible). A Retry-After SHORTER than our bench does not make the cooldown
854
+ * authoritative: everything past the provider's own retry time is our guess.
855
+ */
856
+ export function getCooldownDecisionForLimit(platform, modelId, keyId, limits, retryAfterMs, opts) {
857
+ // Local inference endpoint (loopback/RFC1918 base_url): no quota exists, so
858
+ // neither the transient 90s bench nor the escalation ladder applies — a
859
+ // failure there means "busy right now", and a few seconds is all the pool
860
+ // needs before retrying the user's (usually only) local route. Applies even
861
+ // to a literal 429 (local servers emit them under concurrent load) and
862
+ // ignores any Retry-After: both describe momentary busyness, not a quota.
863
+ if (isLocalEndpointKey(keyId)) {
864
+ return { durationMs: LOCAL_ENDPOINT_COOLDOWN_MS, source: 'heuristic' };
865
+ }
866
+ const quotaSignal = opts?.quotaSignal ?? true;
867
+ const now = Date.now();
868
+ const rpdExhausted = limits.rpd !== null && requestCount(platform, modelId, keyId, DAY, now) >= limits.rpd;
869
+ const tpdExhausted = limits.tpd !== null && tokenCount(platform, modelId, keyId, DAY, now) >= limits.tpd;
870
+ // No daily quota published → use repeated-429 heuristic: 2+ 429s in the
871
+ // last hour is treated as effectively daily-exhausted. This unsticks
872
+ // providers that publish no daily cap (ollama, cloudflare, etc.) from the
873
+ // 90s-cooldown-loop without requiring operator-side limit seeding.
874
+ // Gated on quotaSignal (#592): only a REAL 429/rate-limit error is evidence
875
+ // of quota exhaustion. Timeouts and 5xx used to feed this counter too, so
876
+ // two slow generations on a null-limits route escalated 2m→10m→…→24h.
877
+ const unknownLimits = limits.rpd === null && limits.tpd === null;
878
+ let heuristicallyExhausted = false;
879
+ if (unknownLimits && quotaSignal) {
880
+ // The current hit is recorded first so the threshold can be reached across
881
+ // consecutive 429s, but only for providers where counters cannot decide.
882
+ recordNullLimitHit(platform, modelId, keyId, now);
883
+ heuristicallyExhausted =
884
+ recentHitCount(platform, modelId, keyId, now) >= NULL_LIMIT_HIT_THRESHOLD;
885
+ }
886
+ const dailyExhausted = rpdExhausted || tpdExhausted;
887
+ let base = TRANSIENT_COOLDOWN_MS;
888
+ if (dailyExhausted || heuristicallyExhausted) {
889
+ // The ladder step is taken either way, so a route whose real limits get
890
+ // seeded (or learned) later resumes escalating where it left off rather
891
+ // than restarting at 2 minutes.
892
+ base = getNextCooldownDuration(platform, modelId, keyId);
893
+ // Guessed exhaustion (no published RPD/TPD) never benches past the cap.
894
+ if (!dailyExhausted)
895
+ base = Math.min(base, UNKNOWN_LIMIT_MAX_COOLDOWN_MS);
896
+ }
897
+ // Honor an upstream Retry-After as a floor: never bench shorter than our own
898
+ // heuristic, but extend (capped at a day) when the provider explicitly asks
899
+ // to wait longer than we otherwise would.
900
+ if (retryAfterMs != null && retryAfterMs > base) {
901
+ return { durationMs: Math.min(retryAfterMs, DAY), source: 'authoritative' };
902
+ }
903
+ return { durationMs: base, source: 'heuristic' };
904
+ }
905
+ function persistedCooldownExpiry(platform, modelId, keyId) {
906
+ return withDb(db => {
907
+ const row = db.prepare(`
908
+ SELECT expires_at_ms
909
+ FROM rate_limit_cooldowns
910
+ WHERE platform = ?
911
+ AND model_id = ?
912
+ AND key_id = ?
913
+ `).get(platform, modelId, keyId);
914
+ return row?.expires_at_ms ?? null;
915
+ });
916
+ }
917
+ function persistCooldown(platform, modelId, keyId, expiresAtMs, source, setAtMs) {
918
+ withDb(db => {
919
+ db.prepare(`
920
+ INSERT INTO rate_limit_cooldowns (platform, model_id, key_id, expires_at_ms, source, set_at_ms)
921
+ VALUES (?, ?, ?, ?, ?, ?)
922
+ ON CONFLICT(platform, model_id, key_id)
923
+ DO UPDATE SET expires_at_ms = excluded.expires_at_ms,
924
+ source = excluded.source,
925
+ set_at_ms = excluded.set_at_ms
926
+ `).run(platform, modelId, keyId, expiresAtMs, source, setAtMs);
927
+ });
928
+ }
929
+ function clearPersistedCooldown(platform, modelId, keyId) {
930
+ withDb(db => {
931
+ db.prepare(`
932
+ DELETE FROM rate_limit_cooldowns
933
+ WHERE platform = ?
934
+ AND model_id = ?
935
+ AND key_id = ?
936
+ `).run(platform, modelId, keyId);
937
+ });
938
+ }
939
+ /**
940
+ * Delete every already-expired cooldown, on disk and in memory, and return how
941
+ * many rows went. Expiry is otherwise purely lazy: isOnCooldown drops a row only
942
+ * when something asks about that exact (platform, model, key), so a row whose
943
+ * model was retired, whose key was deleted, or that simply outlived a process
944
+ * that had every route benched is never collected — the table only grows, and
945
+ * every cooldown query (status rollups, the probe scan, penalty inspector) pays
946
+ * for the dead rows. Called once at boot (index.ts) so a restart starts clean.
947
+ */
948
+ export function cleanupExpiredCooldowns(now = Date.now()) {
949
+ const cleared = withDb(db => {
950
+ const result = db.prepare('DELETE FROM rate_limit_cooldowns WHERE expires_at_ms <= ?').run(now);
951
+ return Number(result.changes ?? 0);
952
+ }) ?? 0;
953
+ // Only the expired entries: this is a sweep, not clearCooldownsForKey — an
954
+ // active bench must survive it.
955
+ for (const [key, expiry] of [...cooldowns]) {
956
+ if (expiry <= now)
957
+ cooldowns.delete(key);
958
+ }
959
+ return cleared;
960
+ }
961
+ export function setCooldown(platform, modelId, keyId, durationMs = 60_000, source = 'heuristic') {
962
+ const key = `${platform}:${modelId}:${keyId}:cooldown`;
963
+ const now = Date.now();
964
+ const expiresAtMs = now + durationMs;
965
+ cooldowns.set(key, expiresAtMs);
966
+ persistCooldown(platform, modelId, keyId, expiresAtMs, source, now);
967
+ }
968
+ export function isOnCooldown(platform, modelId, keyId) {
969
+ const key = `${platform}:${modelId}:${keyId}:cooldown`;
970
+ const now = Date.now();
971
+ const persistedExpiry = persistedCooldownExpiry(platform, modelId, keyId);
972
+ if (persistedExpiry !== undefined && persistedExpiry !== null) {
973
+ if (now > persistedExpiry) {
974
+ cooldowns.delete(key);
975
+ clearPersistedCooldown(platform, modelId, keyId);
976
+ return false;
977
+ }
978
+ cooldowns.set(key, persistedExpiry);
979
+ return true;
980
+ }
981
+ const expiry = cooldowns.get(key);
982
+ if (!expiry)
983
+ return false;
984
+ if (now > expiry) {
985
+ cooldowns.delete(key);
986
+ return false;
987
+ }
988
+ return true;
989
+ }
990
+ /**
991
+ * Active cooldowns for the given keys, grouped by key id. Batched into one query
992
+ * so the dashboard can render "why is this key idle?" without N round-trips.
993
+ * Without this, a key benched by an escalated cooldown (up to 24h) is invisible:
994
+ * it reads as healthy and enabled while the router silently skips it.
995
+ */
996
+ export function getActiveCooldownsForKeys(keyIds, now = Date.now()) {
997
+ const grouped = new Map();
998
+ if (keyIds.length === 0)
999
+ return grouped;
1000
+ const unique = [...new Set(keyIds.filter(id => Number.isInteger(id)))];
1001
+ if (unique.length === 0)
1002
+ return grouped;
1003
+ const placeholders = unique.map(() => '?').join(', ');
1004
+ const rows = withDb(db => db.prepare(`
1005
+ SELECT platform, model_id, key_id, expires_at_ms
1006
+ FROM rate_limit_cooldowns
1007
+ WHERE expires_at_ms > ?
1008
+ AND key_id IN (${placeholders})
1009
+ ORDER BY expires_at_ms ASC
1010
+ `).all(now, ...unique)) ?? [];
1011
+ for (const row of rows) {
1012
+ const list = grouped.get(row.key_id) ?? [];
1013
+ list.push({
1014
+ platform: row.platform,
1015
+ modelId: row.model_id,
1016
+ keyId: row.key_id,
1017
+ expiresAtMs: row.expires_at_ms,
1018
+ remainingMs: Math.max(0, row.expires_at_ms - now),
1019
+ });
1020
+ grouped.set(row.key_id, list);
1021
+ }
1022
+ return grouped;
1023
+ }
1024
+ /**
1025
+ * Drop every cooldown for one key, in memory and on disk, and return how many
1026
+ * were cleared. Escalated cooldowns can bench a key for up to 24h off a single
1027
+ * bad window; an operator who has fixed the cause (raised a quota, waited out a
1028
+ * provider incident) otherwise has no way back except restarting and waiting.
1029
+ */
1030
+ export function clearCooldownsForKey(keyId) {
1031
+ if (!Number.isInteger(keyId))
1032
+ return 0;
1033
+ const cleared = withDb(db => {
1034
+ const result = db.prepare('DELETE FROM rate_limit_cooldowns WHERE key_id = ?').run(keyId);
1035
+ return Number(result.changes ?? 0);
1036
+ }) ?? 0;
1037
+ // The in-memory map is authoritative when the DB read fails, so purge it too.
1038
+ // Key format is "platform:modelId:keyId:cooldown" and modelId may contain ':',
1039
+ // so match on the trailing segments rather than splitting.
1040
+ const suffix = `:${keyId}:cooldown`;
1041
+ let memoryCleared = 0;
1042
+ for (const key of [...cooldowns.keys()]) {
1043
+ if (key.endsWith(suffix)) {
1044
+ cooldowns.delete(key);
1045
+ cooldownHits.delete(key.slice(0, -':cooldown'.length));
1046
+ memoryCleared++;
1047
+ }
1048
+ }
1049
+ return Math.max(cleared, memoryCleared);
1050
+ }
1051
+ /**
1052
+ * Drop EVERY cooldown (all keys, all models), the escalation-ladder counters
1053
+ * and the null-limits hit windows — the "my whole pool is stuck" escape hatch
1054
+ * (#952). Returns how many active benches were lifted. Penalties and the
1055
+ * model-failure windows live elsewhere; services/penalty-inspector.ts
1056
+ * composes all three into one clear.
1057
+ */
1058
+ export function clearAllCooldowns(now = Date.now()) {
1059
+ const cleared = withDb(db => {
1060
+ const result = db.prepare('DELETE FROM rate_limit_cooldowns WHERE expires_at_ms > ?').run(now);
1061
+ // Expired rows are dead weight either way; sweep them in the same pass.
1062
+ db.prepare('DELETE FROM rate_limit_cooldowns WHERE expires_at_ms <= ?').run(now);
1063
+ return Number(result.changes ?? 0);
1064
+ }) ?? 0;
1065
+ let memoryCleared = 0;
1066
+ for (const expiry of cooldowns.values())
1067
+ if (expiry > now)
1068
+ memoryCleared++;
1069
+ cooldowns.clear();
1070
+ cooldownHits.clear();
1071
+ nullLimitHits.clear();
1072
+ return Math.max(cleared, memoryCleared);
1073
+ }
1074
+ /**
1075
+ * Active cooldowns whose expiry is OUR OWN guess ('heuristic'), i.e. the only
1076
+ * ones probe-based early recovery may touch. Authoritative/credit/tier rows are
1077
+ * excluded at the query so the prober cannot even see them. Deliberately
1078
+ * DB-only, no in-memory fallback: the probe job is a best-effort optimisation,
1079
+ * and when the DB is unavailable the right amount of probing is none.
1080
+ */
1081
+ export function getProbeableCooldowns(now = Date.now()) {
1082
+ const rows = withDb(db => db.prepare(`
1083
+ SELECT platform, model_id, key_id, expires_at_ms, set_at_ms
1084
+ FROM rate_limit_cooldowns
1085
+ WHERE expires_at_ms > ?
1086
+ AND source = 'heuristic'
1087
+ ORDER BY expires_at_ms ASC
1088
+ `).all(now)) ?? [];
1089
+ return rows.map(row => ({
1090
+ platform: row.platform,
1091
+ modelId: row.model_id,
1092
+ keyId: row.key_id,
1093
+ expiresAtMs: row.expires_at_ms,
1094
+ setAtMs: row.set_at_ms,
1095
+ }));
1096
+ }
1097
+ /**
1098
+ * Clear ONE model+key cooldown before its timer expires, after a probe showed
1099
+ * the key is serving again. Narrower than clearCooldownsForKey (the operator
1100
+ * override): the escalation history in cooldownHits is deliberately kept, so a
1101
+ * key that 429s again right after an early recovery re-enters the ladder where
1102
+ * it left off instead of starting back at 2 minutes.
1103
+ */
1104
+ export function clearCooldownEarly(platform, modelId, keyId) {
1105
+ cooldowns.delete(`${platform}:${modelId}:${keyId}:cooldown`);
1106
+ clearPersistedCooldown(platform, modelId, keyId);
1107
+ }
1108
+ /**
1109
+ * Soonest moment any active cooldown expires, in ms since epoch, or null when
1110
+ * nothing is cooling down. Used to tell an exhausted caller roughly when to
1111
+ * retry (#423) instead of the bare "wait for rate limits to reset".
1112
+ */
1113
+ export function getSoonestCooldownExpiry(now = Date.now()) {
1114
+ return withDb(db => {
1115
+ const row = db.prepare(`
1116
+ SELECT MIN(expires_at_ms) AS soonest
1117
+ FROM rate_limit_cooldowns
1118
+ WHERE expires_at_ms > ?
1119
+ `).get(now);
1120
+ return row?.soonest ?? null;
1121
+ }) ?? null;
1122
+ }
1123
+ export function getRateLimitStatus(platform, modelId, keyId, limits) {
1124
+ const now = Date.now();
1125
+ return {
1126
+ rpm: { used: requestCount(platform, modelId, keyId, MINUTE, now), limit: limits.rpm },
1127
+ rpd: { used: requestCount(platform, modelId, keyId, DAY, now), limit: limits.rpd },
1128
+ tpm: { used: tokenCount(platform, modelId, keyId, MINUTE, now), limit: limits.tpm },
1129
+ };
1130
+ }
1131
+ // Order matters: check the per-DAY axes before per-MINUTE so "tokens per day"
1132
+ // isn't shadowed by the "tpm" word-boundary alternative, and tokens before
1133
+ // requests so a body mentioning both lands on the more specific token ceiling.
1134
+ const LIMIT_AXIS_PATTERNS = [
1135
+ { kind: 'tpd', re: /tokens?\s*per\s*day|\btpd\b/i },
1136
+ { kind: 'tpm', re: /tokens?\s*per\s*min(?:ute)?|\btpm\b/i },
1137
+ { kind: 'rpd', re: /requests?\s*per\s*day|\brpd\b/i },
1138
+ { kind: 'rpm', re: /requests?\s*per\s*min(?:ute)?|\brpm\b/i },
1139
+ ];
1140
+ /**
1141
+ * Pure parser: pull a provider-reported ceiling out of an error message. Returns
1142
+ * null unless BOTH a numeric "Limit N" and a confident axis (TPM/TPD/RPM/RPD)
1143
+ * are present — guessing the axis would write the wrong column and mis-route
1144
+ * every future request, so we refuse to guess.
1145
+ */
1146
+ export function parseProviderLimit(message) {
1147
+ if (!message)
1148
+ return null;
1149
+ const m = message.match(/\blimit[:\s]+([\d,]+)/i);
1150
+ if (!m)
1151
+ return null;
1152
+ const limit = Number(m[1].replace(/,/g, ''));
1153
+ if (!Number.isFinite(limit) || limit <= 0)
1154
+ return null;
1155
+ for (const { kind, re } of LIMIT_AXIS_PATTERNS) {
1156
+ if (re.test(message))
1157
+ return { kind, limit };
1158
+ }
1159
+ return null;
1160
+ }
1161
+ // Whitelisted column names — the only values ever interpolated into the UPDATE
1162
+ // below, so there is no injection surface despite the template literal.
1163
+ const LIMIT_COLUMN = {
1164
+ tpm: 'tpm_limit', tpd: 'tpd_limit', rpm: 'rpm_limit', rpd: 'rpd_limit',
1165
+ };
1166
+ /**
1167
+ * Persist a provider-reported limit onto the model row, but ONLY when it makes
1168
+ * us more conservative: fill a NULL (unknown) limit, or LOWER an existing one
1169
+ * that was too high. Never raises a limit — hitting a ceiling means our pre-check
1170
+ * already let too much through, so the true limit is at or below what we used.
1171
+ * Returns the learned limit when a row was actually changed, else null.
1172
+ * DB-guarded (no-op when the DB is unavailable), like the rest of this module.
1173
+ */
1174
+ export function learnLimitFromError(modelDbId, err) {
1175
+ const parsed = parseProviderLimit(err?.message);
1176
+ if (!parsed)
1177
+ return null;
1178
+ const col = LIMIT_COLUMN[parsed.kind];
1179
+ const result = withDb(db => db.prepare(`UPDATE models SET ${col} = ? WHERE id = ? AND (${col} IS NULL OR ${col} > ?)`).run(parsed.limit, modelDbId, parsed.limit));
1180
+ return result && result.changes > 0 ? parsed : null;
1181
+ }
1182
+ //# sourceMappingURL=ratelimit.js.map