cartethyia 2.7.5 → 2.7.7
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/.env.example +8 -3
- package/README.md +26 -2
- package/dist/dashboard/assets/BannedPage-B05aF8Dg.js +1 -0
- package/dist/dashboard/assets/BannedPage-B05aF8Dg.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-CLUhmjUy.js +3 -0
- package/dist/dashboard/assets/CliToolDetailPage-CLUhmjUy.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-CQmrqqrk.js +1 -0
- package/dist/dashboard/assets/CliToolsPage-CQmrqqrk.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-CHG_1Wr3.js +1 -0
- package/dist/dashboard/assets/CombosPage-CHG_1Wr3.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-D5rol2j2.js → ConsoleLogPage-DPgbklad.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-DPgbklad.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-BKj_r0rp.js → CustomizationPage-pnfAqr_-.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-pnfAqr_-.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-DGJuU2A6.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-DGJuU2A6.js.gz +0 -0
- package/dist/dashboard/assets/{InjectSystemPromptPanel-B5nZvmf0.js → InjectSystemPromptPanel-C-e2AMz6.js} +245 -2
- package/dist/dashboard/assets/InjectSystemPromptPanel-C-e2AMz6.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-J8wPbmfT.js +1 -0
- package/dist/dashboard/assets/LoginPage-J8wPbmfT.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-DnQPSIA8.js → ModelPicker-C6WlpU5E.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-C6WlpU5E.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-BSQnF2Lp.js +1 -0
- package/dist/dashboard/assets/OverviewPage-BSQnF2Lp.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-DK_e0FJz.js +7 -0
- package/dist/dashboard/assets/ProviderDetailPage-DK_e0FJz.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js +1 -0
- package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-D_a3r6Gc.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-D_a3r6Gc.js.gz +0 -0
- package/dist/dashboard/assets/{ProxyPage-5SnUSy3j.js → ProxyPage-BBTRBAQ3.js} +1 -1
- package/dist/dashboard/assets/ProxyPage-BBTRBAQ3.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-B2WqMSG1.js +1 -0
- package/dist/dashboard/assets/QuotaPage-B2WqMSG1.js.gz +0 -0
- package/dist/dashboard/assets/{SettingsPage-BJtRZ4qD.js → SettingsPage-D7xp5Ij_.js} +1 -1
- package/dist/dashboard/assets/SettingsPage-D7xp5Ij_.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-LND6_Ss7.js +1 -0
- package/dist/dashboard/assets/SetupPage-LND6_Ss7.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-BfAb5-o_.js → StudioPage-BzNnm6Bi.js} +4 -4
- package/dist/dashboard/assets/StudioPage-BzNnm6Bi.js.gz +0 -0
- package/dist/dashboard/assets/ToolIcon-CfeLA-Z_.js +1 -0
- package/dist/dashboard/assets/ToolIcon-CfeLA-Z_.js.gz +0 -0
- package/dist/dashboard/assets/{UsagePage-BNzeZ2Vk.js → UsagePage-C1TbL24N.js} +13 -13
- package/dist/dashboard/assets/UsagePage-C1TbL24N.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-KvX83mRe.js +1 -0
- package/dist/dashboard/assets/api-keys-KvX83mRe.js.gz +0 -0
- package/dist/dashboard/assets/app-DRnQLhsc.js +9 -0
- package/dist/dashboard/assets/app-DRnQLhsc.js.gz +0 -0
- package/dist/dashboard/assets/app-mweRcsKp.css +2 -0
- package/dist/dashboard/assets/app-mweRcsKp.css.gz +0 -0
- package/dist/dashboard/assets/{assert-C0K-c4eo.js → assert-CidQVoNo.js} +1 -1
- package/dist/dashboard/assets/assert-CidQVoNo.js.gz +0 -0
- package/dist/dashboard/assets/{badge-DfbhQU4k.js → badge-BaX2GTrn.js} +1 -1
- package/dist/dashboard/assets/badge-BaX2GTrn.js.gz +0 -0
- package/dist/dashboard/assets/{banner-BllT4LcF.js → banner-DFH4Bkte.js} +1 -1
- package/dist/dashboard/assets/banner-DFH4Bkte.js.gz +0 -0
- package/dist/dashboard/assets/{card-CtkX66Wd.js → card-CxDw0lK6.js} +1 -1
- package/dist/dashboard/assets/card-CxDw0lK6.js.gz +0 -0
- package/dist/dashboard/assets/circle-question-mark-Cz4un8sH.js +1 -0
- package/dist/dashboard/assets/circle-question-mark-Cz4un8sH.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-CQXVMn4P.js → cli-tools-Bwv5-zGZ.js} +1 -1
- package/dist/dashboard/assets/cli-tools-Bwv5-zGZ.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-BQqBQd2n.js → clipboard-button-DfS1ntiq.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-DfS1ntiq.js.gz +0 -0
- package/dist/dashboard/assets/dialog-BK-NRr5u.js +1 -0
- package/dist/dashboard/assets/dialog-BK-NRr5u.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-BrTEZ8zu.js → drawer-QqPul6sC.js} +1 -1
- package/dist/dashboard/assets/drawer-QqPul6sC.js.gz +0 -0
- package/dist/dashboard/assets/entry-BMlYJVFj.js +1 -0
- package/dist/dashboard/assets/entry-BMlYJVFj.js.gz +0 -0
- package/dist/dashboard/assets/entry-Dmx7gBiN.js +1 -0
- package/dist/dashboard/assets/entry-Dmx7gBiN.js.gz +0 -0
- package/dist/dashboard/assets/entry-vqGb8yVz.js +4 -0
- package/dist/dashboard/assets/entry-vqGb8yVz.js.gz +0 -0
- package/dist/dashboard/assets/formatters-DP-5E01-.js +1 -0
- package/dist/dashboard/assets/formatters-DP-5E01-.js.gz +0 -0
- package/dist/dashboard/assets/house-h9AZlJuT.js +1 -0
- package/dist/dashboard/assets/house-h9AZlJuT.js.gz +0 -0
- package/dist/dashboard/assets/{index-FQlj0HUT.js → index-D36vCZht.js} +2 -2
- package/dist/dashboard/assets/index-D36vCZht.js.gz +0 -0
- package/dist/dashboard/assets/{inline-C4gU5iOL.js → inline-CHJQwxLa.js} +1 -1
- package/dist/dashboard/assets/inline-CHJQwxLa.js.gz +0 -0
- package/dist/dashboard/assets/{input-BHHX_Tmm.js → input-BlmlizVV.js} +1 -1
- package/dist/dashboard/assets/input-BlmlizVV.js.gz +0 -0
- package/dist/dashboard/assets/{layout-CajrYUnD.js → layout-Bz93I-yE.js} +1 -1
- package/dist/dashboard/assets/layout-Bz93I-yE.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-Bx2I7XUe.js → list-row-Ml16KCfu.js} +1 -1
- package/dist/dashboard/assets/list-row-Ml16KCfu.js.gz +0 -0
- package/dist/dashboard/assets/{live-DNNqRbj2.js → live-7_QpnYGu.js} +1 -1
- package/dist/dashboard/assets/live-7_QpnYGu.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-Rr0TIvcj.js → mutation-GU7XdEjz.js} +1 -1
- package/dist/dashboard/assets/mutation-GU7XdEjz.js.gz +0 -0
- package/dist/dashboard/assets/{network-C5UBACrJ.js → network-CuMDaTzT.js} +1 -1
- package/dist/dashboard/assets/network-CuMDaTzT.js.gz +0 -0
- package/dist/dashboard/assets/numbers-CoqpaDMx.js +2 -0
- package/dist/dashboard/assets/numbers-CoqpaDMx.js.gz +0 -0
- package/dist/dashboard/assets/{pagination-Xsi9Qx04.js → pagination-B5aOoweZ.js} +1 -1
- package/dist/dashboard/assets/pagination-B5aOoweZ.js.gz +0 -0
- package/dist/dashboard/assets/providers/zai.webp +0 -0
- package/dist/dashboard/assets/providers/zcode.webp +0 -0
- package/dist/dashboard/assets/{query-keys-BloKtaXJ.js → query-keys-YSoQOUh6.js} +1 -1
- package/dist/dashboard/assets/{query-keys-BloKtaXJ.js.gz → query-keys-YSoQOUh6.js.gz} +0 -0
- package/dist/dashboard/assets/quota-DJ1QlAOq.js +1 -0
- package/dist/dashboard/assets/quota-DJ1QlAOq.js.gz +0 -0
- package/dist/dashboard/assets/{routing-in0f_IfK.js → routing-fnCsxgNO.js} +1 -1
- package/dist/dashboard/assets/routing-fnCsxgNO.js.gz +0 -0
- package/dist/dashboard/assets/{select-BLULNqTp.js → select-C9Ssle9b.js} +2 -2
- package/dist/dashboard/assets/select-C9Ssle9b.js.gz +0 -0
- package/dist/dashboard/assets/{settings-CcGczj6I.js → settings-BxslsErn.js} +1 -1
- package/dist/dashboard/assets/settings-BxslsErn.js.gz +0 -0
- package/dist/dashboard/assets/{stack-D2lIR0tg.js → stack-j7cXjgYX.js} +1 -1
- package/dist/dashboard/assets/stack-j7cXjgYX.js.gz +0 -0
- package/dist/dashboard/assets/{state-DTARjfiW.js → state-DHmEnLa2.js} +1 -1
- package/dist/dashboard/assets/state-DHmEnLa2.js.gz +0 -0
- package/dist/dashboard/assets/{switch-c0oPSnqW.js → switch-DmZcyTIC.js} +1 -1
- package/dist/dashboard/assets/switch-DmZcyTIC.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-m8WGNPrz.js → useMutation-CbCP4w1o.js} +1 -1
- package/dist/dashboard/assets/useMutation-CbCP4w1o.js.gz +0 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +57 -57
- package/migrations/full-postgres/0042_add_telemetry_degenerate_stop.sql +14 -0
- package/migrations/full-postgres/0043_add_provider_custom_user_agent.sql +14 -0
- package/migrations/lite-sqlite3/0042_add_telemetry_degenerate_stop.sql +12 -0
- package/migrations/lite-sqlite3/0043_add_provider_custom_user_agent.sql +10 -0
- package/package.json +3 -1
- package/src/boot/dependencies.ts +10 -2
- package/src/config/providers.ts +13 -7
- package/src/config/security.ts +1 -1
- package/src/config/spec.ts +31 -17
- package/src/config/usage.ts +27 -0
- package/src/console/api-keys/routes.ts +19 -23
- package/src/console/cli-tools/cartethyia-websearch-plugin.ts +186 -0
- package/src/console/cli-tools/contracts.ts +39 -295
- package/src/console/cli-tools/fs-ops.ts +29 -0
- package/src/console/cli-tools/injectors/claude.ts +3 -3
- package/src/console/cli-tools/injectors/codex.ts +6 -6
- package/src/console/cli-tools/injectors/driver.ts +8 -105
- package/src/console/cli-tools/injectors/kilo.ts +146 -21
- package/src/console/cli-tools/injectors/opencode.ts +68 -17
- package/src/console/cli-tools/routes.ts +6 -0
- package/src/console/cli-tools/service.ts +7 -6
- package/src/console/observability/contracts.ts +6 -0
- package/src/console/observability/store.ts +3 -0
- package/src/console/providers/catalog/contracts.ts +4 -0
- package/src/console/providers/catalog/routes.ts +1 -0
- package/src/console/providers/detail/routes.ts +8 -0
- package/src/console/providers/detail/store.ts +4 -0
- package/src/console/quota/account-quota-tenant.ts +74 -0
- package/src/console/share/share-stats.ts +7 -9
- package/src/observability/event-log.ts +7 -0
- package/src/observability/event-query.ts +2 -0
- package/src/observability/telemetry-buffer.ts +9 -1
- package/src/persistence/boot.ts +13 -4
- package/src/persistence/schema.ts +15 -1
- package/src/persistence/stores/api-key-store.ts +19 -1
- package/src/protocol/canonical-model.ts +7 -0
- package/src/protocol/dsml.ts +5 -76
- package/src/protocol/request/gemini.ts +20 -1
- package/src/protocol/tool-identity.ts +15 -1
- package/src/providers/adapters/compatible-adapter.ts +29 -1
- package/src/providers/default-registry.ts +20 -4
- package/src/providers/discovery/discovery-types.ts +18 -0
- package/src/providers/discovery/probe-phases.ts +45 -0
- package/src/providers/discovery/probing-service.ts +41 -6
- package/src/providers/integrations/antigravity/antigravity-protocol.ts +108 -16
- package/src/providers/integrations/antigravity/antigravity-quota.ts +18 -6
- package/src/providers/integrations/antigravity/antigravity.ts +44 -11
- package/src/providers/integrations/codex/codex.ts +28 -1
- package/src/providers/integrations/cursor/cursor.ts +2 -3
- package/src/providers/integrations/github/github-discovery.ts +15 -2
- package/src/providers/integrations/github/github-oauth.ts +178 -9
- package/src/providers/integrations/github/github.ts +95 -14
- package/src/providers/integrations/kiro/kiro.ts +1 -2
- package/src/providers/integrations/qoder/chat-proto.ts +328 -0
- package/src/providers/integrations/qoder/claim-qoder-credits.ts +258 -0
- package/src/providers/integrations/qoder/qoder-adapter.ts +275 -0
- package/src/providers/integrations/qoder/qoder-auth.ts +201 -0
- package/src/providers/integrations/qoder/qoder-body.ts +78 -0
- package/src/providers/integrations/qoder/qoder-catalog.ts +14 -4
- package/src/providers/integrations/qoder/qoder-chat.ts +514 -0
- package/src/providers/integrations/qoder/qoder-cosy.ts +162 -0
- package/src/providers/integrations/qoder/qoder-grpc.ts +667 -0
- package/src/providers/integrations/qoder/qoder-ide.ts +584 -0
- package/src/providers/integrations/qoder/qoder-oauth.ts +4 -4
- package/src/providers/integrations/qoder/qoder-quota.ts +1 -1
- package/src/providers/integrations/qoder/qoder-realm.ts +40 -4
- package/src/providers/integrations/qoder/qoder-types.ts +51 -0
- package/src/providers/integrations/qoder/qoder-umid.ts +123 -8
- package/src/providers/integrations/qoder/qoder.ts +147 -1830
- package/src/providers/integrations/trae/trae.ts +2 -2
- package/src/providers/operations/account-error-classification.ts +67 -17
- package/src/providers/operations/byok-wire-profile.ts +19 -15
- package/src/providers/provider-contracts.ts +7 -0
- package/src/providers/provider-usage.ts +24 -55
- package/src/security/abuse.ts +5 -5
- package/src/security/admission/service.ts +11 -4
- package/src/security/api-key-auth.ts +5 -3
- package/src/transport/dispatch/attempt-finalize.ts +69 -21
- package/src/transport/dispatch/attempt-loop.ts +22 -0
- package/src/transport/dispatch/upstream.ts +4 -2
- package/src/transport/failure-policy.ts +15 -1
- package/src/transport/guards/error-lifecycle.ts +4 -0
- package/src/transport/request/state.ts +6 -0
- package/src/transport/routing/route-catalog.ts +8 -0
- package/src/transport/routing/route-model.ts +4 -0
- package/src/transport/routing/router.ts +8 -110
- package/src/transport/surface/reasoning-summary.ts +9 -14
- package/src/transport/vision-handoff.ts +83 -28
- package/dist/dashboard/assets/BannedPage-Cj15GsMp.js +0 -1
- package/dist/dashboard/assets/BannedPage-Cj15GsMp.js.gz +0 -0
- package/dist/dashboard/assets/ClaimGiftsButton-D83pBJ6J.js +0 -1
- package/dist/dashboard/assets/ClaimGiftsButton-D83pBJ6J.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-DZKmVuJN.js +0 -1
- package/dist/dashboard/assets/CliToolDetailPage-DZKmVuJN.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-BIccZmeL.js +0 -1
- package/dist/dashboard/assets/CliToolsPage-BIccZmeL.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-DEEKkYD-.js +0 -1
- package/dist/dashboard/assets/CombosPage-DEEKkYD-.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-D5rol2j2.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-BKj_r0rp.js.gz +0 -0
- package/dist/dashboard/assets/InjectSystemPromptPanel-B5nZvmf0.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-DkXb6dx-.js +0 -1
- package/dist/dashboard/assets/LoginPage-DkXb6dx-.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-DnQPSIA8.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-Ci2wQKUz.js +0 -1
- package/dist/dashboard/assets/OverviewPage-Ci2wQKUz.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-odlgYrOg.js +0 -5
- package/dist/dashboard/assets/ProviderDetailPage-odlgYrOg.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-EfeloBn4.js +0 -1
- package/dist/dashboard/assets/ProviderIcon-EfeloBn4.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-CqFBQEB8.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-CqFBQEB8.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-5SnUSy3j.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-CmnMDzhP.js +0 -1
- package/dist/dashboard/assets/QuotaPage-CmnMDzhP.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-BJtRZ4qD.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-WTa0sktC.js +0 -1
- package/dist/dashboard/assets/SetupPage-WTa0sktC.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-BfAb5-o_.js.gz +0 -0
- package/dist/dashboard/assets/ToolIcon-K1ckcDKg.js +0 -1
- package/dist/dashboard/assets/ToolIcon-K1ckcDKg.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-BNzeZ2Vk.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-l29JROPv.js +0 -1
- package/dist/dashboard/assets/api-keys-l29JROPv.js.gz +0 -0
- package/dist/dashboard/assets/app--PJWq8_C.css +0 -2
- package/dist/dashboard/assets/app--PJWq8_C.css.gz +0 -0
- package/dist/dashboard/assets/app-Bgd3zANQ.js +0 -9
- package/dist/dashboard/assets/app-Bgd3zANQ.js.gz +0 -0
- package/dist/dashboard/assets/assert-C0K-c4eo.js.gz +0 -0
- package/dist/dashboard/assets/badge-DfbhQU4k.js.gz +0 -0
- package/dist/dashboard/assets/banner-BllT4LcF.js.gz +0 -0
- package/dist/dashboard/assets/card-CtkX66Wd.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-CQXVMn4P.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-BQqBQd2n.js.gz +0 -0
- package/dist/dashboard/assets/dialog-BEK7KP0H.js +0 -1
- package/dist/dashboard/assets/dialog-BEK7KP0H.js.gz +0 -0
- package/dist/dashboard/assets/drawer-BrTEZ8zu.js.gz +0 -0
- package/dist/dashboard/assets/entry-CHWxbS56.js +0 -1
- package/dist/dashboard/assets/entry-CHWxbS56.js.gz +0 -0
- package/dist/dashboard/assets/entry-EBHJ3DrE.js +0 -1
- package/dist/dashboard/assets/entry-EBHJ3DrE.js.gz +0 -0
- package/dist/dashboard/assets/entry-LCS85hiq.js +0 -4
- package/dist/dashboard/assets/entry-LCS85hiq.js.gz +0 -0
- package/dist/dashboard/assets/formatters-BAACA727.js +0 -1
- package/dist/dashboard/assets/formatters-BAACA727.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-YSCE4UC2.js +0 -1
- package/dist/dashboard/assets/github-badge-YSCE4UC2.js.gz +0 -0
- package/dist/dashboard/assets/index-FQlj0HUT.js.gz +0 -0
- package/dist/dashboard/assets/inline-C4gU5iOL.js.gz +0 -0
- package/dist/dashboard/assets/input-BHHX_Tmm.js.gz +0 -0
- package/dist/dashboard/assets/layout-CajrYUnD.js.gz +0 -0
- package/dist/dashboard/assets/list-row-Bx2I7XUe.js.gz +0 -0
- package/dist/dashboard/assets/live-DNNqRbj2.js.gz +0 -0
- package/dist/dashboard/assets/mutation-Rr0TIvcj.js.gz +0 -0
- package/dist/dashboard/assets/network-C5UBACrJ.js.gz +0 -0
- package/dist/dashboard/assets/numbers-CUWAZK4J.js +0 -2
- package/dist/dashboard/assets/numbers-CUWAZK4J.js.gz +0 -0
- package/dist/dashboard/assets/pagination-Xsi9Qx04.js.gz +0 -0
- package/dist/dashboard/assets/providers/zai-coding-plan.svg +0 -6
- package/dist/dashboard/assets/providers/zai-coding-plan.svg.gz +0 -0
- package/dist/dashboard/assets/quota-DvQbD_sr.js +0 -1
- package/dist/dashboard/assets/quota-DvQbD_sr.js.gz +0 -0
- package/dist/dashboard/assets/routing-in0f_IfK.js.gz +0 -0
- package/dist/dashboard/assets/select-BLULNqTp.js.gz +0 -0
- package/dist/dashboard/assets/settings-CcGczj6I.js.gz +0 -0
- package/dist/dashboard/assets/stack-D2lIR0tg.js.gz +0 -0
- package/dist/dashboard/assets/state-DTARjfiW.js.gz +0 -0
- package/dist/dashboard/assets/switch-c0oPSnqW.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-m8WGNPrz.js.gz +0 -0
- package/src/console/cli-tools/injectors/cline.ts +0 -110
- package/src/console/cli-tools/injectors/copilot.ts +0 -108
- package/src/console/cli-tools/injectors/cowork.ts +0 -64
- package/src/console/cli-tools/injectors/deepseek-tui.ts +0 -85
- package/src/console/cli-tools/injectors/droid.ts +0 -96
- package/src/console/cli-tools/injectors/grok-build.ts +0 -70
- package/src/console/cli-tools/injectors/hermes.ts +0 -101
- package/src/console/cli-tools/injectors/jcode.ts +0 -115
- package/src/console/cli-tools/injectors/openclaw.ts +0 -161
- package/src/providers/integrations/qoder/qoder-text-tools.ts +0 -277
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { GatewayError } from "../../../protocol/gateway-error";
|
|
8
8
|
import type { CanonicalEvent, CanonicalMessage, CanonicalRequest, CanonicalStopReason } from "../../../protocol/canonical-model";
|
|
9
9
|
import type { UsageRecord } from "../../../protocol/canonical-model";
|
|
10
|
-
import {
|
|
10
|
+
import { usageFromProvider } from "../../provider-usage";
|
|
11
11
|
import {
|
|
12
12
|
readCredentialSecret,
|
|
13
13
|
assertWireFamily,
|
|
@@ -482,7 +482,7 @@ class TraeAdapter implements ProviderAdapter {
|
|
|
482
482
|
// prefixes we estimate them locally (see trae-cache.ts). Then the
|
|
483
483
|
// operator's cache mode decides whether a hit makes the whole input count
|
|
484
484
|
// as cached.
|
|
485
|
-
return
|
|
485
|
+
return estimateTraeSessionCache(record, sessionId, messages);
|
|
486
486
|
}
|
|
487
487
|
|
|
488
488
|
}
|
|
@@ -458,6 +458,26 @@ function banWording(lower: string): boolean {
|
|
|
458
458
|
);
|
|
459
459
|
}
|
|
460
460
|
|
|
461
|
+
/**
|
|
462
|
+
* Whether GitHub Copilot stated the account lacks a license — a ban-shaped
|
|
463
|
+
* verdict on the account, not a refreshable auth rejection.
|
|
464
|
+
*
|
|
465
|
+
* Observed live (2026-11-03): the health log showed
|
|
466
|
+
* `unauthorized: not licensed to use Copilot` and `forbidden` on
|
|
467
|
+
* `gpt-6-luna`. Copilot answers 401 for a bad credential and 403 for a
|
|
468
|
+
* license/entitlement refusal, so `forbidden`/`unauthorized` on this surface
|
|
469
|
+
* is the same verdict. No amount of waiting or refreshing changes it — the
|
|
470
|
+
* subscription cannot serve this credential.
|
|
471
|
+
*/
|
|
472
|
+
function copilotNotLicensed(lower: string): boolean {
|
|
473
|
+
return (
|
|
474
|
+
lower.includes("not licensed") ||
|
|
475
|
+
lower.includes("forbidden") ||
|
|
476
|
+
lower.includes("unauthorized") ||
|
|
477
|
+
lower.includes("license")
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
|
|
461
481
|
/**
|
|
462
482
|
* Bounded to a day, matching `MAX_COOLDOWN_MS` in the failure policy: a stated
|
|
463
483
|
* window longer than that is a monthly or plan-level limit, and parking an
|
|
@@ -1263,6 +1283,33 @@ const ACCOUNT_ERROR_RULES: readonly AccountErrorRule[] = [
|
|
|
1263
1283
|
true,
|
|
1264
1284
|
),
|
|
1265
1285
|
},
|
|
1286
|
+
{
|
|
1287
|
+
// GitHub Copilot answers a license/entitlement refusal with 403 and
|
|
1288
|
+
// "not licensed to use Copilot" / "forbidden" / "unauthorized". That is a
|
|
1289
|
+
// verdict on the account: the subscription cannot serve this credential,
|
|
1290
|
+
// and no amount of waiting or refreshing changes it. Cooldown cycling
|
|
1291
|
+
// (auth_invalidated + 5m backoff) kept a dead account in rotation where a
|
|
1292
|
+
// client's retry budget burned sibling accounts — the same failure the
|
|
1293
|
+
// buddy ban exists to prevent. Disable immediately. Must run before
|
|
1294
|
+
// `credential-rejected`, which would claim the bare 403 as a refreshable
|
|
1295
|
+
// auth rejection.
|
|
1296
|
+
name: "github-copilot-not-licensed",
|
|
1297
|
+
match: (s) =>
|
|
1298
|
+
s.origin === "upstream" &&
|
|
1299
|
+
s.providerId === "github" &&
|
|
1300
|
+
copilotNotLicensed(s.lower),
|
|
1301
|
+
decide: (s) =>
|
|
1302
|
+
s.result(
|
|
1303
|
+
"policy_blocked_banned",
|
|
1304
|
+
"disabled",
|
|
1305
|
+
0,
|
|
1306
|
+
null,
|
|
1307
|
+
s.reason("GitHub Copilot account not licensed"),
|
|
1308
|
+
// A license refusal is evidence about the account even at provider
|
|
1309
|
+
// scope, for the same reason a named revocation or a buddy ban is.
|
|
1310
|
+
true,
|
|
1311
|
+
),
|
|
1312
|
+
},
|
|
1266
1313
|
{
|
|
1267
1314
|
name: "credential-rejected",
|
|
1268
1315
|
match: (s) =>
|
|
@@ -1471,7 +1518,7 @@ const ACCOUNT_ERROR_RULES: readonly AccountErrorRule[] = [
|
|
|
1471
1518
|
},
|
|
1472
1519
|
{
|
|
1473
1520
|
name: "quota-exhausted",
|
|
1474
|
-
match: (s) => s.quotaSignal,
|
|
1521
|
+
match: (s) => s.origin === "upstream" && s.quotaSignal,
|
|
1475
1522
|
decide: (s) => {
|
|
1476
1523
|
// xAI Grok Build's free tier resets on a rolling window. Use a duration
|
|
1477
1524
|
// stated by the provider when present; otherwise park for the conservative
|
|
@@ -1525,6 +1572,7 @@ const ACCOUNT_ERROR_RULES: readonly AccountErrorRule[] = [
|
|
|
1525
1572
|
// moves to the next account and the flagged one returns almost at once.
|
|
1526
1573
|
name: "inception-per-minute-limit",
|
|
1527
1574
|
match: (s) =>
|
|
1575
|
+
s.origin === "upstream" &&
|
|
1528
1576
|
s.providerId === "inception" &&
|
|
1529
1577
|
(s.statusCode === 429 ||
|
|
1530
1578
|
s.providerCode === "rate_limit_reached" ||
|
|
@@ -1549,15 +1597,16 @@ const ACCOUNT_ERROR_RULES: readonly AccountErrorRule[] = [
|
|
|
1549
1597
|
{
|
|
1550
1598
|
name: "rate-limit",
|
|
1551
1599
|
match: (s) =>
|
|
1552
|
-
s.
|
|
1553
|
-
s.
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1600
|
+
s.origin === "upstream" &&
|
|
1601
|
+
(s.statusCode === 429 ||
|
|
1602
|
+
s.providerCode === "rate_limit_exceeded" ||
|
|
1603
|
+
s.lower.includes("rate_limit") ||
|
|
1604
|
+
s.lower.includes("rate limit") ||
|
|
1605
|
+
s.lower.includes("too many requests") ||
|
|
1606
|
+
s.lower.includes("requests per minute") ||
|
|
1607
|
+
s.lower.includes("tokens per minute") ||
|
|
1608
|
+
s.lower.includes("tpm limit") ||
|
|
1609
|
+
s.lower.includes("rpm limit")),
|
|
1561
1610
|
decide: (s) => {
|
|
1562
1611
|
const cooldownMs = s.statedCooldownMs ?? DEFAULT_RATE_LIMIT_COOLDOWN_MS();
|
|
1563
1612
|
// Same per-account reasoning as quota above: the failing account (or
|
|
@@ -1576,13 +1625,14 @@ const ACCOUNT_ERROR_RULES: readonly AccountErrorRule[] = [
|
|
|
1576
1625
|
{
|
|
1577
1626
|
name: "model-capacity",
|
|
1578
1627
|
match: (s) =>
|
|
1579
|
-
s.
|
|
1580
|
-
s.statusCode ===
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1628
|
+
s.origin === "upstream" &&
|
|
1629
|
+
(s.statusCode === 503 ||
|
|
1630
|
+
s.statusCode === 529 ||
|
|
1631
|
+
s.providerCode === "model_capacity" ||
|
|
1632
|
+
s.queuedUpstream ||
|
|
1633
|
+
s.lower.includes("model overloaded") ||
|
|
1634
|
+
s.lower.includes("capacity exceeded") ||
|
|
1635
|
+
s.lower.includes("temporarily unavailable")),
|
|
1586
1636
|
decide: (s) => {
|
|
1587
1637
|
const cooldownMs = s.statedCooldownMs ?? MODEL_CAPACITY_COOLDOWN_MS();
|
|
1588
1638
|
const modelScopedMutation = s.origin === "upstream" && s.modelId !== undefined;
|
|
@@ -92,12 +92,11 @@ const ENDPOINT_TAIL = /\/(?:v\d+\/)?(?:messages|chat\/completions|responses|comp
|
|
|
92
92
|
* The root an operator's pasted URL really meant, plus the endpoint tail that
|
|
93
93
|
* was removed.
|
|
94
94
|
*
|
|
95
|
-
*
|
|
96
|
-
* `https://api.
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* that is already a root comes back with an empty `stripped`.
|
|
95
|
+
* Only the **tail** is stripped. Any path before it belongs to the gateway
|
|
96
|
+
* root and survives, so `https://api.longcat.ai/anthropic/v1/messages`
|
|
97
|
+
* resolves to `https://api.longcat.ai/anthropic` (not the bare origin) — the
|
|
98
|
+
* `/anthropic` prefix is where LongCat actually serves the API. A URL that is
|
|
99
|
+
* already a root comes back with an empty `stripped`.
|
|
101
100
|
*/
|
|
102
101
|
export function splitEndpointBaseUrl(baseUrl: string): { readonly baseUrl: string; readonly stripped: string } {
|
|
103
102
|
const trimmed = baseUrl.replace(/\/+$/, "");
|
|
@@ -110,20 +109,22 @@ export function splitEndpointBaseUrl(baseUrl: string): { readonly baseUrl: strin
|
|
|
110
109
|
const basePath = parsed.pathname.replace(/\/+$/, "");
|
|
111
110
|
const match = ENDPOINT_TAIL.exec(basePath);
|
|
112
111
|
if (match === null) return { baseUrl: trimmed, stripped: "" };
|
|
113
|
-
|
|
112
|
+
const keptPath = basePath.slice(0, match.index);
|
|
113
|
+
return { baseUrl: `${parsed.origin}${keptPath}`, stripped: basePath };
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
/**
|
|
117
117
|
* Base URL an operator's model list lives under (`<base>/models`). A bare host
|
|
118
118
|
* has no version segment, so it gets the `/v1` prefix; a base that already
|
|
119
119
|
* carries a path (conventionally `/v1`) is used as-is. A pasted endpoint path
|
|
120
|
-
* is stripped first, so `…/anthropic/v1/messages` discovers at
|
|
121
|
-
*
|
|
120
|
+
* is stripped first, so `…/anthropic/v1/messages` discovers at
|
|
121
|
+
* `<origin>/anthropic/v1/models` — the gateway prefix before the endpoint tail
|
|
122
|
+
* belongs to the root and is preserved.
|
|
122
123
|
*/
|
|
123
124
|
export function modelDiscoveryBaseUrl(baseUrl: string): string {
|
|
124
|
-
// A pasted endpoint
|
|
125
|
-
//
|
|
126
|
-
//
|
|
125
|
+
// A pasted endpoint has its tail removed; whatever path came before it is a
|
|
126
|
+
// real gateway prefix and stays. A base that already IS a root — bare host,
|
|
127
|
+
// or a version/gateway prefix — keeps that path, because
|
|
127
128
|
// `https://gateway.example/api/v1` legitimately serves its list there.
|
|
128
129
|
const trimmed = baseUrl.replace(/\/+$/, "");
|
|
129
130
|
let parsed: URL;
|
|
@@ -133,9 +134,12 @@ export function modelDiscoveryBaseUrl(baseUrl: string): string {
|
|
|
133
134
|
return `${trimmed}/v1`;
|
|
134
135
|
}
|
|
135
136
|
const basePath = parsed.pathname.replace(/\/+$/, "");
|
|
136
|
-
const
|
|
137
|
-
const listBase =
|
|
138
|
-
|
|
137
|
+
const match = ENDPOINT_TAIL.exec(basePath);
|
|
138
|
+
const listBase =
|
|
139
|
+
match === null
|
|
140
|
+
? `${parsed.origin}${basePath}`
|
|
141
|
+
: `${parsed.origin}${basePath.slice(0, match.index)}`;
|
|
142
|
+
if (match !== null || basePath === "" || basePath === "/") return `${listBase}/v1`;
|
|
139
143
|
return listBase;
|
|
140
144
|
}
|
|
141
145
|
|
|
@@ -270,6 +270,13 @@ export interface ProviderDispatchTarget {
|
|
|
270
270
|
readonly capabilities: Readonly<Record<string, boolean>>;
|
|
271
271
|
/** Explicit wire dialect (from `models.dialect`), when set. Absent = resolver fallback. */
|
|
272
272
|
readonly dialect?: string;
|
|
273
|
+
/**
|
|
274
|
+
* Transport override for a one-shot probe, when the operator explicitly
|
|
275
|
+
* asked for one. Live dispatch never sets this — it is a probe-only knob
|
|
276
|
+
* that lets a provider expose a surface the production transports do not
|
|
277
|
+
* use (Qoder's IDE surface is the only one that echoes strategy metadata).
|
|
278
|
+
*/
|
|
279
|
+
readonly probe_surface?: "cosy" | "grpc" | "ide";
|
|
273
280
|
}
|
|
274
281
|
|
|
275
282
|
/** Provider adapter boundary: canonical request/events in, no public HTTP routes. */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { UsageRecord } from "../protocol/canonical-model";
|
|
2
|
+
import { resolveUsageInputMode } from "../config/usage";
|
|
2
3
|
export interface OpenAiUsage {
|
|
3
4
|
prompt_tokens: number;
|
|
4
5
|
completion_tokens: number;
|
|
@@ -11,7 +12,6 @@ export interface OpenAiUsage {
|
|
|
11
12
|
};
|
|
12
13
|
}
|
|
13
14
|
import { modelsDevCatalog } from "./discovery/models-dev-catalog";
|
|
14
|
-
import { resolveCacheMode } from "../config/providers";
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Extract cost multipliers from catalog model cost JSONB.
|
|
@@ -126,36 +126,6 @@ export function repriceUsage(
|
|
|
126
126
|
: { ...usage, estimated_cost: estimatedCost };
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
/**
|
|
130
|
-
* Simplified cache accounting: when the provider reported a cache hit, the
|
|
131
|
-
* whole prompt counts as cached — request input ber-cache = cache-eligible →
|
|
132
|
-
* input = cache. The cache-write volume is already inside the all-in input
|
|
133
|
-
* total on cache-aware shapes, so it stays separate and is not double-counted
|
|
134
|
-
* as a hit. A reported `0` or an absent report stay a cache miss. In `real`
|
|
135
|
-
* mode the provider's reported split is kept as-is.
|
|
136
|
-
*
|
|
137
|
-
* Exported so the hand-built record producers (Cursor, Kiro) apply the same
|
|
138
|
-
* rule as `normalizeUsage`.
|
|
139
|
-
*/
|
|
140
|
-
export function applyCacheMode(usage: UsageRecord): UsageRecord {
|
|
141
|
-
if (resolveCacheMode() !== "simplified") return usage;
|
|
142
|
-
if (
|
|
143
|
-
typeof usage.cached_input_tokens === "number" &&
|
|
144
|
-
usage.cached_input_tokens > 0 &&
|
|
145
|
-
usage.input_tokens > 0
|
|
146
|
-
) {
|
|
147
|
-
const writes =
|
|
148
|
-
typeof usage.cache_write_tokens === "number" ? Math.max(0, usage.cache_write_tokens) : 0;
|
|
149
|
-
const cached = Math.min(usage.input_tokens, Math.max(0, usage.input_tokens - writes));
|
|
150
|
-
return {
|
|
151
|
-
...usage,
|
|
152
|
-
cached_input_tokens: cached,
|
|
153
|
-
uncached_input_tokens: 0,
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
return usage;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
129
|
/**
|
|
160
130
|
* Normalize provider-specific usage data into canonical UsageRecord.
|
|
161
131
|
* Handles Anthropic (cache_read_input_tokens, cache_creation_input_tokens, output_tokens_details.thinking_tokens)
|
|
@@ -266,10 +236,21 @@ export function normalizeUsage(input: Record<string, unknown>): UsageRecord {
|
|
|
266
236
|
const reportedTotal = typeof input.total_tokens === "number" ? input.total_tokens : null;
|
|
267
237
|
const inputAlreadyIncludesCache =
|
|
268
238
|
reportedTotal !== null && reportedTotal === input_tokens + output_tokens;
|
|
269
|
-
|
|
239
|
+
// `inclusive` (default) folds cache into input so every provider reads alike:
|
|
240
|
+
// IN is always the whole prompt and CACHED its subset. Anthropic's raw shape
|
|
241
|
+
// is fresh-only with cache separate — and its `total_tokens` equals
|
|
242
|
+
// input+output by contract, so the `inputAlreadyIncludesCache` heuristic
|
|
243
|
+
// would otherwise mistake it for already-inclusive and never add the cache,
|
|
244
|
+
// under-reporting prompt by the entire cached span. `as-is` keeps the raw
|
|
245
|
+
// upstream semantics (and the legacy heuristic for already-inclusive bridges).
|
|
246
|
+
const accountInputInclusive = resolveUsageInputMode() === "inclusive";
|
|
247
|
+
const cacheIsSeparate = anthropic_cache_read !== null;
|
|
248
|
+
const shouldFoldCache = accountInputInclusive
|
|
249
|
+
? cacheIsSeparate
|
|
250
|
+
: cacheIsSeparate && !inputAlreadyIncludesCache;
|
|
270
251
|
let uncached_input_tokens: number | "unavailable" = "unavailable";
|
|
271
252
|
if (typeof cached_input_tokens === "number") {
|
|
272
|
-
if (
|
|
253
|
+
if (shouldFoldCache) {
|
|
273
254
|
uncached_input_tokens = input_tokens;
|
|
274
255
|
input_tokens +=
|
|
275
256
|
cached_input_tokens + (typeof cache_write_tokens === "number" ? cache_write_tokens : 0);
|
|
@@ -277,25 +258,10 @@ export function normalizeUsage(input: Record<string, unknown>): UsageRecord {
|
|
|
277
258
|
uncached_input_tokens = Math.max(0, input_tokens - Math.min(input_tokens, cached_input_tokens));
|
|
278
259
|
cached_input_tokens = Math.min(input_tokens, cached_input_tokens);
|
|
279
260
|
}
|
|
280
|
-
} else if (
|
|
261
|
+
} else if (shouldFoldCache && typeof cache_write_tokens === "number") {
|
|
281
262
|
input_tokens += cache_write_tokens;
|
|
282
263
|
}
|
|
283
264
|
|
|
284
|
-
// Simplified cache accounting: a reported hit makes the whole prompt count as
|
|
285
|
-
// cached — request input ber-cache = cache-eligible → input = cache. The
|
|
286
|
-
// cache-write volume is already inside the all-in input total on cache-aware
|
|
287
|
-
// shapes, so it stays separate and is not double-counted as a hit. Applied
|
|
288
|
-
// before pricing so the estimated cost reads the same simplified split as the
|
|
289
|
-
// record.
|
|
290
|
-
if (typeof cached_input_tokens === "number" && cached_input_tokens > 0 && input_tokens > 0) {
|
|
291
|
-
if (resolveCacheMode() === "simplified") {
|
|
292
|
-
const writes =
|
|
293
|
-
typeof cache_write_tokens === "number" ? Math.max(0, cache_write_tokens) : 0;
|
|
294
|
-
cached_input_tokens = Math.min(input_tokens, Math.max(0, input_tokens - writes));
|
|
295
|
-
uncached_input_tokens = 0;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
265
|
// Handle reasoning tokens: Anthropic and OpenAI both use output_tokens_details
|
|
300
266
|
let reasoning_tokens: number | "unavailable" = "unavailable";
|
|
301
267
|
const details: NonNullable<UsageRecord["details"]> = {};
|
|
@@ -333,6 +299,14 @@ export function normalizeUsage(input: Record<string, unknown>): UsageRecord {
|
|
|
333
299
|
},
|
|
334
300
|
rawCost,
|
|
335
301
|
) ?? null;
|
|
302
|
+
// Provider-billed credits: an explicit `credit_used` wins, then Qoder's
|
|
303
|
+
// `credits` (COSY terminal frame), then Tencent buddy-meter `credit`.
|
|
304
|
+
// Negative/NaN values are dropped, never clamped.
|
|
305
|
+
const creditRaw = input.credit_used ?? input.credits ?? input.credit;
|
|
306
|
+
const creditUsed =
|
|
307
|
+
typeof creditRaw === "number" && Number.isFinite(creditRaw) && creditRaw >= 0
|
|
308
|
+
? { credit_used: creditRaw }
|
|
309
|
+
: {};
|
|
336
310
|
return {
|
|
337
311
|
input_tokens,
|
|
338
312
|
cached_input_tokens,
|
|
@@ -341,12 +315,7 @@ export function normalizeUsage(input: Record<string, unknown>): UsageRecord {
|
|
|
341
315
|
output_tokens,
|
|
342
316
|
reasoning_tokens,
|
|
343
317
|
estimated_cost,
|
|
344
|
-
|
|
345
|
-
// (probed live on cb/deepseek-v4.1-flash-sg: 0 on a 29-token turn, 1.01
|
|
346
|
-
// on a 72k-token turn). Negative/NaN values are dropped, never clamped.
|
|
347
|
-
...(typeof input.credit === "number" && Number.isFinite(input.credit) && input.credit >= 0
|
|
348
|
-
? { credit_used: input.credit }
|
|
349
|
-
: {}),
|
|
318
|
+
...creditUsed,
|
|
350
319
|
...(Object.keys(details).length > 0 ? { details } : {}),
|
|
351
320
|
};
|
|
352
321
|
}
|
package/src/security/abuse.ts
CHANGED
|
@@ -201,7 +201,7 @@ export class InMemoryIpAbuseStore implements IpAbuseStore {
|
|
|
201
201
|
typeof windowOrOptions === "number" ? { windowMs: windowOrOptions } : windowOrOptions;
|
|
202
202
|
this.windowMs = options.windowMs ?? 60_000;
|
|
203
203
|
this.clock = options.clock ?? (() => Date.now());
|
|
204
|
-
// 10k keys is a few MB even at the default
|
|
204
|
+
// 10k keys is a few MB even at the default 540 RPM ring size; the previous
|
|
205
205
|
// 100k bound could reach ~190 MB, and a fan-out over unique source IPs is
|
|
206
206
|
// the one input that can actually reach it.
|
|
207
207
|
this.maxKeys = options.maxKeys ?? 10_000;
|
|
@@ -382,14 +382,14 @@ export class IpAbuseProtectionService {
|
|
|
382
382
|
) {
|
|
383
383
|
if (!store)
|
|
384
384
|
throw new Error("IpAbuseProtectionService requires a store — fail-closed, not no-op");
|
|
385
|
-
//
|
|
386
|
-
// with the documented default: unset env →
|
|
385
|
+
// 540 requests per 60 s window = 9 RPS/IP steady-state cap. Aligns
|
|
386
|
+
// with the documented default: unset env → 540 RPM per IP, not
|
|
387
387
|
// unlimited. Operators raise or lower via IP_RATE_MAX_PER_WINDOW.
|
|
388
|
-
this.maxRequestsPerWindow = opts.maxRequestsPerWindow ??
|
|
388
|
+
this.maxRequestsPerWindow = opts.maxRequestsPerWindow ?? 540;
|
|
389
389
|
// A ban only makes sense once the limit was breached, so the threshold is
|
|
390
390
|
// clamped above the cap: below it, escalation would be unreachable (the
|
|
391
391
|
// count can never exceed `maxRequestsPerWindow` without being rejected).
|
|
392
|
-
this.banThreshold = Math.max(opts.banThreshold ??
|
|
392
|
+
this.banThreshold = Math.max(opts.banThreshold ?? 1080, this.maxRequestsPerWindow + 1);
|
|
393
393
|
this.banDurationMs = opts.banDurationMs ?? 60 * 60 * 1000;
|
|
394
394
|
}
|
|
395
395
|
|
|
@@ -164,13 +164,20 @@ export class ApiKeyAdmissionService {
|
|
|
164
164
|
const actual = usageTokens(usage);
|
|
165
165
|
await this.store.reconcile(apiKeyId, estimatedTokens, actual, reservationId);
|
|
166
166
|
finalized = true;
|
|
167
|
-
// Persist against the authenticating key
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
167
|
+
// Persist the spend against BOTH the authenticating key and the family
|
|
168
|
+
// owner. The authenticating key (`snapshot.api_key_id`, the child for
|
|
169
|
+
// a share recipient) carries attribution so `sumChildrenConsumed` and
|
|
170
|
+
// recipient rows stay correct. The family owner (`admission_identity`,
|
|
171
|
+
// the template itself for a share child) is the live accumulator the
|
|
172
|
+
// console reads for total usage — without it the template column never
|
|
173
|
+
// moved and every figure depended on telemetry rows alone.
|
|
171
174
|
if (this.persistLifetimeUsage) {
|
|
175
|
+
const ownerId = snapshot.admission_identity ?? snapshot.api_key_id;
|
|
172
176
|
try {
|
|
173
177
|
await this.persistLifetimeUsage({ apiKeyId: snapshot.api_key_id, delta: actual });
|
|
178
|
+
if (ownerId !== snapshot.api_key_id) {
|
|
179
|
+
await this.persistLifetimeUsage({ apiKeyId: ownerId, delta: actual });
|
|
180
|
+
}
|
|
174
181
|
} catch {
|
|
175
182
|
// Non-fatal: the transient counter already reflects reality; a
|
|
176
183
|
// subsequent request will overwrite the row with the fresh count.
|
|
@@ -388,9 +388,11 @@ export async function resolveApiKeyAuthorization(
|
|
|
388
388
|
tenant_id: row.tenantId,
|
|
389
389
|
// Family quota / concurrency share one admission counter namespace so two
|
|
390
390
|
// recipients cannot each burn a full lifetime/daily budget. In `per_key`
|
|
391
|
-
// concurrency mode the child
|
|
392
|
-
//
|
|
393
|
-
|
|
391
|
+
// concurrency mode the child keeps its own cap and counter for
|
|
392
|
+
// *concurrency* only — token budgets (daily/monthly/lifetime) stay
|
|
393
|
+
// family-wide on the parent, so the admission identity is always the
|
|
394
|
+
// parent's.
|
|
395
|
+
...(parent ? { admission_identity: parent.id } : {}),
|
|
394
396
|
...(policy.modelList ? { model_list: policy.modelList as string[] } : {}),
|
|
395
397
|
model_access_mode: policy.modelAccessMode ?? "whitelist",
|
|
396
398
|
...(policy.clientRouterDenylist
|
|
@@ -16,6 +16,7 @@ import { finalizeRequestTelemetry } from "../guards/error-lifecycle";
|
|
|
16
16
|
import { debitCachedQuota, quotaLens } from "../../providers/quota/cache";
|
|
17
17
|
import { BUDDY_PROVIDER_IDS } from "../../providers/provider-metadata";
|
|
18
18
|
import { log } from "../../observability/logger";
|
|
19
|
+
import { modelsDevCatalog } from "../../providers/discovery/models-dev-catalog";
|
|
19
20
|
import {
|
|
20
21
|
disablePoolForProxyHttpStatus,
|
|
21
22
|
recordPoolDispatchOutcome,
|
|
@@ -711,6 +712,32 @@ async function debitBuddySpend(completion: AttemptCompletion): Promise<void> {
|
|
|
711
712
|
}
|
|
712
713
|
}
|
|
713
714
|
|
|
715
|
+
/**
|
|
716
|
+
* Whether a completed attempt is a "degenerate stop": output far smaller than
|
|
717
|
+
* the model's ceiling, ended by an ordinary `stop` rather than `length`.
|
|
718
|
+
*
|
|
719
|
+
* This is the signature of a model truncating itself on a cached prefix — the
|
|
720
|
+
* pattern that surfaced as short, cut-off chat replies that the upstream still
|
|
721
|
+
* reported as a clean stop. It is measured so the operator can see how often
|
|
722
|
+
* it happens instead of eyeballing request detail.
|
|
723
|
+
*/
|
|
724
|
+
export function isDegenerateStop(
|
|
725
|
+
usage: UsageRecord | undefined,
|
|
726
|
+
outputLimit: number | undefined,
|
|
727
|
+
): boolean {
|
|
728
|
+
if (usage === undefined) return false;
|
|
729
|
+
const output = usage.output_tokens;
|
|
730
|
+
const reasoning = typeof usage.reasoning_tokens === "number" ? usage.reasoning_tokens : 0;
|
|
731
|
+
const totalOutput = output + reasoning;
|
|
732
|
+
if (totalOutput === 0 || totalOutput > 512) return false;
|
|
733
|
+
// A model with no meaningful ceiling cannot be "far smaller than it can
|
|
734
|
+
// produce", so the flag is only meaningful when a ceiling exists.
|
|
735
|
+
if (!(typeof outputLimit === "number" && Number.isFinite(outputLimit) && outputLimit > 0)) {
|
|
736
|
+
return false;
|
|
737
|
+
}
|
|
738
|
+
return totalOutput <= Math.max(1, Math.floor(outputLimit / 10));
|
|
739
|
+
}
|
|
740
|
+
|
|
714
741
|
export async function completeAttempt(
|
|
715
742
|
state: ProxyRequestState,
|
|
716
743
|
completion: AttemptCompletion,
|
|
@@ -761,6 +788,15 @@ export async function completeAttempt(
|
|
|
761
788
|
...(completion.firstContentDeltaAtMs === undefined ? {} : { firstContentDeltaAtMs: completion.firstContentDeltaAtMs }),
|
|
762
789
|
...(completion.lastEventAtMs === undefined ? {} : { lastEventAtMs: completion.lastEventAtMs }),
|
|
763
790
|
...(completion.reportedModel === undefined ? {} : { reportedModel: completion.reportedModel }),
|
|
791
|
+
...(completion.status === "completed" &&
|
|
792
|
+
isDegenerateStop(
|
|
793
|
+
completion.usage,
|
|
794
|
+
completion.providerId !== undefined && completion.modelId !== undefined
|
|
795
|
+
? (modelsDevCatalog.resolve(completion.providerId, completion.modelId)?.outputLimit ?? undefined)
|
|
796
|
+
: undefined,
|
|
797
|
+
)
|
|
798
|
+
? { degenerateStop: true }
|
|
799
|
+
: {}),
|
|
764
800
|
};
|
|
765
801
|
if (completion.terminal) state.completed = true;
|
|
766
802
|
// Usage reconciliation is bookkeeping, and it is the last thing standing
|
|
@@ -785,27 +821,39 @@ export async function completeAttempt(
|
|
|
785
821
|
// advisory: a failed report must never re-enter the retry path (a retry
|
|
786
822
|
// would see `completed` and skip completion entirely), so it is swallowed.
|
|
787
823
|
if (completion.status !== "cancelled" && !disableProxy) {
|
|
788
|
-
//
|
|
789
|
-
//
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
824
|
+
// Account health records only failures that are evidence about the
|
|
825
|
+
// provider's own account: upstream verdicts (quota, auth, model
|
|
826
|
+
// capacity, rate limits), or credential-scoped refusals from the
|
|
827
|
+
// upstream. Local rejections — API-key admission (`max_concurrent`,
|
|
828
|
+
// rpm/token budgets), routing capacity, pool selection, request
|
|
829
|
+
// preparation — are decisions this gateway made about its own limits;
|
|
830
|
+
// they say nothing about the account and must not cool it or write a
|
|
831
|
+
// phantom `logged -> cooldown` transition. Success always reports so a
|
|
832
|
+
// cooling account recovers.
|
|
833
|
+
const succeeded = completion.status === "completed";
|
|
834
|
+
const gatewayError = completion.error instanceof GatewayError ? completion.error : undefined;
|
|
835
|
+
const upstreamVerdict = gatewayError?.origin === "upstream";
|
|
836
|
+
const isProxyDeploymentError =
|
|
837
|
+
completion.error instanceof Error ? completion.error.message.toLowerCase().includes("deployment_disabled") || (completion.error.message.toLowerCase().includes("sin1::") && completion.error.message.toLowerCase().includes("deployment")) : false;
|
|
838
|
+
if (succeeded || isProxyDeploymentError || upstreamVerdict) {
|
|
839
|
+
const evidence = isProxyDeploymentError
|
|
840
|
+
? { scope: "pool" as const, origin: "network" as const }
|
|
841
|
+
: { ...classifyUpstreamFailure(completion.error) };
|
|
842
|
+
try {
|
|
843
|
+
await reportAttemptOutcome(completion.db, {
|
|
844
|
+
accountId: completion.accountId,
|
|
845
|
+
...(completion.modelId === undefined ? {} : { modelId: completion.modelId }),
|
|
846
|
+
...(completion.status === "failed"
|
|
847
|
+
? {
|
|
848
|
+
error: completion.error,
|
|
849
|
+
evidence,
|
|
850
|
+
}
|
|
851
|
+
: {}),
|
|
852
|
+
...(completion.snapshotService ? { snapshotService: completion.snapshotService } : {}),
|
|
853
|
+
});
|
|
854
|
+
} catch {
|
|
855
|
+
// Swallowed: health reporting is non-critical bookkeeping.
|
|
856
|
+
}
|
|
809
857
|
}
|
|
810
858
|
}
|
|
811
859
|
if (completion.networkPoolId && completion.status !== "cancelled") {
|
|
@@ -61,6 +61,27 @@ function comboCandidateKey(candidate: RouteCandidate): string {
|
|
|
61
61
|
].join(":");
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* True when the rejection is about the API key or the gateway's own policy,
|
|
66
|
+
* not about the provider account that was dialled.
|
|
67
|
+
*
|
|
68
|
+
* These are decided before the upstream is ever called, so every candidate in
|
|
69
|
+
* the plan fails identically; walking the rest of the list would just burn
|
|
70
|
+
* attempts and write phantom account-health rows (a local `capacity_exhausted`
|
|
71
|
+
* used to read as a provider `rate_limit_transient` cooldown).
|
|
72
|
+
*/
|
|
73
|
+
function isClientScopedRejection(error: unknown): boolean {
|
|
74
|
+
if (!(error instanceof GatewayError)) return false;
|
|
75
|
+
return (
|
|
76
|
+
error.code === "capacity_exhausted" ||
|
|
77
|
+
error.code === "tenant_capacity_exhausted" ||
|
|
78
|
+
error.code === "admission_unavailable" ||
|
|
79
|
+
error.code === "model_not_allowed" ||
|
|
80
|
+
error.code === "ambiguous_model" ||
|
|
81
|
+
error.code === "route_unavailable"
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
64
85
|
/**
|
|
65
86
|
* Which combo member a candidate belongs to.
|
|
66
87
|
*
|
|
@@ -402,6 +423,7 @@ export async function runAttemptLoop<TResult, TAdapter>(
|
|
|
402
423
|
}
|
|
403
424
|
const terminalAttempt =
|
|
404
425
|
cancelled ||
|
|
426
|
+
isClientScopedRejection(error) ||
|
|
405
427
|
!isRetryableFailure(error) ||
|
|
406
428
|
(index === candidates.length - 1 && !poolScopedFailure);
|
|
407
429
|
// Track only the most recent fired exchange; the array this replaced
|
|
@@ -85,8 +85,10 @@ export function buildUpstreamDispatchContext(input: {
|
|
|
85
85
|
const requestHeaders =
|
|
86
86
|
init?.headers ?? (request instanceof Request ? request.headers : undefined);
|
|
87
87
|
const headers = new Headers(requestHeaders);
|
|
88
|
-
//
|
|
89
|
-
|
|
88
|
+
// A route-level user agent is an explicit operator override: it
|
|
89
|
+
// replaces whatever identity the provider would otherwise send.
|
|
90
|
+
// Absent route identity leaves the provider's own header untouched.
|
|
91
|
+
if (userAgent) headers.set("user-agent", userAgent);
|
|
90
92
|
return sourceFetch(request, { ...init, headers });
|
|
91
93
|
}
|
|
92
94
|
: sourceFetch;
|
|
@@ -920,7 +920,21 @@ export async function mapUpstreamHttpError(response: Response, providerId: strin
|
|
|
920
920
|
? { code: "context_length_exceeded" as const, status: 413, origin: "upstream" as const }
|
|
921
921
|
: classifyUpstreamError(response.status, identifier);
|
|
922
922
|
const extracted = extractUpstreamMessage(parsedBody ?? text);
|
|
923
|
-
|
|
923
|
+
let message =
|
|
924
|
+
extracted.length > 0
|
|
925
|
+
? extracted
|
|
926
|
+
: text.trim() || `Provider returned HTTP ${response.status}.`;
|
|
927
|
+
// A bare "Provider returned HTTP <5xx>" tells the operator nothing actionable;
|
|
928
|
+
// platform-unavailable fallbacks get the same human sign-off every other
|
|
929
|
+
// gateway error carries. The raw upstream body stays in `details.raw` for
|
|
930
|
+
// diagnostics either way.
|
|
931
|
+
if (
|
|
932
|
+
classification.code === "platform_unavailable" &&
|
|
933
|
+
message === `Provider returned HTTP ${response.status}.`
|
|
934
|
+
) {
|
|
935
|
+
message = `The provider's service is temporarily unavailable (HTTP ${response.status}) (request {requestId}). Your request did not go through — wait a moment and try again. Hold tight ☁️`;
|
|
936
|
+
}
|
|
937
|
+
message = message.slice(0, MAX_UPSTREAM_ERROR_BYTES).replace(/[\r\n]+/g, " ");
|
|
924
938
|
const headerWaitMs = parseUpstreamBackoff(response.headers);
|
|
925
939
|
// A queue carries its own interval, which is the one the caller must honour:
|
|
926
940
|
// it is what the upstream promised, while `Retry-After` may be absent or may
|
|
@@ -312,6 +312,7 @@ function requestLogMetadata(
|
|
|
312
312
|
cachedInputTokens: state.outcome.usage.cached_input_tokens,
|
|
313
313
|
reasoningTokens: state.outcome.usage.reasoning_tokens,
|
|
314
314
|
estimatedCost: state.outcome.usage.estimated_cost,
|
|
315
|
+
...(state.outcome.degenerateStop ? { degenerateStop: true } : {}),
|
|
315
316
|
},
|
|
316
317
|
}
|
|
317
318
|
: {}),
|
|
@@ -385,6 +386,9 @@ function requestTelemetryEvent(
|
|
|
385
386
|
...(state.payloadCaptureState === undefined
|
|
386
387
|
? {}
|
|
387
388
|
: { payloadCaptureState: state.payloadCaptureState }),
|
|
389
|
+
// Degenerate stop (short output ended by an ordinary `stop`) — measured
|
|
390
|
+
// so a remote operator can spot the pattern in the request list.
|
|
391
|
+
...(state.outcome?.degenerateStop ? { degenerateStop: true } : {}),
|
|
388
392
|
};
|
|
389
393
|
}
|
|
390
394
|
|