cartethyia 2.4.6 → 2.5.1
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/README.md +45 -13
- package/dist/dashboard/assets/BannedPage-ChqnTRpZ.js +1 -0
- package/dist/dashboard/assets/BannedPage-ChqnTRpZ.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-Bcy150Jo.js → CliToolDetailPage-Bb9CNuoh.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-Bb9CNuoh.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-DyNXefph.js → CliToolsPage-BXUaRCQQ.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-BXUaRCQQ.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-CisNZOUt.js +1 -0
- package/dist/dashboard/assets/CombosPage-CisNZOUt.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-CATpAEOz.js → ConsoleLogPage-BOpI_LOH.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-BOpI_LOH.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-D6jEU7nL.js → CustomizationPage-DAkdvJ4k.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-DAkdvJ4k.js.gz +0 -0
- package/dist/dashboard/assets/{HealthEventsModal-CZkpn0a7.js → HealthEventsModal-DeT-VXHd.js} +1 -1
- package/dist/dashboard/assets/HealthEventsModal-DeT-VXHd.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-BJlMVN4U.js → LoginPage-CytxSeGP.js} +1 -1
- package/dist/dashboard/assets/LoginPage-CytxSeGP.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-Br3CXEau.js → ModelPicker-oQ-SDXp8.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-oQ-SDXp8.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-CU7S1wJs.js +1 -0
- package/dist/dashboard/assets/OverviewPage-CU7S1wJs.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-DLm2baot.js +3 -0
- package/dist/dashboard/assets/ProviderDetailPage-DLm2baot.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js +1 -0
- package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-DOXfuzfL.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-DOXfuzfL.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-wvk0ciQN.js +7 -0
- package/dist/dashboard/assets/ProxyPage-wvk0ciQN.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-Cn_fcUHE.js +1 -0
- package/dist/dashboard/assets/QuotaPage-Cn_fcUHE.js.gz +0 -0
- package/dist/dashboard/assets/{SettingsPage-BHW4Zt06.js → SettingsPage-8-eVadhc.js} +1 -1
- package/dist/dashboard/assets/SettingsPage-8-eVadhc.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-VOYC_lIa.js → SetupPage-uEIXPyGO.js} +1 -1
- package/dist/dashboard/assets/SetupPage-uEIXPyGO.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-DCY0TPTZ.js +1 -0
- package/dist/dashboard/assets/SortableList-DCY0TPTZ.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-BNIDLiU6.js → StudioPage-DqK5E26W.js} +4 -4
- package/dist/dashboard/assets/StudioPage-DqK5E26W.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-38JfiZ0H.js +56 -0
- package/dist/dashboard/assets/UsagePage-38JfiZ0H.js.gz +0 -0
- package/dist/dashboard/assets/{api-keys-BETfaMYZ.js → api-keys-BmDbsior.js} +1 -1
- package/dist/dashboard/assets/api-keys-BmDbsior.js.gz +0 -0
- package/dist/dashboard/assets/app-BbpSarVV.css +2 -0
- package/dist/dashboard/assets/app-BbpSarVV.css.gz +0 -0
- package/dist/dashboard/assets/assert-BHfaei-X.js +1 -0
- package/dist/dashboard/assets/assert-BHfaei-X.js.gz +0 -0
- package/dist/dashboard/assets/{badge-BTyGk_DL.js → badge-DpXT4Ixt.js} +1 -1
- package/dist/dashboard/assets/badge-DpXT4Ixt.js.gz +0 -0
- package/dist/dashboard/assets/{banner-bujTWYM4.js → banner-CSAAWLJv.js} +1 -1
- package/dist/dashboard/assets/banner-CSAAWLJv.js.gz +0 -0
- package/dist/dashboard/assets/{card-B0Bnx5CA.js → card-DEhRXXw4.js} +1 -1
- package/dist/dashboard/assets/card-DEhRXXw4.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-DE2ug812.js → cli-tools-B0wBq-RL.js} +1 -1
- package/dist/dashboard/assets/cli-tools-B0wBq-RL.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-R_rArH8S.js → clipboard-button-DOo6EU8O.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-DOo6EU8O.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-BWIP_EvW.js → dialog-BvQeBMfP.js} +1 -1
- package/dist/dashboard/assets/dialog-BvQeBMfP.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-BKcgVugF.js → drawer-CMwPXwQB.js} +1 -1
- package/dist/dashboard/assets/drawer-CMwPXwQB.js.gz +0 -0
- package/dist/dashboard/assets/entry--yj84zk3.js +4 -0
- package/dist/dashboard/assets/entry--yj84zk3.js.gz +0 -0
- package/dist/dashboard/assets/{entry-CcWkGR4a.js → entry-D4O2229X.js} +1 -1
- package/dist/dashboard/assets/{entry-CcWkGR4a.js.gz → entry-D4O2229X.js.gz} +0 -0
- package/dist/dashboard/assets/{entry-kTiTFrvh.js → entry-DKvr5Q9K.js} +1 -1
- package/dist/dashboard/assets/entry-DKvr5Q9K.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-Booc5mNe.js → github-badge-zliLvBwr.js} +1 -1
- package/dist/dashboard/assets/github-badge-zliLvBwr.js.gz +0 -0
- package/dist/dashboard/assets/{index-D70pChfn.js → index-CcD1eSJ6.js} +2 -2
- package/dist/dashboard/assets/index-CcD1eSJ6.js.gz +0 -0
- package/dist/dashboard/assets/{inline-D1TCqM2D.js → inline-D1IlZZ8H.js} +1 -1
- package/dist/dashboard/assets/inline-D1IlZZ8H.js.gz +0 -0
- package/dist/dashboard/assets/{input-B9oyU93y.js → input-BpVZdrbc.js} +1 -1
- package/dist/dashboard/assets/input-BpVZdrbc.js.gz +0 -0
- package/dist/dashboard/assets/{layout-BArzXirQ.js → layout-BioLW7gN.js} +1 -1
- package/dist/dashboard/assets/layout-BioLW7gN.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-BVZzaoHF.js → list-row-CwVcGAIc.js} +1 -1
- package/dist/dashboard/assets/list-row-CwVcGAIc.js.gz +0 -0
- package/dist/dashboard/assets/{live-EmuIayqp.js → live-yfEJFF9T.js} +1 -1
- package/dist/dashboard/assets/live-yfEJFF9T.js.gz +0 -0
- package/dist/dashboard/assets/{model-bans-CSPQ9xB0.js → model-bans-CT-TxSNX.js} +1 -1
- package/dist/dashboard/assets/model-bans-CT-TxSNX.js.gz +0 -0
- package/dist/dashboard/assets/providers/cursor.webp +0 -0
- package/dist/dashboard/assets/providers/trae.webp +0 -0
- package/dist/dashboard/assets/{routing-BExgxxBU.js → routing-Cs6jl49L.js} +1 -1
- package/dist/dashboard/assets/routing-Cs6jl49L.js.gz +0 -0
- package/dist/dashboard/assets/select-XPJ-dIw3.js +41 -0
- package/dist/dashboard/assets/select-XPJ-dIw3.js.gz +0 -0
- package/dist/dashboard/assets/settings-CfcTcOyU.js +1 -0
- package/dist/dashboard/assets/settings-CfcTcOyU.js.gz +0 -0
- package/dist/dashboard/assets/{stack-Ca5HxI5i.js → stack-C9_uUjaA.js} +1 -1
- package/dist/dashboard/assets/stack-C9_uUjaA.js.gz +0 -0
- package/dist/dashboard/assets/{state-CzUPfLXo.js → state-DA0B95UQ.js} +1 -1
- package/dist/dashboard/assets/state-DA0B95UQ.js.gz +0 -0
- package/dist/dashboard/assets/{switch-DOUIky_i.js → switch-Cittxy-x.js} +1 -1
- package/dist/dashboard/assets/switch-Cittxy-x.js.gz +0 -0
- package/dist/dashboard/assets/{toast-C76WrMhc.js → toast-CNt_jzO4.js} +1 -1
- package/dist/dashboard/assets/toast-CNt_jzO4.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-D87FZwH0.js → useMutation-BQKMcJ1H.js} +1 -1
- package/dist/dashboard/assets/useMutation-BQKMcJ1H.js.gz +0 -0
- package/dist/dashboard/index.html +1 -1
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +51 -52
- package/migrations/full-postgres/0015_add_payload_stages.sql +18 -0
- package/migrations/full-postgres/0016_add_share_link_slug.sql +20 -0
- package/migrations/full-postgres/0017_add_models_pro_only.sql +18 -0
- package/migrations/full-postgres/0018_add_accounts_quota_plan.sql +18 -0
- package/migrations/full-postgres/0019_add_models_free_tier.sql +15 -0
- package/migrations/lite-sqlite3/0015_add_payload_stages.sql +18 -0
- package/migrations/lite-sqlite3/0016_add_share_link_slug.sql +20 -0
- package/migrations/lite-sqlite3/0017_add_models_pro_only.sql +18 -0
- package/migrations/lite-sqlite3/0018_add_accounts_quota_plan.sql +18 -0
- package/migrations/lite-sqlite3/0019_add_models_free_tier.sql +15 -0
- package/package.json +3 -2
- package/scripts/cli/command/console-menu.ts +30 -11
- package/scripts/cli/command/start.ts +77 -13
- package/scripts/cli/command/tray.ts +47 -8
- package/scripts/cli/command/update.ts +1 -1
- package/scripts/cli/gateway/instance-lock.ts +105 -0
- package/scripts/cli/gateway/spawn.ts +173 -101
- package/scripts/cli/ui/background.ts +110 -49
- package/src/boot/app.ts +20 -6
- package/src/boot/dependencies.ts +19 -0
- package/src/boot/elysia-typebox-bridge.ts +37 -0
- package/src/boot/lifecycle.ts +2 -0
- package/src/config/cooldowns.ts +138 -138
- package/src/config/providers.ts +5 -5
- package/src/config/runtime.ts +19 -3
- package/src/config/spec.ts +9 -0
- package/src/console/api-keys/contracts.ts +414 -391
- package/src/console/api-keys/routes.ts +880 -870
- package/src/console/auth/account-reset-store.ts +14 -0
- package/src/console/auth/account-reset.ts +3 -2
- package/src/console/auth/setup-service.ts +6 -3
- package/src/console/domain-registration.ts +9 -2
- package/src/console/observability/contracts.ts +475 -455
- package/src/console/observability/store.ts +1015 -952
- package/src/console/providers/catalog/contracts.ts +31 -0
- package/src/console/providers/catalog/provider-operations.ts +8 -0
- package/src/console/providers/catalog/store.ts +1532 -1525
- package/src/console/providers/oauth/callback-listener.ts +18 -3
- package/src/console/providers/oauth/routes.ts +38 -15
- package/src/console/quota/refresh.ts +19 -0
- package/src/console/updates/routes.ts +162 -4
- package/src/main.ts +3 -0
- package/src/network/http2-bidi.ts +85 -0
- package/src/network/outbound-fetch.ts +1 -26
- package/src/network/pool/agent.ts +478 -460
- package/src/network/pool/resolver.ts +1104 -544
- package/src/network/pool-health-machine.ts +12 -5
- package/src/observability/payload-capture.ts +344 -324
- package/src/persistence/db-pg.ts +9 -3
- package/src/persistence/db-sqlite.ts +497 -495
- package/src/persistence/health-event-flush.ts +137 -0
- package/src/persistence/migrate.ts +181 -129
- package/src/persistence/schema.ts +1360 -1295
- package/src/persistence/settings-revision.ts +25 -0
- package/src/persistence/shared-cache.ts +217 -0
- package/src/persistence/stores/share-store.ts +556 -528
- package/src/persistence/usage-flush.ts +61 -1
- package/src/protocol/canonical-model.ts +23 -1
- package/src/protocol/response/chat.ts +143 -10
- package/src/protocol/response/omp-tool-call-markup.ts +221 -0
- package/src/providers/default-registry.ts +173 -1
- package/src/providers/discovery/openai-model-discovery.ts +39 -3
- package/src/providers/discovery/openrouter-free-roster.ts +98 -0
- package/src/providers/discovery/probing-service.ts +1022 -1001
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +39 -0
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +913 -914
- package/src/providers/integrations/buddy/buddy-conversation-headers.ts +18 -5
- package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +4 -0
- package/src/providers/integrations/buddy/codebuddy/provider.ts +180 -175
- package/src/providers/integrations/buddy/codebuddy/shared.ts +15 -6
- package/src/providers/integrations/buddy/workbuddy/provider.ts +206 -204
- package/src/providers/integrations/buddy/workbuddy/shared.ts +3 -3
- package/src/providers/integrations/claude/claude.ts +606 -582
- package/src/providers/integrations/cline/cline-quota.ts +18 -16
- package/src/providers/integrations/cline/cline.ts +539 -468
- package/src/providers/integrations/commandcode/commandcode.ts +675 -541
- package/src/providers/integrations/commandcode/discovery.ts +116 -0
- package/src/providers/integrations/commandcode/quota.ts +160 -0
- package/src/providers/integrations/cursor/cursor-auth.ts +418 -0
- package/src/providers/integrations/cursor/cursor-catalog.ts +368 -0
- package/src/providers/integrations/cursor/cursor-http2.ts +300 -0
- package/src/providers/integrations/cursor/cursor-identity.ts +92 -0
- package/src/providers/integrations/cursor/cursor-request.ts +471 -0
- package/src/providers/integrations/cursor/cursor-session-manager.ts +281 -0
- package/src/providers/integrations/cursor/cursor-shared.ts +108 -0
- package/src/providers/integrations/cursor/cursor.ts +960 -0
- package/src/providers/integrations/cursor/protocol/framing.ts +129 -0
- package/src/providers/integrations/cursor/protocol/messages.ts +2355 -0
- package/src/providers/integrations/cursor/protocol/struct.ts +305 -0
- package/src/providers/integrations/github/github.ts +4 -4
- package/src/providers/integrations/hermes.ts +79 -8
- package/src/providers/integrations/kilo/kilo-discovery.ts +10 -1
- package/src/providers/integrations/kilo/kilo.ts +153 -146
- package/src/providers/integrations/opencode/provider.ts +532 -538
- package/src/providers/integrations/openrouter/provider.ts +139 -281
- package/src/providers/integrations/qoder/qoder-catalog.ts +450 -0
- package/src/providers/integrations/qoder/qoder-oauth.ts +310 -257
- package/src/providers/integrations/qoder/qoder-realm.ts +106 -0
- package/src/providers/integrations/qoder/qoder-virtual.ts +77 -0
- package/src/providers/integrations/qoder/qoder.ts +865 -730
- package/src/providers/integrations/trae/trae-catalog.ts +190 -0
- package/src/providers/integrations/trae/trae-headers.ts +149 -0
- package/src/providers/integrations/trae/trae-oauth.ts +773 -0
- package/src/providers/integrations/trae/trae-protocol.ts +400 -0
- package/src/providers/integrations/trae/trae-realm.ts +220 -0
- package/src/providers/integrations/trae/trae.ts +458 -0
- package/src/providers/operations/account-health-service.ts +107 -10
- package/src/providers/operations/client-versions.ts +133 -6
- package/src/providers/operations/provider-catalog-seeder.ts +95 -86
- package/src/providers/operations/provider-credential-service.ts +46 -7
- package/src/providers/provider-contracts.ts +76 -0
- package/src/providers/provider-metadata.ts +78 -6
- package/src/providers/provider-model-definition.ts +373 -349
- package/src/security/api-key-auth.ts +126 -8
- package/src/transport/contracts.ts +3 -0
- package/src/transport/dispatch/attempt-finalize.ts +32 -1
- package/src/transport/dispatch/attempt-loop.ts +530 -515
- package/src/transport/dispatch/generation.ts +88 -21
- package/src/transport/dispatch/leases.ts +99 -2
- package/src/transport/dispatch/proxy-request.ts +60 -3
- package/src/transport/dispatch/streaming/finalize.ts +16 -1
- package/src/transport/dispatch/streaming/stream.ts +22 -2
- package/src/transport/dispatch/upstream.ts +19 -1
- package/src/transport/guards/auth-guard.ts +26 -1
- package/src/transport/guards/body-policy.ts +5 -4
- package/src/transport/request/preparer.ts +103 -0
- package/src/transport/request/state.ts +14 -0
- package/src/transport/routing/plan-gate.ts +45 -0
- package/src/transport/routing/route-catalog.ts +799 -777
- package/src/transport/routing/route-model.ts +52 -1
- package/src/transport/routing/router.ts +51 -0
- package/dist/dashboard/assets/BannedPage-QhvbJB9o.js +0 -1
- package/dist/dashboard/assets/BannedPage-QhvbJB9o.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-Bcy150Jo.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-DyNXefph.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-Cak0ljnO.js +0 -1
- package/dist/dashboard/assets/CombosPage-Cak0ljnO.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-CATpAEOz.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-D6jEU7nL.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-CZkpn0a7.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-BJlMVN4U.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-Br3CXEau.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-CQxXbo2g.js +0 -1
- package/dist/dashboard/assets/OverviewPage-CQxXbo2g.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage--0RBNh99.js +0 -3
- package/dist/dashboard/assets/ProviderDetailPage--0RBNh99.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-paOCq8I0.js +0 -1
- package/dist/dashboard/assets/ProviderIcon-paOCq8I0.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-1M-C3mbu.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-1M-C3mbu.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-CXiumA53.js +0 -7
- package/dist/dashboard/assets/ProxyPage-CXiumA53.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-BGDIfIYP.js +0 -1
- package/dist/dashboard/assets/QuotaPage-BGDIfIYP.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-BHW4Zt06.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-VOYC_lIa.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-DpOTnGuH.js +0 -1
- package/dist/dashboard/assets/SortableList-DpOTnGuH.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-BNIDLiU6.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-BAFaSDQV.js +0 -56
- package/dist/dashboard/assets/UsagePage-BAFaSDQV.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-BETfaMYZ.js.gz +0 -0
- package/dist/dashboard/assets/app-BLCvGdzb.css +0 -2
- package/dist/dashboard/assets/app-BLCvGdzb.css.gz +0 -0
- package/dist/dashboard/assets/assert-7J6_LVD3.js +0 -1
- package/dist/dashboard/assets/assert-7J6_LVD3.js.gz +0 -0
- package/dist/dashboard/assets/badge-BTyGk_DL.js.gz +0 -0
- package/dist/dashboard/assets/banner-bujTWYM4.js.gz +0 -0
- package/dist/dashboard/assets/card-B0Bnx5CA.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-DE2ug812.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-R_rArH8S.js.gz +0 -0
- package/dist/dashboard/assets/dialog-BWIP_EvW.js.gz +0 -0
- package/dist/dashboard/assets/drawer-BKcgVugF.js.gz +0 -0
- package/dist/dashboard/assets/entry-7dpgvpJ3.js +0 -4
- package/dist/dashboard/assets/entry-7dpgvpJ3.js.gz +0 -0
- package/dist/dashboard/assets/entry-kTiTFrvh.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-Booc5mNe.js.gz +0 -0
- package/dist/dashboard/assets/index-D70pChfn.js.gz +0 -0
- package/dist/dashboard/assets/inline-D1TCqM2D.js.gz +0 -0
- package/dist/dashboard/assets/input-B9oyU93y.js.gz +0 -0
- package/dist/dashboard/assets/layout-BArzXirQ.js.gz +0 -0
- package/dist/dashboard/assets/list-row-BVZzaoHF.js.gz +0 -0
- package/dist/dashboard/assets/live-EmuIayqp.js.gz +0 -0
- package/dist/dashboard/assets/model-bans-CSPQ9xB0.js.gz +0 -0
- package/dist/dashboard/assets/routing-BExgxxBU.js.gz +0 -0
- package/dist/dashboard/assets/select-C9mMoEpJ.js +0 -41
- package/dist/dashboard/assets/select-C9mMoEpJ.js.gz +0 -0
- package/dist/dashboard/assets/settings-C4_Gn_wq.js +0 -1
- package/dist/dashboard/assets/settings-C4_Gn_wq.js.gz +0 -0
- package/dist/dashboard/assets/sliders-horizontal-ClG7dBeJ.js +0 -1
- package/dist/dashboard/assets/sliders-horizontal-ClG7dBeJ.js.gz +0 -0
- package/dist/dashboard/assets/stack-Ca5HxI5i.js.gz +0 -0
- package/dist/dashboard/assets/state-CzUPfLXo.js.gz +0 -0
- package/dist/dashboard/assets/switch-DOUIky_i.js.gz +0 -0
- package/dist/dashboard/assets/toast-C76WrMhc.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-D87FZwH0.js.gz +0 -0
- package/src/network/tls/bun-client-hello.ts +0 -112
- /package/dist/dashboard/assets/{app-Cjq7nrwu.js → app-C-Y4oZT0.js} +0 -0
- /package/dist/dashboard/assets/{app-Cjq7nrwu.js.gz → app-C-Y4oZT0.js.gz} +0 -0
|
@@ -80,11 +80,30 @@ async function writeLifetimeDeltas(
|
|
|
80
80
|
const FLUSH_STATEMENT_MAX_KEYS = 500;
|
|
81
81
|
/** Max distinct keys retained across windows; oldest dropped first. */
|
|
82
82
|
const FLUSH_PENDING_MAX_KEYS = 5000;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Consecutive fully-failed flushes before the writer backs off.
|
|
86
|
+
*
|
|
87
|
+
* A database that is down stays down: retrying every `intervalMs` means a
|
|
88
|
+
* write attempt per second against a backend that is already overloaded, which
|
|
89
|
+
* is load the operator cannot distinguish from real traffic and which delays
|
|
90
|
+
* recovery. After this many failures the buffer stops probing and waits out a
|
|
91
|
+
* cooldown instead.
|
|
92
|
+
*/
|
|
93
|
+
const FLUSH_FAILURE_STREAK_LIMIT = 3;
|
|
94
|
+
/** How long a failing writer waits before probing again. */
|
|
95
|
+
const FLUSH_BACKOFF_MS = 15_000;
|
|
83
96
|
export class UsageFlushBuffer {
|
|
84
97
|
private readonly pending = new Map<string, number>();
|
|
85
98
|
private timer: ReturnType<typeof setTimeout> | undefined;
|
|
86
99
|
private flushing = false;
|
|
87
100
|
private stopped = false;
|
|
101
|
+
/** Consecutive flushes that failed outright; drives the backoff below. */
|
|
102
|
+
private failureStreak = 0;
|
|
103
|
+
/** Wall-clock time before which no flush is attempted. */
|
|
104
|
+
private backoffUntil = 0;
|
|
105
|
+
/** Deltas abandoned because the buffer could not persist them. */
|
|
106
|
+
private droppedDelta = 0;
|
|
88
107
|
|
|
89
108
|
constructor(
|
|
90
109
|
private readonly db: CartethyiaDatabase,
|
|
@@ -104,6 +123,19 @@ export class UsageFlushBuffer {
|
|
|
104
123
|
}
|
|
105
124
|
}
|
|
106
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Deltas recorded but not yet persisted, for one key.
|
|
128
|
+
*
|
|
129
|
+
* Exposed so a reader of `lifetime_tokens_consumed` can add what is in
|
|
130
|
+
* flight to what Postgres last stored. Without it such a reader saw a value
|
|
131
|
+
* that was behind by up to one flush interval, and — because the flush is
|
|
132
|
+
* coalescing by design — behind by the largest amount exactly when the key
|
|
133
|
+
* was busiest.
|
|
134
|
+
*/
|
|
135
|
+
pendingFor(apiKeyId: string): number {
|
|
136
|
+
return this.pending.get(apiKeyId) ?? 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
107
139
|
/**
|
|
108
140
|
* Writes everything accumulated so far, in bounded statements of at most
|
|
109
141
|
* 500 keys each. One giant statement grows the SQL string with the
|
|
@@ -116,9 +148,14 @@ export class UsageFlushBuffer {
|
|
|
116
148
|
*/
|
|
117
149
|
async flush(): Promise<void> {
|
|
118
150
|
if (this.flushing || this.pending.size === 0) return;
|
|
151
|
+
// Backing off during a sustained outage: without this the writer probed a
|
|
152
|
+
// failing database on every tick, adding write load to the very overload
|
|
153
|
+
// that caused the failure.
|
|
154
|
+
if (Date.now() < this.backoffUntil) return;
|
|
119
155
|
this.flushing = true;
|
|
120
156
|
const batch = new Map(this.pending);
|
|
121
157
|
this.pending.clear();
|
|
158
|
+
let failed = 0;
|
|
122
159
|
try {
|
|
123
160
|
const entries = [...batch];
|
|
124
161
|
for (let offset = 0; offset < entries.length; offset += FLUSH_STATEMENT_MAX_KEYS) {
|
|
@@ -134,15 +171,38 @@ export class UsageFlushBuffer {
|
|
|
134
171
|
try {
|
|
135
172
|
await this.write(this.db, apiKeyId, delta);
|
|
136
173
|
} catch {
|
|
137
|
-
|
|
174
|
+
// A transient failure retries, but only while the buffer is not
|
|
175
|
+
// itself saturated: an outage that outlasts the cap must drop
|
|
176
|
+
// accounting rather than grow the pending map without bound.
|
|
177
|
+
if (this.pending.size < FLUSH_PENDING_MAX_KEYS) {
|
|
178
|
+
this.pending.set(apiKeyId, (this.pending.get(apiKeyId) ?? 0) + delta);
|
|
179
|
+
} else {
|
|
180
|
+
this.droppedDelta += delta;
|
|
181
|
+
}
|
|
182
|
+
failed += 1;
|
|
138
183
|
}
|
|
139
184
|
}
|
|
140
185
|
}
|
|
141
186
|
} finally {
|
|
187
|
+
if (failed > 0) {
|
|
188
|
+
this.failureStreak += 1;
|
|
189
|
+
if (this.failureStreak >= FLUSH_FAILURE_STREAK_LIMIT) {
|
|
190
|
+
this.backoffUntil = Date.now() + FLUSH_BACKOFF_MS;
|
|
191
|
+
this.failureStreak = 0;
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
this.failureStreak = 0;
|
|
195
|
+
this.backoffUntil = 0;
|
|
196
|
+
}
|
|
142
197
|
this.flushing = false;
|
|
143
198
|
}
|
|
144
199
|
}
|
|
145
200
|
|
|
201
|
+
/** Total usage delta abandoned after sustained write failure. */
|
|
202
|
+
get droppedTotal(): number {
|
|
203
|
+
return this.droppedDelta;
|
|
204
|
+
}
|
|
205
|
+
|
|
146
206
|
/** Stops the timer. Pass `{ flush: true }` to persist what is still pending. */
|
|
147
207
|
async stop(options?: { flush?: boolean }): Promise<void> {
|
|
148
208
|
this.stopped = true;
|
|
@@ -33,7 +33,16 @@ export type WireFamily = (typeof WIRE_FAMILIES)[number];
|
|
|
33
33
|
* the console's Elysia body schema and its validator both project this list, so
|
|
34
34
|
* a new kind cannot reach one and miss the other.
|
|
35
35
|
*/
|
|
36
|
-
export const SERVICE_KINDS = [
|
|
36
|
+
export const SERVICE_KINDS = [
|
|
37
|
+
"llm",
|
|
38
|
+
"systemone",
|
|
39
|
+
"websearch",
|
|
40
|
+
"imageGeneration",
|
|
41
|
+
"videoGeneration",
|
|
42
|
+
"embedding",
|
|
43
|
+
"audioTranscription",
|
|
44
|
+
"audioSpeech",
|
|
45
|
+
] as const;
|
|
37
46
|
|
|
38
47
|
/** Provider service kind selected by a model's catalog row. */
|
|
39
48
|
export type ServiceKind = (typeof SERVICE_KINDS)[number];
|
|
@@ -354,6 +363,19 @@ export interface CanonicalTerminalEvent {
|
|
|
354
363
|
readonly response_id?: string;
|
|
355
364
|
/** Event timestamp in milliseconds since epoch (optional for profiling). */
|
|
356
365
|
readonly timestamp?: number;
|
|
366
|
+
/**
|
|
367
|
+
* Set by an adapter whose turn is not finished: the stream ended because the
|
|
368
|
+
* model asked for a tool the client must answer, and the provider's
|
|
369
|
+
* authoritative usage arrives with the *later* request that continues the
|
|
370
|
+
* same upstream turn.
|
|
371
|
+
*
|
|
372
|
+
* The settle path reads this to skip charging estimated tokens for the
|
|
373
|
+
* interim response — otherwise one logical turn bills an input/output
|
|
374
|
+
* estimate on the tool-call response and the real usage on the continuation,
|
|
375
|
+
* which double-counts the turn. Only a provider that genuinely spans client
|
|
376
|
+
* requests with one upstream turn sets it.
|
|
377
|
+
*/
|
|
378
|
+
readonly usage_deferred?: true;
|
|
357
379
|
}
|
|
358
380
|
|
|
359
381
|
/** An ordered canonical stream event; sequence numbers must increase monotonically. */
|
|
@@ -11,6 +11,11 @@ import { usageFromProvider, readReasoningText } from "../../providers/provider-u
|
|
|
11
11
|
import { createToolEmitLedger } from "../tool-identity";
|
|
12
12
|
import { MimoThinkSplitter, stripThinkingTags } from "../../providers/integrations/xiaomi-mimo/think-stream";
|
|
13
13
|
import { parseDsmlToolCalls } from "./dsml";
|
|
14
|
+
import {
|
|
15
|
+
OmpToolCallMarkupParser,
|
|
16
|
+
parseOmpToolCallMarkup,
|
|
17
|
+
type OmpTextToolCall,
|
|
18
|
+
} from "./omp-tool-call-markup";
|
|
14
19
|
|
|
15
20
|
|
|
16
21
|
export function mapChatStopReason(
|
|
@@ -43,14 +48,23 @@ export function parseChatResponseToEvents(
|
|
|
43
48
|
const choices = (json["choices"] as Array<Record<string, unknown>>) ?? [];
|
|
44
49
|
const first = choices[0] as Record<string, unknown> | undefined;
|
|
45
50
|
const message = first?.["message"] as Record<string, unknown> | undefined;
|
|
51
|
+
const declaredToolNames = declaredToolNamesForRequest(request);
|
|
52
|
+
const callableToolNames = callableToolNamesForRequest(request);
|
|
53
|
+
let recoveredToolCalls = 0;
|
|
46
54
|
if (message) {
|
|
47
55
|
const reasoningRaw = readReasoningText(message);
|
|
56
|
+
const reasoningMarkup =
|
|
57
|
+
typeof reasoningRaw === "string" && (request.tools?.length ?? 0) > 0
|
|
58
|
+
? parseOmpToolCallMarkup(reasoningRaw, declaredToolNames)
|
|
59
|
+
: undefined;
|
|
48
60
|
// hy4-preview-f (and other buddy reasoning models) sometimes wraps its own
|
|
49
61
|
// reasoning in `<think>…</think>` inside `reasoning_content`, which then
|
|
50
62
|
// reaches the client as literal tag bytes. The field itself is the stream
|
|
51
63
|
// for reasoning here, so the tags cannot carry structure — strip them.
|
|
52
64
|
const reasoning =
|
|
53
|
-
typeof reasoningRaw === "string"
|
|
65
|
+
typeof reasoningRaw === "string"
|
|
66
|
+
? stripThinkingTags(reasoningMarkup?.text ?? reasoningRaw)
|
|
67
|
+
: reasoningRaw;
|
|
54
68
|
if (typeof reasoning === "string" && reasoning.length > 0) {
|
|
55
69
|
events.push({
|
|
56
70
|
type: "content_delta",
|
|
@@ -62,8 +76,16 @@ export function parseChatResponseToEvents(
|
|
|
62
76
|
});
|
|
63
77
|
}
|
|
64
78
|
const content = message["content"] as string | null;
|
|
65
|
-
const
|
|
66
|
-
|
|
79
|
+
const recoveredMarkup =
|
|
80
|
+
typeof content === "string" && (request.tools?.length ?? 0) > 0
|
|
81
|
+
? parseOmpToolCallMarkup(content, declaredToolNames)
|
|
82
|
+
: undefined;
|
|
83
|
+
const contentWithoutMarkup = recoveredMarkup?.text ?? content;
|
|
84
|
+
const recovered =
|
|
85
|
+
typeof contentWithoutMarkup === "string"
|
|
86
|
+
? parseDsmlToolCalls(contentWithoutMarkup)
|
|
87
|
+
: undefined;
|
|
88
|
+
const visible = recovered === undefined ? contentWithoutMarkup : recovered.text;
|
|
67
89
|
if (typeof visible === "string" && visible.includes("<think")) {
|
|
68
90
|
const think = new MimoThinkSplitter();
|
|
69
91
|
const split = think.push(visible);
|
|
@@ -104,9 +126,39 @@ export function parseChatResponseToEvents(
|
|
|
104
126
|
});
|
|
105
127
|
}
|
|
106
128
|
const structuredCalls = message["tool_calls"];
|
|
107
|
-
const
|
|
129
|
+
const hasStructuredCalls =
|
|
130
|
+
Array.isArray(structuredCalls) &&
|
|
131
|
+
structuredCalls.some((candidate) => {
|
|
132
|
+
if (typeof candidate !== "object" || candidate === null) return false;
|
|
133
|
+
const record = candidate as Record<string, unknown>;
|
|
134
|
+
const fn =
|
|
135
|
+
typeof record["function"] === "object" && record["function"] !== null
|
|
136
|
+
? (record["function"] as Record<string, unknown>)
|
|
137
|
+
: undefined;
|
|
138
|
+
const callId = typeof record["id"] === "string" && record["id"].length > 0;
|
|
139
|
+
const name =
|
|
140
|
+
typeof fn?.["name"] === "string" && fn["name"].length > 0
|
|
141
|
+
? fn["name"]
|
|
142
|
+
: typeof record["name"] === "string"
|
|
143
|
+
? record["name"]
|
|
144
|
+
: undefined;
|
|
145
|
+
return callId && name !== undefined && name.length > 0;
|
|
146
|
+
});
|
|
147
|
+
const toolCalls = hasStructuredCalls
|
|
108
148
|
? structuredCalls
|
|
109
|
-
:
|
|
149
|
+
: recoveredMarkup?.calls.some((call) => callableToolNames.has(call.name))
|
|
150
|
+
? recoveredMarkup.calls
|
|
151
|
+
.filter((call) => callableToolNames.has(call.name))
|
|
152
|
+
.map((call) => ({
|
|
153
|
+
id: call.id,
|
|
154
|
+
function: { name: call.name, arguments: call.arguments },
|
|
155
|
+
}))
|
|
156
|
+
: recovered?.calls ?? [];
|
|
157
|
+
if (!hasStructuredCalls && recoveredMarkup !== undefined) {
|
|
158
|
+
recoveredToolCalls = recoveredMarkup.calls.filter((call) =>
|
|
159
|
+
callableToolNames.has(call.name),
|
|
160
|
+
).length;
|
|
161
|
+
}
|
|
110
162
|
for (const tc of toolCalls) {
|
|
111
163
|
if (typeof tc !== "object" || tc === null) continue;
|
|
112
164
|
const record = tc as Record<string, unknown>;
|
|
@@ -166,7 +218,7 @@ export function parseChatResponseToEvents(
|
|
|
166
218
|
sequenceNumber: events.length + 1,
|
|
167
219
|
responseId: id,
|
|
168
220
|
state: "complete",
|
|
169
|
-
stopReason: chatJsonStopReason,
|
|
221
|
+
stopReason: recoveredToolCalls > 0 ? "tool_use" : chatJsonStopReason,
|
|
170
222
|
providerStopReason: chatJsonProviderStopReason,
|
|
171
223
|
usage: usageRecord,
|
|
172
224
|
}),
|
|
@@ -194,9 +246,17 @@ export async function* decodeChatSseStream(
|
|
|
194
246
|
const lastToolNameByIndex = new Map<number, string>();
|
|
195
247
|
let lastSeenToolId: string | undefined;
|
|
196
248
|
let lastSeenToolName: string | undefined;
|
|
249
|
+
let structuredToolCallEmitted = false;
|
|
250
|
+
const recoveredMarkupCalls: OmpTextToolCall[] = [];
|
|
197
251
|
// Single emission ledger: one entry per logical call.
|
|
198
252
|
const ledger = createToolEmitLedger();
|
|
199
253
|
const think = new MimoThinkSplitter();
|
|
254
|
+
const declaredToolNames = declaredToolNamesForRequest(request);
|
|
255
|
+
const callableToolNames = callableToolNamesForRequest(request);
|
|
256
|
+
const contentMarkup =
|
|
257
|
+
(request.tools?.length ?? 0) > 0 ? new OmpToolCallMarkupParser(declaredToolNames) : undefined;
|
|
258
|
+
const reasoningMarkup =
|
|
259
|
+
(request.tools?.length ?? 0) > 0 ? new OmpToolCallMarkupParser(declaredToolNames) : undefined;
|
|
200
260
|
yield {
|
|
201
261
|
type: "response_start",
|
|
202
262
|
sequence_number: seq++,
|
|
@@ -240,8 +300,10 @@ export async function* decodeChatSseStream(
|
|
|
240
300
|
// text/thinking blocks.
|
|
241
301
|
if (typeof delta?.["content"] === "string" && delta["content"].length > 0) {
|
|
242
302
|
const raw = delta["content"] as string;
|
|
243
|
-
const
|
|
244
|
-
|
|
303
|
+
const markup = contentMarkup?.push(raw);
|
|
304
|
+
if (markup !== undefined) recoveredMarkupCalls.push(...markup.calls);
|
|
305
|
+
const recovered = parseDsmlToolCalls(markup?.text ?? raw);
|
|
306
|
+
const visible = recovered?.text ?? markup?.text ?? raw;
|
|
245
307
|
const split = think.push(visible);
|
|
246
308
|
if (split.reasoning.length > 0) {
|
|
247
309
|
yield {
|
|
@@ -270,12 +332,16 @@ export async function* decodeChatSseStream(
|
|
|
270
332
|
}
|
|
271
333
|
}
|
|
272
334
|
const reasoningRaw = delta ? readReasoningText(delta) : undefined;
|
|
335
|
+
const reasoningMarkupResult =
|
|
336
|
+
typeof reasoningRaw === "string" ? reasoningMarkup?.push(reasoningRaw) : undefined;
|
|
273
337
|
// Strip stray `<think>` tags the upstream double-wrapped inside its own
|
|
274
338
|
// reasoning stream (buddy's hy4-preview-f leaks them per-delta). Done
|
|
275
339
|
// per delta rather than once at the end because an open-ended tag that
|
|
276
340
|
// spans multiple deltas is a splitter job, not this one.
|
|
277
341
|
const reasoningText =
|
|
278
|
-
typeof reasoningRaw === "string"
|
|
342
|
+
typeof reasoningRaw === "string"
|
|
343
|
+
? stripThinkingTags(reasoningMarkupResult?.text ?? reasoningRaw)
|
|
344
|
+
: reasoningRaw;
|
|
279
345
|
if (typeof reasoningText === "string" && reasoningText.length > 0) {
|
|
280
346
|
yield {
|
|
281
347
|
type: "content_delta",
|
|
@@ -353,6 +419,7 @@ export async function* decodeChatSseStream(
|
|
|
353
419
|
ledger.isDuplicateDefinition(id, fragName, args);
|
|
354
420
|
if (!duplicate) {
|
|
355
421
|
if (establishedNew) ledger.claimFirstEmission(id, fragName, args);
|
|
422
|
+
structuredToolCallEmitted = true;
|
|
356
423
|
yield {
|
|
357
424
|
type: "tool_call_delta",
|
|
358
425
|
sequence_number: seq++,
|
|
@@ -390,6 +457,38 @@ export async function* decodeChatSseStream(
|
|
|
390
457
|
);
|
|
391
458
|
}
|
|
392
459
|
}
|
|
460
|
+
const contentMarkupTail = contentMarkup?.flush();
|
|
461
|
+
if (contentMarkupTail !== undefined) {
|
|
462
|
+
recoveredMarkupCalls.push(...contentMarkupTail.calls);
|
|
463
|
+
if (contentMarkupTail.text.length > 0) {
|
|
464
|
+
const split = think.push(contentMarkupTail.text);
|
|
465
|
+
if (split.reasoning.length > 0) {
|
|
466
|
+
yield {
|
|
467
|
+
type: "content_delta",
|
|
468
|
+
sequence_number: seq++,
|
|
469
|
+
content: { kind: "reasoning", payload: null, summary: split.reasoning },
|
|
470
|
+
} as CanonicalEvent;
|
|
471
|
+
}
|
|
472
|
+
if (split.text.length > 0) {
|
|
473
|
+
yield {
|
|
474
|
+
type: "content_delta",
|
|
475
|
+
sequence_number: seq++,
|
|
476
|
+
content: { kind: "text", text: split.text },
|
|
477
|
+
} as CanonicalEvent;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
const reasoningMarkupTail = reasoningMarkup?.flush();
|
|
482
|
+
if (reasoningMarkupTail?.text) {
|
|
483
|
+
const reasoningText = stripThinkingTags(reasoningMarkupTail.text);
|
|
484
|
+
if (reasoningText.length > 0) {
|
|
485
|
+
yield {
|
|
486
|
+
type: "content_delta",
|
|
487
|
+
sequence_number: seq++,
|
|
488
|
+
content: { kind: "reasoning", payload: null, summary: reasoningText },
|
|
489
|
+
} as CanonicalEvent;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
393
492
|
const rest = think.flush();
|
|
394
493
|
if (rest.reasoning.length > 0) {
|
|
395
494
|
yield {
|
|
@@ -405,6 +504,20 @@ export async function* decodeChatSseStream(
|
|
|
405
504
|
content: { kind: "text", text: rest.text },
|
|
406
505
|
} as CanonicalEvent;
|
|
407
506
|
}
|
|
507
|
+
let recoveredMarkupToolCallsEmitted = 0;
|
|
508
|
+
if (!structuredToolCallEmitted) {
|
|
509
|
+
for (const call of recoveredMarkupCalls) {
|
|
510
|
+
if (!callableToolNames.has(call.name)) continue;
|
|
511
|
+
yield {
|
|
512
|
+
type: "tool_call_delta",
|
|
513
|
+
sequence_number: seq++,
|
|
514
|
+
call_id: call.id,
|
|
515
|
+
name: call.name,
|
|
516
|
+
arguments_delta: call.arguments,
|
|
517
|
+
} as CanonicalEvent;
|
|
518
|
+
recoveredMarkupToolCallsEmitted += 1;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
408
521
|
const chatStopReason = mapChatStopReason(finishReason);
|
|
409
522
|
const chatProviderStopReason =
|
|
410
523
|
typeof finishReason === "string" ? finishReason : undefined;
|
|
@@ -423,8 +536,28 @@ export async function* decodeChatSseStream(
|
|
|
423
536
|
// `stop`. `chatFinishReason(undefined)` falls back to `stop`, so the client
|
|
424
537
|
// saw a completed turn while telemetry recorded a 502 upstream transport
|
|
425
538
|
// failure — the failure this terminal already carries was invisible to it.
|
|
426
|
-
stopReason:
|
|
539
|
+
stopReason:
|
|
540
|
+
truncated && signal?.aborted !== true
|
|
541
|
+
? "error"
|
|
542
|
+
: !structuredToolCallEmitted && recoveredMarkupToolCallsEmitted > 0
|
|
543
|
+
? "tool_use"
|
|
544
|
+
: chatStopReason,
|
|
427
545
|
providerStopReason: chatProviderStopReason,
|
|
428
546
|
usage: usageFromProvider(rawUsage),
|
|
429
547
|
});
|
|
430
548
|
}
|
|
549
|
+
|
|
550
|
+
function declaredToolNamesForRequest(request: CanonicalRequest): ReadonlySet<string> {
|
|
551
|
+
return new Set((request.tools ?? []).map((tool) => tool.name));
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function callableToolNamesForRequest(request: CanonicalRequest): ReadonlySet<string> {
|
|
555
|
+
const declared = declaredToolNamesForRequest(request);
|
|
556
|
+
const choice = request.tool_choice;
|
|
557
|
+
if (choice === "none") return new Set();
|
|
558
|
+
if (choice === undefined || typeof choice === "string") return declared;
|
|
559
|
+
if (choice.type === "tool" || choice.type === "custom") {
|
|
560
|
+
return declared.has(choice.name) ? new Set([choice.name]) : new Set();
|
|
561
|
+
}
|
|
562
|
+
return new Set(choice.names.filter((name) => declared.has(name)));
|
|
563
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recovers the tool-call envelope some models occasionally emit as assistant
|
|
3
|
+
* text instead of OpenAI `tool_calls`.
|
|
4
|
+
*
|
|
5
|
+
* The wire shape is the captured `<tool_calls:8hex>` / `<tool_call:8hex>` /
|
|
6
|
+
* `<arg_key:8hex>` / `<arg_value:8hex>` form. Only complete envelopes are
|
|
7
|
+
* interpreted. Calls are returned only when their names were declared on the
|
|
8
|
+
* request; an undeclared name is never promoted into executable tool input.
|
|
9
|
+
*/
|
|
10
|
+
export interface OmpTextToolCall {
|
|
11
|
+
readonly id: string;
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly arguments: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Text remaining after complete tool envelopes are removed. */
|
|
17
|
+
export interface OmpTextToolCallResult {
|
|
18
|
+
readonly text: string;
|
|
19
|
+
readonly calls: readonly OmpTextToolCall[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const OPEN_PREFIX = "<tool_calls:";
|
|
23
|
+
const OPEN_PATTERN = /^<tool_calls:([0-9a-f]{8})>/;
|
|
24
|
+
const TOOL_NAME_PATTERN = /^[A-Za-z0-9_][A-Za-z0-9_.:-]{0,63}$/;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Incrementally removes complete captured tool envelopes from text.
|
|
28
|
+
*
|
|
29
|
+
* The parser holds a possible envelope across SSE deltas, so a split opening
|
|
30
|
+
* tag, argument tag, or closing tag is never emitted as visible prose. At
|
|
31
|
+
* end-of-stream an unterminated envelope is dropped, not executed or leaked.
|
|
32
|
+
*/
|
|
33
|
+
export class OmpToolCallMarkupParser {
|
|
34
|
+
readonly #allowedNames: ReadonlySet<string>;
|
|
35
|
+
#pending = "";
|
|
36
|
+
#callSequence = 0;
|
|
37
|
+
|
|
38
|
+
constructor(allowedNames: ReadonlySet<string>) {
|
|
39
|
+
this.#allowedNames = allowedNames;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Feeds one content or reasoning delta and returns only settled text. */
|
|
43
|
+
push(chunk: string): OmpTextToolCallResult {
|
|
44
|
+
this.#pending += chunk;
|
|
45
|
+
let cursor = 0;
|
|
46
|
+
let text = "";
|
|
47
|
+
const calls: OmpTextToolCall[] = [];
|
|
48
|
+
|
|
49
|
+
for (;;) {
|
|
50
|
+
const openIndex = this.#pending.indexOf(OPEN_PREFIX, cursor);
|
|
51
|
+
if (openIndex < 0) {
|
|
52
|
+
const safeEnd = this.#settledEnd(cursor);
|
|
53
|
+
text += this.#pending.slice(cursor, safeEnd);
|
|
54
|
+
cursor = safeEnd;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
text += this.#pending.slice(cursor, openIndex);
|
|
59
|
+
const remainder = this.#pending.slice(openIndex);
|
|
60
|
+
const opening = OPEN_PATTERN.exec(remainder);
|
|
61
|
+
if (opening === null || opening[1] === undefined) {
|
|
62
|
+
if (this.#couldBeOpening(remainder)) {
|
|
63
|
+
cursor = openIndex;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
text += "<";
|
|
67
|
+
cursor = openIndex + 1;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const marker = opening[1];
|
|
72
|
+
const openTag = opening[0];
|
|
73
|
+
const closeTag = `</tool_calls:${marker}>`;
|
|
74
|
+
const closeIndex = this.#pending.indexOf(closeTag, openIndex + openTag.length);
|
|
75
|
+
if (closeIndex < 0) {
|
|
76
|
+
cursor = openIndex;
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const envelopeEnd = closeIndex + closeTag.length;
|
|
81
|
+
const block = this.#pending.slice(openIndex, envelopeEnd);
|
|
82
|
+
const parsed = parseEnvelope(block, marker);
|
|
83
|
+
if (parsed === undefined) {
|
|
84
|
+
text += block;
|
|
85
|
+
} else if (parsed.some((call) => !this.#allowedNames.has(call.name))) {
|
|
86
|
+
// A syntactically valid call to an undeclared tool is model text, not
|
|
87
|
+
// authority to invoke a tool the client did not offer.
|
|
88
|
+
text += block;
|
|
89
|
+
} else {
|
|
90
|
+
for (const call of parsed) {
|
|
91
|
+
calls.push({
|
|
92
|
+
id: `call_omp_${marker}_${this.#callSequence++}`,
|
|
93
|
+
name: call.name,
|
|
94
|
+
arguments: call.arguments,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
cursor = envelopeEnd;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
this.#pending = this.#pending.slice(cursor);
|
|
102
|
+
return { text, calls };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Settles trailing text. A complete internal envelope cannot remain here;
|
|
107
|
+
* an unfinished envelope is deliberately discarded rather than leaked.
|
|
108
|
+
*/
|
|
109
|
+
flush(): OmpTextToolCallResult {
|
|
110
|
+
const pending = this.#pending;
|
|
111
|
+
this.#pending = "";
|
|
112
|
+
if (this.#couldBeOpening(pending) || OPEN_PATTERN.test(pending)) {
|
|
113
|
+
return { text: "", calls: [] };
|
|
114
|
+
}
|
|
115
|
+
return { text: pending, calls: [] };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
#settledEnd(start: number): number {
|
|
119
|
+
const remaining = this.#pending.slice(start);
|
|
120
|
+
const maxPrefixLength = Math.min(OPEN_PREFIX.length - 1, remaining.length);
|
|
121
|
+
for (let length = maxPrefixLength; length > 0; length -= 1) {
|
|
122
|
+
if (OPEN_PREFIX.startsWith(remaining.slice(-length))) {
|
|
123
|
+
return this.#pending.length - length;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return this.#pending.length;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
#couldBeOpening(value: string): boolean {
|
|
130
|
+
if (value.length < OPEN_PREFIX.length) return OPEN_PREFIX.startsWith(value);
|
|
131
|
+
if (!value.startsWith(OPEN_PREFIX)) return false;
|
|
132
|
+
const suffix = value.slice(OPEN_PREFIX.length);
|
|
133
|
+
return /^[0-9a-f]{0,8}$/.test(suffix) || /^[0-9a-f]{8}$/.test(suffix) && !value.endsWith(">");
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Parses complete envelopes from one settled response field.
|
|
139
|
+
*
|
|
140
|
+
* The same parser backs JSON responses and streaming responses so recovery
|
|
141
|
+
* semantics do not differ merely because the upstream selected `stream`.
|
|
142
|
+
*/
|
|
143
|
+
export function parseOmpToolCallMarkup(
|
|
144
|
+
text: string,
|
|
145
|
+
allowedNames: ReadonlySet<string>,
|
|
146
|
+
): OmpTextToolCallResult {
|
|
147
|
+
const parser = new OmpToolCallMarkupParser(allowedNames);
|
|
148
|
+
const first = parser.push(text);
|
|
149
|
+
const last = parser.flush();
|
|
150
|
+
return { text: first.text + last.text, calls: first.calls };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Parses a complete captured tool envelope, returning undefined for malformed
|
|
155
|
+
* structure. Argument values are JSON-decoded when they contain valid JSON;
|
|
156
|
+
* otherwise their exact text is preserved as a string.
|
|
157
|
+
*/
|
|
158
|
+
function parseEnvelope(block: string, marker: string): readonly Omit<OmpTextToolCall, "id">[] | undefined {
|
|
159
|
+
const openTag = `<tool_calls:${marker}>`;
|
|
160
|
+
const closeTag = `</tool_calls:${marker}>`;
|
|
161
|
+
if (!block.startsWith(openTag) || !block.endsWith(closeTag)) return undefined;
|
|
162
|
+
|
|
163
|
+
const body = block.slice(openTag.length, -closeTag.length);
|
|
164
|
+
const calls: Omit<OmpTextToolCall, "id">[] = [];
|
|
165
|
+
const callPattern = new RegExp(`<tool_call:${marker}>([\\s\\S]*?)</tool_call:${marker}>`, "g");
|
|
166
|
+
let cursor = 0;
|
|
167
|
+
|
|
168
|
+
for (const match of body.matchAll(callPattern)) {
|
|
169
|
+
const index = match.index;
|
|
170
|
+
const full = match[0];
|
|
171
|
+
const callBody = match[1];
|
|
172
|
+
if (index === undefined || full === undefined || callBody === undefined) return undefined;
|
|
173
|
+
if (body.slice(cursor, index).trim().length > 0) return undefined;
|
|
174
|
+
|
|
175
|
+
const keyTag = `<arg_key:${marker}>`;
|
|
176
|
+
const firstKey = callBody.indexOf(keyTag);
|
|
177
|
+
const name = (firstKey < 0 ? callBody : callBody.slice(0, firstKey)).trim();
|
|
178
|
+
if (!TOOL_NAME_PATTERN.test(name)) return undefined;
|
|
179
|
+
|
|
180
|
+
const argumentBody = firstKey < 0 ? "" : callBody.slice(firstKey);
|
|
181
|
+
const pairPattern = new RegExp(
|
|
182
|
+
`<arg_key:${marker}>([\\s\\S]*?)</arg_key:${marker}>\\s*<arg_value:${marker}>([\\s\\S]*?)</arg_value:${marker}>`,
|
|
183
|
+
"g",
|
|
184
|
+
);
|
|
185
|
+
const argumentsObject: Record<string, unknown> = {};
|
|
186
|
+
let argumentCursor = 0;
|
|
187
|
+
for (const pair of argumentBody.matchAll(pairPattern)) {
|
|
188
|
+
const pairIndex = pair.index;
|
|
189
|
+
const pairFull = pair[0];
|
|
190
|
+
const key = pair[1]?.trim();
|
|
191
|
+
const value = pair[2];
|
|
192
|
+
if (
|
|
193
|
+
pairIndex === undefined ||
|
|
194
|
+
pairFull === undefined ||
|
|
195
|
+
key === undefined ||
|
|
196
|
+
key.length === 0 ||
|
|
197
|
+
value === undefined ||
|
|
198
|
+
argumentBody.slice(argumentCursor, pairIndex).trim().length > 0
|
|
199
|
+
) {
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
argumentsObject[key] = parseArgumentValue(value);
|
|
203
|
+
argumentCursor = pairIndex + pairFull.length;
|
|
204
|
+
}
|
|
205
|
+
if (argumentBody.slice(argumentCursor).trim().length > 0) return undefined;
|
|
206
|
+
|
|
207
|
+
calls.push({ name, arguments: JSON.stringify(argumentsObject) });
|
|
208
|
+
cursor = index + full.length;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (calls.length === 0 || body.slice(cursor).trim().length > 0) return undefined;
|
|
212
|
+
return calls;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function parseArgumentValue(value: string): unknown {
|
|
216
|
+
try {
|
|
217
|
+
return JSON.parse(value) as unknown;
|
|
218
|
+
} catch {
|
|
219
|
+
return value;
|
|
220
|
+
}
|
|
221
|
+
}
|