cartethyia 2.6.7 → 2.7.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/.env.example +1 -7
- package/README.md +25 -2
- package/dist/dashboard/assets/{BannedPage-Dskhyp0l.js → BannedPage-D-J75hBM.js} +1 -1
- package/dist/dashboard/assets/BannedPage-D-J75hBM.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage--SJhk7qQ.js → CliToolDetailPage-CVe6RaBH.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-CVe6RaBH.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-C203VT-c.js → CliToolsPage-Dd8ZSamp.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-Dd8ZSamp.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-BcS7kGG1.js → CombosPage-QoJ6foxo.js} +1 -1
- package/dist/dashboard/assets/CombosPage-QoJ6foxo.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-DpNRay4C.js +1 -0
- package/dist/dashboard/assets/ConsoleLogPage-DpNRay4C.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-uujbBYwC.js → CustomizationPage-CKUFTcW4.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-CKUFTcW4.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-CzSLiDzr.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-CzSLiDzr.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-DubdQcvX.js +1 -0
- package/dist/dashboard/assets/LoginPage-DubdQcvX.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-CA1kLs45.js → ModelPicker-BH8Tehdi.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-BH8Tehdi.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-DuWzAQFL.js +1 -0
- package/dist/dashboard/assets/OverviewPage-DuWzAQFL.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-BKEKe2Fm.js +4 -0
- package/dist/dashboard/assets/ProviderDetailPage-BKEKe2Fm.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js +1 -0
- package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-Bwotu8UJ.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-Bwotu8UJ.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-D7hpWbg9.js +9 -0
- package/dist/dashboard/assets/ProxyPage-D7hpWbg9.js.gz +0 -0
- package/dist/dashboard/assets/{QuotaPage-BueSWX5D.js → QuotaPage-ATpaxrbl.js} +1 -1
- package/dist/dashboard/assets/QuotaPage-ATpaxrbl.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-CETophpx.js +1 -0
- package/dist/dashboard/assets/SettingsPage-CETophpx.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-BdgECjqq.js +1 -0
- package/dist/dashboard/assets/SetupPage-BdgECjqq.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-DrKeMcGN.js → SortableList-CAK-EN9z.js} +1 -1
- package/dist/dashboard/assets/SortableList-CAK-EN9z.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-qjuYtccc.js → StudioPage-B7W2_RhT.js} +3 -3
- package/dist/dashboard/assets/StudioPage-B7W2_RhT.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-6T4EFDjW.js +56 -0
- package/dist/dashboard/assets/UsagePage-6T4EFDjW.js.gz +0 -0
- package/dist/dashboard/assets/{api-keys-C23S9sgp.js → api-keys-CUi6UXsR.js} +1 -1
- package/dist/dashboard/assets/api-keys-CUi6UXsR.js.gz +0 -0
- package/dist/dashboard/assets/app-KvsUJUMF.css +2 -0
- package/dist/dashboard/assets/app-KvsUJUMF.css.gz +0 -0
- package/dist/dashboard/assets/{assert-DU6qrMA_.js → assert-67xXH5rf.js} +1 -1
- package/dist/dashboard/assets/assert-67xXH5rf.js.gz +0 -0
- package/dist/dashboard/assets/{badge-Bkz_-RrM.js → badge-DC2L43Mj.js} +1 -1
- package/dist/dashboard/assets/badge-DC2L43Mj.js.gz +0 -0
- package/dist/dashboard/assets/{banner-C-7y3VKV.js → banner-BBVcb0J3.js} +1 -1
- package/dist/dashboard/assets/banner-BBVcb0J3.js.gz +0 -0
- package/dist/dashboard/assets/{card-BOPBuBzD.js → card-Cca1XC6G.js} +1 -1
- package/dist/dashboard/assets/card-Cca1XC6G.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-BaFUuFFF.js → cli-tools-KvsWa1Jl.js} +1 -1
- package/dist/dashboard/assets/cli-tools-KvsWa1Jl.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-e-Mlk0FH.js → clipboard-button-DUzEbynq.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-DUzEbynq.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-Iec_ywbD.js → dialog-DFvWhwTa.js} +1 -1
- package/dist/dashboard/assets/dialog-DFvWhwTa.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-Bs8MIIHc.js → drawer-CFM2sMX1.js} +1 -1
- package/dist/dashboard/assets/drawer-CFM2sMX1.js.gz +0 -0
- package/dist/dashboard/assets/{entry-DOddwoc0.js → entry-BLBLxX0h.js} +3 -3
- package/dist/dashboard/assets/entry-BLBLxX0h.js.gz +0 -0
- package/dist/dashboard/assets/{entry-DcobaKFu.js → entry-Bqy8RFvM.js} +1 -1
- package/dist/dashboard/assets/entry-Bqy8RFvM.js.gz +0 -0
- package/dist/dashboard/assets/{entry-BRszm4_Z.js → entry-_xHo5x6r.js} +1 -1
- package/dist/dashboard/assets/entry-_xHo5x6r.js.gz +0 -0
- package/dist/dashboard/assets/formatters-mpBPqAQ8.js +1 -0
- package/dist/dashboard/assets/formatters-mpBPqAQ8.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-BuDsrsPn.js → github-badge-C9e8Ts7e.js} +1 -1
- package/dist/dashboard/assets/github-badge-C9e8Ts7e.js.gz +0 -0
- package/dist/dashboard/assets/{index-BeLRhsmt.js → index-D6QzEC_n.js} +2 -2
- package/dist/dashboard/assets/index-D6QzEC_n.js.gz +0 -0
- package/dist/dashboard/assets/{inline-DToMICwl.js → inline-BFUcvllj.js} +1 -1
- package/dist/dashboard/assets/inline-BFUcvllj.js.gz +0 -0
- package/dist/dashboard/assets/{input-WlyHWeG2.js → input-C_ILAvBb.js} +1 -1
- package/dist/dashboard/assets/input-C_ILAvBb.js.gz +0 -0
- package/dist/dashboard/assets/{layout-DaJ5baJr.js → layout-CBIyT6Rt.js} +1 -1
- package/dist/dashboard/assets/layout-CBIyT6Rt.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-B3oSK13z.js → list-row-ebJ_-Nsb.js} +1 -1
- package/dist/dashboard/assets/list-row-ebJ_-Nsb.js.gz +0 -0
- package/dist/dashboard/assets/{live-ta6GMb-8.js → live-D0mm--ME.js} +1 -1
- package/dist/dashboard/assets/live-D0mm--ME.js.gz +0 -0
- package/dist/dashboard/assets/minimize-2-D-MN8W8a.js +1 -0
- package/dist/dashboard/assets/minimize-2-D-MN8W8a.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-g_fduiNw.js → mutation-sMD965oO.js} +1 -1
- package/dist/dashboard/assets/mutation-sMD965oO.js.gz +0 -0
- package/dist/dashboard/assets/{numbers-Dulqgys5.js → numbers-BO9dKaFr.js} +1 -1
- package/dist/dashboard/assets/numbers-BO9dKaFr.js.gz +0 -0
- package/dist/dashboard/assets/providers/inception.svg +5 -0
- package/dist/dashboard/assets/providers/inception.svg.gz +0 -0
- package/dist/dashboard/assets/{query-keys-UETqfytp.js → query-keys-CfL_Zdoh.js} +1 -1
- package/dist/dashboard/assets/query-keys-CfL_Zdoh.js.gz +0 -0
- package/dist/dashboard/assets/{routing-BPUo-W8d.js → routing-BnIpi1IJ.js} +1 -1
- package/dist/dashboard/assets/routing-BnIpi1IJ.js.gz +0 -0
- package/dist/dashboard/assets/{select-BfZ3SCTM.js → select-Cj5Nw43g.js} +1 -1
- package/dist/dashboard/assets/select-Cj5Nw43g.js.gz +0 -0
- package/dist/dashboard/assets/settings-BAFhDgX8.js +1 -0
- package/dist/dashboard/assets/settings-BAFhDgX8.js.gz +0 -0
- package/dist/dashboard/assets/{stack-B1w5BM0u.js → stack-EMrOMt4I.js} +1 -1
- package/dist/dashboard/assets/stack-EMrOMt4I.js.gz +0 -0
- package/dist/dashboard/assets/{state-DhZBfWgx.js → state-CWoo7ty5.js} +1 -1
- package/dist/dashboard/assets/state-CWoo7ty5.js.gz +0 -0
- package/dist/dashboard/assets/{switch-yD7D99Lg.js → switch-CCjT6IkF.js} +1 -1
- package/dist/dashboard/assets/switch-CCjT6IkF.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-EDFRkn1i.js → useMutation-DjkLHUcE.js} +1 -1
- package/dist/dashboard/assets/useMutation-DjkLHUcE.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 +55 -54
- package/migrations/full-postgres/0024_add_pool_provider_cooldowns.sql +12 -0
- package/migrations/full-postgres/0025_add_pool_random_strategy.sql +3 -0
- package/migrations/lite-sqlite3/0024_add_pool_provider_cooldowns.sql +12 -0
- package/migrations/lite-sqlite3/0025_add_pool_random_strategy.sql +20 -0
- package/package.json +1 -1
- package/scripts/cli/console-menu.ts +5 -2
- package/scripts/cli/tray.ts +21 -6
- package/src/boot/app.ts +24 -62
- package/src/boot/dependencies.ts +732 -750
- package/src/config/cooldowns.ts +16 -0
- package/src/config/providers.ts +13 -1
- package/src/config/resources.ts +13 -80
- package/src/config/security.ts +2 -38
- package/src/config/spec.ts +16 -23
- package/src/console/console-router.ts +7 -7
- package/src/console/domain-registration.ts +19 -12
- package/src/console/providers/catalog/catalog-projections.ts +2 -6
- package/src/console/providers/catalog/contracts.ts +19 -0
- package/src/console/providers/catalog/provider-operations.ts +38 -0
- package/src/console/providers/catalog/routes.ts +14 -0
- package/src/console/providers/catalog/store-helpers.ts +326 -0
- package/src/console/providers/catalog/store.ts +1292 -1563
- package/src/console/quota/account-quota-global.ts +2 -2
- package/src/console/quota/account-quota-shared.ts +1 -1
- package/src/console/quota/account-quota-tenant.ts +35 -12
- package/src/console/quota/quota-view.ts +2 -2
- package/src/console/remote-support/contracts.ts +3 -1
- package/src/console/remote-support/service.ts +115 -6
- package/src/console/routing/pools/contracts.ts +41 -1
- package/src/console/routing/pools/operations.ts +183 -4
- package/src/console/routing/pools/relay-deploy.ts +17 -4
- package/src/console/routing/pools/routes.ts +30 -0
- package/src/console/routing/pools/store.ts +98 -8
- package/src/main.ts +16 -2
- package/src/network/outbound-fetch.ts +226 -365
- package/src/network/pool/agent.ts +8 -0
- package/src/network/pool/pool-state.ts +7 -0
- package/src/network/pool/selector.ts +110 -2
- package/src/network/pool-cooldown-store.ts +252 -0
- package/src/network/pool-health-machine.ts +5 -3
- package/src/network/pool-health.ts +36 -9
- package/src/observability/event-query.ts +57 -0
- package/src/observability/metrics.ts +15 -5
- package/src/observability/runtime-metrics.ts +30 -15
- package/src/persistence/health-event-flush.ts +2 -0
- package/src/persistence/schema.ts +19 -0
- package/src/persistence/usage-flush.ts +8 -3
- package/src/protocol/canonical-model.ts +1 -1
- package/src/protocol/dsml.ts +377 -0
- package/src/protocol/gateway-error.ts +0 -1
- package/src/{providers/provider-reasoning.ts → protocol/reasoning.ts} +10 -9
- package/src/protocol/request/chat.ts +7 -7
- package/src/protocol/request/codex.ts +2 -2
- package/src/protocol/request/gemini.ts +1 -1
- package/src/protocol/request/messages.ts +6 -6
- package/src/protocol/request/responses.ts +7 -7
- package/src/protocol/response/dsml.ts +12 -133
- package/src/{transport → protocol}/translation/attachment-integrity.ts +2 -2
- package/src/{transport → protocol}/translation/cache-controls.ts +1 -1
- package/src/{transport → protocol}/translation/capabilities.ts +2 -2
- package/src/{transport → protocol}/translation/normalize.ts +1 -1
- package/src/{transport → protocol}/translation/quirks.ts +1 -1
- package/src/{transport → protocol}/translation/search.ts +1 -1
- package/src/{transport → protocol}/translation/thinking.ts +23 -1
- package/src/{transport → protocol}/translation/tool-id.ts +1 -1
- package/src/{transport → protocol}/translation/tool-repair.ts +2 -2
- package/src/protocol/upstream-parsing.ts +176 -0
- package/src/providers/adapters/compatible-adapter.ts +11 -2
- package/src/providers/authentication/oauth-refresh-service.ts +9 -5
- package/src/providers/default-registry.ts +21 -5
- package/src/providers/discovery/probe-phases.ts +16 -3
- package/src/providers/discovery/probing-service.ts +4 -1
- package/src/providers/integrations/anthropic.ts +1 -1
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +1 -4
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +141 -8
- package/src/providers/integrations/buddy/buddy-checkin.ts +5 -1
- package/src/providers/integrations/buddy/buddy-growth-shared.ts +0 -82
- package/src/providers/integrations/buddy/buddy-identity.ts +25 -0
- package/src/providers/integrations/buddy/buddy-server-tools.ts +240 -0
- package/src/providers/integrations/buddy/codebuddy/cn-catalog.ts +36 -0
- package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +6 -31
- package/src/providers/integrations/buddy/codebuddy/provider.ts +2 -0
- package/src/providers/integrations/buddy/codebuddy/shared.ts +19 -4
- package/src/providers/integrations/buddy/turing-device-token.ts +262 -0
- package/src/providers/integrations/buddy/workbuddy/provider.ts +2 -0
- package/src/providers/integrations/buddy/workbuddy/shared.ts +56 -3
- package/src/providers/integrations/claude/claude.ts +1 -1
- package/src/providers/integrations/cloudflareai/cloudflareai.ts +1 -1
- package/src/providers/integrations/codex/codex-errors.ts +1 -1
- package/src/providers/integrations/codex/codex.ts +1 -1
- package/src/providers/integrations/commandcode/commandcode.ts +134 -104
- package/src/providers/integrations/commandcode/discovery.ts +6 -5
- package/src/providers/integrations/factory/factory.ts +1 -1
- package/src/providers/integrations/github/github.ts +1 -1
- package/src/providers/integrations/grok/grok.ts +1 -1
- package/src/providers/integrations/inception/provider.ts +91 -0
- package/src/providers/integrations/inferhub.ts +1 -1
- package/src/providers/integrations/kiro/kiro-request.ts +1 -1
- package/src/providers/integrations/mistral.ts +193 -19
- package/src/providers/integrations/opencode/fingerprint.ts +19 -2
- package/src/providers/integrations/opencode/prompt.ts +111 -0
- package/src/providers/integrations/opencode/provider.ts +101 -23
- package/src/providers/integrations/opencode/tool-definitions.ts +4 -4
- package/src/providers/integrations/qoder/qoder-checkin.ts +303 -0
- package/src/providers/integrations/qoder/qoder.ts +1641 -884
- package/src/providers/integrations/shared/install-id.ts +2 -1
- package/src/providers/integrations/trae/trae-cache.ts +110 -0
- package/src/providers/integrations/trae/trae-oauth.ts +29 -4
- package/src/providers/integrations/trae/trae-protocol.ts +2 -1
- package/src/providers/integrations/trae/trae.ts +106 -23
- package/src/providers/operations/account-error-classification.ts +1660 -0
- package/src/providers/operations/account-health-service.ts +621 -2097
- package/src/providers/operations/account-reset-service.ts +2 -0
- package/src/providers/provider-contracts.ts +0 -2
- package/src/providers/provider-metadata.ts +25 -4
- package/src/providers/provider-probe.ts +2 -2
- package/src/{console → providers}/quota/cache.ts +1 -1
- package/src/{console → providers}/quota/refresh.ts +5 -5
- package/src/security/abuse.ts +17 -0
- package/src/transport/dispatch/attempt-finalize.ts +114 -12
- package/src/transport/dispatch/attempt-loop.ts +3 -0
- package/src/transport/dispatch/empty-stream.ts +224 -217
- package/src/transport/dispatch/generation.ts +37 -55
- package/src/transport/dispatch/handler-deps.ts +32 -0
- package/src/transport/dispatch/leases.ts +9 -4
- package/src/transport/dispatch/proxy-request.ts +6 -30
- package/src/transport/dispatch/responses-compact.ts +22 -43
- package/src/transport/dispatch/streaming/finalize.ts +3 -0
- package/src/transport/dispatch/streaming/stream.ts +0 -8
- package/src/transport/dispatch/systemone.ts +22 -42
- package/src/transport/dispatch/token-count.ts +23 -39
- package/src/transport/dispatch/upstream.ts +1 -3
- package/src/transport/dispatch/websearch.ts +22 -41
- package/src/transport/failure-policy.ts +59 -178
- package/src/transport/guards/abuse-guard.ts +10 -0
- package/src/transport/guards/auth-guard.ts +0 -18
- package/src/transport/guards/error-lifecycle.ts +1 -1
- package/src/transport/guards/pipeline.ts +0 -6
- package/src/transport/guards/request-context.ts +18 -91
- package/src/transport/request/degradation.ts +20 -1
- package/src/transport/request/inflight.ts +4 -95
- package/src/transport/request/preparer.ts +6 -6
- package/src/transport/request/state.ts +66 -38
- package/src/transport/request/tenant-preferences.ts +1 -1
- package/src/transport/routing/route-catalog.ts +41 -31
- package/src/transport/routing/router.ts +2 -2
- package/src/transport/surface/messages/parse.ts +1 -1
- package/src/workers/buddy-growth-tasks.ts +346 -425
- package/src/workers/daily-checkin.ts +17 -6
- package/src/workers/quota-refresh-sweep.ts +2 -2
- package/src/workers/scheduler.ts +17 -1
- package/dist/dashboard/assets/BannedPage-Dskhyp0l.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage--SJhk7qQ.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-C203VT-c.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-BcS7kGG1.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-ChmiSLwO.js +0 -1
- package/dist/dashboard/assets/ConsoleLogPage-ChmiSLwO.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-uujbBYwC.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-fKAWd-OQ.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-fKAWd-OQ.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-DoOUthbG.js +0 -1
- package/dist/dashboard/assets/LoginPage-DoOUthbG.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-CA1kLs45.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-C5Z-UBz1.js +0 -1
- package/dist/dashboard/assets/OverviewPage-C5Z-UBz1.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-CFu1qGcT.js +0 -3
- package/dist/dashboard/assets/ProviderDetailPage-CFu1qGcT.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js +0 -1
- package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-CnlJZbxF.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-CnlJZbxF.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-B9mBTweh.js +0 -7
- package/dist/dashboard/assets/ProxyPage-B9mBTweh.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-BueSWX5D.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-B0x00MVY.js +0 -1
- package/dist/dashboard/assets/SettingsPage-B0x00MVY.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-DQ9oDRpi.js +0 -1
- package/dist/dashboard/assets/SetupPage-DQ9oDRpi.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-DrKeMcGN.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-qjuYtccc.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-Be7aNviQ.js +0 -56
- package/dist/dashboard/assets/UsagePage-Be7aNviQ.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-C23S9sgp.js.gz +0 -0
- package/dist/dashboard/assets/app-BUSiYL6F.css +0 -2
- package/dist/dashboard/assets/app-BUSiYL6F.css.gz +0 -0
- package/dist/dashboard/assets/assert-DU6qrMA_.js.gz +0 -0
- package/dist/dashboard/assets/badge-Bkz_-RrM.js.gz +0 -0
- package/dist/dashboard/assets/banner-C-7y3VKV.js.gz +0 -0
- package/dist/dashboard/assets/card-BOPBuBzD.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-BaFUuFFF.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-e-Mlk0FH.js.gz +0 -0
- package/dist/dashboard/assets/cog-CdWhsajs.js +0 -1
- package/dist/dashboard/assets/cog-CdWhsajs.js.gz +0 -0
- package/dist/dashboard/assets/dialog-Iec_ywbD.js.gz +0 -0
- package/dist/dashboard/assets/drawer-Bs8MIIHc.js.gz +0 -0
- package/dist/dashboard/assets/entry-BRszm4_Z.js.gz +0 -0
- package/dist/dashboard/assets/entry-DOddwoc0.js.gz +0 -0
- package/dist/dashboard/assets/entry-DcobaKFu.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-BuDsrsPn.js.gz +0 -0
- package/dist/dashboard/assets/index-BeLRhsmt.js.gz +0 -0
- package/dist/dashboard/assets/inline-DToMICwl.js.gz +0 -0
- package/dist/dashboard/assets/input-WlyHWeG2.js.gz +0 -0
- package/dist/dashboard/assets/layout-DaJ5baJr.js.gz +0 -0
- package/dist/dashboard/assets/list-row-B3oSK13z.js.gz +0 -0
- package/dist/dashboard/assets/live-ta6GMb-8.js.gz +0 -0
- package/dist/dashboard/assets/model-bans-BmYTmbAk.js +0 -1
- package/dist/dashboard/assets/model-bans-BmYTmbAk.js.gz +0 -0
- package/dist/dashboard/assets/mutation-g_fduiNw.js.gz +0 -0
- package/dist/dashboard/assets/numbers-Dulqgys5.js.gz +0 -0
- package/dist/dashboard/assets/query-keys-UETqfytp.js.gz +0 -0
- package/dist/dashboard/assets/routing-BPUo-W8d.js.gz +0 -0
- package/dist/dashboard/assets/select-BfZ3SCTM.js.gz +0 -0
- package/dist/dashboard/assets/settings--3hxSyry.js +0 -1
- package/dist/dashboard/assets/settings--3hxSyry.js.gz +0 -0
- package/dist/dashboard/assets/stack-B1w5BM0u.js.gz +0 -0
- package/dist/dashboard/assets/state-DhZBfWgx.js.gz +0 -0
- package/dist/dashboard/assets/switch-yD7D99Lg.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-EDFRkn1i.js.gz +0 -0
- package/src/console/admin/model-abuse/contracts.ts +0 -38
- package/src/console/admin/model-abuse/routes.ts +0 -97
- package/src/security/model-abuse.ts +0 -704
- package/src/transport/guards/concurrency-guard.ts +0 -125
- /package/dist/dashboard/assets/{app-MdVfwraB.js → app-BaWd0puM.js} +0 -0
- /package/dist/dashboard/assets/{app-MdVfwraB.js.gz → app-BaWd0puM.js.gz} +0 -0
|
@@ -267,6 +267,16 @@ export class PrometheusRegistry {
|
|
|
267
267
|
readonly cartethyia_redis_up: GaugeMetric;
|
|
268
268
|
readonly proxy_requests_total: CounterMetric;
|
|
269
269
|
readonly proxy_admission_total: CounterMetric;
|
|
270
|
+
/**
|
|
271
|
+
* Refusals from the per-IP abuse layer, by reason.
|
|
272
|
+
*
|
|
273
|
+
* These are the only 429s this process produces before dispatch, and they
|
|
274
|
+
* were invisible: a client-facing 429 could come from here, from admission,
|
|
275
|
+
* or from the upstream, and nothing recorded which. `ip-rate-limited` is the
|
|
276
|
+
* sliding window and `ip-banned` the escalation — different remedies, so
|
|
277
|
+
* they must not share a number.
|
|
278
|
+
*/
|
|
279
|
+
readonly cartethyia_ip_abuse_refusals_total: CounterMetric;
|
|
270
280
|
readonly proxy_in_flight: GaugeMetric;
|
|
271
281
|
readonly proxy_request_latency_ms: HistogramMetric;
|
|
272
282
|
readonly cartethyia_telemetry_buffered: GaugeMetric;
|
|
@@ -280,7 +290,6 @@ export class PrometheusRegistry {
|
|
|
280
290
|
readonly cartethyia_quota_cache_entries: GaugeMetric;
|
|
281
291
|
readonly cartethyia_model_catalog_entries: GaugeMetric;
|
|
282
292
|
readonly cartethyia_process_in_flight: GaugeMetric;
|
|
283
|
-
readonly cartethyia_process_concurrency_limit: GaugeMetric;
|
|
284
293
|
readonly cartethyia_telemetry_backlog_items: GaugeMetric;
|
|
285
294
|
readonly cartethyia_telemetry_backlog_bytes: GaugeMetric;
|
|
286
295
|
readonly cartethyia_proxy_dial_proxy_resolved_total: CounterMetric;
|
|
@@ -321,6 +330,11 @@ export class PrometheusRegistry {
|
|
|
321
330
|
"Admission decisions by reason",
|
|
322
331
|
["reason"],
|
|
323
332
|
);
|
|
333
|
+
this.cartethyia_ip_abuse_refusals_total = this.counter(
|
|
334
|
+
"cartethyia_ip_abuse_refusals_total",
|
|
335
|
+
"Requests refused by the per-IP abuse limit, by reason",
|
|
336
|
+
["reason"],
|
|
337
|
+
);
|
|
324
338
|
this.proxy_in_flight = this.gauge(
|
|
325
339
|
"proxy_in_flight",
|
|
326
340
|
"Provider dispatch requests currently in flight on this process",
|
|
@@ -358,10 +372,6 @@ export class PrometheusRegistry {
|
|
|
358
372
|
"cartethyia_process_in_flight",
|
|
359
373
|
"Gateway requests currently admitted into this process",
|
|
360
374
|
);
|
|
361
|
-
this.cartethyia_process_concurrency_limit = this.gauge(
|
|
362
|
-
"cartethyia_process_concurrency_limit",
|
|
363
|
-
"Effective in-flight ceiling for this process, derived from the memory limit",
|
|
364
|
-
);
|
|
365
375
|
this.cartethyia_telemetry_backlog_items = this.gauge(
|
|
366
376
|
"cartethyia_telemetry_backlog_items",
|
|
367
377
|
"Telemetry events queued for the next drain",
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* layers, keeping the observability boundary one-directional.
|
|
9
9
|
*/
|
|
10
10
|
import { readFileSync } from "node:fs";
|
|
11
|
+
import os from "node:os";
|
|
11
12
|
import { metrics } from "./metrics";
|
|
12
13
|
import { trackMemoryUsage } from "./performance-metrics";
|
|
13
14
|
import { log } from "./logger";
|
|
@@ -66,12 +67,34 @@ const PRESSURE_GC_WINDOW_MS = 60_000;
|
|
|
66
67
|
/** Fraction of the resolved limit at which memory pressure is reported. */
|
|
67
68
|
const PRESSURE_THRESHOLD = 0.8;
|
|
68
69
|
/**
|
|
69
|
-
* Fallback limit
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
70
|
+
* Fallback limit when neither an override nor a cgroup limit is discoverable
|
|
71
|
+
* (native development, macOS, Windows).
|
|
72
|
+
*
|
|
73
|
+
* A fixed 512 MiB made every machine without a cgroup look like a 512 MiB
|
|
74
|
+
* container. On a desktop running the gateway natively that is far below what
|
|
75
|
+
* the machine has, so the pressure check fired against a number that described
|
|
76
|
+
* nothing — "RSS 430 MiB exceeds 80% of the 512 MiB limit" on a machine with
|
|
77
|
+
* gigabytes free — and the concurrency cap derived from it admitted a handful
|
|
78
|
+
* of requests while every tenant-level setting sat unreachable above it.
|
|
79
|
+
*
|
|
80
|
+
* The host's real memory is the honest stand-in. It is what the process can
|
|
81
|
+
* actually grow into here, and it is what "no container limit" means.
|
|
73
82
|
*/
|
|
74
|
-
|
|
83
|
+
function defaultMemoryLimitBytes(): number {
|
|
84
|
+
try {
|
|
85
|
+
const total = os.totalmem();
|
|
86
|
+
if (Number.isFinite(total) && total > 0) return total;
|
|
87
|
+
} catch {
|
|
88
|
+
// Platform without totalmem; fall through to the last-resort constant.
|
|
89
|
+
}
|
|
90
|
+
return LAST_RESORT_MEMORY_LIMIT_BYTES;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Used only when the host's memory cannot be read at all. Deliberately large
|
|
95
|
+
* so an unmeasurable host is not treated as a small container.
|
|
96
|
+
*/
|
|
97
|
+
const LAST_RESORT_MEMORY_LIMIT_BYTES = 4 * 1024 * 1024 * 1024;
|
|
75
98
|
/** Cooldown between idle full collections. */
|
|
76
99
|
const IDLE_GC_WINDOW_MS = 5 * 60_000;
|
|
77
100
|
/** RSS retained above the post-collection baseline before another idle collection runs. */
|
|
@@ -88,7 +111,7 @@ const IDLE_GC_MIN_GROWTH_BYTES = 32 * 1024 * 1024;
|
|
|
88
111
|
export function resolveMemoryLimitBytes(): number | undefined {
|
|
89
112
|
const override = resolveMemoryLimitOverrideBytes();
|
|
90
113
|
if (override !== undefined) return override;
|
|
91
|
-
if (process.platform !== "linux") return
|
|
114
|
+
if (process.platform !== "linux") return defaultMemoryLimitBytes();
|
|
92
115
|
for (const path of [
|
|
93
116
|
"/sys/fs/cgroup/memory.max",
|
|
94
117
|
"/sys/fs/cgroup/memory/memory.limit_in_bytes",
|
|
@@ -103,7 +126,7 @@ export function resolveMemoryLimitBytes(): number | undefined {
|
|
|
103
126
|
// File absent (cgroup v1 vs v2) or unreadable — try the next source.
|
|
104
127
|
}
|
|
105
128
|
}
|
|
106
|
-
return
|
|
129
|
+
return defaultMemoryLimitBytes();
|
|
107
130
|
}
|
|
108
131
|
|
|
109
132
|
/** Samples process memory and bounded-collection sizes into the metric registry. */
|
|
@@ -120,8 +143,6 @@ export class RuntimeMetricsSampler {
|
|
|
120
143
|
* first sample runs; consumers must treat that as "unknown", not as zero.
|
|
121
144
|
*/
|
|
122
145
|
private snapshot: RuntimeMemorySnapshot | undefined;
|
|
123
|
-
/** The effective concurrency ceiling, published alongside the gauges. */
|
|
124
|
-
private concurrencyLimit: number | undefined;
|
|
125
146
|
|
|
126
147
|
/**
|
|
127
148
|
* Binds the live in-flight count after the request state store exists. The
|
|
@@ -149,9 +170,6 @@ export class RuntimeMetricsSampler {
|
|
|
149
170
|
* limit, which is known here, but the guard that owns it is constructed in
|
|
150
171
|
* the transport layer.
|
|
151
172
|
*/
|
|
152
|
-
bindConcurrencyLimit(limit: number): void {
|
|
153
|
-
this.concurrencyLimit = limit;
|
|
154
|
-
}
|
|
155
173
|
|
|
156
174
|
constructor(
|
|
157
175
|
private readonly sources: RuntimeMetricSources,
|
|
@@ -198,9 +216,6 @@ export class RuntimeMetricsSampler {
|
|
|
198
216
|
metrics.cartethyia_telemetry_backlog_items.set(backlog.items);
|
|
199
217
|
metrics.cartethyia_telemetry_backlog_bytes.set(backlog.bytes);
|
|
200
218
|
}
|
|
201
|
-
if (this.concurrencyLimit !== undefined) {
|
|
202
|
-
metrics.cartethyia_process_concurrency_limit.set(this.concurrencyLimit);
|
|
203
|
-
}
|
|
204
219
|
|
|
205
220
|
this.checkPressure(memory.rss);
|
|
206
221
|
this.checkIdleReclaim(memory.rss);
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
// Same shape as `persistence/usage-flush.ts`: enqueue and return, a timer
|
|
16
16
|
// flushes, shutdown flushes what remains. A crash loses the unflushed rows and
|
|
17
17
|
// nothing else — the durable state routing depends on was never in here.
|
|
18
|
+
import { randomUUID } from "node:crypto";
|
|
18
19
|
import type { CartethyiaDatabase } from "./db-handle";
|
|
19
20
|
import { healthEvents } from "./schema";
|
|
20
21
|
|
|
@@ -141,6 +142,7 @@ export class HealthEventFlushBuffer {
|
|
|
141
142
|
try {
|
|
142
143
|
await this.db.insert(healthEvents).values(
|
|
143
144
|
batch.map((row) => ({
|
|
145
|
+
id: randomUUID(),
|
|
144
146
|
entityKind: row.entityKind,
|
|
145
147
|
accountId: row.accountId ?? null,
|
|
146
148
|
networkPoolId: row.networkPoolId ?? null,
|
|
@@ -490,6 +490,24 @@ export const networkPools = pgTable("network_pools", {
|
|
|
490
490
|
/** The endpoint the verdict was measured against. Compared on read, so an
|
|
491
491
|
* edited endpoint reports "never probed" instead of a stale verdict. */
|
|
492
492
|
providerProbeEndpoint: text("provider_probe_endpoint"),
|
|
493
|
+
/**
|
|
494
|
+
* The database mirror of the pool selector's provider cooldown state, one
|
|
495
|
+
* entry per cooling provider keyed by its lowercased id:
|
|
496
|
+
* `{ "opencode": { "until": <epoch ms>, "reason": "...", "blocked": false } }`.
|
|
497
|
+
*
|
|
498
|
+
* The selector's own map is process-local (mirrored to Redis when present),
|
|
499
|
+
* so a restart dropped every flag while the upstream kept refusing the
|
|
500
|
+
* egress — dispatch re-picked a spent pool and the console showed nothing.
|
|
501
|
+
* The dispatch-side flaggers write each marker here, and boot reads this
|
|
502
|
+
* column back into the selector, so the state survives a restart the way the
|
|
503
|
+
* upstream's memory of the address does.
|
|
504
|
+
*
|
|
505
|
+
* `null` means no provider was ever flagged. Expired entries are left inert
|
|
506
|
+
* until the next write for the pool rather than swept eagerly: a marker with
|
|
507
|
+
* `until` in the past is skipped by every reader.
|
|
508
|
+
*/
|
|
509
|
+
providerCooldowns: jsonb("provider_cooldowns")
|
|
510
|
+
.$type<Record<string, { until: number; reason: string; blocked?: boolean }>>(),
|
|
493
511
|
/** Metered egress totals, flushed from the in-memory socket counters on
|
|
494
512
|
* every pool-touching write so the quota bar survives a restart. */
|
|
495
513
|
bytesSentTotal: bigint("bytes_sent_total", { mode: "number" }).notNull().default(0),
|
|
@@ -676,6 +694,7 @@ export const providerRoutingSettings = pgTable(
|
|
|
676
694
|
export const poolRoutingStrategy = pgEnum("pool_routing_strategy", [
|
|
677
695
|
"least_loaded",
|
|
678
696
|
"round_robin",
|
|
697
|
+
"random",
|
|
679
698
|
]);
|
|
680
699
|
export type PoolRoutingStrategy = (typeof poolRoutingStrategy.enumValues)[number];
|
|
681
700
|
|
|
@@ -145,13 +145,18 @@ export class UsageFlushBuffer {
|
|
|
145
145
|
* cost the others their delta. A key whose write fails goes back into the
|
|
146
146
|
* pending map, added to anything enqueued while the flush ran, so a
|
|
147
147
|
* transient failure retries instead of dropping.
|
|
148
|
+
*
|
|
149
|
+
* `force` bypasses the outage backoff. The scheduled timer never passes it —
|
|
150
|
+
* a failing database should not be probed on every tick — but `stop({flush:
|
|
151
|
+
* true})` must, or a shutdown during a backoff window would return without
|
|
152
|
+
* even attempting to persist the pending deltas.
|
|
148
153
|
*/
|
|
149
|
-
async flush(): Promise<void> {
|
|
154
|
+
async flush(options?: { force?: boolean }): Promise<void> {
|
|
150
155
|
if (this.flushing || this.pending.size === 0) return;
|
|
151
156
|
// Backing off during a sustained outage: without this the writer probed a
|
|
152
157
|
// failing database on every tick, adding write load to the very overload
|
|
153
158
|
// that caused the failure.
|
|
154
|
-
if (Date.now() < this.backoffUntil) return;
|
|
159
|
+
if (Date.now() < this.backoffUntil && options?.force !== true) return;
|
|
155
160
|
this.flushing = true;
|
|
156
161
|
const batch = new Map(this.pending);
|
|
157
162
|
this.pending.clear();
|
|
@@ -210,7 +215,7 @@ export class UsageFlushBuffer {
|
|
|
210
215
|
clearTimeout(this.timer);
|
|
211
216
|
this.timer = undefined;
|
|
212
217
|
}
|
|
213
|
-
if (options?.flush) await this.flush();
|
|
218
|
+
if (options?.flush) await this.flush({ force: true });
|
|
214
219
|
}
|
|
215
220
|
|
|
216
221
|
private arm(): void {
|
|
@@ -52,7 +52,7 @@ export type ServiceKind = (typeof SERVICE_KINDS)[number];
|
|
|
52
52
|
* their membership set from it, and the probe route's accepted subset is
|
|
53
53
|
* declared separately because it is deliberately narrower.
|
|
54
54
|
*/
|
|
55
|
-
export const REASONING_EFFORTS = ["none", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
55
|
+
export const REASONING_EFFORTS = ["none", "instant", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
56
56
|
|
|
57
57
|
/** One canonical reasoning effort. */
|
|
58
58
|
export type ReasoningEffort = (typeof REASONING_EFFORTS)[number];
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical DSML (DeepSeek Markup Language) tool-call recovery.
|
|
3
|
+
*
|
|
4
|
+
* DeepSeek-family models occasionally write their tool calls as XML-like DSML
|
|
5
|
+
* markup inside the plain-text content stream instead of the structured
|
|
6
|
+
* `tool_calls` field. Two upstream families in this gateway see it:
|
|
7
|
+
*
|
|
8
|
+
* - CodeBuddy/WorkBuddy emit strict blocks (`<|DSML| calls>…</|DSML| calls>`)
|
|
9
|
+
* that must be parsed exactly, with declared-tool allowlisting.
|
|
10
|
+
* - Qoder emits looser blocks (`<[^>]*DSML[^>]*calls>…`) that tolerate any
|
|
11
|
+
* intermediate characters in the tag, and its parameter values are plain
|
|
12
|
+
* strings.
|
|
13
|
+
*
|
|
14
|
+
* The two share the same recovery idea, so the mechanics live here once and
|
|
15
|
+
* each caller picks its dialect through options. The defaults reproduce the
|
|
16
|
+
* CodeBuddy dialect exactly (`app/dsml.py` in codebuddy2api); Qoder passes
|
|
17
|
+
* `fuzzy: true` and its id factory, mirroring `qoder_proxy.py`.
|
|
18
|
+
*/
|
|
19
|
+
import { randomUUID } from "node:crypto";
|
|
20
|
+
|
|
21
|
+
export interface DsmlToolCall {
|
|
22
|
+
readonly id: string;
|
|
23
|
+
readonly name: string;
|
|
24
|
+
readonly arguments: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface DsmlRecovery {
|
|
28
|
+
readonly calls: readonly DsmlToolCall[];
|
|
29
|
+
readonly clean: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface DsmlParseOptions {
|
|
33
|
+
/** Separator spellings a strict block may use (CodeBuddy dialect). */
|
|
34
|
+
readonly separators?: readonly string[];
|
|
35
|
+
/** Block names a strict opener may name. */
|
|
36
|
+
readonly blockNames?: readonly string[];
|
|
37
|
+
/** When true, match the Qoder fuzzy form (`<[^>]*DSML[^>]*…`). */
|
|
38
|
+
readonly fuzzy?: boolean;
|
|
39
|
+
/** How parameter values decode: `json` (CodeBuddy) or `string` (Qoder). */
|
|
40
|
+
readonly params?: "json" | "string";
|
|
41
|
+
/** Call id factory. Defaults to sequential for strict, uuid for fuzzy. */
|
|
42
|
+
readonly id?: (call: { readonly name: string }, index: number, seq: number) => string;
|
|
43
|
+
/** Non-empty allowlist makes the strict dialect reject undeclared names. */
|
|
44
|
+
readonly allowedNames?: ReadonlySet<string>;
|
|
45
|
+
/**
|
|
46
|
+
* When true, JSON stringifies argument objects with recursively sorted keys,
|
|
47
|
+
* matching the reference bridge (`json.dumps(..., sort_keys=True)`). The
|
|
48
|
+
* strict dialect defaults to true; the fuzzy dialect never sorts, matching
|
|
49
|
+
* its reference's `json.dumps(params)`.
|
|
50
|
+
*/
|
|
51
|
+
readonly sortKeys?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Character cap for an unclosed strict block. Past it, the block is treated
|
|
54
|
+
* as plain text and passed through instead of buffered indefinitely — same
|
|
55
|
+
* fail-open as `MAX_BLOCK_BYTES` in the reference bridge (256 KB).
|
|
56
|
+
*/
|
|
57
|
+
readonly maxBlockBytes?: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const DEFAULT_SEPARATORS = ["||", "|", "|"] as const;
|
|
61
|
+
const DEFAULT_BLOCK_NAMES = ["calls", "function_calls"] as const;
|
|
62
|
+
const DEFAULT_MAX_BLOCK_BYTES = 256 * 1024;
|
|
63
|
+
const TOOL_NAME = /^[A-Za-z0-9_][A-Za-z0-9_.:-]{0,63}$/;
|
|
64
|
+
|
|
65
|
+
/** Escapes regex metacharacters so a `|` separator is literal, not alternation. */
|
|
66
|
+
export function escapeRegExp(value: string): string {
|
|
67
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function openers(separators: readonly string[], blockNames: readonly string[]): readonly string[] {
|
|
71
|
+
return separators.flatMap((separator) => blockNames.map((name) => `<${separator}DSML${separator} ${name}>`));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function sequentialId(_call: { readonly name: string }, _index: number, seq: number): string {
|
|
75
|
+
return `call_dsml_${seq}_${_call.name}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function uuidId(_call: { readonly name: string }, _index: number, _seq: number): string {
|
|
79
|
+
return `call_${randomUUID().replace(/-/g, "").slice(0, 16)}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface DsmlParseResult {
|
|
83
|
+
readonly text: string;
|
|
84
|
+
readonly calls: readonly DsmlToolCall[];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Streaming strict-prefix parser (CodeBuddy dialect). Buffers text until a
|
|
89
|
+
* complete block is seen, emits clean prose as it settles, and recovers calls
|
|
90
|
+
* from complete blocks only. Incomplete blocks stay text; an unclosed block
|
|
91
|
+
* past `maxBlockBytes` passes through as text instead of buffering forever.
|
|
92
|
+
* With no allowlist the generic name rule applies; with one, undeclared names
|
|
93
|
+
* reject the whole block. The caller feeds chunks with `push` and drains the
|
|
94
|
+
* tail with `flush`.
|
|
95
|
+
*/
|
|
96
|
+
export class DsmlStreamParser {
|
|
97
|
+
readonly #separators: readonly string[];
|
|
98
|
+
readonly #blockNames: readonly string[];
|
|
99
|
+
readonly #openers: readonly string[];
|
|
100
|
+
readonly #allowedNames: ReadonlySet<string>;
|
|
101
|
+
readonly #params: "json" | "string";
|
|
102
|
+
readonly #sortKeys: boolean;
|
|
103
|
+
readonly #maxBlockBytes: number;
|
|
104
|
+
readonly #id: (call: { readonly name: string }, index: number, seq: number) => string;
|
|
105
|
+
#pending = "";
|
|
106
|
+
#callSequence = 0;
|
|
107
|
+
|
|
108
|
+
constructor(options: DsmlParseOptions = {}) {
|
|
109
|
+
this.#separators = options.separators ?? DEFAULT_SEPARATORS;
|
|
110
|
+
this.#blockNames = options.blockNames ?? DEFAULT_BLOCK_NAMES;
|
|
111
|
+
this.#openers = openers(this.#separators, this.#blockNames);
|
|
112
|
+
this.#allowedNames = options.allowedNames ?? new Set();
|
|
113
|
+
this.#params = options.params ?? "json";
|
|
114
|
+
this.#sortKeys = options.sortKeys ?? true;
|
|
115
|
+
this.#maxBlockBytes = options.maxBlockBytes ?? DEFAULT_MAX_BLOCK_BYTES;
|
|
116
|
+
this.#id = options.id ?? sequentialId;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
push(chunk: string): DsmlParseResult {
|
|
120
|
+
this.#pending += chunk;
|
|
121
|
+
let cursor = 0;
|
|
122
|
+
let visible = "";
|
|
123
|
+
const calls: DsmlToolCall[] = [];
|
|
124
|
+
for (;;) {
|
|
125
|
+
let openIndex = -1;
|
|
126
|
+
let opener = "";
|
|
127
|
+
for (const candidate of this.#openers) {
|
|
128
|
+
const index = this.#pending.indexOf(candidate, cursor);
|
|
129
|
+
if (index >= 0 && (openIndex < 0 || index < openIndex)) {
|
|
130
|
+
openIndex = index;
|
|
131
|
+
opener = candidate;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (openIndex < 0) {
|
|
135
|
+
const safeEnd = this.#settledEnd(cursor);
|
|
136
|
+
visible += this.#pending.slice(cursor, safeEnd);
|
|
137
|
+
cursor = safeEnd;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
visible += this.#pending.slice(cursor, openIndex);
|
|
141
|
+
const separator = this.#separators.find((value) => opener.startsWith(`<${value}DSML`));
|
|
142
|
+
const blockName = this.#blockNames.find((value) => opener.endsWith(` ${value}>`));
|
|
143
|
+
if (separator === undefined || blockName === undefined) {
|
|
144
|
+
visible += "<";
|
|
145
|
+
cursor = openIndex + 1;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const close = `</${separator}DSML${separator} ${blockName}>`;
|
|
149
|
+
const closeIndex = this.#pending.indexOf(close, openIndex + opener.length);
|
|
150
|
+
if (closeIndex < 0) {
|
|
151
|
+
// An unclosed block past the cap is not markup — pass it through as
|
|
152
|
+
// text instead of buffering it indefinitely. Same fail-open as
|
|
153
|
+
// `feed()` in the reference bridge's `MAX_BLOCK_BYTES` guard.
|
|
154
|
+
if (this.#pending.length - openIndex > this.#maxBlockBytes) {
|
|
155
|
+
visible += this.#pending.slice(openIndex);
|
|
156
|
+
cursor = this.#pending.length;
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
cursor = openIndex;
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
const end = closeIndex + close.length;
|
|
163
|
+
const block = this.#pending.slice(openIndex, end);
|
|
164
|
+
const parsed = parseStrictBlock(block, opener, close, separator, this.#allowedNames, this.#params, this.#sortKeys);
|
|
165
|
+
if (parsed === undefined) visible += block;
|
|
166
|
+
else {
|
|
167
|
+
parsed.forEach((call, index) => {
|
|
168
|
+
calls.push({ ...call, id: this.#id(call, index, this.#callSequence++) });
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
cursor = end;
|
|
172
|
+
}
|
|
173
|
+
this.#pending = this.#pending.slice(cursor);
|
|
174
|
+
return { text: visible, calls };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
flush(): DsmlParseResult {
|
|
178
|
+
const text = this.#pending;
|
|
179
|
+
this.#pending = "";
|
|
180
|
+
return { text, calls: [] };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
#settledEnd(start: number): number {
|
|
184
|
+
const remaining = this.#pending.slice(start);
|
|
185
|
+
const maxOpener = this.#openers.reduce((max, opener) => Math.max(max, opener.length - 1), 0);
|
|
186
|
+
for (let length = Math.min(maxOpener, remaining.length); length > 0; length -= 1) {
|
|
187
|
+
const suffix = remaining.slice(-length);
|
|
188
|
+
if (this.#openers.some((opener) => opener.startsWith(suffix))) return this.#pending.length - length;
|
|
189
|
+
}
|
|
190
|
+
return this.#pending.length;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function parseStrictBlock(
|
|
195
|
+
block: string,
|
|
196
|
+
opener: string,
|
|
197
|
+
close: string,
|
|
198
|
+
separator: string,
|
|
199
|
+
allowedNames: ReadonlySet<string>,
|
|
200
|
+
params: "json" | "string",
|
|
201
|
+
sortKeys: boolean,
|
|
202
|
+
): Omit<DsmlToolCall, "id">[] | undefined {
|
|
203
|
+
const inner = block.slice(opener.length, -close.length);
|
|
204
|
+
const pipe = escapeRegExp(separator);
|
|
205
|
+
const invokePattern = new RegExp(`<${pipe}DSML${pipe}\\s+invoke\\s+name="([^"]*)"\\s*>`, "g");
|
|
206
|
+
const calls: Omit<DsmlToolCall, "id">[] = [];
|
|
207
|
+
// The allowlist is strict only when the caller knows the declared tools: a
|
|
208
|
+
// caller without one (a direct parse) keeps the generic name rule, while a
|
|
209
|
+
// dispatch that declared its tools must never execute a name it did not offer.
|
|
210
|
+
const strict = allowedNames.size > 0;
|
|
211
|
+
let cursor = 0;
|
|
212
|
+
for (const invoke of inner.matchAll(invokePattern)) {
|
|
213
|
+
const name = invoke[1] ?? "";
|
|
214
|
+
if (!TOOL_NAME.test(name) || (strict && !allowedNames.has(name)) || invoke.index === undefined) return undefined;
|
|
215
|
+
if (inner.slice(cursor, invoke.index).trim().length > 0) return undefined;
|
|
216
|
+
const bodyStart = invoke.index + invoke[0].length;
|
|
217
|
+
const invokeClose = `</${separator}DSML${separator} invoke>`;
|
|
218
|
+
const invokeEnd = inner.indexOf(invokeClose, bodyStart);
|
|
219
|
+
if (invokeEnd < 0) return undefined;
|
|
220
|
+
const parameterPattern = new RegExp(`<${pipe}DSML${pipe}\\s+parameter\\s+name="([^"]*)"(?:\\s+string="(true|false)")?\\s*>`, "g");
|
|
221
|
+
const body = inner.slice(bodyStart, invokeEnd);
|
|
222
|
+
const args: Record<string, unknown> = {};
|
|
223
|
+
let parameterCursor = 0;
|
|
224
|
+
for (const parameter of body.matchAll(parameterPattern)) {
|
|
225
|
+
const key = parameter[1] ?? "";
|
|
226
|
+
if (key.length === 0 || parameter.index === undefined || body.slice(parameterCursor, parameter.index).trim().length > 0) return undefined;
|
|
227
|
+
const valueStart = parameter.index + parameter[0].length;
|
|
228
|
+
const parameterClose = `</${separator}DSML${separator} parameter>`;
|
|
229
|
+
const valueEnd = body.indexOf(parameterClose, valueStart);
|
|
230
|
+
if (valueEnd < 0) return undefined;
|
|
231
|
+
const raw = body.slice(valueStart, valueEnd);
|
|
232
|
+
if (params === "string") args[key] = raw;
|
|
233
|
+
else if (parameter[2] === "true") args[key] = raw;
|
|
234
|
+
else {
|
|
235
|
+
try { args[key] = JSON.parse(raw); }
|
|
236
|
+
catch { args[key] = raw; }
|
|
237
|
+
}
|
|
238
|
+
parameterCursor = valueEnd + parameterClose.length;
|
|
239
|
+
}
|
|
240
|
+
if (body.slice(parameterCursor).trim().length > 0) return undefined;
|
|
241
|
+
calls.push({ name, arguments: JSON.stringify(sortKeys ? normalizeForSort(args) : args) });
|
|
242
|
+
cursor = invokeEnd + invokeClose.length;
|
|
243
|
+
}
|
|
244
|
+
return calls.length > 0 && inner.slice(cursor).trim().length === 0 ? calls : undefined;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Returns `value` with plain-object keys recursively sorted, mirroring the
|
|
249
|
+
* reference bridge's `json.dumps(..., sort_keys=True)`. Arrays keep their
|
|
250
|
+
* order — sorting applies to object keys only.
|
|
251
|
+
*/
|
|
252
|
+
function normalizeForSort(value: unknown): unknown {
|
|
253
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return value;
|
|
254
|
+
const record = value as Record<string, unknown>;
|
|
255
|
+
const out: Record<string, unknown> = {};
|
|
256
|
+
for (const key of Object.keys(record).sort()) out[key] = normalizeForSort(record[key]);
|
|
257
|
+
return out;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** The Qoder fuzzy block scanner, mirroring the reference bridge's regexes. */
|
|
261
|
+
const FUZZY_CALLS_RE = /<[^>]*DSML[^>]*calls>([\s\S]*?)<\/[^>]*DSML[^>]*calls>/;
|
|
262
|
+
const FUZZY_INVOKE_RE = /<[^>]*DSML[^>]*invoke\s+name=["']([^"']+)["']>([\s\S]*?)<\/[^>]*invoke>/g;
|
|
263
|
+
const FUZZY_PARAM_RE = /<[^>]*DSML[^>]*parameter\s+name=["']([^"']+)["'][^>]*>([\s\S]*?)<\/[^>]*parameter>/g;
|
|
264
|
+
|
|
265
|
+
function parseFuzzyDsml(text: string, options: DsmlParseOptions): DsmlRecovery {
|
|
266
|
+
const id = options.id ?? uuidId;
|
|
267
|
+
if (text.length === 0 || !text.includes("DSML")) return { calls: [], clean: text };
|
|
268
|
+
|
|
269
|
+
const callsMatch = FUZZY_CALLS_RE.exec(text);
|
|
270
|
+
let block: string;
|
|
271
|
+
let clean: string;
|
|
272
|
+
|
|
273
|
+
if (callsMatch) {
|
|
274
|
+
block = callsMatch[1] ?? "";
|
|
275
|
+
const before = text.slice(0, callsMatch.index).trimEnd();
|
|
276
|
+
const after = text.slice(callsMatch.index + callsMatch[0].length).trimStart();
|
|
277
|
+
clean = (before + " " + after).trim();
|
|
278
|
+
} else {
|
|
279
|
+
// No <calls> wrapper (issue #51): find bare <invoke> blocks in the whole
|
|
280
|
+
// text, then cut the prose off at the last `<` before the first invoke —
|
|
281
|
+
// exclusive of the invoke's own tag start, exactly like the reference's
|
|
282
|
+
// `text.rfind("<", 0, first_index)`.
|
|
283
|
+
FUZZY_INVOKE_RE.lastIndex = 0;
|
|
284
|
+
const first = FUZZY_INVOKE_RE.exec(text);
|
|
285
|
+
if (!first) return { calls: [], clean: text };
|
|
286
|
+
block = text;
|
|
287
|
+
const cut = text.lastIndexOf("<", first.index - 1);
|
|
288
|
+
clean = text.slice(0, cut >= 0 ? cut : first.index).trim();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const calls: DsmlToolCall[] = [];
|
|
292
|
+
FUZZY_INVOKE_RE.lastIndex = 0;
|
|
293
|
+
let inv: RegExpExecArray | null;
|
|
294
|
+
let seq = 0;
|
|
295
|
+
while ((inv = FUZZY_INVOKE_RE.exec(block)) !== null) {
|
|
296
|
+
const params: Record<string, string> = {};
|
|
297
|
+
FUZZY_PARAM_RE.lastIndex = 0;
|
|
298
|
+
let par: RegExpExecArray | null;
|
|
299
|
+
while ((par = FUZZY_PARAM_RE.exec(inv[2] ?? "")) !== null) {
|
|
300
|
+
params[par[1] ?? ""] = (par[2] ?? "").trim();
|
|
301
|
+
}
|
|
302
|
+
const call = { name: inv[1] ?? "", arguments: JSON.stringify(params) };
|
|
303
|
+
calls.push({ ...call, id: id(call, calls.length, seq++) });
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return { calls, clean };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Recovers DSML tool calls from one plain-text string using the Qoder fuzzy
|
|
311
|
+
* dialect. Always returns `{ calls, clean }` — no-DSML input returns the text
|
|
312
|
+
* unchanged with no calls.
|
|
313
|
+
*/
|
|
314
|
+
export function parseFuzzyDsmlToolCalls(text: string): DsmlRecovery {
|
|
315
|
+
return parseFuzzyDsml(text, {});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Recovers DSML tool calls from one plain-text string. The dialect is chosen
|
|
320
|
+
* by `options.fuzzy`; each dialect keeps its exact public shape:
|
|
321
|
+
*
|
|
322
|
+
* - strict (CodeBuddy): returns `{ text, calls }`, with `text` the clean
|
|
323
|
+
* prose; `undefined` when nothing was recovered.
|
|
324
|
+
* - fuzzy (Qoder): returns `{ clean, calls }`, with no-DSML input returning
|
|
325
|
+
* `{ calls: [], clean: text }`.
|
|
326
|
+
*
|
|
327
|
+
* Callers that need the streaming strict parser use {@link DsmlStreamParser}.
|
|
328
|
+
*/
|
|
329
|
+
export function parseDsmlToolCalls(
|
|
330
|
+
text: string,
|
|
331
|
+
options: DsmlParseOptions = {},
|
|
332
|
+
): DsmlRecovery | DsmlParseResult | undefined {
|
|
333
|
+
if (options.fuzzy === true) return parseFuzzyDsml(text, options);
|
|
334
|
+
if (!text.includes("DSML")) return undefined;
|
|
335
|
+
const parser = new DsmlStreamParser(options);
|
|
336
|
+
const first = parser.push(text);
|
|
337
|
+
const last = parser.flush();
|
|
338
|
+
const recovered = [...first.calls, ...last.calls];
|
|
339
|
+
return recovered.length === 0 ? undefined : { text: `${first.text}${last.text}`.trim(), calls: recovered };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* The holdable tail of a content chunk: a short suffix that looks like the
|
|
344
|
+
* start of a DSML tag and may finish in the next frame. Empty when there is
|
|
345
|
+
* nothing worth holding. Mirrors `_dsml_tail_hold` in the reference bridge.
|
|
346
|
+
*/
|
|
347
|
+
export function dsmlTailHold(text: string): string {
|
|
348
|
+
const at = text.lastIndexOf("<");
|
|
349
|
+
if (at < 0 || text.length - at > 24) return "";
|
|
350
|
+
const tail = text.slice(at);
|
|
351
|
+
// A complete tag already — nothing to wait for.
|
|
352
|
+
if (tail.includes(">")) return "";
|
|
353
|
+
const body = tail.replace(/\uFF5C/g, "|").toUpperCase().slice(1).replace(/^\//, "");
|
|
354
|
+
if (body.length === 0) return tail; // bare "<" or "</"
|
|
355
|
+
if (!body.startsWith("|")) return "";
|
|
356
|
+
const rest = body.slice(1);
|
|
357
|
+
if (rest.length === 0) return tail;
|
|
358
|
+
if ("DSML".startsWith(rest) || rest.startsWith("DSML")) return tail;
|
|
359
|
+
return "";
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const DSML_CLOSE_TAG_RE = /<\/(?:[^>]*DSML[^>]*|parameter|invoke|calls|tool_calls|function_calls)\s*>\s*$/i;
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Removes leftover DSML closing tags from the tail of a text. Anchored to the
|
|
366
|
+
* end and looped, so several trailing tags come off one per pass; a closing
|
|
367
|
+
* tag in the middle of a text is not residue and is preserved.
|
|
368
|
+
*/
|
|
369
|
+
export function stripDsmlCloseTags(text: string): string {
|
|
370
|
+
let previous: string | null = null;
|
|
371
|
+
let out = text.trimEnd();
|
|
372
|
+
while (previous !== out) {
|
|
373
|
+
previous = out;
|
|
374
|
+
out = out.replace(DSML_CLOSE_TAG_RE, "").trimEnd();
|
|
375
|
+
}
|
|
376
|
+
return out;
|
|
377
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { CanonicalRequest, ReasoningEffort, ReasoningIntent } from "
|
|
2
|
-
import { budgetToLevel } from "
|
|
3
|
-
import { isRecord } from "
|
|
1
|
+
import type { CanonicalRequest, ReasoningEffort, ReasoningIntent } from "./canonical-model";
|
|
2
|
+
import { budgetToLevel } from "./translation/thinking";
|
|
3
|
+
import { isRecord } from "./codecs";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* The effective reasoning effort for a canonical intent, deriving one from an
|
|
@@ -132,12 +132,13 @@ const CLAMPED_EFFORT_MIN_OUTPUT_TOKENS = 16_384;
|
|
|
132
132
|
const BUDDY_LEVEL_RANK: Readonly<Record<string, number>> = {
|
|
133
133
|
off: 0,
|
|
134
134
|
none: 0,
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
135
|
+
instant: 1,
|
|
136
|
+
minimal: 2,
|
|
137
|
+
low: 3,
|
|
138
|
+
medium: 4,
|
|
139
|
+
high: 5,
|
|
140
|
+
xhigh: 6,
|
|
141
|
+
max: 7,
|
|
141
142
|
};
|
|
142
143
|
|
|
143
144
|
/** Builds the Anthropic thinking block from canonical reasoning intent. */
|
|
@@ -3,18 +3,18 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { CanonicalRequest, ResponseFormat } from "../canonical-model";
|
|
5
5
|
import { isRecord } from "../codecs";
|
|
6
|
-
import { markLatestBreakpoint } from "../../
|
|
7
|
-
import { isWebSearchTool } from "../../
|
|
8
|
-
import { pickWireSupportedControls } from "../../
|
|
9
|
-
import { normalizeWireMaxTokens } from "../../
|
|
10
|
-
import { resolveWireMaxTokens } from "../../
|
|
6
|
+
import { markLatestBreakpoint } from "../../protocol/translation/cache-controls";
|
|
7
|
+
import { isWebSearchTool } from "../../protocol/translation/search";
|
|
8
|
+
import { pickWireSupportedControls } from "../../protocol/translation/capabilities";
|
|
9
|
+
import { normalizeWireMaxTokens } from "../../protocol/translation/normalize";
|
|
10
|
+
import { resolveWireMaxTokens } from "../../protocol/translation/quirks";
|
|
11
11
|
import {
|
|
12
12
|
clampReasoningEffort,
|
|
13
13
|
resolveSupportedReasoningEfforts,
|
|
14
|
-
} from "../../
|
|
14
|
+
} from "../../protocol/translation/thinking";
|
|
15
15
|
import { isClaudeBillingHeaderText, resolveImageSource } from "../codecs";
|
|
16
16
|
import { resolvePromptCacheKey } from "../../providers/operations/session-resolution";
|
|
17
|
-
import { reasoningEffortFromIntent } from "../../
|
|
17
|
+
import { reasoningEffortFromIntent } from "../../protocol/reasoning";
|
|
18
18
|
import { parseChatResponseToEvents, decodeChatSseStream } from "../response/chat";
|
|
19
19
|
|
|
20
20
|
/**
|