@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,1521 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import { Router } from 'express';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { routeRequest, hasEnabledVisionModel, hasEnabledToolsModel, resolveStickyPreference, routingReserveTokens, resolveModelGroupCandidates, resolveRoutingChain } from '../services/router.js';
|
|
5
|
+
import { getDb } from '../db/index.js';
|
|
6
|
+
import { resolveAuth, prependSystemPrompt } from '../lib/system-prompt.js';
|
|
7
|
+
import { isUnifyEnabled, getModelGroups, resolveRequestedIdForDispatch } from '../services/model-groups.js';
|
|
8
|
+
import { contentToString, estimateInputTokens, messageHasImage } from '../lib/content.js';
|
|
9
|
+
import { routeOutputBudget } from '../lib/output-cap.js';
|
|
10
|
+
import { resolveTaskType } from '../lib/task-type.js';
|
|
11
|
+
import { normalizeMessageImages } from '../lib/image-normalize.js';
|
|
12
|
+
import { repairToolArguments, toolSchemaMap } from '../lib/tool-args.js';
|
|
13
|
+
import { invalidToolArgumentsError, invalidToolCallReasons, isToolArgumentValidationEnabled } from '../lib/tool-validate.js';
|
|
14
|
+
import { rescueInlineToolCalls, startsWithDialectMarker, couldBecomeDialectMarker, containsDialectMarker } from '../lib/tool-call-rescue.js';
|
|
15
|
+
import { extractApiToken, getRequestGroupId, getStickyModel, setStickyModel, streamReasoningText, completionReasoningText, traceRouteEvent, logRequest, } from './proxy.js';
|
|
16
|
+
import { runFallbackLoop, newFallbackState, fallbackRoutingTokens, recordUpstreamSuccess, setFallbackHeaders, exhaustionErrorPayload, setExhaustionHeaders } from '../lib/fallback-loop.js';
|
|
17
|
+
import { routedViaValue, safeHeaderValue } from '../lib/header-value.js';
|
|
18
|
+
import { applyTokenBudget, tokenBudgetMessage } from '../lib/guardrails.js';
|
|
19
|
+
import { samplingParamSchemaFields, pickSamplingParams } from '../lib/sampling-params.js';
|
|
20
|
+
import { enforceJsonContent } from '../lib/structured-output.js';
|
|
21
|
+
import { sanitizeProviderErrorMessage } from '../lib/error-redaction.js';
|
|
22
|
+
import { isClientAbortError, newClientAbortError, newHedgeAbortError } from '../lib/error-classify.js';
|
|
23
|
+
import { inferQuotaPoolKey } from '../services/provider-quota.js';
|
|
24
|
+
import { compressRequest, formatCompressionHeader } from '../services/compression/pipeline.js';
|
|
25
|
+
import { FUSION_MODEL_ID, FusionError, fusionConfigSchema, isFusionModel, runFusion, } from '../services/fusion.js';
|
|
26
|
+
export const responsesRouter = Router();
|
|
27
|
+
const AUTO_MODEL_ID = 'auto';
|
|
28
|
+
function isAutoModel(modelId) {
|
|
29
|
+
if (!modelId)
|
|
30
|
+
return true;
|
|
31
|
+
const lower = modelId.toLowerCase();
|
|
32
|
+
return lower === AUTO_MODEL_ID || lower.startsWith(`${AUTO_MODEL_ID}:`);
|
|
33
|
+
}
|
|
34
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
35
|
+
// OpenAI Responses API shim (POST /v1/responses).
|
|
36
|
+
//
|
|
37
|
+
// Current Codex versions only speak the Responses API — `wire_api = "chat"`
|
|
38
|
+
// is rejected — so the existing /v1/chat/completions endpoint isn't reachable
|
|
39
|
+
// from Codex (see issue #96). This endpoint accepts a Responses-shaped request,
|
|
40
|
+
// translates it to the internal chat-message format, runs it through the SAME
|
|
41
|
+
// shared fallback loop as the proxy (lib/fallback-loop.ts), and translates the
|
|
42
|
+
// result back into the Responses object / SSE event stream Codex expects.
|
|
43
|
+
//
|
|
44
|
+
// A thin adapter: the cooldown/skip/penalty/exhaustion machinery is shared, and
|
|
45
|
+
// only the Responses request/stream translation lives here. This is what fixed
|
|
46
|
+
// the drift where /v1/responses ignored a provider Retry-After and under-benched
|
|
47
|
+
// a 403 (both are now handled identically to /chat/completions by the shared
|
|
48
|
+
// loop) and committed the SSE skeleton on the first raw chunk (the commit point
|
|
49
|
+
// is now held until the first meaningful content, so a pre-content failure fails
|
|
50
|
+
// over invisibly).
|
|
51
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
52
|
+
const MAX_RETRIES = 20;
|
|
53
|
+
function newId(prefix) {
|
|
54
|
+
return `${prefix}_${crypto.randomBytes(18).toString('hex')}`;
|
|
55
|
+
}
|
|
56
|
+
function nowUnix() {
|
|
57
|
+
return Math.floor(Date.now() / 1000);
|
|
58
|
+
}
|
|
59
|
+
// ── Request schema ──────────────────────────────────────────────────────
|
|
60
|
+
// Lenient on purpose: the Responses API surface is large and evolving, and we
|
|
61
|
+
// only consume the fields we can map. Unknown fields (store, reasoning,
|
|
62
|
+
// metadata, previous_response_id, …) are accepted and ignored.
|
|
63
|
+
const contentPartSchema = z.object({ type: z.string() }).passthrough();
|
|
64
|
+
const messageItemSchema = z.object({
|
|
65
|
+
type: z.literal('message').optional(),
|
|
66
|
+
role: z.enum(['system', 'developer', 'user', 'assistant']),
|
|
67
|
+
content: z.union([z.string(), z.array(contentPartSchema)]),
|
|
68
|
+
});
|
|
69
|
+
const functionCallItemSchema = z.object({
|
|
70
|
+
type: z.literal('function_call'),
|
|
71
|
+
call_id: z.string(),
|
|
72
|
+
name: z.string(),
|
|
73
|
+
arguments: z.string(),
|
|
74
|
+
id: z.string().optional(),
|
|
75
|
+
});
|
|
76
|
+
const functionCallOutputItemSchema = z.object({
|
|
77
|
+
type: z.literal('function_call_output'),
|
|
78
|
+
call_id: z.string(),
|
|
79
|
+
output: z.union([z.string(), z.array(contentPartSchema), z.record(z.string(), z.unknown())]),
|
|
80
|
+
});
|
|
81
|
+
// Remaining official ResponseInputItemParam kinds. Codex computer-use round-trips
|
|
82
|
+
// `computer_call` (the model's action request) and `computer_call_output` (the
|
|
83
|
+
// harness's result, incl. screenshots); multi-turn sessions also replay
|
|
84
|
+
// `reasoning` / `local_shell_call` items. We accept them all so validation never
|
|
85
|
+
// 400s on a standard payload; each is then either mapped (below) or dropped
|
|
86
|
+
// because chat-completions upstreams have no equivalent (computer/local_shell).
|
|
87
|
+
// Each schema is permissive — we only consume the fields that matter.
|
|
88
|
+
const computerCallItemSchema = z.object({
|
|
89
|
+
type: z.literal('computer_call'),
|
|
90
|
+
call_id: z.string(),
|
|
91
|
+
action: z.record(z.string(), z.unknown()).optional(),
|
|
92
|
+
id: z.string().optional(),
|
|
93
|
+
}).passthrough();
|
|
94
|
+
const computerCallOutputItemSchema = z.object({
|
|
95
|
+
type: z.literal('computer_call_output'),
|
|
96
|
+
call_id: z.string(),
|
|
97
|
+
output: z.union([
|
|
98
|
+
z.string(),
|
|
99
|
+
z.array(contentPartSchema),
|
|
100
|
+
z.record(z.string(), z.unknown()),
|
|
101
|
+
]).optional(),
|
|
102
|
+
id: z.string().optional(),
|
|
103
|
+
}).passthrough();
|
|
104
|
+
const reasoningItemSchema = z.object({
|
|
105
|
+
type: z.literal('reasoning'),
|
|
106
|
+
summary: z.union([z.string(), z.array(contentPartSchema)]).optional(),
|
|
107
|
+
content: z.union([z.string(), z.array(contentPartSchema)]).optional(),
|
|
108
|
+
id: z.string().optional(),
|
|
109
|
+
}).passthrough();
|
|
110
|
+
const localShellCallItemSchema = z.object({
|
|
111
|
+
type: z.literal('local_shell_call'),
|
|
112
|
+
call_id: z.string().optional(),
|
|
113
|
+
action: z.record(z.string(), z.unknown()).optional(),
|
|
114
|
+
id: z.string().optional(),
|
|
115
|
+
}).passthrough();
|
|
116
|
+
// Codex sends its client-side tool inventory as an `additional_tools` input
|
|
117
|
+
// item on every Responses turn. It is metadata for the harness, not a chat
|
|
118
|
+
// message, so the translator deliberately drops it below. Keep the schema
|
|
119
|
+
// permissive because Codex may add fields (or tool shapes) as the inventory
|
|
120
|
+
// evolves; rejecting the whole request here turns an otherwise valid launch
|
|
121
|
+
// into a misleading `input: Invalid input` 400.
|
|
122
|
+
const additionalToolsItemSchema = z.object({
|
|
123
|
+
type: z.literal('additional_tools'),
|
|
124
|
+
id: z.string().optional(),
|
|
125
|
+
role: z.string().optional(),
|
|
126
|
+
tools: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
127
|
+
}).passthrough();
|
|
128
|
+
// The rest of the official ResponseInputItemParam union: built-in tool calls
|
|
129
|
+
// (web_search, file_search, code interpreter, image generation), MCP items,
|
|
130
|
+
// and item references. None has a chat-completions equivalent — validated
|
|
131
|
+
// loosely so a standard replay never 400s, then skipped at conversion like
|
|
132
|
+
// the kinds above.
|
|
133
|
+
const otherKnownItemSchema = z.object({
|
|
134
|
+
type: z.enum([
|
|
135
|
+
'web_search_call', 'file_search_call', 'code_interpreter_call',
|
|
136
|
+
'image_generation_call', 'mcp_call', 'mcp_list_tools',
|
|
137
|
+
'mcp_approval_request', 'mcp_approval_response', 'item_reference',
|
|
138
|
+
]),
|
|
139
|
+
id: z.string().optional(),
|
|
140
|
+
}).passthrough();
|
|
141
|
+
const inputItemSchema = z.union([
|
|
142
|
+
functionCallItemSchema,
|
|
143
|
+
functionCallOutputItemSchema,
|
|
144
|
+
computerCallItemSchema,
|
|
145
|
+
computerCallOutputItemSchema,
|
|
146
|
+
reasoningItemSchema,
|
|
147
|
+
localShellCallItemSchema,
|
|
148
|
+
additionalToolsItemSchema,
|
|
149
|
+
otherKnownItemSchema,
|
|
150
|
+
messageItemSchema,
|
|
151
|
+
]);
|
|
152
|
+
// Accept ANY tool type, not just 'function'. Codex (Responses API) sends
|
|
153
|
+
// built-in tools like `web_search` / `local_shell` alongside function tools;
|
|
154
|
+
// a strict z.literal('function') rejected the whole request. We validate
|
|
155
|
+
// loosely here and drop non-function tools at conversion (toChatTools), since
|
|
156
|
+
// chat-completions providers only accept type:'function'.
|
|
157
|
+
const responsesToolSchema = z.object({
|
|
158
|
+
type: z.string(),
|
|
159
|
+
name: z.string().optional(),
|
|
160
|
+
description: z.string().nullable().optional(),
|
|
161
|
+
parameters: z.record(z.string(), z.unknown()).nullable().optional(),
|
|
162
|
+
strict: z.boolean().nullable().optional(),
|
|
163
|
+
}).passthrough();
|
|
164
|
+
const responsesRequestSchema = z.object({
|
|
165
|
+
model: z.string().optional(),
|
|
166
|
+
instructions: z.string().nullable().optional(),
|
|
167
|
+
input: z.union([z.string(), z.array(inputItemSchema)]),
|
|
168
|
+
stream: z.boolean().optional(),
|
|
169
|
+
temperature: z.number().min(0).max(2).nullable().optional(),
|
|
170
|
+
top_p: z.number().min(0).max(1).nullable().optional(),
|
|
171
|
+
max_output_tokens: z.number().int().positive().nullable().optional(),
|
|
172
|
+
tools: z.array(responsesToolSchema).optional(),
|
|
173
|
+
// The virtual Fusion model fans the translated conversation out to a
|
|
174
|
+
// diverse panel, then optionally synthesizes the survivors. Keep the
|
|
175
|
+
// Responses surface in parity with /v1/chat/completions.
|
|
176
|
+
fusion: fusionConfigSchema.optional(),
|
|
177
|
+
tool_choice: z.union([
|
|
178
|
+
z.enum(['none', 'auto', 'required']),
|
|
179
|
+
z.object({ type: z.literal('function'), name: z.string() }).passthrough(),
|
|
180
|
+
]).optional(),
|
|
181
|
+
parallel_tool_calls: z.boolean().nullable().optional(),
|
|
182
|
+
// Extended sampling params, validated the same way as /chat/completions.
|
|
183
|
+
// Responses clients express structured output as `text.format` rather than
|
|
184
|
+
// `response_format` — mapped where completionOpts is built.
|
|
185
|
+
...samplingParamSchemaFields,
|
|
186
|
+
text: z.object({
|
|
187
|
+
format: z.object({
|
|
188
|
+
type: z.enum(['text', 'json_object', 'json_schema']),
|
|
189
|
+
name: z.string().optional(),
|
|
190
|
+
strict: z.boolean().nullable().optional(),
|
|
191
|
+
schema: z.record(z.string(), z.unknown()).optional(),
|
|
192
|
+
}).passthrough().optional(),
|
|
193
|
+
}).passthrough().nullable().optional(),
|
|
194
|
+
}).passthrough();
|
|
195
|
+
// Responses content parts → plain text. input_text / output_text both carry
|
|
196
|
+
// `text`; other part types (images, etc.) are dropped (parity with the proxy).
|
|
197
|
+
function partsToString(content) {
|
|
198
|
+
if (typeof content === 'string')
|
|
199
|
+
return content;
|
|
200
|
+
return content
|
|
201
|
+
.map((p) => (typeof p.text === 'string' ? p.text : ''))
|
|
202
|
+
.join('');
|
|
203
|
+
}
|
|
204
|
+
// Responses content parts → internal chat content. Text parts map to text
|
|
205
|
+
// blocks, image parts (Responses `input_image`, chat-style `image_url`/`image`,
|
|
206
|
+
// and computer-use `computer_screenshot`) map to `image_url` blocks so vision
|
|
207
|
+
// routing and the provider adapters see them (parity with /chat/completions).
|
|
208
|
+
// `refusal` parts (assistant history replay) fold into text so the turn isn't
|
|
209
|
+
// silently emptied. All-text content collapses back to a plain string (the
|
|
210
|
+
// shape upstream chat providers and compression expect); an array comes back
|
|
211
|
+
// only once a message actually carries an image.
|
|
212
|
+
function partsToChatContent(content) {
|
|
213
|
+
if (typeof content === 'string')
|
|
214
|
+
return content;
|
|
215
|
+
const blocks = [];
|
|
216
|
+
for (const p of content) {
|
|
217
|
+
const type = p.type;
|
|
218
|
+
const text = p.text;
|
|
219
|
+
if (type === 'text' || type === 'input_text' || type === 'output_text' || type === 'summary_text' || (type === undefined && typeof text === 'string')) {
|
|
220
|
+
if (typeof text === 'string')
|
|
221
|
+
blocks.push({ type: 'text', text });
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (type === 'refusal') {
|
|
225
|
+
// The model's refusal text on a replayed assistant turn; chat providers
|
|
226
|
+
// have no refusal concept, so carry it as ordinary text.
|
|
227
|
+
const refusal = p.refusal;
|
|
228
|
+
if (typeof refusal === 'string')
|
|
229
|
+
blocks.push({ type: 'text', text: refusal });
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (type === 'input_image' || type === 'computer_screenshot' || type === 'image_url' || type === 'image') {
|
|
233
|
+
// The image lives under `image_url`, as a bare data URL string
|
|
234
|
+
// (Responses `input_image` / `computer_screenshot`) or as
|
|
235
|
+
// `{ url }` (chat-style `image_url`). The Responses `detail` hint
|
|
236
|
+
// (low/high/auto/original) rides along; adapters without an equivalent
|
|
237
|
+
// (Gemini inlineData) ignore it. Unresolvable shapes (missing/empty
|
|
238
|
+
// url, file_id-only) are rejected up front by the route's pre-check,
|
|
239
|
+
// so dropping here never answers blind.
|
|
240
|
+
const url = extractPartImageUrl(p);
|
|
241
|
+
if (url) {
|
|
242
|
+
const detail = p.detail;
|
|
243
|
+
blocks.push({ type: 'image_url', image_url: { url, ...(typeof detail === 'string' ? { detail } : {}) } });
|
|
244
|
+
}
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (blocks.every((b) => b.type === 'text')) {
|
|
249
|
+
return blocks.map((b) => b.text).join('');
|
|
250
|
+
}
|
|
251
|
+
return blocks;
|
|
252
|
+
}
|
|
253
|
+
// Shared url extraction so the translation and the pre-check below always
|
|
254
|
+
// agree on what counts as a resolvable image.
|
|
255
|
+
function extractPartImageUrl(p) {
|
|
256
|
+
const raw = p.image_url;
|
|
257
|
+
const url = typeof raw === 'string' ? raw : raw?.url;
|
|
258
|
+
return typeof url === 'string' && url.length > 0 ? url : undefined;
|
|
259
|
+
}
|
|
260
|
+
// Responses `input_image` references an image by `image_url` OR a Files API
|
|
261
|
+
// `file_id`. This proxy has no OpenAI Files backend, and the schema is
|
|
262
|
+
// deliberately lenient, so an image part with no resolvable url (file_id-only,
|
|
263
|
+
// or no url at all) can't survive translation — reject up front instead of
|
|
264
|
+
// silently dropping it and answering blind to an image the client believes
|
|
265
|
+
// was sent. (`computer_screenshot` shares these forms but is intentionally
|
|
266
|
+
// not checked: computer use is rejected wholesale up front, today and until
|
|
267
|
+
// it's a supported feature.)
|
|
268
|
+
export function responsesInputHasFileIdImage(req) {
|
|
269
|
+
if (typeof req.input === 'string')
|
|
270
|
+
return false;
|
|
271
|
+
return req.input.some((item) => {
|
|
272
|
+
const content = item.content;
|
|
273
|
+
if (!Array.isArray(content))
|
|
274
|
+
return false;
|
|
275
|
+
return content.some((p) => {
|
|
276
|
+
const part = p;
|
|
277
|
+
if (part.type !== 'input_image')
|
|
278
|
+
return false;
|
|
279
|
+
return extractPartImageUrl(part) === undefined;
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
// Computer use (the Responses `computer` / `computer_use_preview` tool + its
|
|
284
|
+
// computer_call/computer_call_output items) has no chat-completions equivalent:
|
|
285
|
+
// the harness loop needs the model's computer_actions and screenshot context,
|
|
286
|
+
// neither of which survives translation. Fail clearly (mirroring the image 422)
|
|
287
|
+
// rather than silently dropping the calls and breaking the tool loop.
|
|
288
|
+
export function responsesInputRequestsComputerUse(req) {
|
|
289
|
+
if ((req.tools ?? []).some((t) => t.type === 'computer' || t.type === 'computer_use_preview'))
|
|
290
|
+
return true;
|
|
291
|
+
if (typeof req.input === 'string' || req.input == null)
|
|
292
|
+
return false;
|
|
293
|
+
return req.input.some((item) => {
|
|
294
|
+
const type = item?.type;
|
|
295
|
+
return type === 'computer_call' || type === 'computer_call_output';
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
// ── Translate a Responses request → internal chat messages + options ──────
|
|
299
|
+
export function toChatMessages(req) {
|
|
300
|
+
const systemMessages = [];
|
|
301
|
+
if (req.instructions) {
|
|
302
|
+
systemMessages.push({ role: 'system', content: req.instructions });
|
|
303
|
+
}
|
|
304
|
+
if (typeof req.input === 'string') {
|
|
305
|
+
const messages = [{ role: 'user', content: req.input }];
|
|
306
|
+
return [...systemMessages, ...messages];
|
|
307
|
+
}
|
|
308
|
+
const messages = [];
|
|
309
|
+
const items = req.input;
|
|
310
|
+
for (let i = 0; i < items.length; i++) {
|
|
311
|
+
const item = items[i];
|
|
312
|
+
if ('type' in item && item.type === 'function_call') {
|
|
313
|
+
// Parallel tool calls replay as consecutive standalone function_call
|
|
314
|
+
// items (no preceding assistant message item). Fold the run into one
|
|
315
|
+
// assistant turn, for the same reason as the message+function_call
|
|
316
|
+
// merge below: consecutive assistant turns 400 on strict upstreams.
|
|
317
|
+
const toolCalls = [];
|
|
318
|
+
let j = i;
|
|
319
|
+
while (j < items.length && items[j].type === 'function_call') {
|
|
320
|
+
const fc = items[j];
|
|
321
|
+
toolCalls.push({
|
|
322
|
+
id: fc.call_id,
|
|
323
|
+
type: 'function',
|
|
324
|
+
function: { name: fc.name, arguments: fc.arguments },
|
|
325
|
+
});
|
|
326
|
+
j++;
|
|
327
|
+
}
|
|
328
|
+
messages.push({ role: 'assistant', content: null, tool_calls: toolCalls });
|
|
329
|
+
i = j - 1;
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
if ('type' in item && item.type === 'function_call_output') {
|
|
333
|
+
const output = typeof item.output === 'string'
|
|
334
|
+
? item.output
|
|
335
|
+
: Array.isArray(item.output)
|
|
336
|
+
? partsToString(item.output)
|
|
337
|
+
: JSON.stringify(item.output);
|
|
338
|
+
messages.push({ role: 'tool', tool_call_id: item.call_id, content: output });
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
if ('type' in item && item.type !== 'message') {
|
|
342
|
+
// computer_call / computer_call_output / reasoning / local_shell_call:
|
|
343
|
+
// no chat-message equivalent (the route 422s computer use up front).
|
|
344
|
+
// Skip rather than mis-parse as a message item.
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
// message item
|
|
348
|
+
const m = item;
|
|
349
|
+
// 'developer' is the Responses-era system role.
|
|
350
|
+
const role = m.role === 'developer' ? 'system' : m.role;
|
|
351
|
+
const content = partsToChatContent(m.content);
|
|
352
|
+
if (role === 'system') {
|
|
353
|
+
// Hoist system/developer messages to the start of the conversation:
|
|
354
|
+
// chat providers (Gemini, Claude, Mistral) reject a system message that
|
|
355
|
+
// appears after a user turn. Codex history replay often emits developer
|
|
356
|
+
// items mid-conversation.
|
|
357
|
+
systemMessages.push({ role: 'system', content });
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
if (role === 'assistant') {
|
|
361
|
+
// A Responses assistant turn is a message item followed by its
|
|
362
|
+
// function_call items. Merge them into a single chat assistant message
|
|
363
|
+
// (content + tool_calls); emitting consecutive assistant turns makes
|
|
364
|
+
// Gemini map them to consecutive model turns and strict upstreams
|
|
365
|
+
// (Mistral/Cohere) 400. Drop empty assistant items — an empty turn means
|
|
366
|
+
// nothing to a chat provider. (#96)
|
|
367
|
+
const toolCalls = [];
|
|
368
|
+
let j = i + 1;
|
|
369
|
+
while (j < items.length && items[j].type === 'function_call') {
|
|
370
|
+
const fc = items[j];
|
|
371
|
+
toolCalls.push({
|
|
372
|
+
id: fc.call_id,
|
|
373
|
+
type: 'function',
|
|
374
|
+
function: { name: fc.name, arguments: fc.arguments },
|
|
375
|
+
});
|
|
376
|
+
j++;
|
|
377
|
+
}
|
|
378
|
+
if (toolCalls.length > 0) {
|
|
379
|
+
messages.push({
|
|
380
|
+
role: 'assistant',
|
|
381
|
+
content: content.length > 0 ? content : null,
|
|
382
|
+
tool_calls: toolCalls,
|
|
383
|
+
});
|
|
384
|
+
i = j - 1;
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
if (content.length === 0)
|
|
388
|
+
continue;
|
|
389
|
+
messages.push({ role: 'assistant', content });
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
messages.push({ role, content });
|
|
393
|
+
}
|
|
394
|
+
return [...systemMessages, ...messages];
|
|
395
|
+
}
|
|
396
|
+
export function toChatTools(tools) {
|
|
397
|
+
if (!tools?.length)
|
|
398
|
+
return undefined;
|
|
399
|
+
// Forward only function tools — chat-completions upstreams reject other
|
|
400
|
+
// Responses-API tool types (web_search, local_shell, etc.). Codex sends those
|
|
401
|
+
// extras alongside its function tools (shell/exec, apply_patch); dropping them
|
|
402
|
+
// keeps the request valid without losing the tools that actually do the work.
|
|
403
|
+
const fns = tools.filter((t) => t.type === 'function' && typeof t.name === 'string');
|
|
404
|
+
if (!fns.length)
|
|
405
|
+
return undefined;
|
|
406
|
+
return fns.map((t) => ({
|
|
407
|
+
type: 'function',
|
|
408
|
+
function: {
|
|
409
|
+
name: t.name,
|
|
410
|
+
...(t.description ? { description: t.description } : {}),
|
|
411
|
+
...(t.parameters ? { parameters: t.parameters } : {}),
|
|
412
|
+
...(t.strict != null ? { strict: t.strict } : {}),
|
|
413
|
+
},
|
|
414
|
+
}));
|
|
415
|
+
}
|
|
416
|
+
export function toChatToolChoice(tc) {
|
|
417
|
+
if (!tc)
|
|
418
|
+
return undefined;
|
|
419
|
+
if (typeof tc === 'string')
|
|
420
|
+
return tc;
|
|
421
|
+
return { type: 'function', function: { name: tc.name } };
|
|
422
|
+
}
|
|
423
|
+
function requestDeclaresToolUse(req) {
|
|
424
|
+
return (req.tools?.length ?? 0) > 0 && req.tool_choice !== 'none';
|
|
425
|
+
}
|
|
426
|
+
// ── Build the final (non-stream) Responses object ─────────────────────────
|
|
427
|
+
export function buildResponseObject(opts) {
|
|
428
|
+
const output = [];
|
|
429
|
+
if (opts.text.length > 0) {
|
|
430
|
+
output.push({
|
|
431
|
+
type: 'message',
|
|
432
|
+
id: newId('msg'),
|
|
433
|
+
status: 'completed',
|
|
434
|
+
role: 'assistant',
|
|
435
|
+
content: [{ type: 'output_text', text: opts.text, annotations: [] }],
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
for (const tc of opts.toolCalls) {
|
|
439
|
+
output.push({
|
|
440
|
+
type: 'function_call',
|
|
441
|
+
id: newId('fc'),
|
|
442
|
+
call_id: tc.id,
|
|
443
|
+
name: tc.function.name,
|
|
444
|
+
arguments: tc.function.arguments,
|
|
445
|
+
status: 'completed',
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
return {
|
|
449
|
+
id: opts.id,
|
|
450
|
+
object: 'response',
|
|
451
|
+
created_at: nowUnix(),
|
|
452
|
+
status: 'completed',
|
|
453
|
+
model: opts.model,
|
|
454
|
+
output,
|
|
455
|
+
output_text: opts.text,
|
|
456
|
+
usage: {
|
|
457
|
+
input_tokens: opts.promptTokens,
|
|
458
|
+
input_tokens_details: { cached_tokens: 0 },
|
|
459
|
+
output_tokens: opts.completionTokens,
|
|
460
|
+
output_tokens_details: { reasoning_tokens: opts.reasoningTokens ?? 0 },
|
|
461
|
+
total_tokens: opts.promptTokens + opts.completionTokens,
|
|
462
|
+
},
|
|
463
|
+
...(opts.metadata ? { metadata: opts.metadata } : {}),
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Translate the chat-shaped result returned by the Fusion service into the
|
|
468
|
+
* Responses object clients expect. Chat Fusion uses additive `_fusion` and
|
|
469
|
+
* `x_fusion` fields, but those are not part of the Responses schema and strict
|
|
470
|
+
* SDKs (including Codex) reject them while decoding `response.completed`.
|
|
471
|
+
* Preserve the portable routing summary in the standard string-valued
|
|
472
|
+
* `metadata` map instead; the chat surface keeps its richer fields unchanged.
|
|
473
|
+
*/
|
|
474
|
+
function buildFusionResponseObject(id, result) {
|
|
475
|
+
const fusionResponse = result.response;
|
|
476
|
+
const message = fusionResponse.choices?.[0]?.message;
|
|
477
|
+
const usage = fusionResponse.usage;
|
|
478
|
+
const response = buildResponseObject({
|
|
479
|
+
id,
|
|
480
|
+
model: FUSION_MODEL_ID,
|
|
481
|
+
text: contentToString(message?.content ?? ''),
|
|
482
|
+
toolCalls: message?.tool_calls ?? [],
|
|
483
|
+
promptTokens: usage?.prompt_tokens ?? 0,
|
|
484
|
+
completionTokens: usage?.completion_tokens ?? 0,
|
|
485
|
+
reasoningTokens: usage?.completion_tokens_details?.reasoning_tokens,
|
|
486
|
+
metadata: fusionResponseMetadata(fusionResponse),
|
|
487
|
+
});
|
|
488
|
+
return response;
|
|
489
|
+
}
|
|
490
|
+
function fusionResponseMetadata(result) {
|
|
491
|
+
const metadata = { fusion: 'true' };
|
|
492
|
+
const summary = result._fusion && typeof result._fusion === 'object' && !Array.isArray(result._fusion)
|
|
493
|
+
? result._fusion
|
|
494
|
+
: undefined;
|
|
495
|
+
const details = result.x_fusion && typeof result.x_fusion === 'object' && !Array.isArray(result.x_fusion)
|
|
496
|
+
? result.x_fusion
|
|
497
|
+
: undefined;
|
|
498
|
+
if (Array.isArray(summary?.panel)) {
|
|
499
|
+
const panel = summary.panel
|
|
500
|
+
.map((entry) => {
|
|
501
|
+
if (!entry || typeof entry !== 'object')
|
|
502
|
+
return null;
|
|
503
|
+
const item = entry;
|
|
504
|
+
return typeof item.platform === 'string' && typeof item.model === 'string'
|
|
505
|
+
? `${item.platform}/${item.model}`
|
|
506
|
+
: null;
|
|
507
|
+
})
|
|
508
|
+
.filter((entry) => entry !== null);
|
|
509
|
+
if (panel.length > 0)
|
|
510
|
+
metadata.fusion_panel = panel.join(',');
|
|
511
|
+
}
|
|
512
|
+
if (summary && 'judge' in summary) {
|
|
513
|
+
const judge = summary.judge;
|
|
514
|
+
if (judge && typeof judge === 'object') {
|
|
515
|
+
const item = judge;
|
|
516
|
+
if (typeof item.platform === 'string' && typeof item.model === 'string') {
|
|
517
|
+
metadata.fusion_judge = `${item.platform}/${item.model}`;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
metadata.fusion_judge = 'none';
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
if (typeof summary?.synthesized === 'boolean')
|
|
525
|
+
metadata.fusion_synthesized = String(summary.synthesized);
|
|
526
|
+
if (typeof details?.strategy === 'string')
|
|
527
|
+
metadata.fusion_strategy = details.strategy;
|
|
528
|
+
return metadata;
|
|
529
|
+
}
|
|
530
|
+
/** Apply the same structured-output contract to Fusion as the chat route. */
|
|
531
|
+
function enforceFusionResponseFormat(result, responseFormat) {
|
|
532
|
+
if (!responseFormat)
|
|
533
|
+
return;
|
|
534
|
+
const message = result.response.choices?.[0]?.message;
|
|
535
|
+
if (!message || message.tool_calls?.length)
|
|
536
|
+
return;
|
|
537
|
+
const text = contentToString(message.content ?? '');
|
|
538
|
+
if (!text)
|
|
539
|
+
return;
|
|
540
|
+
const enforced = enforceJsonContent(text);
|
|
541
|
+
if (!enforced.ok) {
|
|
542
|
+
throw new Error(`fusion produced non-JSON output despite response_format=${responseFormat.type} — retry, or pin a structured-output-capable model instead of "fusion"`);
|
|
543
|
+
}
|
|
544
|
+
if (enforced.healed)
|
|
545
|
+
message.content = enforced.content;
|
|
546
|
+
}
|
|
547
|
+
function quotaContextForRoute(route, endpoint) {
|
|
548
|
+
return {
|
|
549
|
+
platform: route.platform,
|
|
550
|
+
keyId: route.keyId,
|
|
551
|
+
modelId: route.modelId,
|
|
552
|
+
quotaPoolKey: inferQuotaPoolKey(route.platform, route.modelId),
|
|
553
|
+
endpoint,
|
|
554
|
+
origin: 'responses',
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
responsesRouter.post('/responses', async (req, res) => {
|
|
558
|
+
const start = Date.now();
|
|
559
|
+
const requestGroupId = getRequestGroupId(req);
|
|
560
|
+
res.setHeader('X-Request-ID', requestGroupId);
|
|
561
|
+
// Same auth as the proxy (accepts Bearer or x-api-key): unified key, or a
|
|
562
|
+
// client-profile key carrying a server-enforced system prompt (#411).
|
|
563
|
+
const auth = resolveAuth(extractApiToken(req));
|
|
564
|
+
if (!auth) {
|
|
565
|
+
res.status(401).json({ error: { message: 'Invalid API key', type: 'authentication_error' } });
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
const parsed = responsesRequestSchema.safeParse(req.body);
|
|
569
|
+
if (!parsed.success) {
|
|
570
|
+
res.status(400).json({
|
|
571
|
+
error: {
|
|
572
|
+
message: `Invalid request: ${parsed.error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ')}`,
|
|
573
|
+
type: 'invalid_request_error',
|
|
574
|
+
},
|
|
575
|
+
});
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
const reqData = parsed.data;
|
|
579
|
+
// Computer use can't survive the chat-completions translation either (no
|
|
580
|
+
// computer tool, no screenshot context). Fail clearly instead of silently
|
|
581
|
+
// dropping the calls and breaking Codex's computer-use tool loop.
|
|
582
|
+
if (responsesInputRequestsComputerUse(reqData)) {
|
|
583
|
+
res.status(422).json({
|
|
584
|
+
error: {
|
|
585
|
+
message: 'Computer use is not yet supported on /v1/responses (the computer / computer_use_preview tool and computer_call items have no chat-completions equivalent).',
|
|
586
|
+
type: 'invalid_request_error',
|
|
587
|
+
code: 'no_computer_use_model',
|
|
588
|
+
},
|
|
589
|
+
});
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
// Unresolvable image parts (file_id-only, or no usable url) can't survive
|
|
593
|
+
// translation — fail clearly rather than dropping the image and answering
|
|
594
|
+
// blind.
|
|
595
|
+
if (responsesInputHasFileIdImage(reqData)) {
|
|
596
|
+
res.status(422).json({
|
|
597
|
+
error: {
|
|
598
|
+
message: "This request contains an image part that can't be resolved: input_image needs an image_url (an https URL or a base64 data URL); Files API file_id references aren't supported by this proxy.",
|
|
599
|
+
type: 'invalid_request_error',
|
|
600
|
+
code: 'unsupported_image_input',
|
|
601
|
+
},
|
|
602
|
+
});
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
const stream = reqData.stream ?? false;
|
|
606
|
+
let messages = toChatMessages(reqData);
|
|
607
|
+
const tools = toChatTools(reqData.tools);
|
|
608
|
+
// name → parameter schema, for repairing double-encoded tool arguments on
|
|
609
|
+
// the way back out (see lib/tool-args.ts).
|
|
610
|
+
const toolSchemas = toolSchemaMap(tools);
|
|
611
|
+
const tool_choice = tools?.length ? toChatToolChoice(reqData.tool_choice) : undefined;
|
|
612
|
+
// Responses-API structured output arrives as `text.format`; translate it to
|
|
613
|
+
// the internal response_format shape (an explicit response_format on the
|
|
614
|
+
// body, unusual for this surface but valid, wins).
|
|
615
|
+
const samplingParams = pickSamplingParams(reqData);
|
|
616
|
+
const textFormat = reqData.text?.format;
|
|
617
|
+
if (!samplingParams.response_format && textFormat && textFormat.type !== 'text') {
|
|
618
|
+
samplingParams.response_format = textFormat.type === 'json_schema'
|
|
619
|
+
? { type: 'json_schema', json_schema: { name: textFormat.name, strict: textFormat.strict, schema: textFormat.schema } }
|
|
620
|
+
: { type: 'json_object' };
|
|
621
|
+
}
|
|
622
|
+
const completionOpts = {
|
|
623
|
+
temperature: reqData.temperature ?? undefined,
|
|
624
|
+
max_tokens: reqData.max_output_tokens ?? undefined,
|
|
625
|
+
top_p: reqData.top_p ?? undefined,
|
|
626
|
+
tools,
|
|
627
|
+
tool_choice,
|
|
628
|
+
parallel_tool_calls: reqData.parallel_tool_calls ?? undefined,
|
|
629
|
+
...samplingParams,
|
|
630
|
+
};
|
|
631
|
+
const hasCacheControl = typeof reqData.input !== 'string' && reqData.input.some(item => {
|
|
632
|
+
const content = item.content;
|
|
633
|
+
return Array.isArray(content)
|
|
634
|
+
&& content.some(block => block && typeof block === 'object' && 'cache_control' in block);
|
|
635
|
+
});
|
|
636
|
+
const compressionResult = compressRequest(messages, {
|
|
637
|
+
header: req.headers['x-freellm-compress'],
|
|
638
|
+
tools,
|
|
639
|
+
cacheControlPrefixLength: hasCacheControl ? messages.length : 0,
|
|
640
|
+
});
|
|
641
|
+
messages = compressionResult.messages;
|
|
642
|
+
res.setHeader('X-FreeLLM-Compress', formatCompressionHeader(compressionResult));
|
|
643
|
+
// Server-enforced system prompt (#411): after compression so it is never
|
|
644
|
+
// compressed away, first in the list so the caller's own instructions
|
|
645
|
+
// (`instructions` / system input items) follow it and cannot override it.
|
|
646
|
+
messages = prependSystemPrompt(messages, auth.systemPrompt);
|
|
647
|
+
// Downscale over-threshold inline images before estimation/routing so the
|
|
648
|
+
// token budget, payload limits, and upstream transfer all see the shrunk
|
|
649
|
+
// bytes (see lib/image-normalize.ts). Mutates the image blocks in place.
|
|
650
|
+
await normalizeMessageImages(messages);
|
|
651
|
+
const estimatedInputTokens = estimateInputTokens(messages, tools);
|
|
652
|
+
// Image requests must route to a vision-capable model (mirrors
|
|
653
|
+
// /chat/completions, proxy.ts). Reject up front with a clear message when
|
|
654
|
+
// none is enabled; when vision models are available, requireVision routing
|
|
655
|
+
// skips text-only models — including a pinned/sticky one — and falls back to
|
|
656
|
+
// a vision-capable peer (#118, #125). A rough per-image token cost keeps
|
|
657
|
+
// budget routing from being skewed by content the text heuristic can't see.
|
|
658
|
+
const hasImage = messageHasImage(messages);
|
|
659
|
+
if (hasImage && !hasEnabledVisionModel()) {
|
|
660
|
+
res.status(422).json({
|
|
661
|
+
error: {
|
|
662
|
+
message: 'This request includes an image, but no vision-capable model is enabled. Enable a vision model (e.g. Gemini 2.5 Flash, Llama 4 Scout) in the Fallback Chain.',
|
|
663
|
+
type: 'invalid_request_error',
|
|
664
|
+
code: 'no_vision_model',
|
|
665
|
+
},
|
|
666
|
+
});
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
const IMAGE_TOKEN_ESTIMATE = 1000;
|
|
670
|
+
const imageCount = messages.reduce((n, m) => n + (Array.isArray(m.content) ? m.content.filter(b => b?.type === 'image_url' || b?.type === 'image').length : 0), 0);
|
|
671
|
+
// Capped output reserve so a large max_output_tokens can't falsely exclude the
|
|
672
|
+
// model pool (#470); input counts in full. Threaded to the router separately:
|
|
673
|
+
// it is exact and must not be inflated by the context-window safety margin
|
|
674
|
+
// (#956 review).
|
|
675
|
+
const outputReserve = routingReserveTokens(reqData.max_output_tokens);
|
|
676
|
+
const estimatedTotal = estimatedInputTokens + imageCount * IMAGE_TOKEN_ESTIMATE + outputReserve;
|
|
677
|
+
// Guardrail: per-request token budget (request_max_tokens_budget, default
|
|
678
|
+
// off). A request with no max_output_tokens gets its output capped to the
|
|
679
|
+
// budget remainder instead of a rejection.
|
|
680
|
+
const budgetCheck = applyTokenBudget(estimatedInputTokens + imageCount * IMAGE_TOKEN_ESTIMATE, completionOpts.max_tokens);
|
|
681
|
+
if (budgetCheck.rejection) {
|
|
682
|
+
res.status(413).json({
|
|
683
|
+
error: { message: tokenBudgetMessage(budgetCheck.rejection), type: 'invalid_request_error', code: 'request_token_budget' },
|
|
684
|
+
});
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
completionOpts.max_tokens = budgetCheck.maxTokens;
|
|
688
|
+
// Optional client-managed session affinity (mirrors /chat/completions).
|
|
689
|
+
const rawSessionId = req.headers['x-codex-session-id']
|
|
690
|
+
?? req.headers['session-id']
|
|
691
|
+
?? req.headers['x-session-id'];
|
|
692
|
+
const sessionIdHeader = Array.isArray(rawSessionId) ? rawSessionId[0] : rawSessionId;
|
|
693
|
+
const requestedModelLabel = reqData.model ?? 'auto';
|
|
694
|
+
// Explicit `model` field pins routing. If the catalog has no enabled row
|
|
695
|
+
// matching the requested id, return 400 — silently auto-routing to a
|
|
696
|
+
// different model would be surprising to Responses API clients.
|
|
697
|
+
// Priority: explicit model > sticky session > auto routing.
|
|
698
|
+
let preferredModel;
|
|
699
|
+
let groupChain;
|
|
700
|
+
let resolvedChain;
|
|
701
|
+
// Sticky scope for a named auto:<profile> (or auto:smart) chain. Plain `auto`
|
|
702
|
+
// stays on the unscoped key so existing session affinity is unchanged.
|
|
703
|
+
let stickyStrategyKey;
|
|
704
|
+
if (isAutoModel(requestedModelLabel)) {
|
|
705
|
+
try {
|
|
706
|
+
resolvedChain = resolveRoutingChain(requestedModelLabel);
|
|
707
|
+
}
|
|
708
|
+
catch (err) {
|
|
709
|
+
if (err?.status === 400) {
|
|
710
|
+
res.status(400).json({
|
|
711
|
+
error: { message: err.message, type: 'invalid_request_error' },
|
|
712
|
+
});
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
throw err;
|
|
716
|
+
}
|
|
717
|
+
stickyStrategyKey = resolvedChain.strategyKey === 'auto' ? undefined : resolvedChain.strategyKey;
|
|
718
|
+
preferredModel = resolveStickyPreference(getStickyModel(messages, sessionIdHeader, stickyStrategyKey), resolvedChain.chain);
|
|
719
|
+
}
|
|
720
|
+
else if (isFusionModel(requestedModelLabel)) {
|
|
721
|
+
// Fusion is a virtual model, not a row in the catalog. Its panel and
|
|
722
|
+
// judge each resolve through the normal router inside runFusion below.
|
|
723
|
+
}
|
|
724
|
+
else {
|
|
725
|
+
const db = getDb();
|
|
726
|
+
const resolved = isUnifyEnabled() ? resolveRequestedIdForDispatch(requestedModelLabel, getModelGroups()) : null;
|
|
727
|
+
const members = resolved?.memberDbIds ?? null;
|
|
728
|
+
if (members && members.length > 0) {
|
|
729
|
+
groupChain = resolveModelGroupCandidates(members, resolved.demotedDbIds);
|
|
730
|
+
if (groupChain.length === 0) {
|
|
731
|
+
const placeholders = members.map(() => '?').join(',');
|
|
732
|
+
const anyEnabled = db.prepare(`SELECT 1 FROM models WHERE id IN (${placeholders}) AND enabled = 1 LIMIT 1`).get(...members);
|
|
733
|
+
const reason = anyEnabled ? 'has no providers with an enabled key' : 'is disabled';
|
|
734
|
+
res.status(400).json({
|
|
735
|
+
error: {
|
|
736
|
+
message: `Model '${requestedModelLabel}' ${reason}. Use 'auto' (or omit the 'model' field) to auto-route, or call /v1/models for the available list.`,
|
|
737
|
+
type: 'invalid_request_error',
|
|
738
|
+
code: 'model_not_found',
|
|
739
|
+
},
|
|
740
|
+
});
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
const sticky = getStickyModel(messages, sessionIdHeader, requestedModelLabel);
|
|
744
|
+
preferredModel = (sticky != null && groupChain.some(r => r.model_db_id === sticky)) ? sticky : undefined;
|
|
745
|
+
}
|
|
746
|
+
else {
|
|
747
|
+
const enabled = db.prepare('SELECT id FROM models WHERE model_id = ? AND enabled = 1').get(requestedModelLabel);
|
|
748
|
+
if (enabled) {
|
|
749
|
+
preferredModel = enabled.id;
|
|
750
|
+
}
|
|
751
|
+
else {
|
|
752
|
+
const disabled = db.prepare('SELECT id FROM models WHERE model_id = ?').get(requestedModelLabel);
|
|
753
|
+
const reason = disabled ? 'is disabled' : 'is not in the catalog';
|
|
754
|
+
res.status(400).json({
|
|
755
|
+
error: {
|
|
756
|
+
message: `Model '${requestedModelLabel}' ${reason}. Use 'auto' (or omit the 'model' field) to auto-route, or call /v1/models for the available list.`,
|
|
757
|
+
type: 'invalid_request_error',
|
|
758
|
+
code: 'model_not_found',
|
|
759
|
+
},
|
|
760
|
+
});
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
// Tool-bearing requests (the normal case for Codex/agent clients on this
|
|
766
|
+
// endpoint) must stay on models that emit structured tool_calls. Make the
|
|
767
|
+
// routing decision from the original Responses payload, not the subset of
|
|
768
|
+
// function tools we can forward to chat providers, because Codex may include
|
|
769
|
+
// built-in tool descriptors alongside or instead of function descriptors.
|
|
770
|
+
const wantsTools = requestDeclaresToolUse(reqData);
|
|
771
|
+
if (wantsTools && !hasEnabledToolsModel()) {
|
|
772
|
+
res.status(422).json({
|
|
773
|
+
error: {
|
|
774
|
+
message: 'This request includes tools, but no tool-capable model is enabled. Enable a tool-calling model (e.g. GPT-OSS 120B, Gemini 3.5 Flash, GLM-4.7) in the Fallback Chain.',
|
|
775
|
+
type: 'invalid_request_error',
|
|
776
|
+
code: 'no_tools_model',
|
|
777
|
+
},
|
|
778
|
+
});
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
const responseId = newId('resp');
|
|
782
|
+
const state = newFallbackState();
|
|
783
|
+
// Lets the failover loop learn which models reject tool calls (#1230).
|
|
784
|
+
state.wantsTools = wantsTools;
|
|
785
|
+
const attemptLog = [];
|
|
786
|
+
// Client-disconnect fan-out: the flag stops the loop before the NEXT
|
|
787
|
+
// attempt; the AbortController (threaded to the provider as
|
|
788
|
+
// CompletionOptions.signal) additionally cancels the IN-FLIGHT upstream
|
|
789
|
+
// fetch and any body/stream read, so tokens stop burning and the in-flight
|
|
790
|
+
// lease frees immediately. 'close' also fires on normal completion —
|
|
791
|
+
// writableEnded distinguishes a real disconnect.
|
|
792
|
+
let clientGone = false;
|
|
793
|
+
const clientAbort = new AbortController();
|
|
794
|
+
// Fallback-v2 hedging: the loop aborts this controller (via abortInFlight)
|
|
795
|
+
// when the wall-clock retry budget expires mid-attempt, canceling the
|
|
796
|
+
// in-flight upstream instead of waiting for a stalled attempt to time out.
|
|
797
|
+
const hedgeAbort = new AbortController();
|
|
798
|
+
res.on('close', () => {
|
|
799
|
+
if (!res.writableEnded) {
|
|
800
|
+
clientGone = true;
|
|
801
|
+
clientAbort.abort(newClientAbortError());
|
|
802
|
+
}
|
|
803
|
+
});
|
|
804
|
+
const dispatchOpts = { ...completionOpts, signal: AbortSignal.any([clientAbort.signal, hedgeAbort.signal]) };
|
|
805
|
+
// ── Fusion: Responses-API virtual model ────────────────────────────────
|
|
806
|
+
// `fusion` is intentionally not a catalog row. The chat route handles this
|
|
807
|
+
// virtual id before normal model dispatch; do the same here after the common
|
|
808
|
+
// Responses translation/guardrails so Codex and other Responses clients can
|
|
809
|
+
// opt into the exact same panel + judge behavior.
|
|
810
|
+
if (isFusionModel(requestedModelLabel)) {
|
|
811
|
+
const fusionConfig = reqData.fusion ?? {};
|
|
812
|
+
const fusionOptions = { ...dispatchOpts };
|
|
813
|
+
if (!stream) {
|
|
814
|
+
try {
|
|
815
|
+
const result = await runFusion({
|
|
816
|
+
messages,
|
|
817
|
+
config: fusionConfig,
|
|
818
|
+
options: fusionOptions,
|
|
819
|
+
estimatedTokens: estimatedTotal,
|
|
820
|
+
vision: hasImage,
|
|
821
|
+
});
|
|
822
|
+
enforceFusionResponseFormat(result, samplingParams.response_format);
|
|
823
|
+
res.setHeader('X-Routed-Via', safeHeaderValue(result.routedVia));
|
|
824
|
+
res.json(buildFusionResponseObject(responseId, result));
|
|
825
|
+
}
|
|
826
|
+
catch (err) {
|
|
827
|
+
if (err instanceof FusionError) {
|
|
828
|
+
res.status(err.status).json({
|
|
829
|
+
error: {
|
|
830
|
+
message: err.message,
|
|
831
|
+
type: err.status === 429 ? 'rate_limit_error' : err.status >= 500 ? 'server_error' : 'invalid_request_error',
|
|
832
|
+
},
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
else {
|
|
836
|
+
res.status(502).json({
|
|
837
|
+
error: {
|
|
838
|
+
message: `fusion error: ${sanitizeProviderErrorMessage(err?.message)}`,
|
|
839
|
+
type: 'server_error',
|
|
840
|
+
},
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
// A streamed Fusion judge can emit deltas through runFusion's hook. Panel
|
|
847
|
+
// and best-of/single-survivor paths remain valid Responses streams too:
|
|
848
|
+
// they simply emit the completed answer once the fan-out settles.
|
|
849
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
850
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
851
|
+
res.setHeader('Connection', 'keep-alive');
|
|
852
|
+
// The detailed panel is only known after runFusion settles, but the
|
|
853
|
+
// standard header can still identify this stream as Fusion before the
|
|
854
|
+
// response is committed. The final metadata carries panel/judge details.
|
|
855
|
+
res.setHeader('X-Routed-Via', FUSION_MODEL_ID);
|
|
856
|
+
let fusionSequence = 0;
|
|
857
|
+
const fusionSse = (event, payload) => {
|
|
858
|
+
if (res.writableEnded)
|
|
859
|
+
return;
|
|
860
|
+
try {
|
|
861
|
+
res.write(`event: ${event}\n`);
|
|
862
|
+
res.write(`data: ${JSON.stringify({ type: event, sequence_number: fusionSequence++, ...payload })}\n\n`);
|
|
863
|
+
}
|
|
864
|
+
catch {
|
|
865
|
+
// The close listener aborts the provider signal; a late write can still
|
|
866
|
+
// race the socket teardown and is intentionally ignored.
|
|
867
|
+
}
|
|
868
|
+
};
|
|
869
|
+
const skeleton = {
|
|
870
|
+
id: responseId,
|
|
871
|
+
object: 'response',
|
|
872
|
+
created_at: nowUnix(),
|
|
873
|
+
status: 'in_progress',
|
|
874
|
+
model: FUSION_MODEL_ID,
|
|
875
|
+
output: [],
|
|
876
|
+
output_text: '',
|
|
877
|
+
metadata: { fusion: 'true' },
|
|
878
|
+
};
|
|
879
|
+
fusionSse('response.created', { response: skeleton });
|
|
880
|
+
fusionSse('response.in_progress', { response: skeleton });
|
|
881
|
+
// Fusion fans out to several upstream models before the judge can emit a
|
|
882
|
+
// token. That gap can exceed the ~15s idle timeout used by Codex's
|
|
883
|
+
// Responses client even though the HTTP connection is healthy. A standard
|
|
884
|
+
// in-progress event keeps the client-side event watchdog alive; comments
|
|
885
|
+
// are ignored by Codex's watchdog even though they refresh curl's stream.
|
|
886
|
+
const fusionHeartbeat = setInterval(() => {
|
|
887
|
+
if (res.writableEnded || res.destroyed)
|
|
888
|
+
return;
|
|
889
|
+
try {
|
|
890
|
+
fusionSse('response.in_progress', { response: skeleton });
|
|
891
|
+
}
|
|
892
|
+
catch {
|
|
893
|
+
// The close listener aborts the in-flight fan-out; a late heartbeat
|
|
894
|
+
// racing socket teardown is harmless.
|
|
895
|
+
}
|
|
896
|
+
}, 5000);
|
|
897
|
+
fusionHeartbeat.unref?.();
|
|
898
|
+
let textItemId = null;
|
|
899
|
+
const textOutputIndex = 0;
|
|
900
|
+
let streamedText = '';
|
|
901
|
+
const openTextItem = (id = newId('msg')) => {
|
|
902
|
+
if (textItemId !== null)
|
|
903
|
+
return;
|
|
904
|
+
textItemId = id;
|
|
905
|
+
fusionSse('response.output_item.added', {
|
|
906
|
+
output_index: textOutputIndex,
|
|
907
|
+
item: { id, type: 'message', status: 'in_progress', role: 'assistant', content: [] },
|
|
908
|
+
});
|
|
909
|
+
fusionSse('response.content_part.added', {
|
|
910
|
+
item_id: id,
|
|
911
|
+
output_index: textOutputIndex,
|
|
912
|
+
content_index: 0,
|
|
913
|
+
part: { type: 'output_text', text: '', annotations: [] },
|
|
914
|
+
});
|
|
915
|
+
};
|
|
916
|
+
try {
|
|
917
|
+
const result = await runFusion({
|
|
918
|
+
messages,
|
|
919
|
+
config: fusionConfig,
|
|
920
|
+
options: fusionOptions,
|
|
921
|
+
estimatedTokens: estimatedTotal,
|
|
922
|
+
vision: hasImage,
|
|
923
|
+
hooks: {
|
|
924
|
+
onJudgeDelta: (delta) => {
|
|
925
|
+
if (!delta)
|
|
926
|
+
return;
|
|
927
|
+
openTextItem();
|
|
928
|
+
fusionSse('response.output_text.delta', {
|
|
929
|
+
item_id: textItemId,
|
|
930
|
+
output_index: textOutputIndex,
|
|
931
|
+
content_index: 0,
|
|
932
|
+
delta,
|
|
933
|
+
});
|
|
934
|
+
streamedText += delta;
|
|
935
|
+
},
|
|
936
|
+
},
|
|
937
|
+
});
|
|
938
|
+
// Like the chat route's streamed Fusion branch, do not run a structured
|
|
939
|
+
// output verdict after judge deltas have already reached the client: a
|
|
940
|
+
// failed verdict could not retract the partial answer. Non-streaming
|
|
941
|
+
// Fusion is checked above before the response is committed.
|
|
942
|
+
const responseObject = buildFusionResponseObject(responseId, result);
|
|
943
|
+
const outputs = Array.isArray(responseObject.output) ? responseObject.output : [];
|
|
944
|
+
const messageOutput = outputs.find((item) => item?.type === 'message');
|
|
945
|
+
const finalText = typeof responseObject.output_text === 'string' ? responseObject.output_text : '';
|
|
946
|
+
if (messageOutput) {
|
|
947
|
+
// Keep the final Response object's message id aligned with any judge
|
|
948
|
+
// deltas already emitted on the wire.
|
|
949
|
+
if (textItemId === null)
|
|
950
|
+
openTextItem(messageOutput.id);
|
|
951
|
+
else
|
|
952
|
+
messageOutput.id = textItemId;
|
|
953
|
+
if (finalText && !streamedText) {
|
|
954
|
+
fusionSse('response.output_text.delta', {
|
|
955
|
+
item_id: textItemId,
|
|
956
|
+
output_index: textOutputIndex,
|
|
957
|
+
content_index: 0,
|
|
958
|
+
delta: finalText,
|
|
959
|
+
});
|
|
960
|
+
streamedText = finalText;
|
|
961
|
+
}
|
|
962
|
+
else if (finalText.length > streamedText.length && finalText.startsWith(streamedText)) {
|
|
963
|
+
fusionSse('response.output_text.delta', {
|
|
964
|
+
item_id: textItemId,
|
|
965
|
+
output_index: textOutputIndex,
|
|
966
|
+
content_index: 0,
|
|
967
|
+
delta: finalText.slice(streamedText.length),
|
|
968
|
+
});
|
|
969
|
+
streamedText = finalText;
|
|
970
|
+
}
|
|
971
|
+
fusionSse('response.output_text.done', {
|
|
972
|
+
item_id: textItemId,
|
|
973
|
+
output_index: textOutputIndex,
|
|
974
|
+
content_index: 0,
|
|
975
|
+
text: finalText,
|
|
976
|
+
});
|
|
977
|
+
fusionSse('response.content_part.done', {
|
|
978
|
+
item_id: textItemId,
|
|
979
|
+
output_index: textOutputIndex,
|
|
980
|
+
content_index: 0,
|
|
981
|
+
part: { type: 'output_text', text: finalText, annotations: [] },
|
|
982
|
+
});
|
|
983
|
+
fusionSse('response.output_item.done', {
|
|
984
|
+
output_index: textOutputIndex,
|
|
985
|
+
item: messageOutput,
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
let outputIndex = messageOutput ? 1 : 0;
|
|
989
|
+
for (const output of outputs) {
|
|
990
|
+
if (output?.type !== 'function_call')
|
|
991
|
+
continue;
|
|
992
|
+
const item = { ...output, status: 'in_progress', arguments: '' };
|
|
993
|
+
fusionSse('response.output_item.added', { output_index: outputIndex, item });
|
|
994
|
+
if (output.arguments) {
|
|
995
|
+
fusionSse('response.function_call_arguments.delta', {
|
|
996
|
+
item_id: output.id,
|
|
997
|
+
output_index: outputIndex,
|
|
998
|
+
delta: output.arguments,
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
fusionSse('response.function_call_arguments.done', {
|
|
1002
|
+
item_id: output.id,
|
|
1003
|
+
output_index: outputIndex,
|
|
1004
|
+
arguments: output.arguments ?? '',
|
|
1005
|
+
});
|
|
1006
|
+
fusionSse('response.output_item.done', { output_index: outputIndex, item: output });
|
|
1007
|
+
outputIndex++;
|
|
1008
|
+
}
|
|
1009
|
+
fusionSse('response.completed', { response: responseObject });
|
|
1010
|
+
res.end();
|
|
1011
|
+
}
|
|
1012
|
+
catch (err) {
|
|
1013
|
+
const message = err instanceof FusionError
|
|
1014
|
+
? err.message
|
|
1015
|
+
: `fusion error: ${sanitizeProviderErrorMessage(err?.message)}`;
|
|
1016
|
+
const type = err instanceof FusionError
|
|
1017
|
+
? (err.status === 429 ? 'rate_limit_error' : err.status >= 500 ? 'server_error' : 'invalid_request_error')
|
|
1018
|
+
: 'server_error';
|
|
1019
|
+
fusionSse('response.failed', {
|
|
1020
|
+
response: {
|
|
1021
|
+
...skeleton,
|
|
1022
|
+
status: 'failed',
|
|
1023
|
+
error: { message, type },
|
|
1024
|
+
},
|
|
1025
|
+
});
|
|
1026
|
+
res.end();
|
|
1027
|
+
}
|
|
1028
|
+
finally {
|
|
1029
|
+
clearInterval(fusionHeartbeat);
|
|
1030
|
+
}
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
// Stream bookkeeping (used only when stream === true). `streamStarted` is the
|
|
1034
|
+
// commit flag: true once the response.created/in_progress skeleton has left,
|
|
1035
|
+
// after which failover is no longer possible. seq/streamStarted span attempts
|
|
1036
|
+
// so the SSE sequence numbers stay monotonic and a committed stream can't be
|
|
1037
|
+
// re-committed by a later attempt.
|
|
1038
|
+
let seq = 0;
|
|
1039
|
+
let streamStarted = false;
|
|
1040
|
+
const sse = (event, payload) => {
|
|
1041
|
+
res.write(`event: ${event}\n`);
|
|
1042
|
+
res.write(`data: ${JSON.stringify({ type: event, sequence_number: seq++, ...payload })}\n\n`);
|
|
1043
|
+
};
|
|
1044
|
+
await runFallbackLoop({
|
|
1045
|
+
maxRetries: MAX_RETRIES,
|
|
1046
|
+
state,
|
|
1047
|
+
attemptLog,
|
|
1048
|
+
logIdentity: { surface: 'responses', requestId: requestGroupId, requestedModel: requestedModelLabel },
|
|
1049
|
+
clientGone: () => clientGone,
|
|
1050
|
+
abortInFlight: () => hedgeAbort.abort(newHedgeAbortError()),
|
|
1051
|
+
route: () => {
|
|
1052
|
+
// Task-type routing (#1127): same header/derivation as /chat/completions.
|
|
1053
|
+
const taskType = resolveTaskType(req, tools, messages);
|
|
1054
|
+
// #507: inflate the routing estimate from any provider-reported REQUESTED
|
|
1055
|
+
// size latched onto state so the existing size gates in router.ts skip
|
|
1056
|
+
// low-TPM / small-context models on retry.
|
|
1057
|
+
const routingTotal = fallbackRoutingTokens(state, estimatedTotal, outputReserve);
|
|
1058
|
+
return routeRequest(routingTotal, state.skipKeys.size > 0 ? state.skipKeys : undefined, preferredModel, hasImage, wantsTools, state.skipModels.size > 0 ? state.skipModels : undefined, groupChain ?? resolvedChain?.chain, completionOpts.response_format !== undefined, state.skipPlatforms.size > 0 ? state.skipPlatforms : undefined, outputReserve, taskType);
|
|
1059
|
+
},
|
|
1060
|
+
dispatch: async (route, attempt, ctx) => {
|
|
1061
|
+
const contextBudget = routeOutputBudget(route, estimatedInputTokens);
|
|
1062
|
+
const routeOpts = contextBudget != null ? { ...dispatchOpts, contextBudget } : dispatchOpts;
|
|
1063
|
+
traceRouteEvent('Responses', {
|
|
1064
|
+
event: attempt === 0 ? 'start' : 'next',
|
|
1065
|
+
requestId: requestGroupId,
|
|
1066
|
+
attempt,
|
|
1067
|
+
platform: route.platform,
|
|
1068
|
+
model: route.modelId,
|
|
1069
|
+
requestedModel: attempt === 0 ? requestedModelLabel : undefined,
|
|
1070
|
+
});
|
|
1071
|
+
if (stream) {
|
|
1072
|
+
// Every output item (message text + each function_call) claims the next
|
|
1073
|
+
// free index. OpenAI's streaming SDK indexes snapshot.output by
|
|
1074
|
+
// output_index, so indices MUST be dense & unique — reusing 0 for the
|
|
1075
|
+
// text item after a tool-call item had already taken it makes the SDK
|
|
1076
|
+
// crash on `snapshot.output[output_index]` (#96, Codex computer-use).
|
|
1077
|
+
let outputIndex = 0;
|
|
1078
|
+
let msgItemId = null;
|
|
1079
|
+
let msgText = '';
|
|
1080
|
+
// output_index of the open text item (valid while msgItemId !== null).
|
|
1081
|
+
let textOutputIndex = 0;
|
|
1082
|
+
// tool-call accumulator keyed by the provider's tool_call index
|
|
1083
|
+
const toolAcc = new Map();
|
|
1084
|
+
let totalOutputTokens = 0;
|
|
1085
|
+
// #764: thinking tokens are tracked separately so the final Response
|
|
1086
|
+
// object can report `output_tokens_details.reasoning_tokens` truthfully
|
|
1087
|
+
// instead of a hardcoded 0.
|
|
1088
|
+
let totalReasoningTokens = 0;
|
|
1089
|
+
// #764: ttfb = first token of ANY kind (content or reasoning), recorded
|
|
1090
|
+
// in the pump loop; commit() only backfills streams that never produced
|
|
1091
|
+
// one. This path previously logged no ttfb at all, so Analytics showed
|
|
1092
|
+
// a null speed for every /v1/responses turn.
|
|
1093
|
+
let ttfbMs = null;
|
|
1094
|
+
// Inline-dialect hold window (#231): first text is held until it
|
|
1095
|
+
// either matches a tool-call dialect marker (held to the end and
|
|
1096
|
+
// rescued into function_call items) or provably cannot (flushed and
|
|
1097
|
+
// streamed normally). Mirrors the /chat/completions stream loop.
|
|
1098
|
+
let dialectMode = 'undecided';
|
|
1099
|
+
let heldText = '';
|
|
1100
|
+
let upstreamFinish = null;
|
|
1101
|
+
// Commit point: headers + the response.created/in_progress skeleton go
|
|
1102
|
+
// out only when the first MEANINGFUL output item is about to be emitted
|
|
1103
|
+
// (converged with /chat/completions — responses previously committed on
|
|
1104
|
+
// the first raw chunk, even a role-only one). Until then a connect-time
|
|
1105
|
+
// error, an empty completion, or an unparseable dialect turn fails over
|
|
1106
|
+
// on the same connection with no bytes on the wire. Idempotent.
|
|
1107
|
+
const commit = () => {
|
|
1108
|
+
if (streamStarted)
|
|
1109
|
+
return;
|
|
1110
|
+
if (ttfbMs === null)
|
|
1111
|
+
ttfbMs = Date.now() - start;
|
|
1112
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
1113
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
1114
|
+
res.setHeader('Connection', 'keep-alive');
|
|
1115
|
+
res.setHeader('X-Routed-Via', routedViaValue(route.platform, route.modelId));
|
|
1116
|
+
setFallbackHeaders(res, attempt, attemptLog);
|
|
1117
|
+
const skeleton = {
|
|
1118
|
+
id: responseId, object: 'response', created_at: nowUnix(),
|
|
1119
|
+
status: 'in_progress', model: route.modelId, output: [], output_text: '',
|
|
1120
|
+
};
|
|
1121
|
+
sse('response.created', { response: skeleton });
|
|
1122
|
+
sse('response.in_progress', { response: skeleton });
|
|
1123
|
+
streamStarted = true;
|
|
1124
|
+
// Committed: the answer is on its way, so the retry budget must no
|
|
1125
|
+
// longer cancel this attempt (it could not fail over now anyway).
|
|
1126
|
+
ctx.disarmHedge();
|
|
1127
|
+
};
|
|
1128
|
+
// Open the text output item and stream `text` as its first delta.
|
|
1129
|
+
// The text item takes the next free output index (it is NOT always 0 —
|
|
1130
|
+
// when the model emits tool_calls first, the function_call items own
|
|
1131
|
+
// the low indices). Every later text delta/done must reference this
|
|
1132
|
+
// same index or the SDK snapshot lookup misroutes the deltas.
|
|
1133
|
+
const openTextItem = (text) => {
|
|
1134
|
+
commit();
|
|
1135
|
+
msgItemId = newId('msg');
|
|
1136
|
+
textOutputIndex = outputIndex++;
|
|
1137
|
+
sse('response.output_item.added', {
|
|
1138
|
+
output_index: textOutputIndex,
|
|
1139
|
+
item: { id: msgItemId, type: 'message', status: 'in_progress', role: 'assistant', content: [] },
|
|
1140
|
+
});
|
|
1141
|
+
sse('response.content_part.added', {
|
|
1142
|
+
item_id: msgItemId, output_index: textOutputIndex, content_index: 0,
|
|
1143
|
+
part: { type: 'output_text', text: '', annotations: [] },
|
|
1144
|
+
});
|
|
1145
|
+
if (text) {
|
|
1146
|
+
sse('response.output_text.delta', { item_id: msgItemId, output_index: textOutputIndex, content_index: 0, delta: text });
|
|
1147
|
+
msgText += text;
|
|
1148
|
+
}
|
|
1149
|
+
};
|
|
1150
|
+
try {
|
|
1151
|
+
const gen = route.provider.streamChatCompletion(route.apiKey, messages, route.modelId, routeOpts, quotaContextForRoute(route, 'responses'));
|
|
1152
|
+
for await (const chunk of gen) {
|
|
1153
|
+
if (clientGone)
|
|
1154
|
+
break; // client hung up: stop pulling; reader.cancel() aborts upstream
|
|
1155
|
+
// In-band upstream error frame ({"error":...} inside a 200 SSE
|
|
1156
|
+
// stream — observed live from Groq). Throwing hands it to the catch
|
|
1157
|
+
// below: pre-commit it fails over, post-commit it surfaces
|
|
1158
|
+
// response.failed.
|
|
1159
|
+
const anyChunk = chunk;
|
|
1160
|
+
if (anyChunk.error && !anyChunk.choices) {
|
|
1161
|
+
throw new Error(`in-band provider error from ${route.displayName}: ${anyChunk.error.message ?? 'provider error'}`);
|
|
1162
|
+
}
|
|
1163
|
+
const choice0 = chunk.choices?.[0];
|
|
1164
|
+
if (choice0?.finish_reason)
|
|
1165
|
+
upstreamFinish = choice0.finish_reason;
|
|
1166
|
+
const delta = choice0?.delta;
|
|
1167
|
+
if (!delta)
|
|
1168
|
+
continue;
|
|
1169
|
+
// Text deltas → output_text events on a single message item, after
|
|
1170
|
+
// the dialect hold window has decided the text is real prose.
|
|
1171
|
+
const text = delta.content ?? '';
|
|
1172
|
+
// #764: reasoning models stream thinking before the first answer
|
|
1173
|
+
// token — count that first token as ttfb so Analytics speed
|
|
1174
|
+
// reflects the real head-of-stream, and count thinking tokens as
|
|
1175
|
+
// real output consumption.
|
|
1176
|
+
const reasoning = streamReasoningText(chunk);
|
|
1177
|
+
if (ttfbMs === null && (text.length > 0 || reasoning.length > 0)) {
|
|
1178
|
+
ttfbMs = Date.now() - start;
|
|
1179
|
+
}
|
|
1180
|
+
if (text) {
|
|
1181
|
+
totalOutputTokens += Math.ceil((text.length + reasoning.length) / 4);
|
|
1182
|
+
if (reasoning.length > 0)
|
|
1183
|
+
totalReasoningTokens += Math.ceil(reasoning.length / 4);
|
|
1184
|
+
if (dialectMode === 'passthrough') {
|
|
1185
|
+
if (msgItemId === null)
|
|
1186
|
+
openTextItem('');
|
|
1187
|
+
sse('response.output_text.delta', {
|
|
1188
|
+
item_id: msgItemId, output_index: textOutputIndex, content_index: 0, delta: text,
|
|
1189
|
+
});
|
|
1190
|
+
msgText += text;
|
|
1191
|
+
}
|
|
1192
|
+
else {
|
|
1193
|
+
heldText += text;
|
|
1194
|
+
if (dialectMode === 'undecided') {
|
|
1195
|
+
const probe = heldText.trimStart();
|
|
1196
|
+
if (startsWithDialectMarker(probe)) {
|
|
1197
|
+
dialectMode = 'dialect';
|
|
1198
|
+
}
|
|
1199
|
+
else if (!couldBecomeDialectMarker(probe) || heldText.length > 256) {
|
|
1200
|
+
dialectMode = 'passthrough';
|
|
1201
|
+
openTextItem(heldText);
|
|
1202
|
+
heldText = '';
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
else if (reasoning.length > 0) {
|
|
1208
|
+
// #764: thinking-only chunk (no visible text yet) — count tokens.
|
|
1209
|
+
totalOutputTokens += Math.ceil(reasoning.length / 4);
|
|
1210
|
+
totalReasoningTokens += Math.ceil(reasoning.length / 4);
|
|
1211
|
+
}
|
|
1212
|
+
// Tool-call deltas → function_call item + argument deltas.
|
|
1213
|
+
for (const tc of delta.tool_calls ?? []) {
|
|
1214
|
+
const idx = tc.index ?? 0;
|
|
1215
|
+
let acc = toolAcc.get(idx);
|
|
1216
|
+
if (!acc) {
|
|
1217
|
+
// First time we see this tool call: open a new output item.
|
|
1218
|
+
commit();
|
|
1219
|
+
if (msgItemId !== null && msgText.length > 0) {
|
|
1220
|
+
// close the text item (at its own output index) before starting a function_call item
|
|
1221
|
+
sse('response.output_text.done', { item_id: msgItemId, output_index: textOutputIndex, content_index: 0, text: msgText });
|
|
1222
|
+
sse('response.content_part.done', { item_id: msgItemId, output_index: textOutputIndex, content_index: 0, part: { type: 'output_text', text: msgText, annotations: [] } });
|
|
1223
|
+
sse('response.output_item.done', { output_index: textOutputIndex, item: { id: msgItemId, type: 'message', status: 'completed', role: 'assistant', content: [{ type: 'output_text', text: msgText, annotations: [] }] } });
|
|
1224
|
+
msgItemId = null;
|
|
1225
|
+
}
|
|
1226
|
+
acc = { outputIndex: outputIndex++, itemId: newId('fc'), callId: tc.id || newId('call'), name: tc.function?.name ?? '', args: '' };
|
|
1227
|
+
toolAcc.set(idx, acc);
|
|
1228
|
+
sse('response.output_item.added', {
|
|
1229
|
+
output_index: acc.outputIndex,
|
|
1230
|
+
item: { id: acc.itemId, type: 'function_call', status: 'in_progress', call_id: acc.callId, name: acc.name, arguments: '' },
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
const argFrag = tc.function?.arguments ?? '';
|
|
1234
|
+
if (tc.function?.name && !acc.name)
|
|
1235
|
+
acc.name = tc.function.name;
|
|
1236
|
+
if (argFrag) {
|
|
1237
|
+
acc.args += argFrag;
|
|
1238
|
+
sse('response.function_call_arguments.delta', { item_id: acc.itemId, output_index: acc.outputIndex, delta: argFrag });
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
// Resolve the dialect hold window now that the full text is known.
|
|
1243
|
+
// Held text was never emitted, so a dead dialect turn can still fail
|
|
1244
|
+
// over on the same SSE stream (nothing has been committed yet).
|
|
1245
|
+
if (heldText.length > 0) {
|
|
1246
|
+
const rescue = (wantsTools && (dialectMode === 'dialect' || containsDialectMarker(heldText)))
|
|
1247
|
+
? rescueInlineToolCalls(heldText, new Set((tools ?? []).map(t => t.function.name)))
|
|
1248
|
+
: { detected: false, calls: null, cleanText: heldText };
|
|
1249
|
+
if (rescue.detected && !rescue.calls) {
|
|
1250
|
+
// Unparseable dialect turn: throw so the shared loop cooldowns this
|
|
1251
|
+
// model+key and fails over (streamStarted is still false).
|
|
1252
|
+
throw new Error(`unparseable inline tool-call dialect from ${route.displayName}: ${heldText.slice(0, 120)}`);
|
|
1253
|
+
}
|
|
1254
|
+
if (rescue.detected && rescue.calls) {
|
|
1255
|
+
// Rescued calls become function_call items, exactly as if the
|
|
1256
|
+
// provider had streamed them structurally.
|
|
1257
|
+
console.log(`[Responses] Rescued ${rescue.calls.length} inline tool call(s) from ${route.displayName}`);
|
|
1258
|
+
if (rescue.cleanText.length > 0 && msgItemId === null)
|
|
1259
|
+
openTextItem(rescue.cleanText);
|
|
1260
|
+
let rescuedIdx = 0;
|
|
1261
|
+
for (const c of rescue.calls) {
|
|
1262
|
+
const idx = 1000 + rescuedIdx++; // synthetic accumulator keys, past any provider index
|
|
1263
|
+
commit();
|
|
1264
|
+
const acc = {
|
|
1265
|
+
outputIndex: outputIndex++,
|
|
1266
|
+
itemId: newId('fc'), callId: newId('call'), name: c.name, args: c.arguments,
|
|
1267
|
+
};
|
|
1268
|
+
toolAcc.set(idx, acc);
|
|
1269
|
+
sse('response.output_item.added', {
|
|
1270
|
+
output_index: acc.outputIndex,
|
|
1271
|
+
item: { id: acc.itemId, type: 'function_call', status: 'in_progress', call_id: acc.callId, name: acc.name, arguments: '' },
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
else if (msgItemId === null) {
|
|
1276
|
+
// Plain short answer that never left the hold window (e.g. "Hi").
|
|
1277
|
+
openTextItem(heldText);
|
|
1278
|
+
}
|
|
1279
|
+
heldText = '';
|
|
1280
|
+
}
|
|
1281
|
+
// Empty completion — the provider returned 200 with no text AND no
|
|
1282
|
+
// tool calls. Seen in production from nemotron-3-super on ~65k-token
|
|
1283
|
+
// contexts: transport-level "success", zero usable output. Nothing has
|
|
1284
|
+
// been committed yet (the skeleton is lazy), so throwing lets the
|
|
1285
|
+
// shared loop fail over to the next model on the same SSE connection.
|
|
1286
|
+
if (msgText.length === 0 && toolAcc.size === 0) {
|
|
1287
|
+
// Disconnect before the commit point: the break above fired with
|
|
1288
|
+
// nothing accumulated — CLIENT behavior, not a provider failure.
|
|
1289
|
+
// Falling through to the empty-completion throw benched a healthy
|
|
1290
|
+
// model+key for 90s on every Ctrl-C during a reasoning model's
|
|
1291
|
+
// TTFB window.
|
|
1292
|
+
if (clientGone && !streamStarted) {
|
|
1293
|
+
console.log(`[Responses] client disconnected before first token from ${route.displayName} — dropping attempt without benching`);
|
|
1294
|
+
traceRouteEvent('Responses', {
|
|
1295
|
+
event: 'canceled',
|
|
1296
|
+
requestId: requestGroupId,
|
|
1297
|
+
attempt,
|
|
1298
|
+
platform: route.platform,
|
|
1299
|
+
model: route.modelId,
|
|
1300
|
+
});
|
|
1301
|
+
return 'committed';
|
|
1302
|
+
}
|
|
1303
|
+
// finish_reason 'length' = the model spent the whole output budget
|
|
1304
|
+
// on hidden reasoning before any visible text: fail over, but skip
|
|
1305
|
+
// the cooldown/penalty (not a provider-health signal).
|
|
1306
|
+
throw Object.assign(new Error(`empty completion from ${route.displayName}`), upstreamFinish === 'length' ? { skipBench: true } : {});
|
|
1307
|
+
}
|
|
1308
|
+
// Finalize any open text item.
|
|
1309
|
+
if (msgItemId !== null) {
|
|
1310
|
+
sse('response.output_text.done', { item_id: msgItemId, output_index: textOutputIndex, content_index: 0, text: msgText });
|
|
1311
|
+
sse('response.content_part.done', { item_id: msgItemId, output_index: textOutputIndex, content_index: 0, part: { type: 'output_text', text: msgText, annotations: [] } });
|
|
1312
|
+
sse('response.output_item.done', { output_index: textOutputIndex, item: { id: msgItemId, type: 'message', status: 'completed', role: 'assistant', content: [{ type: 'output_text', text: msgText, annotations: [] }] } });
|
|
1313
|
+
}
|
|
1314
|
+
// Finalize tool-call items. Arguments are repaired against the tool's
|
|
1315
|
+
// parameter schema at this point (after the full string accumulated):
|
|
1316
|
+
// models like GLM double-encode nested arrays/objects as strings, and
|
|
1317
|
+
// Codex hard-rejects the call ("invalid type: string, expected a
|
|
1318
|
+
// sequence"). Clients consume the *.done events / final response for
|
|
1319
|
+
// arguments, so repairing here covers the streamed path too.
|
|
1320
|
+
//
|
|
1321
|
+
// No schema verdict here, deliberately. This surface commits on the
|
|
1322
|
+
// FIRST tool-call delta (`commit()` above, where the function_call
|
|
1323
|
+
// item is opened) — the arguments are not complete until this point,
|
|
1324
|
+
// which is long after the skeleton and the item.added events have
|
|
1325
|
+
// left. A verdict here could only turn a delivered-but-invalid call
|
|
1326
|
+
// into a `response.failed` on a stream the client is already reading,
|
|
1327
|
+
// which is strictly worse. Wiring it would mean holding the
|
|
1328
|
+
// function_call item until its arguments finish, the way
|
|
1329
|
+
// /chat/completions buffers — a change to this route's commit point,
|
|
1330
|
+
// not to validation.
|
|
1331
|
+
const finalToolCalls = [];
|
|
1332
|
+
for (const acc of toolAcc.values()) {
|
|
1333
|
+
const repairedArgs = repairToolArguments(acc.args, toolSchemas.get(acc.name));
|
|
1334
|
+
sse('response.function_call_arguments.done', { item_id: acc.itemId, output_index: acc.outputIndex, arguments: repairedArgs });
|
|
1335
|
+
sse('response.output_item.done', { output_index: acc.outputIndex, item: { id: acc.itemId, type: 'function_call', status: 'completed', call_id: acc.callId, name: acc.name, arguments: repairedArgs } });
|
|
1336
|
+
finalToolCalls.push({ id: acc.callId, type: 'function', function: { name: acc.name, arguments: repairedArgs } });
|
|
1337
|
+
}
|
|
1338
|
+
const finalResponse = buildResponseObject({
|
|
1339
|
+
id: responseId, model: route.modelId, text: msgText,
|
|
1340
|
+
toolCalls: finalToolCalls, promptTokens: estimatedInputTokens, completionTokens: totalOutputTokens,
|
|
1341
|
+
reasoningTokens: totalReasoningTokens,
|
|
1342
|
+
});
|
|
1343
|
+
sse('response.completed', { response: finalResponse });
|
|
1344
|
+
res.end();
|
|
1345
|
+
recordUpstreamSuccess(route, estimatedInputTokens + totalOutputTokens, state);
|
|
1346
|
+
setStickyModel(messages, route.modelDbId, sessionIdHeader, stickyStrategyKey);
|
|
1347
|
+
traceRouteEvent('Responses', {
|
|
1348
|
+
event: 'ok',
|
|
1349
|
+
requestId: requestGroupId,
|
|
1350
|
+
attempt,
|
|
1351
|
+
platform: route.platform,
|
|
1352
|
+
model: route.modelId,
|
|
1353
|
+
latencyMs: Date.now() - start,
|
|
1354
|
+
inputTokens: estimatedInputTokens,
|
|
1355
|
+
outputTokens: totalOutputTokens,
|
|
1356
|
+
});
|
|
1357
|
+
logRequest(route.platform, route.modelId, route.keyId, 'success', estimatedInputTokens, totalOutputTokens, Date.now() - start, null, ttfbMs, null, null, 'http');
|
|
1358
|
+
return 'done';
|
|
1359
|
+
}
|
|
1360
|
+
catch (streamErr) {
|
|
1361
|
+
// Client abort mid-stream: the pump's own `if (clientGone) break`
|
|
1362
|
+
// can lose the race against the fetch-signal rejection, so the
|
|
1363
|
+
// abort may surface here instead. Rethrow — the shared loop's
|
|
1364
|
+
// client-abort branch stops the ladder without benching or an
|
|
1365
|
+
// error log row (the socket is gone; nothing to render).
|
|
1366
|
+
if (isClientAbortError(streamErr))
|
|
1367
|
+
throw streamErr;
|
|
1368
|
+
// A committed stream can't fail over (bytes already sent) — surface a
|
|
1369
|
+
// response.failed event honestly and stop. A pre-commit failure throws
|
|
1370
|
+
// through to the shared loop for cooldown + failover.
|
|
1371
|
+
if (streamStarted) {
|
|
1372
|
+
const safe = sanitizeProviderErrorMessage(streamErr.message);
|
|
1373
|
+
traceRouteEvent('Responses', {
|
|
1374
|
+
event: 'fail',
|
|
1375
|
+
requestId: requestGroupId,
|
|
1376
|
+
attempt,
|
|
1377
|
+
platform: route.platform,
|
|
1378
|
+
model: route.modelId,
|
|
1379
|
+
latencyMs: Date.now() - start,
|
|
1380
|
+
error: safe,
|
|
1381
|
+
});
|
|
1382
|
+
logRequest(route.platform, route.modelId, route.keyId, 'error', estimatedInputTokens, 0, Date.now() - start, safe, ttfbMs, null, null, 'http');
|
|
1383
|
+
sse('response.failed', { response: { id: responseId, object: 'response', status: 'failed', error: { message: `Provider error (${route.displayName}): stream interrupted`, type: 'stream_error' } } });
|
|
1384
|
+
res.end();
|
|
1385
|
+
return 'committed';
|
|
1386
|
+
}
|
|
1387
|
+
throw streamErr;
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
const result = await route.provider.chatCompletion(route.apiKey, messages, route.modelId, routeOpts, quotaContextForRoute(route, 'responses'));
|
|
1391
|
+
const msg = result.choices[0]?.message;
|
|
1392
|
+
let text = contentToString(msg?.content ?? '');
|
|
1393
|
+
let toolCalls = (msg?.tool_calls ?? []).map((tc) => ({
|
|
1394
|
+
...tc,
|
|
1395
|
+
function: { ...tc.function, arguments: repairToolArguments(tc.function.arguments, toolSchemas.get(tc.function.name)) },
|
|
1396
|
+
}));
|
|
1397
|
+
// Inline tool-call dialect rescue (#231) — see /chat/completions.
|
|
1398
|
+
if (wantsTools && toolCalls.length === 0 && text) {
|
|
1399
|
+
const rescue = rescueInlineToolCalls(text, new Set((tools ?? []).map(t => t.function.name)));
|
|
1400
|
+
if (rescue.detected) {
|
|
1401
|
+
if (!rescue.calls) {
|
|
1402
|
+
throw new Error(`unparseable inline tool-call dialect from ${route.displayName}: ${text.slice(0, 120)}`);
|
|
1403
|
+
}
|
|
1404
|
+
console.log(`[Responses] Rescued ${rescue.calls.length} inline tool call(s) from ${route.displayName}`);
|
|
1405
|
+
toolCalls = rescue.calls.map((c, i) => ({
|
|
1406
|
+
id: `call_rescued_${i + 1}`,
|
|
1407
|
+
type: 'function',
|
|
1408
|
+
function: { name: c.name, arguments: repairToolArguments(c.arguments, toolSchemas.get(c.name)) },
|
|
1409
|
+
}));
|
|
1410
|
+
text = rescue.cleanText;
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
// Opt-in schema verdict on what the repair could not fix. Thrown before
|
|
1414
|
+
// anything is written, so the failover hop is invisible to the client.
|
|
1415
|
+
if (isToolArgumentValidationEnabled() && toolCalls.length > 0) {
|
|
1416
|
+
const invalid = invalidToolCallReasons(toolCalls, toolSchemas);
|
|
1417
|
+
if (invalid.length > 0)
|
|
1418
|
+
throw invalidToolArgumentsError(route.displayName, invalid);
|
|
1419
|
+
}
|
|
1420
|
+
const promptTokens = result.usage?.prompt_tokens ?? estimatedInputTokens;
|
|
1421
|
+
// #764: include reasoning tokens (message.reasoning_content / reasoning)
|
|
1422
|
+
// in the chars/4 estimate so thinking models aren't undercounted when the
|
|
1423
|
+
// provider omits `usage`.
|
|
1424
|
+
const completionTokens = result.usage?.completion_tokens
|
|
1425
|
+
?? Math.ceil((text.length + completionReasoningText(result).length) / 4);
|
|
1426
|
+
// #764: report reasoning_tokens truthfully — the provider's own count
|
|
1427
|
+
// when advertised, else the same chars/4 estimate of the thinking text.
|
|
1428
|
+
const reasoningTokens = result.usage?.completion_tokens_details?.reasoning_tokens
|
|
1429
|
+
?? Math.ceil(completionReasoningText(result).length / 4);
|
|
1430
|
+
// Empty completion → fail over via the shared loop (see the streaming
|
|
1431
|
+
// path); finish_reason 'length' skips the cooldown/penalty.
|
|
1432
|
+
if (!text && toolCalls.length === 0) {
|
|
1433
|
+
throw Object.assign(new Error(`empty completion from ${route.displayName}`), result.choices[0]?.finish_reason === 'length' ? { skipBench: true } : {});
|
|
1434
|
+
}
|
|
1435
|
+
// Structured-output enforcement — see /chat/completions. Heal fenced or
|
|
1436
|
+
// prose-wrapped JSON in place, fail over (skipBench + skipModelForRequest
|
|
1437
|
+
// — a sibling key would misbehave identically) when the model ignored the
|
|
1438
|
+
// requested format; finish_reason 'length' gets an honest "truncated"
|
|
1439
|
+
// class instead, since the JSON was cut off by max_tokens, not ignored.
|
|
1440
|
+
if (completionOpts.response_format && text && toolCalls.length === 0) {
|
|
1441
|
+
const enforced = enforceJsonContent(text);
|
|
1442
|
+
if (!enforced.ok) {
|
|
1443
|
+
const truncated = result.choices[0]?.finish_reason === 'length';
|
|
1444
|
+
throw Object.assign(new Error(truncated
|
|
1445
|
+
? `truncated JSON from ${route.displayName} (finish_reason=length — raise max_tokens for this ${completionOpts.response_format.type} request)`
|
|
1446
|
+
: `${route.displayName} ignored response_format (returned non-JSON despite ${completionOpts.response_format.type})`), { skipBench: true, skipModelForRequest: true });
|
|
1447
|
+
}
|
|
1448
|
+
if (enforced.healed)
|
|
1449
|
+
text = enforced.content;
|
|
1450
|
+
}
|
|
1451
|
+
// Usage fallback: a missing provider `usage` block used to record 0
|
|
1452
|
+
// tokens against the rate-limit ledger; promptTokens/completionTokens
|
|
1453
|
+
// above already carry the chars/4 estimate.
|
|
1454
|
+
recordUpstreamSuccess(route, result.usage?.total_tokens ?? (promptTokens + completionTokens), state);
|
|
1455
|
+
setStickyModel(messages, route.modelDbId, sessionIdHeader, stickyStrategyKey);
|
|
1456
|
+
res.setHeader('X-Routed-Via', routedViaValue(route.platform, route.modelId));
|
|
1457
|
+
setFallbackHeaders(res, attempt, attemptLog);
|
|
1458
|
+
res.json(buildResponseObject({
|
|
1459
|
+
id: responseId, model: route.modelId, text, toolCalls,
|
|
1460
|
+
promptTokens, completionTokens, reasoningTokens,
|
|
1461
|
+
}));
|
|
1462
|
+
traceRouteEvent('Responses', {
|
|
1463
|
+
event: 'ok',
|
|
1464
|
+
requestId: requestGroupId,
|
|
1465
|
+
attempt,
|
|
1466
|
+
platform: route.platform,
|
|
1467
|
+
model: route.modelId,
|
|
1468
|
+
latencyMs: Date.now() - start,
|
|
1469
|
+
inputTokens: promptTokens,
|
|
1470
|
+
outputTokens: completionTokens,
|
|
1471
|
+
});
|
|
1472
|
+
logRequest(route.platform, route.modelId, route.keyId, 'success', promptTokens, completionTokens, Date.now() - start, null, null, null, null, 'http');
|
|
1473
|
+
return 'done';
|
|
1474
|
+
},
|
|
1475
|
+
logFailure: (route, err, attempt) => {
|
|
1476
|
+
const latency = Date.now() - start;
|
|
1477
|
+
const safeError = sanitizeProviderErrorMessage(err.message);
|
|
1478
|
+
traceRouteEvent('Responses', {
|
|
1479
|
+
event: 'fail',
|
|
1480
|
+
requestId: requestGroupId,
|
|
1481
|
+
attempt,
|
|
1482
|
+
platform: route.platform,
|
|
1483
|
+
model: route.modelId,
|
|
1484
|
+
latencyMs: latency,
|
|
1485
|
+
error: safeError,
|
|
1486
|
+
});
|
|
1487
|
+
logRequest(route.platform, route.modelId, route.keyId, 'error', estimatedInputTokens, 0, latency, safeError, null, null, null, 'http');
|
|
1488
|
+
},
|
|
1489
|
+
onFatal: (route, err, attempt) => {
|
|
1490
|
+
setFallbackHeaders(res, attempt, attemptLog);
|
|
1491
|
+
res.status(502).json({ error: { message: `Provider error (${route.displayName}): ${sanitizeProviderErrorMessage(err.message)}`, type: 'provider_error' } });
|
|
1492
|
+
},
|
|
1493
|
+
onRoutingExhausted: (lastError, routeErr, exhaustion, info) => {
|
|
1494
|
+
if (streamStarted) {
|
|
1495
|
+
// Headers are already on the wire — the honest status/Retry-After can
|
|
1496
|
+
// only travel in the failed-event payload.
|
|
1497
|
+
sse('response.failed', { response: { id: responseId, object: 'response', status: 'failed', error: exhaustionErrorPayload(exhaustion) } });
|
|
1498
|
+
res.end();
|
|
1499
|
+
}
|
|
1500
|
+
else {
|
|
1501
|
+
setFallbackHeaders(res, info.attempts.length, info.attempts);
|
|
1502
|
+
setExhaustionHeaders(res, exhaustion);
|
|
1503
|
+
res.status(exhaustion.status).json({ error: exhaustionErrorPayload(exhaustion) });
|
|
1504
|
+
}
|
|
1505
|
+
},
|
|
1506
|
+
onExhausted: (exhaustion, info) => {
|
|
1507
|
+
// The streaming skeleton may already be on the wire — close the SSE stream
|
|
1508
|
+
// with a failed event instead of writing JSON onto a committed response.
|
|
1509
|
+
if (streamStarted) {
|
|
1510
|
+
sse('response.failed', { response: { id: responseId, object: 'response', status: 'failed', error: exhaustionErrorPayload(exhaustion) } });
|
|
1511
|
+
res.end();
|
|
1512
|
+
}
|
|
1513
|
+
else {
|
|
1514
|
+
setFallbackHeaders(res, info.attempts.length, info.attempts);
|
|
1515
|
+
setExhaustionHeaders(res, exhaustion);
|
|
1516
|
+
res.status(exhaustion.status).json({ error: exhaustionErrorPayload(exhaustion) });
|
|
1517
|
+
}
|
|
1518
|
+
},
|
|
1519
|
+
});
|
|
1520
|
+
});
|
|
1521
|
+
//# sourceMappingURL=responses.js.map
|