@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.
- package/LICENSE +21 -0
- package/client/dist/assets/am-TQ7Jmdqe.js +1 -0
- package/client/dist/assets/ar-Bf5a7yfC.js +1 -0
- package/client/dist/assets/az-CrosE1xH.js +1 -0
- package/client/dist/assets/bg-BvWnmcUz.js +1 -0
- package/client/dist/assets/bn-ams5TsJp.js +1 -0
- package/client/dist/assets/cs-Dh67CaJ_.js +1 -0
- package/client/dist/assets/da-9WlT8Iq7.js +1 -0
- package/client/dist/assets/de-lWhJJzz7.js +1 -0
- package/client/dist/assets/el-Cu7_GhNq.js +1 -0
- package/client/dist/assets/es-BZuzsBcP.js +1 -0
- package/client/dist/assets/fa-Dwdn-jKS.js +1 -0
- package/client/dist/assets/fi-BFrFyOTy.js +1 -0
- package/client/dist/assets/fr-XhbtmPpj.js +1 -0
- package/client/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/client/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/client/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/client/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/client/dist/assets/geist-mono-cyrillic-ext-wght-normal-I4S5GZfc.woff2 +0 -0
- package/client/dist/assets/geist-mono-cyrillic-wght-normal-BmXc_FBt.woff2 +0 -0
- package/client/dist/assets/geist-mono-latin-ext-wght-normal-DrnZ1wKl.woff2 +0 -0
- package/client/dist/assets/geist-mono-latin-wght-normal-B_7UjwxQ.woff2 +0 -0
- package/client/dist/assets/geist-mono-symbols2-wght-normal-GZpp1pK2.woff2 +0 -0
- package/client/dist/assets/geist-mono-vietnamese-wght-normal-D8KDMBhC.woff2 +0 -0
- package/client/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/client/dist/assets/gu-D0uhLUB8.js +1 -0
- package/client/dist/assets/ha-CVlm9pqp.js +1 -0
- package/client/dist/assets/he-CMIFeoWr.js +1 -0
- package/client/dist/assets/hi-ENTwhBpn.js +1 -0
- package/client/dist/assets/hr-fc03vMy_.js +1 -0
- package/client/dist/assets/hu-YBo2BIYt.js +1 -0
- package/client/dist/assets/id-BEWXYX69.js +1 -0
- package/client/dist/assets/ig-BKLZFKka.js +1 -0
- package/client/dist/assets/index-D9I4KtvR.js +145 -0
- package/client/dist/assets/index-txhEmCdb.css +2 -0
- package/client/dist/assets/it-Bzmei1Y2.js +1 -0
- package/client/dist/assets/ja-BfcpuqEl.js +1 -0
- package/client/dist/assets/ka-CnWjRgLo.js +1 -0
- package/client/dist/assets/km-BAjPR3Qj.js +1 -0
- package/client/dist/assets/kn-B15WKU5m.js +1 -0
- package/client/dist/assets/ko-C61fAB-w.js +1 -0
- package/client/dist/assets/lt-CYTr4_YW.js +1 -0
- package/client/dist/assets/ml-CtqxgyH3.js +1 -0
- package/client/dist/assets/mr-X5TMY7aR.js +1 -0
- package/client/dist/assets/ms-DSg8RqPS.js +1 -0
- package/client/dist/assets/my-D7RqR8uE.js +1 -0
- package/client/dist/assets/ne-DaflxkDj.js +1 -0
- package/client/dist/assets/nl-BOqqtsC8.js +1 -0
- package/client/dist/assets/no-BSHp_StB.js +1 -0
- package/client/dist/assets/or-BkLR9Vub.js +1 -0
- package/client/dist/assets/pa-CarJ2XgI.js +1 -0
- package/client/dist/assets/pl-C2n0V-0k.js +1 -0
- package/client/dist/assets/pt-BR-CqJdNhg2.js +1 -0
- package/client/dist/assets/pt-PT-C_HpMyKz.js +1 -0
- package/client/dist/assets/ro-DVRZqSAS.js +1 -0
- package/client/dist/assets/ru-DwAJG7VC.js +1 -0
- package/client/dist/assets/si-BxUezXm0.js +1 -0
- package/client/dist/assets/sk-eI1VcIfY.js +1 -0
- package/client/dist/assets/sr-CJF3auCr.js +1 -0
- package/client/dist/assets/srouter-logo-C6ZfjGIi.svg +14 -0
- package/client/dist/assets/sv-nqvwHAq_.js +1 -0
- package/client/dist/assets/sw-DWynE2pW.js +1 -0
- package/client/dist/assets/ta-BVM12knC.js +1 -0
- package/client/dist/assets/te-oyzEkF7_.js +1 -0
- package/client/dist/assets/th-COVeE0ZQ.js +1 -0
- package/client/dist/assets/tl-DsJANnQz.js +1 -0
- package/client/dist/assets/tr-B-Fl-urO.js +1 -0
- package/client/dist/assets/uk-C5qketAO.js +1 -0
- package/client/dist/assets/ur-QU9KugfV.js +1 -0
- package/client/dist/assets/uz-DL9cG_mY.js +1 -0
- package/client/dist/assets/vi-CQiVy3Pu.js +1 -0
- package/client/dist/assets/yo-DqazMdrZ.js +1 -0
- package/client/dist/assets/zh-CN-DnYL_Kio.js +1 -0
- package/client/dist/assets/zh-TW-5bdbosKj.js +1 -0
- package/client/dist/favicon.svg +14 -0
- package/client/dist/icons.svg +24 -0
- package/client/dist/index.html +37 -0
- package/dist/app.d.ts +4 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +372 -0
- package/dist/app.js.map +1 -0
- package/dist/db/index.d.ts +49 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +203 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/migrate/TEMPLATE.d.ts +4 -0
- package/dist/db/migrate/TEMPLATE.d.ts.map +1 -0
- package/dist/db/migrate/TEMPLATE.js +18 -0
- package/dist/db/migrate/TEMPLATE.js.map +1 -0
- package/dist/db/migrate/cli.d.ts +2 -0
- package/dist/db/migrate/cli.d.ts.map +1 -0
- package/dist/db/migrate/cli.js +177 -0
- package/dist/db/migrate/cli.js.map +1 -0
- package/dist/db/migrate/defaults.d.ts +52 -0
- package/dist/db/migrate/defaults.d.ts.map +1 -0
- package/dist/db/migrate/defaults.js +126 -0
- package/dist/db/migrate/defaults.js.map +1 -0
- package/dist/db/migrate/runner.d.ts +16 -0
- package/dist/db/migrate/runner.d.ts.map +1 -0
- package/dist/db/migrate/runner.js +178 -0
- package/dist/db/migrate/runner.js.map +1 -0
- package/dist/db/migrations/20260101_000000_legacy_baseline.d.ts +4 -0
- package/dist/db/migrations/20260101_000000_legacy_baseline.d.ts.map +1 -0
- package/dist/db/migrations/20260101_000000_legacy_baseline.js +2240 -0
- package/dist/db/migrations/20260101_000000_legacy_baseline.js.map +1 -0
- package/dist/db/migrations/20260627_000001_custom_provider_modalities.d.ts +4 -0
- package/dist/db/migrations/20260627_000001_custom_provider_modalities.d.ts.map +1 -0
- package/dist/db/migrations/20260627_000001_custom_provider_modalities.js +27 -0
- package/dist/db/migrations/20260627_000001_custom_provider_modalities.js.map +1 -0
- package/dist/db/migrations/20260627_000002_catalog_model_state.d.ts +4 -0
- package/dist/db/migrations/20260627_000002_catalog_model_state.d.ts.map +1 -0
- package/dist/db/migrations/20260627_000002_catalog_model_state.js +30 -0
- package/dist/db/migrations/20260627_000002_catalog_model_state.js.map +1 -0
- package/dist/db/migrations/20260628_120000_request_aggregates.d.ts +6 -0
- package/dist/db/migrations/20260628_120000_request_aggregates.d.ts.map +1 -0
- package/dist/db/migrations/20260628_120000_request_aggregates.js +104 -0
- package/dist/db/migrations/20260628_120000_request_aggregates.js.map +1 -0
- package/dist/db/migrations/20260630_000001_github_gpt41_context.d.ts +9 -0
- package/dist/db/migrations/20260630_000001_github_gpt41_context.d.ts.map +1 -0
- package/dist/db/migrations/20260630_000001_github_gpt41_context.js +22 -0
- package/dist/db/migrations/20260630_000001_github_gpt41_context.js.map +1 -0
- package/dist/db/migrations/20260706_000001_request_client_info.d.ts +4 -0
- package/dist/db/migrations/20260706_000001_request_client_info.d.ts.map +1 -0
- package/dist/db/migrations/20260706_000001_request_client_info.js +30 -0
- package/dist/db/migrations/20260706_000001_request_client_info.js.map +1 -0
- package/dist/db/migrations/20260706_000002_custom_model_tool_support.d.ts +17 -0
- package/dist/db/migrations/20260706_000002_custom_model_tool_support.d.ts.map +1 -0
- package/dist/db/migrations/20260706_000002_custom_model_tool_support.js +20 -0
- package/dist/db/migrations/20260706_000002_custom_model_tool_support.js.map +1 -0
- package/dist/db/migrations/20260714_000001_profile_chain_backfill.d.ts +12 -0
- package/dist/db/migrations/20260714_000001_profile_chain_backfill.d.ts.map +1 -0
- package/dist/db/migrations/20260714_000001_profile_chain_backfill.js +45 -0
- package/dist/db/migrations/20260714_000001_profile_chain_backfill.js.map +1 -0
- package/dist/db/migrations/20260720_000001_key_health_error.d.ts +5 -0
- package/dist/db/migrations/20260720_000001_key_health_error.d.ts.map +1 -0
- package/dist/db/migrations/20260720_000001_key_health_error.js +16 -0
- package/dist/db/migrations/20260720_000001_key_health_error.js.map +1 -0
- package/dist/db/migrations/20260726_000001_cooldown_probe_provenance.d.ts +4 -0
- package/dist/db/migrations/20260726_000001_cooldown_probe_provenance.d.ts.map +1 -0
- package/dist/db/migrations/20260726_000001_cooldown_probe_provenance.js +38 -0
- package/dist/db/migrations/20260726_000001_cooldown_probe_provenance.js.map +1 -0
- package/dist/db/migrations/20260726_000002_request_attempts.d.ts +4 -0
- package/dist/db/migrations/20260726_000002_request_attempts.d.ts.map +1 -0
- package/dist/db/migrations/20260726_000002_request_attempts.js +43 -0
- package/dist/db/migrations/20260726_000002_request_attempts.js.map +1 -0
- package/dist/db/migrations/20260726_000003_model_source_provenance.d.ts +4 -0
- package/dist/db/migrations/20260726_000003_model_source_provenance.d.ts.map +1 -0
- package/dist/db/migrations/20260726_000003_model_source_provenance.js +80 -0
- package/dist/db/migrations/20260726_000003_model_source_provenance.js.map +1 -0
- package/dist/db/migrations/20260726_000004_media_model_meta.d.ts +4 -0
- package/dist/db/migrations/20260726_000004_media_model_meta.d.ts.map +1 -0
- package/dist/db/migrations/20260726_000004_media_model_meta.js +34 -0
- package/dist/db/migrations/20260726_000004_media_model_meta.js.map +1 -0
- package/dist/db/migrations/20260726_000005_request_served_model.d.ts +4 -0
- package/dist/db/migrations/20260726_000005_request_served_model.d.ts.map +1 -0
- package/dist/db/migrations/20260726_000005_request_served_model.js +31 -0
- package/dist/db/migrations/20260726_000005_request_served_model.js.map +1 -0
- package/dist/db/migrations/20260726_000006_attempt_error_summary.d.ts +4 -0
- package/dist/db/migrations/20260726_000006_attempt_error_summary.d.ts.map +1 -0
- package/dist/db/migrations/20260726_000006_attempt_error_summary.js +29 -0
- package/dist/db/migrations/20260726_000006_attempt_error_summary.js.map +1 -0
- package/dist/db/migrations/20260727_000001_agent_compatibility.d.ts +4 -0
- package/dist/db/migrations/20260727_000001_agent_compatibility.d.ts.map +1 -0
- package/dist/db/migrations/20260727_000001_agent_compatibility.js +41 -0
- package/dist/db/migrations/20260727_000001_agent_compatibility.js.map +1 -0
- package/dist/db/migrations/20260728_000001_tombstone_provenance.d.ts +12 -0
- package/dist/db/migrations/20260728_000001_tombstone_provenance.d.ts.map +1 -0
- package/dist/db/migrations/20260728_000001_tombstone_provenance.js +37 -0
- package/dist/db/migrations/20260728_000001_tombstone_provenance.js.map +1 -0
- package/dist/db/migrations/20260729_000001_custom_model_endpoint_identity.d.ts +4 -0
- package/dist/db/migrations/20260729_000001_custom_model_endpoint_identity.d.ts.map +1 -0
- package/dist/db/migrations/20260729_000001_custom_model_endpoint_identity.js +145 -0
- package/dist/db/migrations/20260729_000001_custom_model_endpoint_identity.js.map +1 -0
- package/dist/db/migrations/20260802_000001_custom_endpoint_host_labels.d.ts +6 -0
- package/dist/db/migrations/20260802_000001_custom_endpoint_host_labels.d.ts.map +1 -0
- package/dist/db/migrations/20260802_000001_custom_endpoint_host_labels.js +52 -0
- package/dist/db/migrations/20260802_000001_custom_endpoint_host_labels.js.map +1 -0
- package/dist/db/migrations/20260805_000001_key_model_scope.d.ts +6 -0
- package/dist/db/migrations/20260805_000001_key_model_scope.d.ts.map +1 -0
- package/dist/db/migrations/20260805_000001_key_model_scope.js +17 -0
- package/dist/db/migrations/20260805_000001_key_model_scope.js.map +1 -0
- package/dist/db/migrations/20260805_000002_client_profiles.d.ts +4 -0
- package/dist/db/migrations/20260805_000002_client_profiles.d.ts.map +1 -0
- package/dist/db/migrations/20260805_000002_client_profiles.js +33 -0
- package/dist/db/migrations/20260805_000002_client_profiles.js.map +1 -0
- package/dist/db/migrations/20260810_000001_api_key_proxy.d.ts +4 -0
- package/dist/db/migrations/20260810_000001_api_key_proxy.d.ts.map +1 -0
- package/dist/db/migrations/20260810_000001_api_key_proxy.js +33 -0
- package/dist/db/migrations/20260810_000001_api_key_proxy.js.map +1 -0
- package/dist/db/migrations/20260819_000001_custom_model_tombstones.d.ts +4 -0
- package/dist/db/migrations/20260819_000001_custom_model_tombstones.d.ts.map +1 -0
- package/dist/db/migrations/20260819_000001_custom_model_tombstones.js +19 -0
- package/dist/db/migrations/20260819_000001_custom_model_tombstones.js.map +1 -0
- package/dist/db/migrations/20260820_000001_playground_conversations.d.ts +4 -0
- package/dist/db/migrations/20260820_000001_playground_conversations.d.ts.map +1 -0
- package/dist/db/migrations/20260820_000001_playground_conversations.js +47 -0
- package/dist/db/migrations/20260820_000001_playground_conversations.js.map +1 -0
- package/dist/db/migrations/20260823_000001_server_logs.d.ts +4 -0
- package/dist/db/migrations/20260823_000001_server_logs.d.ts.map +1 -0
- package/dist/db/migrations/20260823_000001_server_logs.js +53 -0
- package/dist/db/migrations/20260823_000001_server_logs.js.map +1 -0
- package/dist/db/migrations/20260823_000002_backups_table.d.ts +8 -0
- package/dist/db/migrations/20260823_000002_backups_table.d.ts.map +1 -0
- package/dist/db/migrations/20260823_000002_backups_table.js +22 -0
- package/dist/db/migrations/20260823_000002_backups_table.js.map +1 -0
- package/dist/db/migrations/20260823_000003_attempt_key_label.d.ts +19 -0
- package/dist/db/migrations/20260823_000003_attempt_key_label.d.ts.map +1 -0
- package/dist/db/migrations/20260823_000003_attempt_key_label.js +28 -0
- package/dist/db/migrations/20260823_000003_attempt_key_label.js.map +1 -0
- package/dist/db/migrations/20260823_000004_profile_auto_include.d.ts +14 -0
- package/dist/db/migrations/20260823_000004_profile_auto_include.d.ts.map +1 -0
- package/dist/db/migrations/20260823_000004_profile_auto_include.js +25 -0
- package/dist/db/migrations/20260823_000004_profile_auto_include.js.map +1 -0
- package/dist/db/migrations/20260901_000001_idempotency_claims.d.ts +4 -0
- package/dist/db/migrations/20260901_000001_idempotency_claims.d.ts.map +1 -0
- package/dist/db/migrations/20260901_000001_idempotency_claims.js +46 -0
- package/dist/db/migrations/20260901_000001_idempotency_claims.js.map +1 -0
- package/dist/db/migrations/20260901_000002_quota_observation_lookup.d.ts +4 -0
- package/dist/db/migrations/20260901_000002_quota_observation_lookup.d.ts.map +1 -0
- package/dist/db/migrations/20260901_000002_quota_observation_lookup.js +37 -0
- package/dist/db/migrations/20260901_000002_quota_observation_lookup.js.map +1 -0
- package/dist/db/migrations/20260901_000003_request_caller.d.ts +4 -0
- package/dist/db/migrations/20260901_000003_request_caller.d.ts.map +1 -0
- package/dist/db/migrations/20260901_000003_request_caller.js +27 -0
- package/dist/db/migrations/20260901_000003_request_caller.js.map +1 -0
- package/dist/db/migrations/20260902_000001_analytics_latency_percentile_index.d.ts +4 -0
- package/dist/db/migrations/20260902_000001_analytics_latency_percentile_index.d.ts.map +1 -0
- package/dist/db/migrations/20260902_000001_analytics_latency_percentile_index.js +35 -0
- package/dist/db/migrations/20260902_000001_analytics_latency_percentile_index.js.map +1 -0
- package/dist/db/migrations/20260903_000001_mcp_enabled_default.d.ts +4 -0
- package/dist/db/migrations/20260903_000001_mcp_enabled_default.d.ts.map +1 -0
- package/dist/db/migrations/20260903_000001_mcp_enabled_default.js +37 -0
- package/dist/db/migrations/20260903_000001_mcp_enabled_default.js.map +1 -0
- package/dist/db/migrations/20260903_000002_response_cache.d.ts +4 -0
- package/dist/db/migrations/20260903_000002_response_cache.d.ts.map +1 -0
- package/dist/db/migrations/20260903_000002_response_cache.js +53 -0
- package/dist/db/migrations/20260903_000002_response_cache.js.map +1 -0
- package/dist/db/migrations/20260904_000001_key_monthly_budget.d.ts +13 -0
- package/dist/db/migrations/20260904_000001_key_monthly_budget.d.ts.map +1 -0
- package/dist/db/migrations/20260904_000001_key_monthly_budget.js +35 -0
- package/dist/db/migrations/20260904_000001_key_monthly_budget.js.map +1 -0
- package/dist/db/migrations/20260913_000001_request_model_attribution.d.ts +4 -0
- package/dist/db/migrations/20260913_000001_request_model_attribution.d.ts.map +1 -0
- package/dist/db/migrations/20260913_000001_request_model_attribution.js +68 -0
- package/dist/db/migrations/20260913_000001_request_model_attribution.js.map +1 -0
- package/dist/db/migrations/20260914_000001_key_monthly_usage.d.ts +5 -0
- package/dist/db/migrations/20260914_000001_key_monthly_usage.d.ts.map +1 -0
- package/dist/db/migrations/20260914_000001_key_monthly_usage.js +37 -0
- package/dist/db/migrations/20260914_000001_key_monthly_usage.js.map +1 -0
- package/dist/db/migrations/20260915_000001_quota_snapshot_freshness.d.ts +4 -0
- package/dist/db/migrations/20260915_000001_quota_snapshot_freshness.d.ts.map +1 -0
- package/dist/db/migrations/20260915_000001_quota_snapshot_freshness.js +28 -0
- package/dist/db/migrations/20260915_000001_quota_snapshot_freshness.js.map +1 -0
- package/dist/db/migrations/20261004_000001_unified_key_sr_prefix.d.ts +4 -0
- package/dist/db/migrations/20261004_000001_unified_key_sr_prefix.d.ts.map +1 -0
- package/dist/db/migrations/20261004_000001_unified_key_sr_prefix.js +25 -0
- package/dist/db/migrations/20261004_000001_unified_key_sr_prefix.js.map +1 -0
- package/dist/db/migrations/20261005_000001_key_budget_period.d.ts +10 -0
- package/dist/db/migrations/20261005_000001_key_budget_period.d.ts.map +1 -0
- package/dist/db/migrations/20261005_000001_key_budget_period.js +78 -0
- package/dist/db/migrations/20261005_000001_key_budget_period.js.map +1 -0
- package/dist/db/migrations/20261005_000002_key_budget_period_auto.d.ts +21 -0
- package/dist/db/migrations/20261005_000002_key_budget_period_auto.d.ts.map +1 -0
- package/dist/db/migrations/20261005_000002_key_budget_period_auto.js +41 -0
- package/dist/db/migrations/20261005_000002_key_budget_period_auto.js.map +1 -0
- package/dist/db/model-pricing.d.ts +39 -0
- package/dist/db/model-pricing.d.ts.map +1 -0
- package/dist/db/model-pricing.js +274 -0
- package/dist/db/model-pricing.js.map +1 -0
- package/dist/db/node-sqlite.d.ts +9 -0
- package/dist/db/node-sqlite.d.ts.map +1 -0
- package/dist/db/node-sqlite.js +87 -0
- package/dist/db/node-sqlite.js.map +1 -0
- package/dist/db/types.d.ts +22 -0
- package/dist/db/types.d.ts.map +1 -0
- package/dist/db/types.js +2 -0
- package/dist/db/types.js.map +1 -0
- package/dist/docs/docs-page.d.ts +2 -0
- package/dist/docs/docs-page.d.ts.map +1 -0
- package/dist/docs/docs-page.js +319 -0
- package/dist/docs/docs-page.js.map +1 -0
- package/dist/docs/openapi.d.ts +1896 -0
- package/dist/docs/openapi.d.ts.map +1 -0
- package/dist/docs/openapi.js +1096 -0
- package/dist/docs/openapi.js.map +1 -0
- package/dist/env.d.ts +2 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +9 -0
- package/dist/env.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +144 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/anthropic-documents.d.ts +41 -0
- package/dist/lib/anthropic-documents.d.ts.map +1 -0
- package/dist/lib/anthropic-documents.js +132 -0
- package/dist/lib/anthropic-documents.js.map +1 -0
- package/dist/lib/app-version.d.ts +5 -0
- package/dist/lib/app-version.d.ts.map +1 -0
- package/dist/lib/app-version.js +61 -0
- package/dist/lib/app-version.js.map +1 -0
- package/dist/lib/attempt-trace.d.ts +23 -0
- package/dist/lib/attempt-trace.d.ts.map +1 -0
- package/dist/lib/attempt-trace.js +32 -0
- package/dist/lib/attempt-trace.js.map +1 -0
- package/dist/lib/budget.d.ts +6 -0
- package/dist/lib/budget.d.ts.map +1 -0
- package/dist/lib/budget.js +40 -0
- package/dist/lib/budget.js.map +1 -0
- package/dist/lib/client-classifier.d.ts +10 -0
- package/dist/lib/client-classifier.d.ts.map +1 -0
- package/dist/lib/client-classifier.js +126 -0
- package/dist/lib/client-classifier.js.map +1 -0
- package/dist/lib/client-context.d.ts +10 -0
- package/dist/lib/client-context.d.ts.map +1 -0
- package/dist/lib/client-context.js +39 -0
- package/dist/lib/client-context.js.map +1 -0
- package/dist/lib/config.d.ts +33 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +94 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/content.d.ts +33 -0
- package/dist/lib/content.d.ts.map +1 -0
- package/dist/lib/content.js +201 -0
- package/dist/lib/content.js.map +1 -0
- package/dist/lib/credential.d.ts +11 -0
- package/dist/lib/credential.d.ts.map +1 -0
- package/dist/lib/credential.js +18 -0
- package/dist/lib/credential.js.map +1 -0
- package/dist/lib/crypto.d.ts +31 -0
- package/dist/lib/crypto.d.ts.map +1 -0
- package/dist/lib/crypto.js +206 -0
- package/dist/lib/crypto.js.map +1 -0
- package/dist/lib/custom-provider-cleanup.d.ts +13 -0
- package/dist/lib/custom-provider-cleanup.d.ts.map +1 -0
- package/dist/lib/custom-provider-cleanup.js +43 -0
- package/dist/lib/custom-provider-cleanup.js.map +1 -0
- package/dist/lib/db-backup.d.ts +31 -0
- package/dist/lib/db-backup.d.ts.map +1 -0
- package/dist/lib/db-backup.js +255 -0
- package/dist/lib/db-backup.js.map +1 -0
- package/dist/lib/endpoint-scope.d.ts +42 -0
- package/dist/lib/endpoint-scope.d.ts.map +1 -0
- package/dist/lib/endpoint-scope.js +95 -0
- package/dist/lib/endpoint-scope.js.map +1 -0
- package/dist/lib/env-drift.d.ts +17 -0
- package/dist/lib/env-drift.d.ts.map +1 -0
- package/dist/lib/env-drift.js +108 -0
- package/dist/lib/env-drift.js.map +1 -0
- package/dist/lib/error-classify.d.ts +40 -0
- package/dist/lib/error-classify.d.ts.map +1 -0
- package/dist/lib/error-classify.js +687 -0
- package/dist/lib/error-classify.js.map +1 -0
- package/dist/lib/error-redaction.d.ts +8 -0
- package/dist/lib/error-redaction.d.ts.map +1 -0
- package/dist/lib/error-redaction.js +45 -0
- package/dist/lib/error-redaction.js.map +1 -0
- package/dist/lib/fallback-loop.d.ts +306 -0
- package/dist/lib/fallback-loop.d.ts.map +1 -0
- package/dist/lib/fallback-loop.js +1336 -0
- package/dist/lib/fallback-loop.js.map +1 -0
- package/dist/lib/file-permissions.d.ts +98 -0
- package/dist/lib/file-permissions.d.ts.map +1 -0
- package/dist/lib/file-permissions.js +159 -0
- package/dist/lib/file-permissions.js.map +1 -0
- package/dist/lib/gemini-wire.d.ts +76 -0
- package/dist/lib/gemini-wire.d.ts.map +1 -0
- package/dist/lib/gemini-wire.js +392 -0
- package/dist/lib/gemini-wire.js.map +1 -0
- package/dist/lib/guardrails.d.ts +37 -0
- package/dist/lib/guardrails.d.ts.map +1 -0
- package/dist/lib/guardrails.js +106 -0
- package/dist/lib/guardrails.js.map +1 -0
- package/dist/lib/header-value.d.ts +8 -0
- package/dist/lib/header-value.d.ts.map +1 -0
- package/dist/lib/header-value.js +55 -0
- package/dist/lib/header-value.js.map +1 -0
- package/dist/lib/image-normalize.d.ts +21 -0
- package/dist/lib/image-normalize.d.ts.map +1 -0
- package/dist/lib/image-normalize.js +218 -0
- package/dist/lib/image-normalize.js.map +1 -0
- package/dist/lib/inbound-chat.d.ts +48 -0
- package/dist/lib/inbound-chat.d.ts.map +1 -0
- package/dist/lib/inbound-chat.js +406 -0
- package/dist/lib/inbound-chat.js.map +1 -0
- package/dist/lib/key-parser.d.ts +79 -0
- package/dist/lib/key-parser.d.ts.map +1 -0
- package/dist/lib/key-parser.js +701 -0
- package/dist/lib/key-parser.js.map +1 -0
- package/dist/lib/key-proxy.d.ts +49 -0
- package/dist/lib/key-proxy.d.ts.map +1 -0
- package/dist/lib/key-proxy.js +92 -0
- package/dist/lib/key-proxy.js.map +1 -0
- package/dist/lib/log-redaction.d.ts +28 -0
- package/dist/lib/log-redaction.d.ts.map +1 -0
- package/dist/lib/log-redaction.js +166 -0
- package/dist/lib/log-redaction.js.map +1 -0
- package/dist/lib/model-scope.d.ts +9 -0
- package/dist/lib/model-scope.d.ts.map +1 -0
- package/dist/lib/model-scope.js +29 -0
- package/dist/lib/model-scope.js.map +1 -0
- package/dist/lib/one-time-code.d.ts +15 -0
- package/dist/lib/one-time-code.d.ts.map +1 -0
- package/dist/lib/one-time-code.js +41 -0
- package/dist/lib/one-time-code.js.map +1 -0
- package/dist/lib/output-cap.d.ts +24 -0
- package/dist/lib/output-cap.d.ts.map +1 -0
- package/dist/lib/output-cap.js +80 -0
- package/dist/lib/output-cap.js.map +1 -0
- package/dist/lib/password.d.ts +3 -0
- package/dist/lib/password.d.ts.map +1 -0
- package/dist/lib/password.js +27 -0
- package/dist/lib/password.js.map +1 -0
- package/dist/lib/process-safety-net.d.ts +32 -0
- package/dist/lib/process-safety-net.d.ts.map +1 -0
- package/dist/lib/process-safety-net.js +123 -0
- package/dist/lib/process-safety-net.js.map +1 -0
- package/dist/lib/provider-identity.d.ts +37 -0
- package/dist/lib/provider-identity.d.ts.map +1 -0
- package/dist/lib/provider-identity.js +110 -0
- package/dist/lib/provider-identity.js.map +1 -0
- package/dist/lib/provider-size-parser.d.ts +6 -0
- package/dist/lib/provider-size-parser.d.ts.map +1 -0
- package/dist/lib/provider-size-parser.js +72 -0
- package/dist/lib/provider-size-parser.js.map +1 -0
- package/dist/lib/provider-timeout.d.ts +17 -0
- package/dist/lib/provider-timeout.d.ts.map +1 -0
- package/dist/lib/provider-timeout.js +70 -0
- package/dist/lib/provider-timeout.js.map +1 -0
- package/dist/lib/proxy.d.ts +180 -0
- package/dist/lib/proxy.d.ts.map +1 -0
- package/dist/lib/proxy.js +1002 -0
- package/dist/lib/proxy.js.map +1 -0
- package/dist/lib/request-log.d.ts +4 -0
- package/dist/lib/request-log.d.ts.map +1 -0
- package/dist/lib/request-log.js +161 -0
- package/dist/lib/request-log.js.map +1 -0
- package/dist/lib/reset-code.d.ts +5 -0
- package/dist/lib/reset-code.d.ts.map +1 -0
- package/dist/lib/reset-code.js +39 -0
- package/dist/lib/reset-code.js.map +1 -0
- package/dist/lib/retry-hint.d.ts +14 -0
- package/dist/lib/retry-hint.d.ts.map +1 -0
- package/dist/lib/retry-hint.js +36 -0
- package/dist/lib/retry-hint.js.map +1 -0
- package/dist/lib/route-live.d.ts +29 -0
- package/dist/lib/route-live.d.ts.map +1 -0
- package/dist/lib/route-live.js +97 -0
- package/dist/lib/route-live.js.map +1 -0
- package/dist/lib/sampling-params.d.ts +183 -0
- package/dist/lib/sampling-params.d.ts.map +1 -0
- package/dist/lib/sampling-params.js +386 -0
- package/dist/lib/sampling-params.js.map +1 -0
- package/dist/lib/scheduler.d.ts +13 -0
- package/dist/lib/scheduler.d.ts.map +1 -0
- package/dist/lib/scheduler.js +11 -0
- package/dist/lib/scheduler.js.map +1 -0
- package/dist/lib/served-model.d.ts +16 -0
- package/dist/lib/served-model.d.ts.map +1 -0
- package/dist/lib/served-model.js +0 -0
- package/dist/lib/served-model.js.map +1 -0
- package/dist/lib/server-logs.d.ts +132 -0
- package/dist/lib/server-logs.d.ts.map +1 -0
- package/dist/lib/server-logs.js +473 -0
- package/dist/lib/server-logs.js.map +1 -0
- package/dist/lib/setup-code.d.ts +5 -0
- package/dist/lib/setup-code.d.ts.map +1 -0
- package/dist/lib/setup-code.js +35 -0
- package/dist/lib/setup-code.js.map +1 -0
- package/dist/lib/structured-output.d.ts +9 -0
- package/dist/lib/structured-output.d.ts.map +1 -0
- package/dist/lib/structured-output.js +72 -0
- package/dist/lib/structured-output.js.map +1 -0
- package/dist/lib/system-prompt.d.ts +30 -0
- package/dist/lib/system-prompt.d.ts.map +1 -0
- package/dist/lib/system-prompt.js +66 -0
- package/dist/lib/system-prompt.js.map +1 -0
- package/dist/lib/task-type.d.ts +18 -0
- package/dist/lib/task-type.d.ts.map +1 -0
- package/dist/lib/task-type.js +92 -0
- package/dist/lib/task-type.js.map +1 -0
- package/dist/lib/think-tags.d.ts +37 -0
- package/dist/lib/think-tags.d.ts.map +1 -0
- package/dist/lib/think-tags.js +203 -0
- package/dist/lib/think-tags.js.map +1 -0
- package/dist/lib/tool-args.d.ts +36 -0
- package/dist/lib/tool-args.d.ts.map +1 -0
- package/dist/lib/tool-args.js +190 -0
- package/dist/lib/tool-args.js.map +1 -0
- package/dist/lib/tool-call-rescue.d.ts +62 -0
- package/dist/lib/tool-call-rescue.d.ts.map +1 -0
- package/dist/lib/tool-call-rescue.js +258 -0
- package/dist/lib/tool-call-rescue.js.map +1 -0
- package/dist/lib/tool-capability.d.ts +13 -0
- package/dist/lib/tool-capability.d.ts.map +1 -0
- package/dist/lib/tool-capability.js +70 -0
- package/dist/lib/tool-capability.js.map +1 -0
- package/dist/lib/tool-validate.d.ts +44 -0
- package/dist/lib/tool-validate.d.ts.map +1 -0
- package/dist/lib/tool-validate.js +165 -0
- package/dist/lib/tool-validate.js.map +1 -0
- package/dist/lib/ttfb-budget.d.ts +13 -0
- package/dist/lib/ttfb-budget.d.ts.map +1 -0
- package/dist/lib/ttfb-budget.js +153 -0
- package/dist/lib/ttfb-budget.js.map +1 -0
- package/dist/lib/url-guard.d.ts +47 -0
- package/dist/lib/url-guard.d.ts.map +1 -0
- package/dist/lib/url-guard.js +229 -0
- package/dist/lib/url-guard.js.map +1 -0
- package/dist/lib/wake-detect.d.ts +12 -0
- package/dist/lib/wake-detect.d.ts.map +1 -0
- package/dist/lib/wake-detect.js +99 -0
- package/dist/lib/wake-detect.js.map +1 -0
- package/dist/middleware/errorHandler.d.ts +3 -0
- package/dist/middleware/errorHandler.d.ts.map +1 -0
- package/dist/middleware/errorHandler.js +45 -0
- package/dist/middleware/errorHandler.js.map +1 -0
- package/dist/middleware/rateLimit.d.ts +4 -0
- package/dist/middleware/rateLimit.d.ts.map +1 -0
- package/dist/middleware/rateLimit.js +125 -0
- package/dist/middleware/rateLimit.js.map +1 -0
- package/dist/middleware/requireAuth.d.ts +3 -0
- package/dist/middleware/requireAuth.d.ts.map +1 -0
- package/dist/middleware/requireAuth.js +17 -0
- package/dist/middleware/requireAuth.js.map +1 -0
- package/dist/providers/aclide.d.ts +18 -0
- package/dist/providers/aclide.d.ts.map +1 -0
- package/dist/providers/aclide.js +174 -0
- package/dist/providers/aclide.js.map +1 -0
- package/dist/providers/aihorde.d.ts +42 -0
- package/dist/providers/aihorde.d.ts.map +1 -0
- package/dist/providers/aihorde.js +192 -0
- package/dist/providers/aihorde.js.map +1 -0
- package/dist/providers/airforce.d.ts +11 -0
- package/dist/providers/airforce.d.ts.map +1 -0
- package/dist/providers/airforce.js +32 -0
- package/dist/providers/airforce.js.map +1 -0
- package/dist/providers/base.d.ts +177 -0
- package/dist/providers/base.d.ts.map +1 -0
- package/dist/providers/base.js +354 -0
- package/dist/providers/base.js.map +1 -0
- package/dist/providers/blaze.d.ts +11 -0
- package/dist/providers/blaze.d.ts.map +1 -0
- package/dist/providers/blaze.js +33 -0
- package/dist/providers/blaze.js.map +1 -0
- package/dist/providers/blockrun.d.ts +14 -0
- package/dist/providers/blockrun.d.ts.map +1 -0
- package/dist/providers/blockrun.js +36 -0
- package/dist/providers/blockrun.js.map +1 -0
- package/dist/providers/clod.d.ts +11 -0
- package/dist/providers/clod.d.ts.map +1 -0
- package/dist/providers/clod.js +39 -0
- package/dist/providers/clod.js.map +1 -0
- package/dist/providers/cloudflare.d.ts +15 -0
- package/dist/providers/cloudflare.d.ts.map +1 -0
- package/dist/providers/cloudflare.js +175 -0
- package/dist/providers/cloudflare.js.map +1 -0
- package/dist/providers/cohere.d.ts +11 -0
- package/dist/providers/cohere.d.ts.map +1 -0
- package/dist/providers/cohere.js +117 -0
- package/dist/providers/cohere.js.map +1 -0
- package/dist/providers/dreamprompting.d.ts +11 -0
- package/dist/providers/dreamprompting.d.ts.map +1 -0
- package/dist/providers/dreamprompting.js +41 -0
- package/dist/providers/dreamprompting.js.map +1 -0
- package/dist/providers/electronhub.d.ts +10 -0
- package/dist/providers/electronhub.d.ts.map +1 -0
- package/dist/providers/electronhub.js +56 -0
- package/dist/providers/electronhub.js.map +1 -0
- package/dist/providers/experiential.d.ts +10 -0
- package/dist/providers/experiential.d.ts.map +1 -0
- package/dist/providers/experiential.js +33 -0
- package/dist/providers/experiential.js.map +1 -0
- package/dist/providers/gizmo.d.ts +15 -0
- package/dist/providers/gizmo.d.ts.map +1 -0
- package/dist/providers/gizmo.js +54 -0
- package/dist/providers/gizmo.js.map +1 -0
- package/dist/providers/google.d.ts +30 -0
- package/dist/providers/google.d.ts.map +1 -0
- package/dist/providers/google.js +766 -0
- package/dist/providers/google.js.map +1 -0
- package/dist/providers/index.d.ts +13 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +561 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/llmtr.d.ts +15 -0
- package/dist/providers/llmtr.d.ts.map +1 -0
- package/dist/providers/llmtr.js +51 -0
- package/dist/providers/llmtr.js.map +1 -0
- package/dist/providers/logfare.d.ts +11 -0
- package/dist/providers/logfare.d.ts.map +1 -0
- package/dist/providers/logfare.js +33 -0
- package/dist/providers/logfare.js.map +1 -0
- package/dist/providers/lucidity.d.ts +11 -0
- package/dist/providers/lucidity.d.ts.map +1 -0
- package/dist/providers/lucidity.js +37 -0
- package/dist/providers/lucidity.js.map +1 -0
- package/dist/providers/modelscope.d.ts +50 -0
- package/dist/providers/modelscope.d.ts.map +1 -0
- package/dist/providers/modelscope.js +143 -0
- package/dist/providers/modelscope.js.map +1 -0
- package/dist/providers/moondream.d.ts +17 -0
- package/dist/providers/moondream.d.ts.map +1 -0
- package/dist/providers/moondream.js +140 -0
- package/dist/providers/moondream.js.map +1 -0
- package/dist/providers/openai-compat.d.ts +140 -0
- package/dist/providers/openai-compat.d.ts.map +1 -0
- package/dist/providers/openai-compat.js +537 -0
- package/dist/providers/openai-compat.js.map +1 -0
- package/dist/providers/opencode-free.d.ts +24 -0
- package/dist/providers/opencode-free.d.ts.map +1 -0
- package/dist/providers/opencode-free.js +262 -0
- package/dist/providers/opencode-free.js.map +1 -0
- package/dist/providers/pollinations.d.ts +32 -0
- package/dist/providers/pollinations.d.ts.map +1 -0
- package/dist/providers/pollinations.js +65 -0
- package/dist/providers/pollinations.js.map +1 -0
- package/dist/providers/router9.d.ts +16 -0
- package/dist/providers/router9.d.ts.map +1 -0
- package/dist/providers/router9.js +42 -0
- package/dist/providers/router9.js.map +1 -0
- package/dist/providers/sail.d.ts +44 -0
- package/dist/providers/sail.d.ts.map +1 -0
- package/dist/providers/sail.js +302 -0
- package/dist/providers/sail.js.map +1 -0
- package/dist/providers/septor.d.ts +10 -0
- package/dist/providers/septor.d.ts.map +1 -0
- package/dist/providers/septor.js +36 -0
- package/dist/providers/septor.js.map +1 -0
- package/dist/providers/speechify.d.ts +14 -0
- package/dist/providers/speechify.d.ts.map +1 -0
- package/dist/providers/speechify.js +28 -0
- package/dist/providers/speechify.js.map +1 -0
- package/dist/providers/speka.d.ts +12 -0
- package/dist/providers/speka.d.ts.map +1 -0
- package/dist/providers/speka.js +37 -0
- package/dist/providers/speka.js.map +1 -0
- package/dist/providers/waterfall.d.ts +11 -0
- package/dist/providers/waterfall.d.ts.map +1 -0
- package/dist/providers/waterfall.js +33 -0
- package/dist/providers/waterfall.js.map +1 -0
- package/dist/providers/xkiro.d.ts +71 -0
- package/dist/providers/xkiro.d.ts.map +1 -0
- package/dist/providers/xkiro.js +119 -0
- package/dist/providers/xkiro.js.map +1 -0
- package/dist/providers/zhipu.d.ts +43 -0
- package/dist/providers/zhipu.d.ts.map +1 -0
- package/dist/providers/zhipu.js +95 -0
- package/dist/providers/zhipu.js.map +1 -0
- package/dist/routes/analytics.d.ts +2 -0
- package/dist/routes/analytics.d.ts.map +1 -0
- package/dist/routes/analytics.js +738 -0
- package/dist/routes/analytics.js.map +1 -0
- package/dist/routes/anthropic.d.ts +7 -0
- package/dist/routes/anthropic.d.ts.map +1 -0
- package/dist/routes/anthropic.js +1076 -0
- package/dist/routes/anthropic.js.map +1 -0
- package/dist/routes/auth.d.ts +2 -0
- package/dist/routes/auth.d.ts.map +1 -0
- package/dist/routes/auth.js +310 -0
- package/dist/routes/auth.js.map +1 -0
- package/dist/routes/backups.d.ts +2 -0
- package/dist/routes/backups.d.ts.map +1 -0
- package/dist/routes/backups.js +127 -0
- package/dist/routes/backups.js.map +1 -0
- package/dist/routes/cache.d.ts +2 -0
- package/dist/routes/cache.d.ts.map +1 -0
- package/dist/routes/cache.js +40 -0
- package/dist/routes/cache.js.map +1 -0
- package/dist/routes/client-profiles.d.ts +2 -0
- package/dist/routes/client-profiles.d.ts.map +1 -0
- package/dist/routes/client-profiles.js +128 -0
- package/dist/routes/client-profiles.js.map +1 -0
- package/dist/routes/compression.d.ts +2 -0
- package/dist/routes/compression.d.ts.map +1 -0
- package/dist/routes/compression.js +82 -0
- package/dist/routes/compression.js.map +1 -0
- package/dist/routes/conversations.d.ts +3 -0
- package/dist/routes/conversations.d.ts.map +1 -0
- package/dist/routes/conversations.js +215 -0
- package/dist/routes/conversations.js.map +1 -0
- package/dist/routes/docs.d.ts +2 -0
- package/dist/routes/docs.d.ts.map +1 -0
- package/dist/routes/docs.js +21 -0
- package/dist/routes/docs.js.map +1 -0
- package/dist/routes/embeddings.d.ts +2 -0
- package/dist/routes/embeddings.d.ts.map +1 -0
- package/dist/routes/embeddings.js +255 -0
- package/dist/routes/embeddings.js.map +1 -0
- package/dist/routes/fallback.d.ts +2 -0
- package/dist/routes/fallback.d.ts.map +1 -0
- package/dist/routes/fallback.js +631 -0
- package/dist/routes/fallback.js.map +1 -0
- package/dist/routes/free-tier.d.ts +22 -0
- package/dist/routes/free-tier.d.ts.map +1 -0
- package/dist/routes/free-tier.js +175 -0
- package/dist/routes/free-tier.js.map +1 -0
- package/dist/routes/gemini.d.ts +2 -0
- package/dist/routes/gemini.d.ts.map +1 -0
- package/dist/routes/gemini.js +218 -0
- package/dist/routes/gemini.js.map +1 -0
- package/dist/routes/health.d.ts +2 -0
- package/dist/routes/health.d.ts.map +1 -0
- package/dist/routes/health.js +72 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/keys.d.ts +17 -0
- package/dist/routes/keys.d.ts.map +1 -0
- package/dist/routes/keys.js +1787 -0
- package/dist/routes/keys.js.map +1 -0
- package/dist/routes/logs.d.ts +2 -0
- package/dist/routes/logs.d.ts.map +1 -0
- package/dist/routes/logs.js +88 -0
- package/dist/routes/logs.js.map +1 -0
- package/dist/routes/mcp.d.ts +4 -0
- package/dist/routes/mcp.d.ts.map +1 -0
- package/dist/routes/mcp.js +563 -0
- package/dist/routes/mcp.js.map +1 -0
- package/dist/routes/media.d.ts +2 -0
- package/dist/routes/media.d.ts.map +1 -0
- package/dist/routes/media.js +180 -0
- package/dist/routes/media.js.map +1 -0
- package/dist/routes/models.d.ts +2 -0
- package/dist/routes/models.d.ts.map +1 -0
- package/dist/routes/models.js +439 -0
- package/dist/routes/models.js.map +1 -0
- package/dist/routes/ollama.d.ts +7 -0
- package/dist/routes/ollama.d.ts.map +1 -0
- package/dist/routes/ollama.js +595 -0
- package/dist/routes/ollama.js.map +1 -0
- package/dist/routes/profiles.d.ts +3 -0
- package/dist/routes/profiles.d.ts.map +1 -0
- package/dist/routes/profiles.js +431 -0
- package/dist/routes/profiles.js.map +1 -0
- package/dist/routes/proxy.d.ts +32 -0
- package/dist/routes/proxy.d.ts.map +1 -0
- package/dist/routes/proxy.js +2793 -0
- package/dist/routes/proxy.js.map +1 -0
- package/dist/routes/responses.d.ts +1161 -0
- package/dist/routes/responses.d.ts.map +1 -0
- package/dist/routes/responses.js +1521 -0
- package/dist/routes/responses.js.map +1 -0
- package/dist/routes/settings.d.ts +2 -0
- package/dist/routes/settings.d.ts.map +1 -0
- package/dist/routes/settings.js +549 -0
- package/dist/routes/settings.js.map +1 -0
- package/dist/routes/status.d.ts +3 -0
- package/dist/routes/status.d.ts.map +1 -0
- package/dist/routes/status.js +214 -0
- package/dist/routes/status.js.map +1 -0
- package/dist/routes/update.d.ts +31 -0
- package/dist/routes/update.d.ts.map +1 -0
- package/dist/routes/update.js +536 -0
- package/dist/routes/update.js.map +1 -0
- package/dist/routes/url-tokens.d.ts +2 -0
- package/dist/routes/url-tokens.d.ts.map +1 -0
- package/dist/routes/url-tokens.js +31 -0
- package/dist/routes/url-tokens.js.map +1 -0
- package/dist/scripts/export-catalog.d.ts +2 -0
- package/dist/scripts/export-catalog.d.ts.map +1 -0
- package/dist/scripts/export-catalog.js +114 -0
- package/dist/scripts/export-catalog.js.map +1 -0
- package/dist/scripts/rotate-encryption-key.d.ts +81 -0
- package/dist/scripts/rotate-encryption-key.d.ts.map +1 -0
- package/dist/scripts/rotate-encryption-key.js +232 -0
- package/dist/scripts/rotate-encryption-key.js.map +1 -0
- package/dist/scripts/routing-sim.d.ts +2 -0
- package/dist/scripts/routing-sim.d.ts.map +1 -0
- package/dist/scripts/routing-sim.js +130 -0
- package/dist/scripts/routing-sim.js.map +1 -0
- package/dist/scripts/test-all-models.d.ts +2 -0
- package/dist/scripts/test-all-models.d.ts.map +1 -0
- package/dist/scripts/test-all-models.js +56 -0
- package/dist/scripts/test-all-models.js.map +1 -0
- package/dist/services/anthropic-map.d.ts +39 -0
- package/dist/services/anthropic-map.d.ts.map +1 -0
- package/dist/services/anthropic-map.js +138 -0
- package/dist/services/anthropic-map.js.map +1 -0
- package/dist/services/auth.d.ts +30 -0
- package/dist/services/auth.d.ts.map +1 -0
- package/dist/services/auth.js +124 -0
- package/dist/services/auth.js.map +1 -0
- package/dist/services/auto-discover.d.ts +77 -0
- package/dist/services/auto-discover.d.ts.map +1 -0
- package/dist/services/auto-discover.js +1047 -0
- package/dist/services/auto-discover.js.map +1 -0
- package/dist/services/backups.d.ts +67 -0
- package/dist/services/backups.d.ts.map +1 -0
- package/dist/services/backups.js +444 -0
- package/dist/services/backups.js.map +1 -0
- package/dist/services/builtin-model-discovery.d.ts +107 -0
- package/dist/services/builtin-model-discovery.d.ts.map +1 -0
- package/dist/services/builtin-model-discovery.js +296 -0
- package/dist/services/builtin-model-discovery.js.map +1 -0
- package/dist/services/cache.d.ts +190 -0
- package/dist/services/cache.d.ts.map +1 -0
- package/dist/services/cache.js +617 -0
- package/dist/services/cache.js.map +1 -0
- package/dist/services/catalog-sync.d.ts +257 -0
- package/dist/services/catalog-sync.d.ts.map +1 -0
- package/dist/services/catalog-sync.js +1041 -0
- package/dist/services/catalog-sync.js.map +1 -0
- package/dist/services/compression/config.d.ts +33 -0
- package/dist/services/compression/config.d.ts.map +1 -0
- package/dist/services/compression/config.js +158 -0
- package/dist/services/compression/config.js.map +1 -0
- package/dist/services/compression/engines/aging.d.ts +2 -0
- package/dist/services/compression/engines/aging.d.ts.map +1 -0
- package/dist/services/compression/engines/aging.js +53 -0
- package/dist/services/compression/engines/aging.js.map +1 -0
- package/dist/services/compression/engines/custom-filters.d.ts +4 -0
- package/dist/services/compression/engines/custom-filters.d.ts.map +1 -0
- package/dist/services/compression/engines/custom-filters.js +49 -0
- package/dist/services/compression/engines/custom-filters.js.map +1 -0
- package/dist/services/compression/engines/dedup.d.ts +2 -0
- package/dist/services/compression/engines/dedup.d.ts.map +1 -0
- package/dist/services/compression/engines/dedup.js +61 -0
- package/dist/services/compression/engines/dedup.js.map +1 -0
- package/dist/services/compression/engines/filter-definitions.d.ts +75 -0
- package/dist/services/compression/engines/filter-definitions.d.ts.map +1 -0
- package/dist/services/compression/engines/filter-definitions.js +45 -0
- package/dist/services/compression/engines/filter-definitions.js.map +1 -0
- package/dist/services/compression/engines/hard-budget.d.ts +2 -0
- package/dist/services/compression/engines/hard-budget.d.ts.map +1 -0
- package/dist/services/compression/engines/hard-budget.js +52 -0
- package/dist/services/compression/engines/hard-budget.js.map +1 -0
- package/dist/services/compression/engines/index.d.ts +9 -0
- package/dist/services/compression/engines/index.d.ts.map +1 -0
- package/dist/services/compression/engines/index.js +9 -0
- package/dist/services/compression/engines/index.js.map +1 -0
- package/dist/services/compression/engines/jsoncompact.d.ts +3 -0
- package/dist/services/compression/engines/jsoncompact.d.ts.map +1 -0
- package/dist/services/compression/engines/jsoncompact.js +145 -0
- package/dist/services/compression/engines/jsoncompact.js.map +1 -0
- package/dist/services/compression/engines/lite.d.ts +2 -0
- package/dist/services/compression/engines/lite.d.ts.map +1 -0
- package/dist/services/compression/engines/lite.js +34 -0
- package/dist/services/compression/engines/lite.js.map +1 -0
- package/dist/services/compression/engines/read-lifecycle.d.ts +2 -0
- package/dist/services/compression/engines/read-lifecycle.d.ts.map +1 -0
- package/dist/services/compression/engines/read-lifecycle.js +70 -0
- package/dist/services/compression/engines/read-lifecycle.js.map +1 -0
- package/dist/services/compression/engines/relevance.d.ts +2 -0
- package/dist/services/compression/engines/relevance.d.ts.map +1 -0
- package/dist/services/compression/engines/relevance.js +74 -0
- package/dist/services/compression/engines/relevance.js.map +1 -0
- package/dist/services/compression/engines/toolfilter.d.ts +2 -0
- package/dist/services/compression/engines/toolfilter.d.ts.map +1 -0
- package/dist/services/compression/engines/toolfilter.js +195 -0
- package/dist/services/compression/engines/toolfilter.js.map +1 -0
- package/dist/services/compression/fidelity-gate.d.ts +12 -0
- package/dist/services/compression/fidelity-gate.d.ts.map +1 -0
- package/dist/services/compression/fidelity-gate.js +90 -0
- package/dist/services/compression/fidelity-gate.js.map +1 -0
- package/dist/services/compression/helpers.d.ts +9 -0
- package/dist/services/compression/helpers.d.ts.map +1 -0
- package/dist/services/compression/helpers.js +52 -0
- package/dist/services/compression/helpers.js.map +1 -0
- package/dist/services/compression/pipeline.d.ts +7 -0
- package/dist/services/compression/pipeline.d.ts.map +1 -0
- package/dist/services/compression/pipeline.js +245 -0
- package/dist/services/compression/pipeline.js.map +1 -0
- package/dist/services/compression/preservation.d.ts +27 -0
- package/dist/services/compression/preservation.d.ts.map +1 -0
- package/dist/services/compression/preservation.js +109 -0
- package/dist/services/compression/preservation.js.map +1 -0
- package/dist/services/compression/registry.d.ts +6 -0
- package/dist/services/compression/registry.d.ts.map +1 -0
- package/dist/services/compression/registry.js +16 -0
- package/dist/services/compression/registry.js.map +1 -0
- package/dist/services/compression/stats.d.ts +29 -0
- package/dist/services/compression/stats.d.ts.map +1 -0
- package/dist/services/compression/stats.js +55 -0
- package/dist/services/compression/stats.js.map +1 -0
- package/dist/services/compression/types.d.ts +90 -0
- package/dist/services/compression/types.d.ts.map +1 -0
- package/dist/services/compression/types.js +2 -0
- package/dist/services/compression/types.js.map +1 -0
- package/dist/services/context-handoff.d.ts +22 -0
- package/dist/services/context-handoff.d.ts.map +1 -0
- package/dist/services/context-handoff.js +164 -0
- package/dist/services/context-handoff.js.map +1 -0
- package/dist/services/cooldown-probe.d.ts +24 -0
- package/dist/services/cooldown-probe.d.ts.map +1 -0
- package/dist/services/cooldown-probe.js +181 -0
- package/dist/services/cooldown-probe.js.map +1 -0
- package/dist/services/custom-endpoint.d.ts +58 -0
- package/dist/services/custom-endpoint.d.ts.map +1 -0
- package/dist/services/custom-endpoint.js +167 -0
- package/dist/services/custom-endpoint.js.map +1 -0
- package/dist/services/custom-media-register.d.ts +22 -0
- package/dist/services/custom-media-register.d.ts.map +1 -0
- package/dist/services/custom-media-register.js +45 -0
- package/dist/services/custom-media-register.js.map +1 -0
- package/dist/services/custom-model-register.d.ts +50 -0
- package/dist/services/custom-model-register.d.ts.map +1 -0
- package/dist/services/custom-model-register.js +103 -0
- package/dist/services/custom-model-register.js.map +1 -0
- package/dist/services/custom-model-seed.d.ts +18 -0
- package/dist/services/custom-model-seed.d.ts.map +1 -0
- package/dist/services/custom-model-seed.js +42 -0
- package/dist/services/custom-model-seed.js.map +1 -0
- package/dist/services/custom-model-sync.d.ts +37 -0
- package/dist/services/custom-model-sync.d.ts.map +1 -0
- package/dist/services/custom-model-sync.js +118 -0
- package/dist/services/custom-model-sync.js.map +1 -0
- package/dist/services/custom-model-tombstone.d.ts +5 -0
- package/dist/services/custom-model-tombstone.d.ts.map +1 -0
- package/dist/services/custom-model-tombstone.js +30 -0
- package/dist/services/custom-model-tombstone.js.map +1 -0
- package/dist/services/declarative-config.d.ts +356 -0
- package/dist/services/declarative-config.d.ts.map +1 -0
- package/dist/services/declarative-config.js +427 -0
- package/dist/services/declarative-config.js.map +1 -0
- package/dist/services/degradation.d.ts +40 -0
- package/dist/services/degradation.d.ts.map +1 -0
- package/dist/services/degradation.js +119 -0
- package/dist/services/degradation.js.map +1 -0
- package/dist/services/embeddings.d.ts +76 -0
- package/dist/services/embeddings.d.ts.map +1 -0
- package/dist/services/embeddings.js +319 -0
- package/dist/services/embeddings.js.map +1 -0
- package/dist/services/fusion.d.ts +162 -0
- package/dist/services/fusion.d.ts.map +1 -0
- package/dist/services/fusion.js +789 -0
- package/dist/services/fusion.js.map +1 -0
- package/dist/services/gemini-map.d.ts +14 -0
- package/dist/services/gemini-map.d.ts.map +1 -0
- package/dist/services/gemini-map.js +78 -0
- package/dist/services/gemini-map.js.map +1 -0
- package/dist/services/health.d.ts +52 -0
- package/dist/services/health.d.ts.map +1 -0
- package/dist/services/health.js +352 -0
- package/dist/services/health.js.map +1 -0
- package/dist/services/idempotency.d.ts +49 -0
- package/dist/services/idempotency.d.ts.map +1 -0
- package/dist/services/idempotency.js +140 -0
- package/dist/services/idempotency.js.map +1 -0
- package/dist/services/key-budget.d.ts +74 -0
- package/dist/services/key-budget.d.ts.map +1 -0
- package/dist/services/key-budget.js +200 -0
- package/dist/services/key-budget.js.map +1 -0
- package/dist/services/media.d.ts +123 -0
- package/dist/services/media.d.ts.map +1 -0
- package/dist/services/media.js +954 -0
- package/dist/services/media.js.map +1 -0
- package/dist/services/model-discovery.d.ts +126 -0
- package/dist/services/model-discovery.d.ts.map +1 -0
- package/dist/services/model-discovery.js +662 -0
- package/dist/services/model-discovery.js.map +1 -0
- package/dist/services/model-groups.d.ts +149 -0
- package/dist/services/model-groups.d.ts.map +1 -0
- package/dist/services/model-groups.js +314 -0
- package/dist/services/model-groups.js.map +1 -0
- package/dist/services/model-listing.d.ts +18 -0
- package/dist/services/model-listing.d.ts.map +1 -0
- package/dist/services/model-listing.js +94 -0
- package/dist/services/model-listing.js.map +1 -0
- package/dist/services/model-retirement.d.ts +29 -0
- package/dist/services/model-retirement.d.ts.map +1 -0
- package/dist/services/model-retirement.js +86 -0
- package/dist/services/model-retirement.js.map +1 -0
- package/dist/services/model-state.d.ts +90 -0
- package/dist/services/model-state.d.ts.map +1 -0
- package/dist/services/model-state.js +342 -0
- package/dist/services/model-state.js.map +1 -0
- package/dist/services/model-weight-overrides.d.ts +48 -0
- package/dist/services/model-weight-overrides.d.ts.map +1 -0
- package/dist/services/model-weight-overrides.js +127 -0
- package/dist/services/model-weight-overrides.js.map +1 -0
- package/dist/services/notification-emitter.d.ts +50 -0
- package/dist/services/notification-emitter.d.ts.map +1 -0
- package/dist/services/notification-emitter.js +177 -0
- package/dist/services/notification-emitter.js.map +1 -0
- package/dist/services/opencode-free-sync.d.ts +29 -0
- package/dist/services/opencode-free-sync.d.ts.map +1 -0
- package/dist/services/opencode-free-sync.js +206 -0
- package/dist/services/opencode-free-sync.js.map +1 -0
- package/dist/services/penalty-inspector.d.ts +56 -0
- package/dist/services/penalty-inspector.d.ts.map +1 -0
- package/dist/services/penalty-inspector.js +167 -0
- package/dist/services/penalty-inspector.js.map +1 -0
- package/dist/services/profile-models.d.ts +5 -0
- package/dist/services/profile-models.d.ts.map +1 -0
- package/dist/services/profile-models.js +62 -0
- package/dist/services/profile-models.js.map +1 -0
- package/dist/services/provider-credential.d.ts +19 -0
- package/dist/services/provider-credential.d.ts.map +1 -0
- package/dist/services/provider-credential.js +51 -0
- package/dist/services/provider-credential.js.map +1 -0
- package/dist/services/provider-quota.d.ts +64 -0
- package/dist/services/provider-quota.d.ts.map +1 -0
- package/dist/services/provider-quota.js +563 -0
- package/dist/services/provider-quota.js.map +1 -0
- package/dist/services/quirks.d.ts +0 -0
- package/dist/services/quirks.d.ts.map +1 -0
- package/dist/services/quirks.js +0 -0
- package/dist/services/quirks.js.map +1 -0
- package/dist/services/quota-forecast.d.ts +50 -0
- package/dist/services/quota-forecast.d.ts.map +1 -0
- package/dist/services/quota-forecast.js +150 -0
- package/dist/services/quota-forecast.js.map +1 -0
- package/dist/services/quota-outlook.d.ts +4 -0
- package/dist/services/quota-outlook.d.ts.map +1 -0
- package/dist/services/quota-outlook.js +92 -0
- package/dist/services/quota-outlook.js.map +1 -0
- package/dist/services/ratelimit.d.ts +249 -0
- package/dist/services/ratelimit.d.ts.map +1 -0
- package/dist/services/ratelimit.js +1182 -0
- package/dist/services/ratelimit.js.map +1 -0
- package/dist/services/request-retention.d.ts +60 -0
- package/dist/services/request-retention.d.ts.map +1 -0
- package/dist/services/request-retention.js +229 -0
- package/dist/services/request-retention.js.map +1 -0
- package/dist/services/router.d.ts +384 -0
- package/dist/services/router.d.ts.map +1 -0
- package/dist/services/router.js +1986 -0
- package/dist/services/router.js.map +1 -0
- package/dist/services/scoring.d.ts +156 -0
- package/dist/services/scoring.d.ts.map +1 -0
- package/dist/services/scoring.js +435 -0
- package/dist/services/scoring.js.map +1 -0
- package/dist/services/url-tokens.d.ts +15 -0
- package/dist/services/url-tokens.d.ts.map +1 -0
- package/dist/services/url-tokens.js +55 -0
- package/dist/services/url-tokens.js.map +1 -0
- package/package.json +77 -4
- 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
|