cartethyia 2.7.8 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +3 -3
- package/README.md +29 -59
- package/dist/dashboard/assets/BannedPage-Cc0vKpMG.js +1 -0
- package/dist/dashboard/assets/BannedPage-Cc0vKpMG.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-BRWKPQPf.js → CliToolDetailPage-Bqj2aa1T.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-Bqj2aa1T.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-BP-Y5iHN.js → CliToolsPage-CXsaOAO2.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-CXsaOAO2.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-BeLpJ4ma.js +1 -0
- package/dist/dashboard/assets/CombosPage-BeLpJ4ma.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-DnV0uZ3m.js +1 -0
- package/dist/dashboard/assets/ConsoleLogPage-DnV0uZ3m.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-CBxAjo63.js +1 -0
- package/dist/dashboard/assets/CustomizationPage-CBxAjo63.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-B5yEE0k6.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-B5yEE0k6.js.gz +0 -0
- package/dist/dashboard/assets/{InjectSystemPromptPanel-Cv-2Ol_K.js → InjectSystemPromptPanel-BWFLECmz.js} +2 -2
- package/dist/dashboard/assets/InjectSystemPromptPanel-BWFLECmz.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-bKfz_b1K.js → LoginPage-CgoPc5B9.js} +1 -1
- package/dist/dashboard/assets/LoginPage-CgoPc5B9.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-B-WQxx_H.js → ModelPicker-D9bFQern.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-D9bFQern.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-B_p4tL8k.js +1 -0
- package/dist/dashboard/assets/OverviewPage-B_p4tL8k.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-rs5E2rxv.js +8 -0
- package/dist/dashboard/assets/ProviderDetailPage-rs5E2rxv.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-D_0PiZtg.js +1 -0
- package/dist/dashboard/assets/ProviderIcon-D_0PiZtg.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-CLI2bJTK.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-CLI2bJTK.js.gz +0 -0
- package/dist/dashboard/assets/{ProxyPage-emlTuxz6.js → ProxyPage-BwNC_-Cx.js} +1 -1
- package/dist/dashboard/assets/ProxyPage-BwNC_-Cx.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-DA-a1iSN.js +1 -0
- package/dist/dashboard/assets/QuotaPage-DA-a1iSN.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-CctTga2C.js +1 -0
- package/dist/dashboard/assets/SettingsPage-CctTga2C.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-CUZrKs9r.js → SetupPage-febLU4r1.js} +1 -1
- package/dist/dashboard/assets/SetupPage-febLU4r1.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-jWmUdDpy.js → StudioPage-M6fvR5Wz.js} +1 -1
- package/dist/dashboard/assets/StudioPage-M6fvR5Wz.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-nyeuRqz9.js +56 -0
- package/dist/dashboard/assets/UsagePage-nyeuRqz9.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-BFOlvrJC.js +1 -0
- package/dist/dashboard/assets/api-keys-BFOlvrJC.js.gz +0 -0
- package/dist/dashboard/assets/app-Dx2QgsNj.css +2 -0
- package/dist/dashboard/assets/app-Dx2QgsNj.css.gz +0 -0
- package/dist/dashboard/assets/{assert-CidQVoNo.js → assert-CqTLw5e7.js} +1 -1
- package/dist/dashboard/assets/assert-CqTLw5e7.js.gz +0 -0
- package/dist/dashboard/assets/{badge-DyI_mJfi.js → badge-C83EqLB0.js} +1 -1
- package/dist/dashboard/assets/badge-C83EqLB0.js.gz +0 -0
- package/dist/dashboard/assets/ban-durations-B58FyRxC.js +1 -0
- package/dist/dashboard/assets/ban-durations-B58FyRxC.js.gz +0 -0
- package/dist/dashboard/assets/{banner-B-yXtCqb.js → banner-D1xn7pIe.js} +1 -1
- package/dist/dashboard/assets/banner-D1xn7pIe.js.gz +0 -0
- package/dist/dashboard/assets/brand/bansos_page.webp +0 -0
- package/dist/dashboard/assets/brand/denia_bansos.webp +0 -0
- package/dist/dashboard/assets/brand/favicon.webp +0 -0
- package/dist/dashboard/assets/brand/login_register.webp +0 -0
- package/dist/dashboard/assets/brand/login_register_initial.webp +0 -0
- package/dist/dashboard/assets/brand/og_bansos.webp +0 -0
- package/dist/dashboard/assets/brand/og_image.webp +0 -0
- package/dist/dashboard/assets/brand/sidebar_ico.webp +0 -0
- package/dist/dashboard/assets/{card-_HwAEqFF.js → card-DU3Lq5Lm.js} +1 -1
- package/dist/dashboard/assets/card-DU3Lq5Lm.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-Dw1U_drK.js +1 -0
- package/dist/dashboard/assets/cli-tools-Dw1U_drK.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-ByjCk0o9.js → clipboard-button-CEL5Sg_J.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-CEL5Sg_J.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-BZhOlfz-.js → dialog-BjFR5UHy.js} +1 -1
- package/dist/dashboard/assets/dialog-BjFR5UHy.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-BADqIRYP.js → drawer-HvBW0AjI.js} +1 -1
- package/dist/dashboard/assets/drawer-HvBW0AjI.js.gz +0 -0
- package/dist/dashboard/assets/entry-BSu3gFlt.js +4 -0
- package/dist/dashboard/assets/entry-BSu3gFlt.js.gz +0 -0
- package/dist/dashboard/assets/{entry-COaC-irN.js → entry-CECVELyy.js} +1 -1
- package/dist/dashboard/assets/entry-CECVELyy.js.gz +0 -0
- package/dist/dashboard/assets/entry-DYYofQ7W.js +1 -0
- package/dist/dashboard/assets/entry-DYYofQ7W.js.gz +0 -0
- package/dist/dashboard/assets/{formatters-DcHJyRiu.js → formatters-DLW4i8zK.js} +1 -1
- package/dist/dashboard/assets/formatters-DLW4i8zK.js.gz +0 -0
- package/dist/dashboard/assets/{index-CgKmFngv.js → index-1-fCqe64.js} +2 -2
- package/dist/dashboard/assets/index-1-fCqe64.js.gz +0 -0
- package/dist/dashboard/assets/{inline-B2BirKry.js → inline-DGvU2oLK.js} +1 -1
- package/dist/dashboard/assets/inline-DGvU2oLK.js.gz +0 -0
- package/dist/dashboard/assets/{input-D-qV9dKY.js → input-iNnO1X2K.js} +1 -1
- package/dist/dashboard/assets/input-iNnO1X2K.js.gz +0 -0
- package/dist/dashboard/assets/{layout-B7tVWhtV.js → layout-0q8UGypb.js} +1 -1
- package/dist/dashboard/assets/layout-0q8UGypb.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-CtkGE3bF.js → list-row-C0w2bQjJ.js} +1 -1
- package/dist/dashboard/assets/list-row-C0w2bQjJ.js.gz +0 -0
- package/dist/dashboard/assets/{live-DGizF06-.js → live-M5aJ_xCa.js} +1 -1
- package/dist/dashboard/assets/live-M5aJ_xCa.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-GU7XdEjz.js → mutation-BQ0ccH_A.js} +1 -1
- package/dist/dashboard/assets/mutation-BQ0ccH_A.js.gz +0 -0
- package/dist/dashboard/assets/{network-C9UE-bOr.js → network-BKeQGT3K.js} +1 -1
- package/dist/dashboard/assets/network-BKeQGT3K.js.gz +0 -0
- package/dist/dashboard/assets/{numbers-BnAd22a_.js → numbers-CivAk3da.js} +1 -1
- package/dist/dashboard/assets/numbers-CivAk3da.js.gz +0 -0
- package/dist/dashboard/assets/{pagination-BAywyMzS.js → pagination-CpzvI5ep.js} +1 -1
- package/dist/dashboard/assets/pagination-CpzvI5ep.js.gz +0 -0
- package/dist/dashboard/assets/providers/devin.svg +9 -0
- package/dist/dashboard/assets/providers/devin.svg.gz +0 -0
- package/dist/dashboard/assets/providers/windsurf.svg +10 -0
- package/dist/dashboard/assets/providers/windsurf.svg.gz +0 -0
- package/dist/dashboard/assets/providers/zro.svg +3 -0
- package/dist/dashboard/assets/providers/zro.svg.gz +0 -0
- package/dist/dashboard/assets/{query-keys-YSoQOUh6.js → query-keys-IKLh0YFd.js} +1 -1
- package/dist/dashboard/assets/query-keys-IKLh0YFd.js.gz +0 -0
- package/dist/dashboard/assets/quota-DiPD6UKH.js +1 -0
- package/dist/dashboard/assets/quota-DiPD6UKH.js.gz +0 -0
- package/dist/dashboard/assets/routing-BFQX2VBc.js +1 -0
- package/dist/dashboard/assets/routing-BFQX2VBc.js.gz +0 -0
- package/dist/dashboard/assets/{select-BCOcv8WK.js → select-CXbck0jG.js} +2 -2
- package/dist/dashboard/assets/select-CXbck0jG.js.gz +0 -0
- package/dist/dashboard/assets/settings-C_XvMHh9.js +1 -0
- package/dist/dashboard/assets/settings-C_XvMHh9.js.gz +0 -0
- package/dist/dashboard/assets/{stack-ByKt3Q1g.js → stack-DggRiCxE.js} +1 -1
- package/dist/dashboard/assets/stack-DggRiCxE.js.gz +0 -0
- package/dist/dashboard/assets/{state-DXbGTx7N.js → state-ClFzfbYp.js} +1 -1
- package/dist/dashboard/assets/state-ClFzfbYp.js.gz +0 -0
- package/dist/dashboard/assets/story/25817331.webp +0 -0
- package/dist/dashboard/assets/story/Shorekeeper.webp +0 -0
- package/dist/dashboard/assets/story/cartethyia-god.webp +0 -0
- package/dist/dashboard/assets/story/fleurdelys_plus.webp +0 -0
- package/dist/dashboard/assets/story/jinhsi-blossom.webp +0 -0
- package/dist/dashboard/assets/story/phrolova-a.webp +0 -0
- package/dist/dashboard/assets/story/requestdeniawokkjpg.webp +0 -0
- package/dist/dashboard/assets/story/wuthering-waves-hiyuki-aemeath-hiyuki-rover.webp +0 -0
- package/dist/dashboard/assets/{switch-ZeTsW623.js → switch-CTIFxKqS.js} +1 -1
- package/dist/dashboard/assets/switch-CTIFxKqS.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-CbCP4w1o.js → useMutation-BClG3yWj.js} +1 -1
- package/dist/dashboard/assets/useMutation-BClG3yWj.js.gz +0 -0
- package/dist/dashboard/console-backgrounds.webp +0 -0
- package/dist/dashboard/console-backgrounds_initial.webp +0 -0
- package/dist/dashboard/donation/qris-risun-marke.webp +0 -0
- package/dist/dashboard/index.html +3 -3
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +59 -56
- package/migrations/full-postgres/0044_add_models_warning_metadata.sql +10 -0
- package/migrations/full-postgres/0045_migrate_workbuddy_to_codebuddy.sql +156 -0
- package/migrations/full-postgres/0046_add_accounts_quota_plan_fetched_at.sql +7 -0
- package/migrations/lite-sqlite3/0044_add_models_warning_metadata.sql +11 -0
- package/migrations/lite-sqlite3/0045_migrate_workbuddy_to_codebuddy.sql +156 -0
- package/migrations/lite-sqlite3/0046_add_accounts_quota_plan_fetched_at.sql +4 -0
- package/package.json +1 -1
- package/src/boot/app.ts +10 -13
- package/src/boot/dependencies.ts +12 -4
- package/src/boot/flush.ts +20 -0
- package/src/boot/lifecycle.ts +7 -5
- package/src/config/cooldowns.ts +1 -1
- package/src/config/routing.ts +26 -0
- package/src/config/spec-metadata.ts +72 -0
- package/src/config/spec.ts +20 -6
- package/src/console/api-keys/contracts.ts +12 -1
- package/src/console/api-keys/routes.ts +50 -31
- package/src/console/auth/account-reset.ts +7 -9
- package/src/console/backup/contracts.ts +4 -0
- package/src/console/backup/export-endpoint.ts +7 -5
- package/src/console/cli-tools/service.ts +6 -12
- package/src/console/console-router-guards.ts +23 -16
- package/src/console/console-router.ts +7 -0
- package/src/console/dashboard-assets.ts +5 -5
- package/src/console/domain-registration.ts +27 -2
- package/src/console/inferhub/normalize.ts +24 -28
- package/src/console/inferhub/types.ts +3 -0
- package/src/console/ip-bans/routes.ts +93 -0
- package/src/console/observability/store.ts +0 -53
- package/src/console/providers/catalog/catalog-projections.ts +5 -2
- package/src/console/providers/catalog/contracts.ts +123 -0
- package/src/console/providers/catalog/discover-constants.ts +6 -0
- package/src/console/providers/catalog/model-operations.ts +122 -10
- package/src/console/providers/catalog/provider-operations.ts +116 -8
- package/src/console/providers/catalog/public-model-store.ts +0 -0
- package/src/console/providers/catalog/routes.ts +65 -4
- package/src/console/providers/catalog/store-helpers.ts +30 -1
- package/src/console/providers/catalog/store.ts +249 -4
- package/src/console/quota/account-quota-global.ts +97 -19
- package/src/console/quota/account-quota-shared.ts +49 -0
- package/src/console/quota/account-quota-tenant.ts +6 -18
- package/src/console/quota/native-identity.ts +2 -0
- package/src/console/quota/quota-view.ts +19 -3
- package/src/console/routing/pools/operations.ts +31 -46
- package/src/console/settings/contracts.ts +18 -0
- package/src/console/settings/store.ts +32 -0
- package/src/console/shared/errors.ts +33 -6
- package/src/console/system-prompt/contracts.ts +0 -1
- package/src/main.ts +3 -3
- package/src/network/pool/byte-accounting.ts +3 -2
- package/src/network/pool/selector.ts +182 -39
- package/src/network/pool-cooldown-store.ts +89 -14
- package/src/network/pool-health.ts +49 -61
- package/src/network/rate-limit-verdict.ts +1 -1
- package/src/network/ssrf.ts +6 -6
- package/src/observability/event-log.ts +7 -361
- package/src/observability/event-query.ts +18 -4
- package/src/observability/performance-metrics.ts +9 -0
- package/src/observability/runtime-metrics.ts +14 -9
- package/src/observability/telemetry-buffer.ts +15 -25
- package/src/observability/telemetry-reconciliation.ts +9 -37
- package/src/observability/telemetry-row.ts +125 -0
- package/src/persistence/db-mode.ts +4 -1
- package/src/persistence/dialect.ts +30 -0
- package/src/persistence/migrate.ts +6 -3
- package/src/persistence/schema.ts +28 -2
- package/src/persistence/ttl-cache.ts +20 -2
- package/src/protocol/codecs.ts +1 -1
- package/src/protocol/dsml.ts +35 -87
- package/src/protocol/gateway-error.ts +1 -0
- package/src/protocol/markup-stream-parser.ts +195 -0
- package/src/protocol/reasoning-text.ts +19 -0
- package/src/protocol/reasoning.ts +1 -1
- package/src/protocol/request/chat.ts +14 -14
- package/src/protocol/request/gemini.ts +8 -2
- package/src/protocol/request/messages.ts +4 -5
- package/src/protocol/response/chat.ts +12 -20
- package/src/protocol/response/gemini.ts +6 -1
- package/src/protocol/response/omp-tool-call-markup.ts +43 -86
- package/src/protocol/response/responses.ts +18 -2
- package/src/protocol/sse-decoder.ts +25 -8
- package/src/protocol/stop-reason.ts +23 -1
- package/src/protocol/stream-error-frames.ts +13 -3
- package/src/protocol/think-split.ts +112 -0
- package/src/protocol/translation/dialect/qwen.ts +26 -58
- package/src/protocol/translation/quirks.ts +1 -1
- package/src/protocol/translation/tool-repair.ts +2 -2
- package/src/protocol/upstream/send-claude-messages.ts +15 -4
- package/src/protocol/upstream-parsing.ts +1 -1
- package/src/providers/adapters/compatible-adapter.ts +78 -17
- package/src/providers/authentication/oauth-client.ts +1 -1
- package/src/providers/authentication/oauth-device-flow.ts +98 -8
- package/src/providers/authentication/oauth-refresh-service.ts +153 -59
- package/src/providers/configured-provider.ts +148 -0
- package/src/providers/default-registry.ts +117 -27
- package/src/providers/discovery/discovery-types.ts +12 -0
- package/src/providers/discovery/probe-phases.ts +74 -33
- package/src/providers/discovery/probing-service.ts +293 -165
- package/src/providers/integrations/alibabacp/alibabacp-models.ts +1 -2
- package/src/providers/integrations/{dashscope-models.ts → alibabacp/dashscope-models.ts} +2 -2
- package/src/providers/integrations/antigravity/antigravity-protocol.ts +121 -2
- package/src/providers/integrations/antigravity/antigravity.ts +13 -2
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +72 -34
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +191 -35
- package/src/providers/integrations/buddy/buddy-checkin.ts +17 -39
- package/src/providers/integrations/buddy/buddy-conversation-headers.ts +2 -2
- package/src/providers/integrations/buddy/buddy-discovery-shared.ts +5 -5
- package/src/providers/integrations/buddy/buddy-identity.ts +89 -6
- package/src/providers/integrations/buddy/buddy-media.ts +4 -5
- package/src/providers/integrations/buddy/buddy-oauth-shared.ts +2 -2
- package/src/providers/integrations/buddy/buddy-quota-shared.ts +1 -1
- package/src/providers/integrations/buddy/buddy-usage-shared.ts +1 -3
- package/src/providers/integrations/buddy/capabilities.ts +6 -43
- package/src/providers/integrations/buddy/codebuddy/cn-catalog.ts +2 -1
- package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +23 -1
- package/src/providers/integrations/buddy/codebuddy/provider.ts +6 -2
- package/src/providers/integrations/buddy/codebuddy/shared.ts +38 -27
- package/src/providers/integrations/claude/claude-quota.ts +14 -17
- package/src/providers/integrations/cline/cline.ts +2 -2
- package/src/providers/integrations/commandcode/quota.ts +22 -28
- package/src/providers/integrations/cursor/cursor-auth.ts +0 -8
- package/src/providers/integrations/cursor/cursor-catalog.ts +2 -0
- package/src/providers/integrations/cursor/cursor-exec.ts +745 -0
- package/src/providers/integrations/cursor/cursor-http2.ts +2 -0
- package/src/providers/integrations/cursor/cursor-request.ts +35 -20
- package/src/providers/integrations/cursor/cursor-shared.ts +26 -5
- package/src/providers/integrations/cursor/cursor.ts +139 -70
- package/src/providers/integrations/cursor/protocol/messages.ts +22 -29
- package/src/providers/integrations/cursor/protocol/protobuf-schema.ts +37 -0
- package/src/providers/integrations/cursor/protocol/tool-canonical.ts +26 -100
- package/src/providers/integrations/devin/devin-auth.ts +353 -0
- package/src/providers/integrations/devin/devin-catalog.ts +207 -0
- package/src/providers/integrations/devin/devin-http.ts +91 -0
- package/src/providers/integrations/devin/devin-quota.ts +214 -0
- package/src/providers/integrations/devin/devin-shared.ts +111 -0
- package/src/providers/integrations/devin/devin.ts +539 -0
- package/src/providers/integrations/devin/protocol/devin-proto.ts +558 -0
- package/src/providers/integrations/devin/protocol/framing.ts +124 -0
- package/src/providers/integrations/factory/factory-oauth.ts +17 -44
- package/src/providers/integrations/factory/factory.ts +2 -2
- package/src/providers/integrations/freebuff/assets/official-system-manager.txt +88 -0
- package/src/providers/integrations/freebuff/assets/official-system-worker.txt +57 -0
- package/src/providers/integrations/freebuff/assets/official-tools.json +1070 -0
- package/src/providers/integrations/freebuff/freebuff-catalog.ts +120 -0
- package/src/providers/integrations/freebuff/freebuff-constants.ts +1233 -0
- package/src/providers/integrations/freebuff/freebuff-discovery.ts +108 -0
- package/src/providers/integrations/freebuff/freebuff-oauth.ts +240 -0
- package/src/providers/integrations/freebuff/freebuff-quota.ts +260 -0
- package/src/providers/integrations/freebuff/freebuff-virtual.ts +89 -0
- package/src/providers/integrations/freebuff/freebuff.ts +682 -0
- package/src/providers/integrations/freepi/freepi-catalog.ts +60 -0
- package/src/providers/integrations/freepi/freepi-discovery.ts +82 -0
- package/src/providers/integrations/freepi/freepi-headers.ts +21 -0
- package/src/providers/integrations/freepi/freepi-oauth.ts +142 -0
- package/src/providers/integrations/freepi/freepi-protocol.ts +242 -0
- package/src/providers/integrations/freepi/freepi-quota.ts +126 -0
- package/src/providers/integrations/freepi/freepi-realm.ts +35 -0
- package/src/providers/integrations/freepi/freepi.ts +292 -0
- package/src/providers/integrations/gemini.ts +2 -2
- package/src/providers/integrations/grok/grok-oauth.ts +48 -45
- package/src/providers/integrations/grok/grok-ratelimit-store.ts +10 -17
- package/src/providers/integrations/grok/grok-turn-index.ts +11 -16
- package/src/providers/integrations/hermes/hermes-oauth.ts +94 -0
- package/src/providers/integrations/{hermes.ts → hermes/hermes.ts} +13 -21
- package/src/providers/integrations/hermes/index.ts +2 -0
- package/src/providers/integrations/kiro/kiro-catalog.ts +17 -1
- package/src/providers/integrations/kiro/kiro-discovery.ts +10 -12
- package/src/providers/integrations/kiro/kiro-quota.ts +8 -4
- package/src/providers/integrations/kiro/kiro.ts +15 -19
- package/src/providers/integrations/ollamacloud/index.ts +2 -0
- package/src/providers/integrations/{ollamacloud-models.ts → ollamacloud/models.ts} +2 -2
- package/src/providers/integrations/{ollamacloud-quota.ts → ollamacloud/quota.ts} +3 -3
- package/src/providers/integrations/opencode/fingerprint.ts +1 -1
- package/src/providers/integrations/opencode/opencode-go-quota.ts +2 -8
- package/src/providers/integrations/perch/perch-catalog.ts +105 -0
- package/src/providers/integrations/perch/perch-credential.ts +25 -0
- package/src/providers/integrations/perch/perch-oauth.ts +170 -0
- package/src/providers/integrations/perch/perch-protocol.ts +208 -0
- package/src/providers/integrations/perch/perch-quota.ts +123 -0
- package/src/providers/integrations/perch/perch-realm.ts +82 -0
- package/src/providers/integrations/perch/perch-system-prompt.ts +13 -0
- package/src/providers/integrations/perch/perch.ts +305 -0
- package/src/providers/integrations/perch/saffron-system-prompt.txt +8 -0
- package/src/providers/integrations/qoder/chat-proto.ts +328 -328
- package/src/providers/integrations/qoder/claim-qoder-credits.ts +19 -60
- package/src/providers/integrations/qoder/qoder-adapter.ts +90 -40
- package/src/providers/integrations/qoder/qoder-auth.ts +101 -29
- package/src/providers/integrations/qoder/qoder-cache.ts +32 -0
- package/src/providers/integrations/qoder/qoder-catalog.ts +26 -6
- package/src/providers/integrations/qoder/qoder-chat.ts +7 -21
- package/src/providers/integrations/qoder/qoder-checkin.ts +6 -6
- package/src/providers/integrations/qoder/qoder-grpc.ts +210 -7
- package/src/providers/integrations/qoder/qoder-ide.ts +5 -108
- package/src/providers/integrations/qoder/qoder-identity-cache.ts +184 -0
- package/src/providers/integrations/qoder/qoder-native-identity-store.ts +83 -18
- package/src/providers/integrations/qoder/qoder-realm.ts +9 -6
- package/src/providers/integrations/qoder/qoder-umid.ts +109 -342
- package/src/providers/integrations/qoder/qoder-virtual.ts +33 -1
- package/src/providers/integrations/qoder/qoder.ts +3 -7
- package/src/providers/integrations/trae/trae-catalog.ts +9 -14
- package/src/providers/integrations/trae/trae-headers.ts +36 -11
- package/src/providers/integrations/trae/trae-oauth.ts +14 -17
- package/src/providers/integrations/trae/trae-protocol.ts +12 -11
- package/src/providers/integrations/trae/trae-quota.ts +96 -36
- package/src/providers/integrations/trae/trae-realm.ts +74 -15
- package/src/providers/integrations/trae/trae.ts +99 -26
- package/src/providers/integrations/zcode-start-plan/provider.ts +226 -23
- package/src/providers/integrations/zcode-start-plan/quota.ts +19 -2
- package/src/providers/integrations/zro/zro.ts +204 -0
- package/src/providers/operations/account-error-classification.ts +235 -278
- package/src/providers/operations/account-health-service.ts +273 -26
- package/src/providers/operations/account-refresh-status.ts +269 -0
- package/src/providers/operations/account-tier.ts +66 -0
- package/src/providers/operations/client-version-resolver.ts +1 -1
- package/src/providers/operations/client-versions.ts +42 -150
- package/src/providers/operations/cooldown-rules.ts +56 -0
- package/src/providers/operations/{model-catalog-cache.ts → provider-caches.ts} +68 -16
- package/src/providers/operations/provider-catalog-seeder.ts +4 -0
- package/src/providers/operations/provider-catalog-service.ts +1 -1
- package/src/providers/operations/provider-credential-service.ts +102 -32
- package/src/providers/operations/provider-rules/github.ts +48 -0
- package/src/providers/operations/provider-rules/grok.ts +33 -0
- package/src/providers/operations/provider-rules/inception.ts +49 -0
- package/src/providers/operations/provider-rules/index.ts +32 -0
- package/src/providers/operations/provider-rules/perch.ts +37 -0
- package/src/providers/operations/provider-rules/qoder.ts +70 -0
- package/src/providers/operations/provider-rules/trae.ts +81 -0
- package/src/providers/provider-contracts.ts +33 -2
- package/src/providers/{provider-credential-resolver.ts → provider-credentials.ts} +40 -3
- package/src/providers/provider-metadata.ts +99 -14
- package/src/providers/provider-model-definition.ts +36 -0
- package/src/providers/provider-probe.ts +15 -1
- package/src/providers/provider-registry.ts +42 -33
- package/src/providers/quota/quota-limit-windows.ts +16 -5
- package/src/providers/quota/quota-support.ts +1 -1
- package/src/providers/quota/quota-window-parser.ts +90 -18
- package/src/providers/quota/refresh.ts +96 -18
- package/src/security/abuse.ts +71 -0
- package/src/security/api-key-auth-cache.ts +1 -1
- package/src/security/api-key-auth.ts +6 -2
- package/src/transport/dispatch/attempt-finalize.ts +14 -13
- package/src/transport/dispatch/attempt-loop.ts +71 -17
- package/src/transport/dispatch/generation.ts +2 -8
- package/src/transport/dispatch/native-shared.ts +25 -0
- package/src/transport/dispatch/proxy-request.ts +9 -46
- package/src/transport/dispatch/responses-compact.ts +2 -8
- package/src/transport/dispatch/streaming/finalize.ts +9 -6
- package/src/transport/dispatch/streaming/stream.ts +9 -36
- package/src/transport/dispatch/systemone.ts +2 -8
- package/src/transport/dispatch/token-count.ts +2 -6
- package/src/transport/dispatch/upstream.ts +67 -1
- package/src/transport/drain-endpoint.ts +31 -4
- package/src/transport/failure-policy.ts +129 -37
- package/src/transport/guards/body-policy.ts +7 -1
- package/src/transport/guards/error-lifecycle.ts +13 -1
- package/src/transport/guards/request-context.ts +22 -14
- package/src/transport/request/preparer.ts +81 -71
- package/src/transport/routing/bounded-lru.ts +14 -0
- package/src/transport/routing/drain-reserve.ts +129 -0
- package/src/transport/routing/plan-gate.ts +7 -1
- package/src/transport/routing/round-robin.ts +60 -0
- package/src/transport/routing/route-catalog.ts +123 -111
- package/src/transport/routing/route-model.ts +51 -9
- package/src/transport/routing/router.ts +418 -33
- package/src/transport/surface/adapters.ts +11 -2
- package/src/transport/surface/chat/adapter.ts +2 -2
- package/src/transport/surface/chat/encode.ts +47 -4
- package/src/transport/surface/chat/parse.ts +2 -12
- package/src/transport/surface/completion.ts +44 -33
- package/src/transport/surface/content-parts.ts +21 -0
- package/src/transport/surface/messages/adapter.ts +9 -2
- package/src/transport/surface/messages/encode.ts +11 -3
- package/src/transport/surface/messages/parse.ts +2 -1
- package/src/transport/surface/messages/stream.ts +33 -17
- package/src/transport/surface/responses/encode.ts +34 -6
- package/src/transport/surface/responses/parse.ts +3 -11
- package/src/workers/daily-checkin.ts +1 -1
- package/src/workers/quota-refresh-sweep.ts +1 -1
- package/src/workers/scheduler.ts +22 -4
- package/dist/dashboard/assets/BannedPage-DeIbIiBU.js +0 -1
- package/dist/dashboard/assets/BannedPage-DeIbIiBU.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-BRWKPQPf.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-BP-Y5iHN.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-B7JHozVa.js +0 -1
- package/dist/dashboard/assets/CombosPage-B7JHozVa.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-GX7iGGcu.js +0 -1
- package/dist/dashboard/assets/ConsoleLogPage-GX7iGGcu.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-HVonbGEa.js +0 -1
- package/dist/dashboard/assets/CustomizationPage-HVonbGEa.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-xaut4OJ9.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-xaut4OJ9.js.gz +0 -0
- package/dist/dashboard/assets/InjectSystemPromptPanel-Cv-2Ol_K.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-bKfz_b1K.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-B-WQxx_H.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-Z9pJZFo8.js +0 -1
- package/dist/dashboard/assets/OverviewPage-Z9pJZFo8.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-CMeDBxR1.js +0 -7
- package/dist/dashboard/assets/ProviderDetailPage-CMeDBxR1.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js +0 -1
- package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-BXPdOpG4.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-BXPdOpG4.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-emlTuxz6.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-CMJKNYWb.js +0 -1
- package/dist/dashboard/assets/QuotaPage-CMJKNYWb.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-BSy7EwGo.js +0 -1
- package/dist/dashboard/assets/SettingsPage-BSy7EwGo.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-CUZrKs9r.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-jWmUdDpy.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-5F9tGVGG.js +0 -56
- package/dist/dashboard/assets/UsagePage-5F9tGVGG.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-KvX83mRe.js +0 -1
- package/dist/dashboard/assets/api-keys-KvX83mRe.js.gz +0 -0
- package/dist/dashboard/assets/app-BEsU8hKs.css +0 -2
- package/dist/dashboard/assets/app-BEsU8hKs.css.gz +0 -0
- package/dist/dashboard/assets/assert-CidQVoNo.js.gz +0 -0
- package/dist/dashboard/assets/badge-DyI_mJfi.js.gz +0 -0
- package/dist/dashboard/assets/banner-B-yXtCqb.js.gz +0 -0
- package/dist/dashboard/assets/brand/bansos_page.png +0 -0
- package/dist/dashboard/assets/brand/denia_bansos.jpg +0 -0
- package/dist/dashboard/assets/brand/favicon.jpg +0 -0
- package/dist/dashboard/assets/brand/login_register.jpg +0 -0
- package/dist/dashboard/assets/brand/login_register_initial.jpg +0 -0
- package/dist/dashboard/assets/brand/og_bansos.jpg +0 -0
- package/dist/dashboard/assets/brand/og_image.jpg +0 -0
- package/dist/dashboard/assets/brand/sidebar_ico.jpg +0 -0
- package/dist/dashboard/assets/card-_HwAEqFF.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-Bwv5-zGZ.js +0 -1
- package/dist/dashboard/assets/cli-tools-Bwv5-zGZ.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-ByjCk0o9.js.gz +0 -0
- package/dist/dashboard/assets/dialog-BZhOlfz-.js.gz +0 -0
- package/dist/dashboard/assets/drawer-BADqIRYP.js.gz +0 -0
- package/dist/dashboard/assets/entry-COaC-irN.js.gz +0 -0
- package/dist/dashboard/assets/entry-Ca7LSCet.js +0 -4
- package/dist/dashboard/assets/entry-Ca7LSCet.js.gz +0 -0
- package/dist/dashboard/assets/entry-PDd27uCd.js +0 -1
- package/dist/dashboard/assets/entry-PDd27uCd.js.gz +0 -0
- package/dist/dashboard/assets/formatters-DcHJyRiu.js.gz +0 -0
- package/dist/dashboard/assets/index-CgKmFngv.js.gz +0 -0
- package/dist/dashboard/assets/inline-B2BirKry.js.gz +0 -0
- package/dist/dashboard/assets/input-D-qV9dKY.js.gz +0 -0
- package/dist/dashboard/assets/layout-B7tVWhtV.js.gz +0 -0
- package/dist/dashboard/assets/list-row-CtkGE3bF.js.gz +0 -0
- package/dist/dashboard/assets/live-DGizF06-.js.gz +0 -0
- package/dist/dashboard/assets/minimize-2-BTmMTK4o.js +0 -1
- package/dist/dashboard/assets/minimize-2-BTmMTK4o.js.gz +0 -0
- package/dist/dashboard/assets/mutation-GU7XdEjz.js.gz +0 -0
- package/dist/dashboard/assets/network-C9UE-bOr.js.gz +0 -0
- package/dist/dashboard/assets/numbers-BnAd22a_.js.gz +0 -0
- package/dist/dashboard/assets/pagination-BAywyMzS.js.gz +0 -0
- package/dist/dashboard/assets/query-keys-YSoQOUh6.js.gz +0 -0
- package/dist/dashboard/assets/quota-DJ1QlAOq.js +0 -1
- package/dist/dashboard/assets/quota-DJ1QlAOq.js.gz +0 -0
- package/dist/dashboard/assets/routing-fnCsxgNO.js +0 -1
- package/dist/dashboard/assets/routing-fnCsxgNO.js.gz +0 -0
- package/dist/dashboard/assets/select-BCOcv8WK.js.gz +0 -0
- package/dist/dashboard/assets/settings-BxslsErn.js +0 -1
- package/dist/dashboard/assets/settings-BxslsErn.js.gz +0 -0
- package/dist/dashboard/assets/stack-ByKt3Q1g.js.gz +0 -0
- package/dist/dashboard/assets/state-DXbGTx7N.js.gz +0 -0
- package/dist/dashboard/assets/switch-ZeTsW623.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-CbCP4w1o.js.gz +0 -0
- package/dist/dashboard/console-backgrounds.jpg +0 -0
- package/dist/dashboard/console-backgrounds_initial.jpg +0 -0
- package/dist/dashboard/donation/qris-risun-marke.png +0 -0
- package/src/providers/bundled-discovery-providers.ts +0 -98
- package/src/providers/configured-openai-providers.ts +0 -120
- package/src/providers/integrations/buddy/turing-device-token.ts +0 -262
- package/src/providers/integrations/buddy/workbuddy/cn-oauth.ts +0 -30
- package/src/providers/integrations/buddy/workbuddy/cn-provider.ts +0 -58
- package/src/providers/integrations/buddy/workbuddy/oauth.ts +0 -31
- package/src/providers/integrations/buddy/workbuddy/provider.ts +0 -209
- package/src/providers/integrations/buddy/workbuddy/quota.ts +0 -161
- package/src/providers/integrations/buddy/workbuddy/shared.ts +0 -285
- package/src/providers/operations/model-discovery-cache.ts +0 -34
- package/src/providers/operations/probe-fetch.ts +0 -15
- package/src/providers/operations/provider-version-cache.ts +0 -30
- package/src/providers/provider-capabilities.ts +0 -33
- package/src/providers/provider-credential-envelope.ts +0 -43
- /package/dist/dashboard/assets/{app-CTxoqyYB.js → app-DIXLjojg.js} +0 -0
- /package/dist/dashboard/assets/{app-CTxoqyYB.js.gz → app-DIXLjojg.js.gz} +0 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Shared telemetry insert-row mapping: the single column spelling for both the
|
|
2
|
+
// write path (telemetry-buffer.ts) and the reconciliation backfill
|
|
3
|
+
// (telemetry-reconciliation.ts). Before this module existed the two paths kept
|
|
4
|
+
// hand-maintained mirrors that drifted — the backfill dropped creditUsed,
|
|
5
|
+
// resolveMs, tokensPerSec, firstContentDeltaAtMs, lastEventAtMs,
|
|
6
|
+
// payloadCaptureState, and degenerateStop, so a recovered row read as if those
|
|
7
|
+
// events never happened. One mapper means a column change lands in both paths.
|
|
8
|
+
import { telemetryEvents } from "../persistence/schema";
|
|
9
|
+
import { clientNameFromUserAgent } from "./client-name";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A normalized telemetry event, shaped like the raw `TelemetryEventRecord` the
|
|
13
|
+
* reconciliation job parses off disk. The write path normalizes its
|
|
14
|
+
* `TelemetryEventInput` (nested `usage`, token-count coercion) into this shape
|
|
15
|
+
* before calling {@link telemetryInsertRow}; the reconciliation path passes its
|
|
16
|
+
* parsed record directly.
|
|
17
|
+
*/
|
|
18
|
+
export interface TelemetryInsertRowSource {
|
|
19
|
+
readonly id: string;
|
|
20
|
+
readonly tenantId: string;
|
|
21
|
+
readonly requestId: string;
|
|
22
|
+
readonly createdAt: Date | string;
|
|
23
|
+
readonly sourceSurface: string | null;
|
|
24
|
+
readonly requestedModel: string | null;
|
|
25
|
+
readonly reportedModel: string | null;
|
|
26
|
+
readonly requestedEffort: string | null;
|
|
27
|
+
readonly providerId: string | null;
|
|
28
|
+
readonly accountId: string | null;
|
|
29
|
+
readonly networkPoolId: string | null;
|
|
30
|
+
readonly endpoint: string | null;
|
|
31
|
+
readonly apiKeyId: string | null;
|
|
32
|
+
readonly userAgent: string | null;
|
|
33
|
+
readonly clientIp: string | null;
|
|
34
|
+
readonly latencyMs: number | null;
|
|
35
|
+
readonly ttfbMs: number | null;
|
|
36
|
+
readonly stream: boolean | null;
|
|
37
|
+
readonly status: string | null;
|
|
38
|
+
readonly httpStatus: number | null;
|
|
39
|
+
readonly errorOrigin: string | null;
|
|
40
|
+
readonly errorCategory: string | null;
|
|
41
|
+
readonly inputTokens: number | null;
|
|
42
|
+
readonly cachedInputTokens: number | null;
|
|
43
|
+
readonly outputTokens: number | null;
|
|
44
|
+
readonly reasoningTokens: number | null;
|
|
45
|
+
readonly estimatedCostUsd: number | null;
|
|
46
|
+
readonly creditUsed: number | null;
|
|
47
|
+
readonly resolveMs: number | null;
|
|
48
|
+
readonly tokensPerSec: number | null;
|
|
49
|
+
readonly firstContentDeltaAtMs: number | null;
|
|
50
|
+
readonly lastEventAtMs: number | null;
|
|
51
|
+
/** Why the drawer payloads are empty, or `captured`; absent when unrecorded. */
|
|
52
|
+
readonly payloadCaptureState?: string | null;
|
|
53
|
+
/** `true` for a degenerate-stop request; absent/null for everything else. */
|
|
54
|
+
readonly degenerateStop?: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Coerces a token count to the integer the schema columns store; `undefined` for an unavailable count. */
|
|
58
|
+
export function tokenCountToInt(value: number | "unavailable" | undefined): number | undefined {
|
|
59
|
+
return typeof value === "number" ? Math.round(value) : undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Maps a normalized event to its `telemetry_events` insert row.
|
|
64
|
+
*
|
|
65
|
+
* Every column below has a live producer on the write path, and the
|
|
66
|
+
* reconciliation backfill relies on this exact spelling to recover the same
|
|
67
|
+
* columns from a raw line. Contract details that are load-bearing:
|
|
68
|
+
*
|
|
69
|
+
* - `id = requestId` is what makes the unique index a working idempotency
|
|
70
|
+
* boundary: a retry of the same request carries the same id.
|
|
71
|
+
* - `estimatedCostUsd` persists NULL when the route is unpriced, never the
|
|
72
|
+
* text `"null"` — the analytics `partial` flag counts completed rows with no
|
|
73
|
+
* persisted cost.
|
|
74
|
+
* - `creditUsed`/`tokensPerSec` use `!= null` so a measured zero is preserved
|
|
75
|
+
* (numeric columns are string-typed in Drizzle).
|
|
76
|
+
* - `clientName` is derived here, once, via `clientNameFromUserAgent`, so the
|
|
77
|
+
* write path and the backfill group the same client the same way.
|
|
78
|
+
*/
|
|
79
|
+
export function telemetryInsertRow(event: TelemetryInsertRowSource): typeof telemetryEvents.$inferInsert {
|
|
80
|
+
return {
|
|
81
|
+
id: event.requestId,
|
|
82
|
+
tenantId: event.tenantId,
|
|
83
|
+
requestId: event.requestId,
|
|
84
|
+
// Stamped here, not by the reader. A row without it has no real time, and a
|
|
85
|
+
// reader that invented one made every such row look like it happened just now.
|
|
86
|
+
createdAt: new Date(event.createdAt),
|
|
87
|
+
sourceSurface: event.sourceSurface as typeof telemetryEvents.$inferInsert.sourceSurface,
|
|
88
|
+
requestedModel: event.requestedModel,
|
|
89
|
+
reportedModel: event.reportedModel,
|
|
90
|
+
requestedEffort: event.requestedEffort,
|
|
91
|
+
endpoint: event.endpoint,
|
|
92
|
+
apiKeyId: event.apiKeyId,
|
|
93
|
+
userAgent: event.userAgent,
|
|
94
|
+
// Canonical client grouping, stamped once so SQL breakdowns group by it.
|
|
95
|
+
clientName: clientNameFromUserAgent(event.userAgent),
|
|
96
|
+
clientIp: event.clientIp,
|
|
97
|
+
providerId: event.providerId,
|
|
98
|
+
accountId: event.accountId,
|
|
99
|
+
networkPoolId: event.networkPoolId,
|
|
100
|
+
latencyMs: event.latencyMs,
|
|
101
|
+
ttfbMs: event.ttfbMs,
|
|
102
|
+
stream: event.stream,
|
|
103
|
+
httpStatus: event.httpStatus,
|
|
104
|
+
status: event.status as typeof telemetryEvents.$inferInsert.status,
|
|
105
|
+
errorCategory: event.errorCategory,
|
|
106
|
+
errorOrigin: event.errorOrigin,
|
|
107
|
+
inputTokens: event.inputTokens,
|
|
108
|
+
cachedInputTokens: event.cachedInputTokens,
|
|
109
|
+
outputTokens: event.outputTokens,
|
|
110
|
+
reasoningTokens: event.reasoningTokens,
|
|
111
|
+
// `estimated_cost` is `null` when the route is unpriced; persist NULL so
|
|
112
|
+
// the analytics `partial` flag can count it. `String(null)` would store the
|
|
113
|
+
// text "null", which the numeric column rejects.
|
|
114
|
+
estimatedCostUsd: event.estimatedCostUsd === null ? null : String(event.estimatedCostUsd),
|
|
115
|
+
// Provider-billed credit (Tencent buddy-meter `credit`, Qoder `credits`);
|
|
116
|
+
// NULL when the upstream reported no credit field.
|
|
117
|
+
creditUsed: event.creditUsed != null ? String(event.creditUsed) : null,
|
|
118
|
+
resolveMs: event.resolveMs,
|
|
119
|
+
tokensPerSec: event.tokensPerSec != null ? String(event.tokensPerSec) : null,
|
|
120
|
+
firstContentDeltaAtMs: event.firstContentDeltaAtMs,
|
|
121
|
+
lastEventAtMs: event.lastEventAtMs,
|
|
122
|
+
payloadCaptureState: event.payloadCaptureState ?? null,
|
|
123
|
+
degenerateStop: event.degenerateStop ?? null,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
@@ -48,7 +48,10 @@ export function resolveDbMode(env: NodeJS.ProcessEnv = process.env): DbMode {
|
|
|
48
48
|
*/
|
|
49
49
|
export function resolveDataDir(env: NodeJS.ProcessEnv = process.env): string {
|
|
50
50
|
const override = env.CARTETHYIA_DATA_DIR?.trim();
|
|
51
|
-
|
|
51
|
+
// Guard against a literal `"undefined"` value: a launcher that stringifies
|
|
52
|
+
// a missing env var (`String(undefined)`) would otherwise make the gateway
|
|
53
|
+
// write its state into a folder named `undefined/` in the CWD.
|
|
54
|
+
if (override && override !== "undefined") return override;
|
|
52
55
|
if (process.platform === "win32") {
|
|
53
56
|
const roaming = env.APPDATA?.trim();
|
|
54
57
|
if (roaming) return join(roaming, "Cartethyia");
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import { sql, type SQL } from "drizzle-orm";
|
|
17
17
|
import { SQLiteBunSession } from "drizzle-orm/bun-sqlite";
|
|
18
18
|
import { resolveDbMode } from "./db-mode";
|
|
19
|
+
import { providerAccounts } from "./schema";
|
|
19
20
|
|
|
20
21
|
/** True when the running backend is the embedded SQLite database. */
|
|
21
22
|
export function sqliteDialect(): boolean {
|
|
@@ -55,6 +56,35 @@ function hasSqliteDialect(database: unknown): boolean {
|
|
|
55
56
|
return (database as { session?: unknown } | undefined)?.session instanceof SQLiteBunSession;
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Dialect of a concrete Drizzle handle, rather than of the process's env.
|
|
61
|
+
*
|
|
62
|
+
* A test can hold an in-memory SQLite database while `CARTETHYIA_DB_MODE`
|
|
63
|
+
* names `full` (the backend suite sets that globally). A raw SQL predicate
|
|
64
|
+
* chosen from the env would emit `jsonb_each_text` into SQLite and fail; the
|
|
65
|
+
* handle's session is the authority, exactly as `mergeJson` already treats it.
|
|
66
|
+
*/
|
|
67
|
+
export function isSqliteDatabase(database: unknown): boolean {
|
|
68
|
+
return hasSqliteDialect(database);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The `EXISTS` subquery for "the column's cooldown object has an entry whose
|
|
73
|
+
* ISO deadline is still in the future". The JSON check is dialect-specific:
|
|
74
|
+
* PostgreSQL uses `jsonb_each_text` (with a shape guard), SQLite `json_each`
|
|
75
|
+
* on its `text` column (D07). Timestamps are stored as ISO strings on both
|
|
76
|
+
* backends, so the lexical comparison against `nowIso` is stable.
|
|
77
|
+
*
|
|
78
|
+
* Callers keep their own scoping around this fragment — a tenant path must
|
|
79
|
+
* never touch `tenant_id IS NULL` rows, and the global path must only touch
|
|
80
|
+
* them — so only the shared JSON subquery lives here.
|
|
81
|
+
*/
|
|
82
|
+
export function liveModelCooldownExistsSql(database: unknown, nowIso: string): SQL {
|
|
83
|
+
return isSqliteDatabase(database)
|
|
84
|
+
? sql`EXISTS (SELECT 1 FROM json_each(${providerAccounts.modelCooldowns}) AS entry WHERE json_valid(${providerAccounts.modelCooldowns}) AND json_type(${providerAccounts.modelCooldowns}) = 'object' AND entry.value > ${nowIso})`
|
|
85
|
+
: sql`EXISTS (SELECT 1 FROM jsonb_each_text(${providerAccounts.modelCooldowns}) AS entry WHERE jsonb_typeof(${providerAccounts.modelCooldowns}) = 'object' AND entry.value > ${nowIso})`;
|
|
86
|
+
}
|
|
87
|
+
|
|
58
88
|
/**
|
|
59
89
|
* The per-model cooldown object with every entry whose timestamp has passed
|
|
60
90
|
* removed, or `'{}'` when nothing remains.
|
|
@@ -25,11 +25,14 @@ export function resolveMigrationsFolder(): string {
|
|
|
25
25
|
return folder;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
export function
|
|
28
|
+
export function migrationFileIds(folder: string): readonly string[] {
|
|
29
29
|
return readdirSync(folder)
|
|
30
30
|
.filter((file) => /^\d{4}_.+\.sql$/.test(file))
|
|
31
|
-
.sort()
|
|
32
|
-
|
|
31
|
+
.sort();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function migrationFiles(folder: string): readonly string[] {
|
|
35
|
+
return migrationFileIds(folder).map((file) => resolve(folder, file));
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
export function migrationIdFor(file: string): string {
|
|
@@ -219,8 +219,8 @@ export interface NativeIdentityRecord {
|
|
|
219
219
|
readonly machineCode: string;
|
|
220
220
|
/** The seed-file content this identity was minted from, if captured. */
|
|
221
221
|
readonly seed?: string;
|
|
222
|
-
/** How this identity was produced: the genuine SG bridge or the fallback. */
|
|
223
|
-
readonly source?: "runtime-info" | "generated";
|
|
222
|
+
/** How this identity was produced: the genuine SG bridge, the OS fingerprint, or the fallback. */
|
|
223
|
+
readonly source?: "runtime-info" | "generated" | "os";
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
export const providerAccounts = pgTable("provider_accounts", {
|
|
@@ -309,6 +309,14 @@ export const providerAccounts = pgTable("provider_accounts", {
|
|
|
309
309
|
* to an account that may well be refused.
|
|
310
310
|
*/
|
|
311
311
|
quotaPlan: text("quota_plan"),
|
|
312
|
+
/**
|
|
313
|
+
* When `quota_plan` was last written by a successful quota read.
|
|
314
|
+
*
|
|
315
|
+
* The plan column is a bare string with no timestamp, so routing cannot tell
|
|
316
|
+
* a fresh plan from one recorded weeks ago. With the freshness horizon, a
|
|
317
|
+
* stale plan is downgraded to `unknown` instead of being trusted blindly.
|
|
318
|
+
*/
|
|
319
|
+
quotaPlanFetchedAt: timestamp("quota_plan_fetched_at", { withTimezone: true }),
|
|
312
320
|
/**
|
|
313
321
|
* The last successful quota read for this account, as a JSON snapshot.
|
|
314
322
|
*
|
|
@@ -432,6 +440,11 @@ export const models = pgTable(
|
|
|
432
440
|
reasoning: boolean("reasoning").notNull().default(false),
|
|
433
441
|
toolCall: boolean("tool_call").notNull().default(false),
|
|
434
442
|
cost: jsonb("cost"),
|
|
443
|
+
// Display-only warning metadata for this row: Postgres `jsonb` (SQLite
|
|
444
|
+
// `text` carrying the JSON string), nullable, `NULL` = no warnings. Shape:
|
|
445
|
+
// `{ warnings: [{ kind, message, severity?, updatedAt? }] }`. Part of the
|
|
446
|
+
// provider-account model-update proposal §3.4 area 06; not read by routing.
|
|
447
|
+
warningMetadata: jsonb("warning_metadata"),
|
|
435
448
|
source: text("source"),
|
|
436
449
|
sourceUpdatedAt: timestamp("source_updated_at", { withTimezone: true }),
|
|
437
450
|
enabled: boolean("enabled").notNull().default(true),
|
|
@@ -1288,6 +1301,19 @@ export interface ConsoleSettingsPreferences {
|
|
|
1288
1301
|
* placeholder behavior.
|
|
1289
1302
|
*/
|
|
1290
1303
|
visionHandoffModels?: readonly string[];
|
|
1304
|
+
/**
|
|
1305
|
+
* Auto-disable provider accounts that keep failing with upstream rate-limit
|
|
1306
|
+
* or quota verdicts. Off by default: without it, those failures land on a
|
|
1307
|
+
* per-model/account cooldown and the account stays rotatable.
|
|
1308
|
+
*/
|
|
1309
|
+
autoDisableRateLimited?: boolean;
|
|
1310
|
+
/**
|
|
1311
|
+
* Modelprobe mode: `detail` (full identity probe) or `fast` (minimal
|
|
1312
|
+
* 32-token check). `detail` when unset.
|
|
1313
|
+
*/
|
|
1314
|
+
modelProbeMode?: "fast" | "detail";
|
|
1315
|
+
/** Fast-probe timeout in seconds; `null`/absent means the built-in default (10). */
|
|
1316
|
+
modelProbeFastTimeoutSeconds?: number | null;
|
|
1291
1317
|
}
|
|
1292
1318
|
|
|
1293
1319
|
export const consoleSettings = pgTable("console_settings", {
|
|
@@ -11,6 +11,12 @@ interface TtlCacheOptions {
|
|
|
11
11
|
readonly ttlMs?: number;
|
|
12
12
|
readonly maxEntries?: number;
|
|
13
13
|
readonly now?: () => number;
|
|
14
|
+
/**
|
|
15
|
+
* LRU eviction order. When set, re-setting an existing key moves it to the
|
|
16
|
+
* newest position (delete-then-set), so oldest-first eviction becomes
|
|
17
|
+
* least-recently-used-first. Off by default: plain FIFO insertion order.
|
|
18
|
+
*/
|
|
19
|
+
readonly lru?: boolean;
|
|
14
20
|
}
|
|
15
21
|
|
|
16
22
|
interface TtlCacheEntry<T> {
|
|
@@ -27,11 +33,13 @@ export class TtlCache<T> {
|
|
|
27
33
|
private readonly ttlMs: number;
|
|
28
34
|
private readonly maxEntries: number;
|
|
29
35
|
private readonly now: () => number;
|
|
36
|
+
private readonly lru: boolean;
|
|
30
37
|
|
|
31
38
|
constructor(opts: TtlCacheOptions = {}) {
|
|
32
39
|
this.ttlMs = opts.ttlMs ?? 5_000;
|
|
33
40
|
this.maxEntries = opts.maxEntries ?? 128;
|
|
34
41
|
this.now = opts.now ?? Date.now;
|
|
42
|
+
this.lru = opts.lru ?? false;
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
/**
|
|
@@ -54,10 +62,21 @@ export class TtlCache<T> {
|
|
|
54
62
|
*/
|
|
55
63
|
set(key: string, value: T): void {
|
|
56
64
|
const now = this.now();
|
|
65
|
+
if (this.lru) this.cache.delete(key); // move to newest position
|
|
57
66
|
this.cache.set(key, { value, at: now });
|
|
58
67
|
this.evictIfNeeded(now);
|
|
59
68
|
}
|
|
60
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Peek at a stored value without checking the TTL or deleting on expiry.
|
|
72
|
+
* Returns only undefined when the key is absent. Callers that need the
|
|
73
|
+
* possibly-expired value (e.g. a stale fallback after a failed refetch)
|
|
74
|
+
* must read it before `get`, which removes expired entries.
|
|
75
|
+
*/
|
|
76
|
+
peek(key: string): T | undefined {
|
|
77
|
+
return this.cache.get(key)?.value;
|
|
78
|
+
}
|
|
79
|
+
|
|
61
80
|
/**
|
|
62
81
|
* Delete a specific key.
|
|
63
82
|
*/
|
|
@@ -144,8 +163,7 @@ interface TtlCacheFamilyOptions {
|
|
|
144
163
|
/**
|
|
145
164
|
* A set of {@link TtlCache} instances keyed by TTL, created on first use.
|
|
146
165
|
*
|
|
147
|
-
* Two provider-side caches
|
|
148
|
-
* `operations/model-discovery-cache.ts`) had each hand-rolled the same
|
|
166
|
+
* Two provider-side caches in `operations/provider-caches.ts` had each hand-rolled the same
|
|
149
167
|
* `Map<number, TtlCache<T>>` + `cacheFor(ttlMs)` memo plus the same
|
|
150
168
|
* `for (const cache of caches.values()) cache.clear()` reset. Tests pass
|
|
151
169
|
* `ttlMs: 0` to force a reload, which is why the per-TTL split exists at all —
|
package/src/protocol/codecs.ts
CHANGED
|
@@ -14,7 +14,7 @@ import type { CanonicalStopReason, CanonicalTerminalEvent, UsageRecord } from ".
|
|
|
14
14
|
import { type WireFamily } from "./canonical-model";
|
|
15
15
|
import type { GenerationControls } from "./canonical-model";
|
|
16
16
|
import { BASE_PROTECTED_HEADERS, HEADER_CONTROL, HEADER_TOKEN } from "../security/outbound-headers";
|
|
17
|
-
import { unwrapProviderToken } from "../providers/provider-
|
|
17
|
+
import { unwrapProviderToken } from "../providers/provider-credentials";
|
|
18
18
|
|
|
19
19
|
const TEXT_ENCODER = new TextEncoder();
|
|
20
20
|
const TEXT_DECODER = new TextDecoder();
|
package/src/protocol/dsml.ts
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
* markup inside the plain-text content stream instead of the structured
|
|
6
6
|
* `tool_calls` field. The gateway sees it from one upstream family:
|
|
7
7
|
*
|
|
8
|
-
* - CodeBuddy
|
|
8
|
+
* - CodeBuddy emits strict blocks (`<|DSML| calls>…</|DSML| calls>`)
|
|
9
9
|
* that must be parsed exactly, with declared-tool allowlisting.
|
|
10
10
|
*/
|
|
11
|
+
import { MarkupStreamParser } from "./markup-stream-parser";
|
|
11
12
|
export interface DsmlToolCall {
|
|
12
13
|
readonly id: string;
|
|
13
14
|
readonly name: string;
|
|
@@ -74,102 +75,49 @@ interface DsmlParseResult {
|
|
|
74
75
|
* With no allowlist the generic name rule applies; with one, undeclared names
|
|
75
76
|
* reject the whole block. The caller feeds chunks with `push` and drains the
|
|
76
77
|
* tail with `flush`.
|
|
78
|
+
*
|
|
79
|
+
* The stateful prefix-buffer loop is the shared {@link MarkupStreamParser}
|
|
80
|
+
* scaffold; this class supplies the DSML grammar as a thin configuration.
|
|
77
81
|
*/
|
|
78
82
|
export class DsmlStreamParser {
|
|
79
|
-
readonly #
|
|
80
|
-
readonly #blockNames: readonly string[];
|
|
81
|
-
readonly #openers: readonly string[];
|
|
82
|
-
readonly #allowedNames: ReadonlySet<string>;
|
|
83
|
-
readonly #params: "json" | "string";
|
|
84
|
-
readonly #sortKeys: boolean;
|
|
85
|
-
readonly #maxBlockBytes: number;
|
|
86
|
-
readonly #id: (call: { readonly name: string }, index: number, seq: number) => string;
|
|
87
|
-
#pending = "";
|
|
88
|
-
#callSequence = 0;
|
|
83
|
+
readonly #parser: MarkupStreamParser<DsmlToolCall>;
|
|
89
84
|
|
|
90
85
|
constructor(options: DsmlParseOptions = {}) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
this.#
|
|
86
|
+
const separators = options.separators ?? DEFAULT_SEPARATORS;
|
|
87
|
+
const blockNames = options.blockNames ?? DEFAULT_BLOCK_NAMES;
|
|
88
|
+
const allowedNames = options.allowedNames ?? new Set();
|
|
89
|
+
const params = options.params ?? "json";
|
|
90
|
+
const sortKeys = options.sortKeys ?? true;
|
|
91
|
+
const maxBlockBytes = options.maxBlockBytes ?? DEFAULT_MAX_BLOCK_BYTES;
|
|
92
|
+
const id = options.id ?? sequentialId;
|
|
93
|
+
this.#parser = new MarkupStreamParser<DsmlToolCall>({
|
|
94
|
+
openers: openers(separators, blockNames),
|
|
95
|
+
classifyOpener: (_pending, _index, opener) => {
|
|
96
|
+
const separator = separators.find((value) => opener.startsWith(`<${value}DSML`));
|
|
97
|
+
const blockName = blockNames.find((value) => opener.endsWith(` ${value}>`));
|
|
98
|
+
if (separator === undefined || blockName === undefined) return { kind: "literal" };
|
|
99
|
+
const close = `</${separator}DSML${separator} ${blockName}>`;
|
|
100
|
+
return {
|
|
101
|
+
kind: "found",
|
|
102
|
+
closer: close,
|
|
103
|
+
parse: (block) =>
|
|
104
|
+
parseStrictBlock(block, opener, close, separator, allowedNames, params, sortKeys),
|
|
105
|
+
callId: (call, index, seq) => ({ ...call, id: id(call, index, seq) }),
|
|
106
|
+
};
|
|
107
|
+
},
|
|
108
|
+
onUnclosed: (pending, openIndex) =>
|
|
109
|
+
pending.length - openIndex > maxBlockBytes
|
|
110
|
+
? { kind: "passThrough" }
|
|
111
|
+
: "hold",
|
|
112
|
+
});
|
|
99
113
|
}
|
|
100
114
|
|
|
101
115
|
push(chunk: string): DsmlParseResult {
|
|
102
|
-
this.#
|
|
103
|
-
let cursor = 0;
|
|
104
|
-
let visible = "";
|
|
105
|
-
const calls: DsmlToolCall[] = [];
|
|
106
|
-
for (;;) {
|
|
107
|
-
let openIndex = -1;
|
|
108
|
-
let opener = "";
|
|
109
|
-
for (const candidate of this.#openers) {
|
|
110
|
-
const index = this.#pending.indexOf(candidate, cursor);
|
|
111
|
-
if (index >= 0 && (openIndex < 0 || index < openIndex)) {
|
|
112
|
-
openIndex = index;
|
|
113
|
-
opener = candidate;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
if (openIndex < 0) {
|
|
117
|
-
const safeEnd = this.#settledEnd(cursor);
|
|
118
|
-
visible += this.#pending.slice(cursor, safeEnd);
|
|
119
|
-
cursor = safeEnd;
|
|
120
|
-
break;
|
|
121
|
-
}
|
|
122
|
-
visible += this.#pending.slice(cursor, openIndex);
|
|
123
|
-
const separator = this.#separators.find((value) => opener.startsWith(`<${value}DSML`));
|
|
124
|
-
const blockName = this.#blockNames.find((value) => opener.endsWith(` ${value}>`));
|
|
125
|
-
if (separator === undefined || blockName === undefined) {
|
|
126
|
-
visible += "<";
|
|
127
|
-
cursor = openIndex + 1;
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
const close = `</${separator}DSML${separator} ${blockName}>`;
|
|
131
|
-
const closeIndex = this.#pending.indexOf(close, openIndex + opener.length);
|
|
132
|
-
if (closeIndex < 0) {
|
|
133
|
-
// An unclosed block past the cap is not markup — pass it through as
|
|
134
|
-
// text instead of buffering it indefinitely. Same fail-open as
|
|
135
|
-
// `feed()` in the reference bridge's `MAX_BLOCK_BYTES` guard.
|
|
136
|
-
if (this.#pending.length - openIndex > this.#maxBlockBytes) {
|
|
137
|
-
visible += this.#pending.slice(openIndex);
|
|
138
|
-
cursor = this.#pending.length;
|
|
139
|
-
break;
|
|
140
|
-
}
|
|
141
|
-
cursor = openIndex;
|
|
142
|
-
break;
|
|
143
|
-
}
|
|
144
|
-
const end = closeIndex + close.length;
|
|
145
|
-
const block = this.#pending.slice(openIndex, end);
|
|
146
|
-
const parsed = parseStrictBlock(block, opener, close, separator, this.#allowedNames, this.#params, this.#sortKeys);
|
|
147
|
-
if (parsed === undefined) visible += block;
|
|
148
|
-
else {
|
|
149
|
-
parsed.forEach((call, index) => {
|
|
150
|
-
calls.push({ ...call, id: this.#id(call, index, this.#callSequence++) });
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
cursor = end;
|
|
154
|
-
}
|
|
155
|
-
this.#pending = this.#pending.slice(cursor);
|
|
156
|
-
return { text: visible, calls };
|
|
116
|
+
return this.#parser.push(chunk);
|
|
157
117
|
}
|
|
158
118
|
|
|
159
119
|
flush(): DsmlParseResult {
|
|
160
|
-
|
|
161
|
-
this.#pending = "";
|
|
162
|
-
return { text, calls: [] };
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
#settledEnd(start: number): number {
|
|
166
|
-
const remaining = this.#pending.slice(start);
|
|
167
|
-
const maxOpener = this.#openers.reduce((max, opener) => Math.max(max, opener.length - 1), 0);
|
|
168
|
-
for (let length = Math.min(maxOpener, remaining.length); length > 0; length -= 1) {
|
|
169
|
-
const suffix = remaining.slice(-length);
|
|
170
|
-
if (this.#openers.some((opener) => opener.startsWith(suffix))) return this.#pending.length - length;
|
|
171
|
-
}
|
|
172
|
-
return this.#pending.length;
|
|
120
|
+
return this.#parser.flush();
|
|
173
121
|
}
|
|
174
122
|
}
|
|
175
123
|
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic streaming prefix-buffer parser for in-band tool-call markup.
|
|
3
|
+
*
|
|
4
|
+
* Three grammars (DSML blocks, OMP `<tool_calls:8hex>` envelopes, Qwen
|
|
5
|
+
* `<tool_call>` JSON blocks) share one stateful loop: append chunks to a
|
|
6
|
+
* pending buffer, scan for the earliest opener, settle visible text up to a
|
|
7
|
+
* safe end that holds back a possible trailing opener fragment, parse a
|
|
8
|
+
* complete block when its closer appears, fail open past a block-size cap, and
|
|
9
|
+
* flush the tail. The grammar-specific parts are supplied as configuration:
|
|
10
|
+
* opener strings, an opener classifier (some grammars must validate beyond the
|
|
11
|
+
* bare prefix, e.g. OMP's hex marker), closer/parse functions, an unclosed
|
|
12
|
+
* block policy (hold vs fail-open pass-through), a flush policy (pass through
|
|
13
|
+
* vs discard an unterminated envelope), and a call-id factory. Each parser
|
|
14
|
+
* instance keeps its own buffer; nothing is shared.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** Result of one push/flush pass: text that settled plus recovered calls. */
|
|
18
|
+
export interface MarkupStreamParseResult<Call extends { readonly id: string }> {
|
|
19
|
+
readonly text: string;
|
|
20
|
+
readonly calls: readonly Call[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Raw call shape produced by a grammar's block parser, without its id. */
|
|
24
|
+
export type MarkupRawCall<Call extends { readonly id: string }> = Omit<Call, "id">;
|
|
25
|
+
|
|
26
|
+
/** Builds a complete call (id included) from a parsed raw call. */
|
|
27
|
+
export type MarkupCallFactory<Call extends { readonly id: string }> = (
|
|
28
|
+
call: MarkupRawCall<Call>,
|
|
29
|
+
index: number,
|
|
30
|
+
seq: number,
|
|
31
|
+
) => Call;
|
|
32
|
+
|
|
33
|
+
/** Classification of one opener candidate occurrence. */
|
|
34
|
+
export type MarkupOpenerClassification<Call extends { readonly id: string }> =
|
|
35
|
+
| {
|
|
36
|
+
/**
|
|
37
|
+
* The candidate may be the start of a real opener (e.g. a partial hex
|
|
38
|
+
* marker): hold everything from the candidate until a later chunk
|
|
39
|
+
* completes or disproves it.
|
|
40
|
+
*/
|
|
41
|
+
readonly kind: "hold";
|
|
42
|
+
}
|
|
43
|
+
| {
|
|
44
|
+
/**
|
|
45
|
+
* The candidate is definitely not an opener: emit it as literal text
|
|
46
|
+
* (one character) and rescan after it.
|
|
47
|
+
*/
|
|
48
|
+
readonly kind: "literal";
|
|
49
|
+
}
|
|
50
|
+
| {
|
|
51
|
+
readonly kind: "found";
|
|
52
|
+
/** Tag that closes the block opened at this position. */
|
|
53
|
+
readonly closer: string;
|
|
54
|
+
/**
|
|
55
|
+
* Parses one complete block (opener through closer). `undefined` means
|
|
56
|
+
* the block is not valid markup and is passed through as text verbatim.
|
|
57
|
+
*/
|
|
58
|
+
readonly parse: (block: string) => readonly MarkupRawCall<Call>[] | undefined;
|
|
59
|
+
/**
|
|
60
|
+
* Call factory for the calls recovered from this block; defaults to the
|
|
61
|
+
* parser-level `callId`. Some grammars derive ids from block-local state
|
|
62
|
+
* (e.g. OMP embeds the envelope marker).
|
|
63
|
+
*/
|
|
64
|
+
readonly callId?: MarkupCallFactory<Call>;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export interface MarkupStreamParserOptions<Call extends { readonly id: string }> {
|
|
68
|
+
/**
|
|
69
|
+
* Opener frontier. The earliest occurrence wins; trailing fragments that
|
|
70
|
+
* could start one are held back from settled text.
|
|
71
|
+
*/
|
|
72
|
+
readonly openers: readonly string[];
|
|
73
|
+
/** Classifies an opener candidate found at a scan position. */
|
|
74
|
+
readonly classifyOpener: (
|
|
75
|
+
pending: string,
|
|
76
|
+
index: number,
|
|
77
|
+
opener: string,
|
|
78
|
+
) => MarkupOpenerClassification<Call>;
|
|
79
|
+
/**
|
|
80
|
+
* Policy when a block's closer has not arrived yet. Default `"hold"`: keep
|
|
81
|
+
* the whole pending buffer from the opener until a later chunk completes or
|
|
82
|
+
* disproves the block. Returning `{ kind: "passThrough" }` fails open:
|
|
83
|
+
* everything from the opener is emitted as text immediately (the DSML
|
|
84
|
+
* block-size cap).
|
|
85
|
+
*/
|
|
86
|
+
readonly onUnclosed?: (
|
|
87
|
+
pending: string,
|
|
88
|
+
openIndex: number,
|
|
89
|
+
) => "hold" | { readonly kind: "passThrough" };
|
|
90
|
+
/**
|
|
91
|
+
* Tail policy at flush. Returns the text to emit for whatever remains in the
|
|
92
|
+
* buffer (default: everything, i.e. pass through). A grammar that discards
|
|
93
|
+
* unterminated envelopes returns `""` when the tail looks like an opener.
|
|
94
|
+
*/
|
|
95
|
+
readonly flush?: (pending: string) => string;
|
|
96
|
+
/** Call factory for recovered calls, unless a block overrides it. */
|
|
97
|
+
readonly callId?: MarkupCallFactory<Call>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export class MarkupStreamParser<Call extends { readonly id: string }> {
|
|
101
|
+
readonly #options: MarkupStreamParserOptions<Call>;
|
|
102
|
+
#pending = "";
|
|
103
|
+
#callSequence = 0;
|
|
104
|
+
|
|
105
|
+
constructor(options: MarkupStreamParserOptions<Call>) {
|
|
106
|
+
this.#options = options;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Feeds one chunk and returns only settled text and complete calls. */
|
|
110
|
+
push(chunk: string): MarkupStreamParseResult<Call> {
|
|
111
|
+
this.#pending += chunk;
|
|
112
|
+
let cursor = 0;
|
|
113
|
+
let visible = "";
|
|
114
|
+
const calls: Call[] = [];
|
|
115
|
+
for (;;) {
|
|
116
|
+
let openIndex = -1;
|
|
117
|
+
let opener = "";
|
|
118
|
+
for (const candidate of this.#options.openers) {
|
|
119
|
+
const index = this.#pending.indexOf(candidate, cursor);
|
|
120
|
+
if (index >= 0 && (openIndex < 0 || index < openIndex)) {
|
|
121
|
+
openIndex = index;
|
|
122
|
+
opener = candidate;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (openIndex < 0) {
|
|
126
|
+
const safeEnd = this.#settledEnd(cursor);
|
|
127
|
+
visible += this.#pending.slice(cursor, safeEnd);
|
|
128
|
+
cursor = safeEnd;
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
visible += this.#pending.slice(cursor, openIndex);
|
|
132
|
+
const classified = this.#options.classifyOpener(this.#pending, openIndex, opener);
|
|
133
|
+
if (classified.kind === "literal") {
|
|
134
|
+
visible += "<";
|
|
135
|
+
cursor = openIndex + 1;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (classified.kind === "hold") {
|
|
139
|
+
cursor = openIndex;
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
const closeIndex = this.#pending.indexOf(classified.closer, openIndex + opener.length);
|
|
143
|
+
if (closeIndex < 0) {
|
|
144
|
+
const decision = this.#options.onUnclosed?.(this.#pending, openIndex) ?? "hold";
|
|
145
|
+
if (decision === "hold") {
|
|
146
|
+
cursor = openIndex;
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
visible += this.#pending.slice(openIndex);
|
|
150
|
+
cursor = this.#pending.length;
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
const end = closeIndex + classified.closer.length;
|
|
154
|
+
const block = this.#pending.slice(openIndex, end);
|
|
155
|
+
const parsed = classified.parse(block);
|
|
156
|
+
if (parsed === undefined) {
|
|
157
|
+
visible += block;
|
|
158
|
+
} else {
|
|
159
|
+
const callFor = classified.callId ?? this.#options.callId;
|
|
160
|
+
if (callFor === undefined) {
|
|
161
|
+
throw new Error("MarkupStreamParser: no call factory configured");
|
|
162
|
+
}
|
|
163
|
+
parsed.forEach((call, index) => {
|
|
164
|
+
calls.push(callFor(call, index, this.#callSequence++));
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
cursor = end;
|
|
168
|
+
}
|
|
169
|
+
this.#pending = this.#pending.slice(cursor);
|
|
170
|
+
return { text: visible, calls };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Settles trailing text per the configured flush policy. */
|
|
174
|
+
flush(): MarkupStreamParseResult<Call> {
|
|
175
|
+
const text = this.#options.flush?.(this.#pending) ?? this.#pending;
|
|
176
|
+
this.#pending = "";
|
|
177
|
+
return { text, calls: [] };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Holds back a trailing fragment that could be the start of an opener. */
|
|
181
|
+
#settledEnd(start: number): number {
|
|
182
|
+
const remaining = this.#pending.slice(start);
|
|
183
|
+
const maxOpener = this.#options.openers.reduce(
|
|
184
|
+
(max, open) => Math.max(max, open.length - 1),
|
|
185
|
+
0,
|
|
186
|
+
);
|
|
187
|
+
for (let length = Math.min(maxOpener, remaining.length); length > 0; length -= 1) {
|
|
188
|
+
const suffix = remaining.slice(-length);
|
|
189
|
+
if (this.#options.openers.some((open) => open.startsWith(suffix))) {
|
|
190
|
+
return this.#pending.length - length;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return this.#pending.length;
|
|
194
|
+
}
|
|
195
|
+
}
|