cartethyia 2.7.2 → 2.7.4
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 +2 -0
- package/dist/dashboard/assets/{BannedPage-XaUmiBrC.js → BannedPage-ChG5svFb.js} +1 -1
- package/dist/dashboard/assets/BannedPage-ChG5svFb.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-BIcWQun7.js +1 -0
- package/dist/dashboard/assets/CliToolDetailPage-BIcWQun7.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-Bf2VelDe.js +1 -0
- package/dist/dashboard/assets/CliToolsPage-Bf2VelDe.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-CXGS_Ct-.js → CombosPage-DCgm5g6g.js} +1 -1
- package/dist/dashboard/assets/CombosPage-DCgm5g6g.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-a8mD0QF1.js +1 -0
- package/dist/dashboard/assets/ConsoleLogPage-a8mD0QF1.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-ggeMSjte.js +1 -0
- package/dist/dashboard/assets/CustomizationPage-ggeMSjte.js.gz +0 -0
- package/dist/dashboard/assets/{HealthEventsModal-BKtAPYVF.js → HealthEventsModal-DzSWzE_j.js} +1 -1
- package/dist/dashboard/assets/HealthEventsModal-DzSWzE_j.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-BdmRLnq2.js → LoginPage-U6AKaJcF.js} +1 -1
- package/dist/dashboard/assets/LoginPage-U6AKaJcF.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-BH8Tehdi.js → ModelPicker-BMS2t8BA.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-BMS2t8BA.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-D2D4IiHl.js +1 -0
- package/dist/dashboard/assets/OverviewPage-D2D4IiHl.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-CrNf7j_X.js +5 -0
- package/dist/dashboard/assets/ProviderDetailPage-CrNf7j_X.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-BSDbquc1.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-BSDbquc1.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-lsrx7NYg.js +9 -0
- package/dist/dashboard/assets/ProxyPage-lsrx7NYg.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-L0skzML8.js +1 -0
- package/dist/dashboard/assets/QuotaPage-L0skzML8.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-CmAbjRHW.js +1 -0
- package/dist/dashboard/assets/SettingsPage-CmAbjRHW.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-f0DE1h0-.js +1 -0
- package/dist/dashboard/assets/SetupPage-f0DE1h0-.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-Dqf4zpS-.js → SortableList-ClzpwyET.js} +1 -1
- package/dist/dashboard/assets/SortableList-ClzpwyET.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-Byj2lAEm.js → StudioPage-Cq5tW7hW.js} +3 -3
- package/dist/dashboard/assets/StudioPage-Cq5tW7hW.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-Dfc9rWQn.js +56 -0
- package/dist/dashboard/assets/UsagePage-Dfc9rWQn.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-C38iXoO4.js +1 -0
- package/dist/dashboard/assets/api-keys-C38iXoO4.js.gz +0 -0
- package/dist/dashboard/assets/app-a159k0ub.css +2 -0
- package/dist/dashboard/assets/app-a159k0ub.css.gz +0 -0
- package/dist/dashboard/assets/arrow-up-59ms1iiT.js +1 -0
- package/dist/dashboard/assets/arrow-up-59ms1iiT.js.gz +0 -0
- package/dist/dashboard/assets/assert-DbRA8EG2.js +15 -0
- package/dist/dashboard/assets/assert-DbRA8EG2.js.gz +0 -0
- package/dist/dashboard/assets/{badge-DC2L43Mj.js → badge-D9CS6dZk.js} +1 -1
- package/dist/dashboard/assets/badge-D9CS6dZk.js.gz +0 -0
- package/dist/dashboard/assets/{banner-BBVcb0J3.js → banner-DympBNq4.js} +1 -1
- package/dist/dashboard/assets/banner-DympBNq4.js.gz +0 -0
- package/dist/dashboard/assets/{card-Cca1XC6G.js → card-ibRWU_uT.js} +1 -1
- package/dist/dashboard/assets/card-ibRWU_uT.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-63qBGaRw.js +1 -0
- package/dist/dashboard/assets/cli-tools-63qBGaRw.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-DUzEbynq.js → clipboard-button-DJm-9pz7.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-DJm-9pz7.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-DFvWhwTa.js → dialog-8fiE6tec.js} +1 -1
- package/dist/dashboard/assets/dialog-8fiE6tec.js.gz +0 -0
- package/dist/dashboard/assets/drawer-CqGYeOrd.js +1 -0
- package/dist/dashboard/assets/drawer-CqGYeOrd.js.gz +0 -0
- package/dist/dashboard/assets/{entry-Bqy8RFvM.js → entry-BSAsYsmV.js} +1 -1
- package/dist/dashboard/assets/entry-BSAsYsmV.js.gz +0 -0
- package/dist/dashboard/assets/entry-CiIQULZD.js +4 -0
- package/dist/dashboard/assets/entry-CiIQULZD.js.gz +0 -0
- package/dist/dashboard/assets/entry-Daz0lJ-W.js +1 -0
- package/dist/dashboard/assets/entry-Daz0lJ-W.js.gz +0 -0
- package/dist/dashboard/assets/{concurrency-BDF0ok7y.js → fingerprint-pattern-BWuHtbW4.js} +1 -1
- package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
- package/dist/dashboard/assets/{formatters-CWWZ-uCZ.js → formatters-D-MTJxMr.js} +1 -1
- package/dist/dashboard/assets/formatters-D-MTJxMr.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-C9e8Ts7e.js → github-badge-CPK47xQx.js} +1 -1
- package/dist/dashboard/assets/github-badge-CPK47xQx.js.gz +0 -0
- package/dist/dashboard/assets/{index-DFVFrnWW.js → index-DBNPOfhV.js} +2 -2
- package/dist/dashboard/assets/index-DBNPOfhV.js.gz +0 -0
- package/dist/dashboard/assets/{inline-BFUcvllj.js → inline-BT0zntEr.js} +1 -1
- package/dist/dashboard/assets/inline-BT0zntEr.js.gz +0 -0
- package/dist/dashboard/assets/{input-C_ILAvBb.js → input-Da2ZViWg.js} +1 -1
- package/dist/dashboard/assets/input-Da2ZViWg.js.gz +0 -0
- package/dist/dashboard/assets/{layout-CBIyT6Rt.js → layout-BXNeLw6M.js} +1 -1
- package/dist/dashboard/assets/layout-BXNeLw6M.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-ebJ_-Nsb.js → list-row-BycOrrIV.js} +1 -1
- package/dist/dashboard/assets/list-row-BycOrrIV.js.gz +0 -0
- package/dist/dashboard/assets/live-DCQNZpF7.js +1 -0
- package/dist/dashboard/assets/live-DCQNZpF7.js.gz +0 -0
- package/dist/dashboard/assets/minimize-2-BTmMTK4o.js +1 -0
- package/dist/dashboard/assets/minimize-2-BTmMTK4o.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-sMD965oO.js → mutation-Rr0TIvcj.js} +1 -1
- package/dist/dashboard/assets/mutation-Rr0TIvcj.js.gz +0 -0
- package/dist/dashboard/assets/network-9Yj2ZyLo.js +1 -0
- package/dist/dashboard/assets/network-9Yj2ZyLo.js.gz +0 -0
- package/dist/dashboard/assets/{numbers-BO9dKaFr.js → numbers-CEagj6ZZ.js} +2 -2
- package/dist/dashboard/assets/numbers-CEagj6ZZ.js.gz +0 -0
- package/dist/dashboard/assets/{query-keys-CfL_Zdoh.js → query-keys-BloKtaXJ.js} +1 -1
- package/dist/dashboard/assets/query-keys-BloKtaXJ.js.gz +0 -0
- package/dist/dashboard/assets/quota-Caw7wqci.js +1 -0
- package/dist/dashboard/assets/quota-Caw7wqci.js.gz +0 -0
- package/dist/dashboard/assets/routing-CO9zCUus.js +1 -0
- package/dist/dashboard/assets/routing-CO9zCUus.js.gz +0 -0
- package/dist/dashboard/assets/{select-DBNVLdUu.js → select-BIYIBkMU.js} +2 -2
- package/dist/dashboard/assets/select-BIYIBkMU.js.gz +0 -0
- package/dist/dashboard/assets/settings-2-Ds9DQp0t.js +1 -0
- package/dist/dashboard/assets/settings-2-Ds9DQp0t.js.gz +0 -0
- package/dist/dashboard/assets/settings-jS3-PNSA.js +1 -0
- package/dist/dashboard/assets/settings-jS3-PNSA.js.gz +0 -0
- package/dist/dashboard/assets/{stack-EMrOMt4I.js → stack-DfgnFDWY.js} +1 -1
- package/dist/dashboard/assets/stack-DfgnFDWY.js.gz +0 -0
- package/dist/dashboard/assets/{state-CWoo7ty5.js → state-CkfdYWel.js} +1 -1
- package/dist/dashboard/assets/state-CkfdYWel.js.gz +0 -0
- package/dist/dashboard/assets/{switch-CCjT6IkF.js → switch-SYm237HG.js} +1 -1
- package/dist/dashboard/assets/switch-SYm237HG.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-DjkLHUcE.js → useMutation-BQqCRNIi.js} +1 -1
- package/dist/dashboard/assets/useMutation-BQqCRNIi.js.gz +0 -0
- package/dist/dashboard/index.html +3 -2
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +118 -115
- package/migrations/full-postgres/0026_add_qoder_native_identity.sql +20 -0
- package/migrations/full-postgres/0027_qoder_identity_genuine_seed.sql +21 -0
- package/migrations/full-postgres/0028_add_family_template_key_telemetry.sql +40 -0
- package/migrations/full-postgres/0029_null_share_child_policy_columns.sql +38 -0
- package/migrations/full-postgres/0030_add_api_key_max_children.sql +10 -0
- package/migrations/full-postgres/0031_tenant_system_prompts.sql +16 -0
- package/migrations/full-postgres/0032_add_provider_pool_accounts.sql +53 -0
- package/migrations/full-postgres/0033_add_api_key_ban.sql +9 -0
- package/migrations/full-postgres/0034_add_api_key_ban_until.sql +7 -0
- package/migrations/full-postgres/0035_align_pool_account_status_enum.sql +21 -0
- package/migrations/lite-sqlite3/0026_add_qoder_native_identity.sql +15 -0
- package/migrations/lite-sqlite3/0027_qoder_identity_genuine_seed.sql +21 -0
- package/migrations/lite-sqlite3/0028_add_family_template_key_telemetry.sql +40 -0
- package/migrations/lite-sqlite3/0029_null_share_child_policy_columns.sql +38 -0
- package/migrations/lite-sqlite3/0030_add_api_key_max_children.sql +10 -0
- package/migrations/lite-sqlite3/0031_tenant_system_prompts.sql +10 -0
- package/migrations/lite-sqlite3/0032_add_provider_pool_accounts.sql +44 -0
- package/migrations/lite-sqlite3/0033_add_api_key_ban.sql +8 -0
- package/migrations/lite-sqlite3/0034_add_api_key_ban_until.sql +5 -0
- package/migrations/lite-sqlite3/0035_align_pool_account_status_enum.sql +6 -0
- package/package.json +2 -1
- package/src/boot/app.ts +9 -8
- package/src/config/network.ts +7 -3
- package/src/console/api-keys/bucket-spend.ts +7 -7
- package/src/console/api-keys/contracts.ts +475 -410
- package/src/console/api-keys/routes.ts +211 -23
- package/src/console/audit/routes.ts +3 -3
- package/src/console/auth/account-reset.ts +4 -4
- package/src/console/auth/session.ts +3 -3
- package/src/console/backup/contracts.ts +2 -0
- package/src/console/backup/export-endpoint.ts +14 -17
- package/src/console/backup/routes.ts +43 -11
- package/src/console/backup/service.ts +11 -6
- package/src/console/backup/store.ts +384 -88
- package/src/console/cli-tools/routes.ts +3 -3
- package/src/console/console-router-guards.ts +2 -2
- package/src/console/console-router.ts +4 -4
- package/src/console/dashboard-assets.ts +16 -1
- package/src/console/data-management/routes.ts +3 -3
- package/src/console/domain-registration.ts +4 -2
- package/src/console/observability/contracts.ts +4 -0
- package/src/console/observability/live.ts +3 -3
- package/src/console/observability/logs.ts +3 -3
- package/src/console/observability/performance.ts +3 -3
- package/src/console/observability/routes.ts +3 -3
- package/src/console/observability/store.ts +16 -16
- package/src/console/providers/catalog/contracts.ts +43 -22
- package/src/console/providers/catalog/provider-operations.ts +17 -2
- package/src/console/providers/catalog/routes.ts +3 -3
- package/src/console/providers/catalog/store-helpers.ts +349 -326
- package/src/console/providers/catalog/store.ts +1523 -1292
- package/src/console/providers/detail/routes.ts +3 -3
- package/src/console/providers/oauth/routes.ts +3 -3
- package/src/console/quota/account-quota-tenant.ts +34 -2
- package/src/console/quota/account-quota.ts +4 -1
- package/src/console/quota/account-test-egress.ts +4 -4
- package/src/console/quota/native-identity.ts +189 -0
- package/src/console/remote-support/contracts.ts +12 -6
- package/src/console/remote-support/service.ts +2 -2
- package/src/console/routing/model/routes.ts +3 -3
- package/src/console/routing/pools/contracts.ts +11 -8
- package/src/console/routing/pools/operations.ts +14 -7
- package/src/console/routing/pools/probe-verdict.ts +8 -3
- package/src/console/routing/pools/relay-deploy.ts +9 -9
- package/src/console/routing/pools/routes.ts +3 -3
- package/src/console/routing/pools/store.ts +21 -1
- package/src/console/settings/routes.ts +3 -3
- package/src/console/share/share-router.ts +51 -18
- package/src/console/share/share-stats.ts +158 -49
- package/src/console/share/share-usage.ts +24 -7
- package/src/console/shared/read-through-cache.ts +38 -2
- package/src/console/system-prompt/contracts.ts +102 -0
- package/src/console/system-prompt/routes.ts +165 -0
- package/src/console/updates/routes.ts +7 -4
- package/src/main.ts +11 -0
- package/src/network/outbound-fetch.ts +75 -4
- package/src/network/pool/resolver.ts +39 -39
- package/src/network/pool-cooldown-store.ts +71 -16
- package/src/network/pool-health-machine.ts +21 -1
- package/src/network/rate-limit-verdict.ts +68 -0
- package/src/network/ssrf.ts +4 -4
- package/src/observability/event-query.ts +437 -22
- package/src/observability/telemetry-status.ts +1 -1
- package/src/persistence/db-pg.ts +42 -0
- package/src/persistence/db-sqlite.ts +7 -2
- package/src/persistence/readiness.ts +18 -6
- package/src/persistence/schema.ts +162 -1
- package/src/persistence/stores/api-key-store.ts +849 -581
- package/src/persistence/stores/share-store.ts +65 -13
- package/src/persistence/stores/telemetry-store.ts +58 -2
- package/src/persistence/system-prompt-store.ts +104 -0
- package/src/protocol/codecs.ts +6 -6
- package/src/protocol/gateway-error.ts +6 -3
- package/src/protocol/image-generation.ts +2 -2
- package/src/protocol/reasoning.ts +65 -1
- package/src/protocol/request/chat.ts +1 -0
- package/src/protocol/response/chat.ts +2 -2
- package/src/protocol/response/codex.ts +1 -1
- package/src/protocol/response/gemini.ts +1 -1
- package/src/protocol/response/messages.ts +5 -5
- package/src/protocol/response/responses.ts +2 -2
- package/src/protocol/sse-decoder.ts +14 -3
- package/src/protocol/translation/capabilities.ts +2 -2
- package/src/protocol/upstream/send-claude-messages.ts +3 -3
- package/src/providers/adapters/base-adapter.ts +1 -1
- package/src/providers/adapters/compatible-adapter.ts +2 -2
- package/src/providers/authentication/oauth-refresh-service.ts +2 -2
- package/src/providers/configured-provider.ts +1 -1
- package/src/providers/discovery/probe-phases.ts +2 -2
- package/src/providers/discovery/probing-service.ts +12 -12
- package/src/providers/integrations/agentrouter/agentrouter.ts +2 -3
- package/src/providers/integrations/alibabacp/alibabacp.ts +2 -2
- package/src/providers/integrations/anthropic.ts +4 -11
- package/src/providers/integrations/antigravity/antigravity.ts +33 -47
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +1 -1
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +30 -2
- package/src/providers/integrations/buddy/buddy-discovery-shared.ts +37 -6
- package/src/providers/integrations/buddy/buddy-identity.ts +22 -1
- package/src/providers/integrations/buddy/buddy-media.ts +21 -17
- package/src/providers/integrations/buddy/codebuddy/provider.ts +6 -2
- package/src/providers/integrations/buddy/workbuddy/provider.ts +6 -2
- package/src/providers/integrations/claude/claude-betas.ts +1 -1
- package/src/providers/integrations/claude/claude-credentials.ts +1 -1
- package/src/providers/integrations/claude/claude-slow-lane-retry.ts +1 -1
- package/src/providers/integrations/claude/claude.ts +3 -10
- package/src/providers/integrations/cline/cline.ts +8 -1
- package/src/providers/integrations/cloudflareai/cloudflareai.ts +4 -7
- package/src/providers/integrations/codex/codex-errors.ts +1 -1
- package/src/providers/integrations/codex/codex.ts +14 -15
- package/src/providers/integrations/commandcode/commandcode.ts +4 -4
- package/src/providers/integrations/commandcode/quota.ts +5 -1
- package/src/providers/integrations/cursor/cursor-http2.ts +11 -11
- package/src/providers/integrations/cursor/cursor.ts +15 -15
- package/src/providers/integrations/factory/factory.ts +4 -4
- package/src/providers/integrations/gemini.ts +11 -11
- package/src/providers/integrations/github/github.ts +26 -7
- package/src/providers/integrations/grok/grok.ts +2 -6
- package/src/providers/integrations/hermes.ts +5 -1
- package/src/providers/integrations/inferhub.ts +4 -11
- package/src/providers/integrations/kilo/kilo.ts +4 -0
- package/src/providers/integrations/kimi/kimi.ts +4 -11
- package/src/providers/integrations/kiro/kiro.ts +10 -10
- package/src/providers/integrations/muse/muse.ts +6 -9
- package/src/providers/integrations/openrouter/provider.ts +4 -0
- package/src/providers/integrations/perplexity.ts +2 -2
- package/src/providers/integrations/qoder/qoder-catalog.ts +47 -9
- package/src/providers/integrations/qoder/qoder-checkin.ts +242 -51
- package/src/providers/integrations/qoder/qoder-native-identity-store.ts +157 -0
- package/src/providers/integrations/qoder/qoder-oauth.ts +12 -3
- package/src/providers/integrations/qoder/qoder-realm.ts +15 -0
- package/src/providers/integrations/qoder/qoder-umid.ts +287 -0
- package/src/providers/integrations/qoder/qoder-virtual.ts +2 -2
- package/src/providers/integrations/qoder/qoder.ts +192 -24
- package/src/providers/integrations/tokenharbor.ts +2 -2
- package/src/providers/integrations/trae/trae.ts +6 -9
- package/src/providers/integrations/zcode/oauth.ts +1 -1
- package/src/providers/operations/account-error-classification.ts +6 -8
- package/src/providers/operations/account-health-service.ts +14 -16
- package/src/providers/operations/client-versions.ts +37 -22
- package/src/providers/operations/provider-catalog-service.ts +4 -4
- package/src/providers/operations/provider-credential-service.ts +16 -4
- package/src/providers/operations/upstream-deadline.ts +3 -3
- package/src/providers/provider-contracts.ts +55 -3
- package/src/providers/provider-credential-resolver.ts +1 -1
- package/src/providers/provider-registry.ts +4 -4
- package/src/providers/quota/quota-contracts.ts +24 -10
- package/src/providers/quota/refresh.ts +2 -2
- package/src/providers/search/search-provider.ts +5 -5
- package/src/security/abuse.ts +4 -4
- package/src/security/access-control.ts +19 -0
- package/src/security/admission/reasons.ts +17 -17
- package/src/security/api-key-auth.ts +18 -5
- package/src/shared/coerce.ts +19 -0
- package/src/transport/dispatch/attempt-finalize.ts +4 -3
- package/src/transport/dispatch/attempt-loop.ts +10 -7
- package/src/transport/dispatch/empty-stream.ts +6 -6
- package/src/transport/dispatch/generation.ts +9 -9
- package/src/transport/dispatch/leases.ts +8 -8
- package/src/transport/dispatch/proxy-request.ts +38 -9
- package/src/transport/dispatch/responses-compact.ts +11 -11
- package/src/transport/dispatch/retry-policy.ts +4 -67
- package/src/transport/dispatch/stream-bridge.ts +1 -1
- package/src/transport/dispatch/streaming/finalize.ts +9 -8
- package/src/transport/dispatch/streaming/stream.ts +16 -1
- package/src/transport/dispatch/systemone.ts +8 -8
- package/src/transport/dispatch/token-count.ts +9 -9
- package/src/transport/dispatch/upstream.ts +10 -0
- package/src/transport/dispatch/websearch.ts +6 -6
- package/src/transport/drain-endpoint.ts +2 -2
- package/src/transport/failure-policy.ts +13 -6
- package/src/transport/guards/auth-guard.ts +6 -5
- package/src/transport/guards/body-policy.ts +4 -4
- package/src/transport/guards/error-lifecycle.ts +18 -3
- package/src/transport/guards/guard-errors.ts +7 -3
- package/src/transport/guards/headroom-guard.ts +1 -1
- package/src/transport/guards/pipeline.ts +6 -6
- package/src/transport/guards/readiness-guard.ts +16 -2
- package/src/transport/guards/request-context.ts +3 -3
- package/src/transport/request/preparer.ts +37 -21
- package/src/transport/request/state.ts +4 -7
- package/src/transport/request/system-prompt-injection.ts +89 -0
- package/src/transport/request/tenant-preferences.ts +43 -0
- package/src/transport/routing/admission.ts +13 -3
- package/src/transport/routing/route-catalog.ts +104 -2
- package/src/transport/routing/route-model.ts +60 -7
- package/src/transport/routing/router.ts +306 -256
- package/src/transport/shutdown-notice.ts +19 -7
- package/src/transport/surface/completion.ts +1 -1
- package/src/transport/surface/messages/encode.ts +1 -1
- package/src/workers/daily-checkin.ts +2 -0
- package/dist/dashboard/assets/BannedPage-XaUmiBrC.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-CqD7QiLM.js +0 -1
- package/dist/dashboard/assets/CliToolDetailPage-CqD7QiLM.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-0XUo4UMn.js +0 -1
- package/dist/dashboard/assets/CliToolsPage-0XUo4UMn.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-CXGS_Ct-.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-DGnGNYnM.js +0 -1
- package/dist/dashboard/assets/ConsoleLogPage-DGnGNYnM.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-DAG5J9EY.js +0 -1
- package/dist/dashboard/assets/CustomizationPage-DAG5J9EY.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-BKtAPYVF.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-BdmRLnq2.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-BH8Tehdi.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-CqMxibJB.js +0 -1
- package/dist/dashboard/assets/OverviewPage-CqMxibJB.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-CqMh2NRX.js +0 -4
- package/dist/dashboard/assets/ProviderDetailPage-CqMh2NRX.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-B9uWu4Nz.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-B9uWu4Nz.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-BoWOhpDO.js +0 -9
- package/dist/dashboard/assets/ProxyPage-BoWOhpDO.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-7P2n--Fv.js +0 -1
- package/dist/dashboard/assets/QuotaPage-7P2n--Fv.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-CxZ1fTkN.js +0 -1
- package/dist/dashboard/assets/SettingsPage-CxZ1fTkN.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-Co12Zzaw.js +0 -1
- package/dist/dashboard/assets/SetupPage-Co12Zzaw.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-Dqf4zpS-.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-Byj2lAEm.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-CTkLAuRc.js +0 -56
- package/dist/dashboard/assets/UsagePage-CTkLAuRc.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-CUi6UXsR.js +0 -1
- package/dist/dashboard/assets/api-keys-CUi6UXsR.js.gz +0 -0
- package/dist/dashboard/assets/app-KvsUJUMF.css +0 -2
- package/dist/dashboard/assets/app-KvsUJUMF.css.gz +0 -0
- package/dist/dashboard/assets/assert-67xXH5rf.js +0 -1
- package/dist/dashboard/assets/assert-67xXH5rf.js.gz +0 -0
- package/dist/dashboard/assets/badge-DC2L43Mj.js.gz +0 -0
- package/dist/dashboard/assets/banner-BBVcb0J3.js.gz +0 -0
- package/dist/dashboard/assets/card-Cca1XC6G.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-KvsWa1Jl.js +0 -1
- package/dist/dashboard/assets/cli-tools-KvsWa1Jl.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-DUzEbynq.js.gz +0 -0
- package/dist/dashboard/assets/concurrency-BDF0ok7y.js.gz +0 -0
- package/dist/dashboard/assets/dialog-DFvWhwTa.js.gz +0 -0
- package/dist/dashboard/assets/drawer-CFM2sMX1.js +0 -1
- package/dist/dashboard/assets/drawer-CFM2sMX1.js.gz +0 -0
- package/dist/dashboard/assets/entry-Bqy8RFvM.js.gz +0 -0
- package/dist/dashboard/assets/entry-ClCMCWWQ.js +0 -4
- package/dist/dashboard/assets/entry-ClCMCWWQ.js.gz +0 -0
- package/dist/dashboard/assets/entry-_xHo5x6r.js +0 -1
- package/dist/dashboard/assets/entry-_xHo5x6r.js.gz +0 -0
- package/dist/dashboard/assets/formatters-CWWZ-uCZ.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-C9e8Ts7e.js.gz +0 -0
- package/dist/dashboard/assets/index-DFVFrnWW.js.gz +0 -0
- package/dist/dashboard/assets/inline-BFUcvllj.js.gz +0 -0
- package/dist/dashboard/assets/input-C_ILAvBb.js.gz +0 -0
- package/dist/dashboard/assets/layout-CBIyT6Rt.js.gz +0 -0
- package/dist/dashboard/assets/list-row-ebJ_-Nsb.js.gz +0 -0
- package/dist/dashboard/assets/live-D0mm--ME.js +0 -1
- package/dist/dashboard/assets/live-D0mm--ME.js.gz +0 -0
- package/dist/dashboard/assets/minimize-2-D-MN8W8a.js +0 -1
- package/dist/dashboard/assets/minimize-2-D-MN8W8a.js.gz +0 -0
- package/dist/dashboard/assets/mutation-sMD965oO.js.gz +0 -0
- package/dist/dashboard/assets/numbers-BO9dKaFr.js.gz +0 -0
- package/dist/dashboard/assets/query-keys-CfL_Zdoh.js.gz +0 -0
- package/dist/dashboard/assets/quota-DZGyp0N5.js +0 -1
- package/dist/dashboard/assets/quota-DZGyp0N5.js.gz +0 -0
- package/dist/dashboard/assets/routing-BnIpi1IJ.js +0 -1
- package/dist/dashboard/assets/routing-BnIpi1IJ.js.gz +0 -0
- package/dist/dashboard/assets/select-DBNVLdUu.js.gz +0 -0
- package/dist/dashboard/assets/settings-BAFhDgX8.js +0 -1
- package/dist/dashboard/assets/settings-BAFhDgX8.js.gz +0 -0
- package/dist/dashboard/assets/stack-EMrOMt4I.js.gz +0 -0
- package/dist/dashboard/assets/state-CWoo7ty5.js.gz +0 -0
- package/dist/dashboard/assets/switch-CCjT6IkF.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-DjkLHUcE.js.gz +0 -0
- /package/dist/dashboard/assets/{app-BaWd0puM.js → app-BayTTy01.js} +0 -0
- /package/dist/dashboard/assets/{app-BaWd0puM.js.gz → app-BayTTy01.js.gz} +0 -0
|
@@ -151,7 +151,7 @@ async function deployCloudflare(
|
|
|
151
151
|
request: RelayDeployRequest,
|
|
152
152
|
): Promise<RelayDeployResult> {
|
|
153
153
|
const accountId = request.accountId?.trim();
|
|
154
|
-
if (!accountId) throw new GatewayError("invalid_request", 400, "Cloudflare account id is required");
|
|
154
|
+
if (!accountId) throw new GatewayError("invalid_request", 400, "Cloudflare account id is required (request {requestId}). Check the shape of what you sent — then resend 🪞");
|
|
155
155
|
const projectName = request.projectName?.trim() || generateName("relay");
|
|
156
156
|
const scriptUrl = `https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(accountId)}/workers/scripts/${encodeURIComponent(projectName)}`;
|
|
157
157
|
const form = new FormData();
|
|
@@ -174,7 +174,7 @@ async function deployCloudflare(
|
|
|
174
174
|
body: form,
|
|
175
175
|
});
|
|
176
176
|
if (!upload.ok)
|
|
177
|
-
throw new GatewayError("platform_unavailable", upload.status, `Cloudflare deploy failed: ${await readError(upload)}
|
|
177
|
+
throw new GatewayError("platform_unavailable", upload.status, `Cloudflare deploy failed (request {requestId}): ${await readError(upload)}. Flip the switch back on when ready 🔌`);
|
|
178
178
|
// Enable the workers.dev route; a failure here is non-fatal because the
|
|
179
179
|
// subdomain lookup below decides the final URL and reports its own error.
|
|
180
180
|
await fetchImpl(`${scriptUrl}/subdomain`, {
|
|
@@ -190,7 +190,7 @@ async function deployCloudflare(
|
|
|
190
190
|
throw new GatewayError(
|
|
191
191
|
"platform_unavailable",
|
|
192
192
|
subdomainRes.status,
|
|
193
|
-
`Worker uploaded but its workers.dev subdomain could not be read: ${await readError(subdomainRes)}
|
|
193
|
+
`Worker uploaded but its workers.dev subdomain could not be read (request {requestId}): ${await readError(subdomainRes)}. Flip the switch back on when ready 🔌`,
|
|
194
194
|
);
|
|
195
195
|
const subdomainData = (await subdomainRes.json().catch(() => ({}))) as {
|
|
196
196
|
result?: { subdomain?: string };
|
|
@@ -200,7 +200,7 @@ async function deployCloudflare(
|
|
|
200
200
|
throw new GatewayError(
|
|
201
201
|
"platform_unavailable",
|
|
202
202
|
502,
|
|
203
|
-
"Worker uploaded but no workers.dev subdomain is configured for this Cloudflare account.",
|
|
203
|
+
"Worker uploaded but no workers.dev subdomain is configured for this Cloudflare account (request {requestId}). Flip the switch back on when ready 🔌",
|
|
204
204
|
);
|
|
205
205
|
return { target: "cloudflare", projectName, relayUrl: `https://${projectName}.${subdomain}.workers.dev` };
|
|
206
206
|
}
|
|
@@ -251,7 +251,7 @@ async function deployVercel(
|
|
|
251
251
|
}),
|
|
252
252
|
});
|
|
253
253
|
if (!deployRes.ok)
|
|
254
|
-
throw new GatewayError("platform_unavailable", deployRes.status, `Vercel deploy failed: ${await readError(deployRes)}
|
|
254
|
+
throw new GatewayError("platform_unavailable", deployRes.status, `Vercel deploy failed (request {requestId}): ${await readError(deployRes)}. Flip the switch back on when ready 🔌`);
|
|
255
255
|
// Pin the region again after the deploy created the project: the PATCH above
|
|
256
256
|
// is a no-op on first deploy (the project does not exist yet), and a redeploy
|
|
257
257
|
// is what Vercel requires for a region change to take effect anyway. The SSO
|
|
@@ -280,7 +280,7 @@ async function deployVercel(
|
|
|
280
280
|
}).catch(() => undefined);
|
|
281
281
|
const deployment = (await deployRes.json().catch(() => ({}))) as { url?: string };
|
|
282
282
|
if (!deployment.url)
|
|
283
|
-
throw new GatewayError("platform_unavailable", 502, "Vercel deploy returned no URL");
|
|
283
|
+
throw new GatewayError("platform_unavailable", 502, "Vercel deploy returned no URL (request {requestId}). Flip the switch back on when ready 🔌");
|
|
284
284
|
return {
|
|
285
285
|
target: "vercel",
|
|
286
286
|
projectName,
|
|
@@ -300,11 +300,11 @@ async function deployDeno(
|
|
|
300
300
|
body: JSON.stringify({ project: projectName, entryPoint: "main.ts", files: { "main.ts": RELAY_DENO_SOURCE } }),
|
|
301
301
|
});
|
|
302
302
|
if (!deployRes.ok)
|
|
303
|
-
throw new GatewayError("platform_unavailable", deployRes.status, `Deno deploy failed: ${await readError(deployRes)}
|
|
303
|
+
throw new GatewayError("platform_unavailable", deployRes.status, `Deno deploy failed (request {requestId}): ${await readError(deployRes)}. Flip the switch back on when ready 🔌`);
|
|
304
304
|
const deployment = (await deployRes.json().catch(() => ({}))) as { url?: string; domains?: string[] };
|
|
305
305
|
const url = deployment.url ?? deployment.domains?.[0];
|
|
306
306
|
if (!url)
|
|
307
|
-
throw new GatewayError("platform_unavailable", 502, "Deno deploy returned no URL");
|
|
307
|
+
throw new GatewayError("platform_unavailable", 502, "Deno deploy returned no URL (request {requestId}). Flip the switch back on when ready 🔌");
|
|
308
308
|
return {
|
|
309
309
|
target: "deno",
|
|
310
310
|
projectName,
|
|
@@ -333,6 +333,6 @@ export async function deployRelay(
|
|
|
333
333
|
request: RelayDeployRequest,
|
|
334
334
|
): Promise<RelayDeployResult> {
|
|
335
335
|
if (!request.token.trim())
|
|
336
|
-
throw new GatewayError("invalid_request", 400, `${request.target} API token is required
|
|
336
|
+
throw new GatewayError("invalid_request", 400, `${request.target} API token is required (request {requestId}). Wrong key, right door — check the credential 🔑`);
|
|
337
337
|
return DEPLOYERS[request.target](fetchImpl, request);
|
|
338
338
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Network-pool control-plane HTTP routes. The only module that touches Elysia.
|
|
2
|
-
import { Elysia, t } from "elysia";
|
|
2
|
+
import { Elysia, t, type AnyElysia } from "elysia";
|
|
3
3
|
import { consoleErrorHandler } from "../../shared/errors";
|
|
4
4
|
import { literalUnion } from "../../shared/elysia-schema";
|
|
5
5
|
import { RELAY_TARGETS, type RelayDeployRequest } from "./relay-deploy";
|
|
@@ -56,7 +56,7 @@ const importPoolsBody = t.Object({
|
|
|
56
56
|
family: t.Optional(t.Union([t.String(), t.Null()])),
|
|
57
57
|
labelPrefix: t.Optional(t.String()),
|
|
58
58
|
});
|
|
59
|
-
export function createNetworkPoolRoutes(config: NetworkPoolConfig):
|
|
59
|
+
export function createNetworkPoolRoutes(config: NetworkPoolConfig): AnyElysia {
|
|
60
60
|
const factory = createNetworkPoolOperations(config);
|
|
61
61
|
return new Elysia({ prefix: "/network/pools" })
|
|
62
62
|
.error(consoleErrorHandler("Network pool operation failed"))
|
|
@@ -207,5 +207,5 @@ export function createNetworkPoolRoutes(config: NetworkPoolConfig): Elysia {
|
|
|
207
207
|
body as Partial<PoolStrategySetting>,
|
|
208
208
|
);
|
|
209
209
|
},
|
|
210
|
-
)
|
|
210
|
+
);
|
|
211
211
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// Drizzle-backed console persistence for network pools.
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
2
3
|
import { and, asc, eq, inArray, sql } from "drizzle-orm";
|
|
3
4
|
import type { CartethyiaDatabase } from "../../../persistence/db-handle";
|
|
4
|
-
import { networkPools, poolRoutingSettings } from "../../../persistence/schema";
|
|
5
|
+
import { networkPools, poolRoutingSettings, providerPoolAccounts } from "../../../persistence/schema";
|
|
6
|
+
import { providerRateLimitIsIpScoped } from "../../../providers/provider-metadata";
|
|
5
7
|
import {
|
|
6
8
|
DEFAULT_POOL_STRATEGY,
|
|
7
9
|
type CreateNetworkPoolRequest,
|
|
@@ -444,6 +446,24 @@ export class DrizzleNetworkPoolStore implements NetworkPoolStore {
|
|
|
444
446
|
...(record.family !== undefined ? { family: record.family } : {}),
|
|
445
447
|
}));
|
|
446
448
|
await this.db.insert(networkPools).values(values);
|
|
449
|
+
|
|
450
|
+
// Materialize pool-as-account rows for ip-scoped, credential-less
|
|
451
|
+
// providers (OpenCode Free today): the operator sees the egress in the
|
|
452
|
+
// provider's Accounts card, and routing excludes a spent egress like a
|
|
453
|
+
// cooling account. Every other provider gets no row.
|
|
454
|
+
if (providerRateLimitIsIpScoped("opencode")) {
|
|
455
|
+
const now = new Date();
|
|
456
|
+
const poolAccounts = values.map((value) => ({
|
|
457
|
+
id: randomUUID(),
|
|
458
|
+
tenantId: value.tenantId,
|
|
459
|
+
networkPoolId: value.id,
|
|
460
|
+
providerId: "opencode",
|
|
461
|
+
label: `Pool ${value.id.slice(0, 8)}`,
|
|
462
|
+
createdAt: now,
|
|
463
|
+
updatedAt: now,
|
|
464
|
+
}));
|
|
465
|
+
await this.db.insert(providerPoolAccounts).values(poolAccounts);
|
|
466
|
+
}
|
|
447
467
|
return values.length;
|
|
448
468
|
}
|
|
449
469
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Elysia } from "elysia";
|
|
1
|
+
import { Elysia, type AnyElysia } from "elysia";
|
|
2
2
|
import {
|
|
3
3
|
createRuntimeSettingsOperations,
|
|
4
4
|
runtimeUpdateBody,
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
type UpdateRuntimeSettingsRequest,
|
|
8
8
|
} from "./contracts";
|
|
9
9
|
|
|
10
|
-
export function createRuntimeSettingsRoutes(config: RuntimeSettingsConfig):
|
|
10
|
+
export function createRuntimeSettingsRoutes(config: RuntimeSettingsConfig): AnyElysia {
|
|
11
11
|
const factory = createRuntimeSettingsOperations(config);
|
|
12
12
|
return new Elysia({ prefix: "/settings/runtime" })
|
|
13
13
|
.get("/", async ({ request, set }) => {
|
|
@@ -26,5 +26,5 @@ export function createRuntimeSettingsRoutes(config: RuntimeSettingsConfig): Elys
|
|
|
26
26
|
} catch (e) {
|
|
27
27
|
return runtimeSettingsErrorResponse(e, set);
|
|
28
28
|
}
|
|
29
|
-
})
|
|
29
|
+
});
|
|
30
30
|
}
|
|
@@ -4,8 +4,8 @@ import { globalOrOwnedBy } from "../../persistence/tenant-scope";
|
|
|
4
4
|
// Share URLs reveal policy and allow one child key to be created per canonical
|
|
5
5
|
// client IP. The child bearer is returned only by the successful issue call.
|
|
6
6
|
|
|
7
|
-
import { Elysia } from "elysia";
|
|
8
|
-
import { and, eq } from "drizzle-orm";
|
|
7
|
+
import { Elysia, type AnyElysia } from "elysia";
|
|
8
|
+
import { and, eq, sql } from "drizzle-orm";
|
|
9
9
|
import type { CartethyiaDatabase } from "../../persistence/db-handle";
|
|
10
10
|
import { apiKeys, models, providers } from "../../persistence/schema";
|
|
11
11
|
import { PublicModelCatalogStore } from "../providers/catalog/public-model-store";
|
|
@@ -34,12 +34,12 @@ const MIN_TOKEN_LENGTH = 20;
|
|
|
34
34
|
/**
|
|
35
35
|
* Cadence of the share stats SSE snapshot.
|
|
36
36
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
37
|
+
* Roughly one tenth of the cache TTL (`SHARE_STATS_CACHE_TTL_SECONDS`, 300
|
|
38
|
+
* s), so nearly every tick is served from the cache and the cold-compute
|
|
39
|
+
* path runs at most once per five minutes per family. Short enough to still
|
|
40
|
+
* read as live.
|
|
41
41
|
*/
|
|
42
|
-
const SHARE_STATS_STREAM_INTERVAL_MS =
|
|
42
|
+
const SHARE_STATS_STREAM_INTERVAL_MS = 30_000;
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
45
|
* Context and capabilities for one allowed model, sourced from the public
|
|
@@ -89,12 +89,12 @@ function json(body: unknown, status = 200): Response {
|
|
|
89
89
|
function notFound(refusal: ShareLinkRefusal = "not_found"): Response {
|
|
90
90
|
const message =
|
|
91
91
|
refusal === "expired"
|
|
92
|
-
? "This share link has expired"
|
|
92
|
+
? "This share link has expired (request {requestId}). Time for a fresh one 🌷"
|
|
93
93
|
: refusal === "revoked"
|
|
94
|
-
? "This share link's key has been revoked"
|
|
94
|
+
? "This share link's key has been revoked (request {requestId}). Ask for a new one 📩"
|
|
95
95
|
: refusal === "disabled"
|
|
96
|
-
? "This share link's key is paused"
|
|
97
|
-
: "Share link is unavailable";
|
|
96
|
+
? "This share link's key is paused (request {requestId}). Re-enable it when ready 🌱"
|
|
97
|
+
: "Share link is unavailable (request {requestId}). This link leads nowhere — check it again 🔗";
|
|
98
98
|
const error = new GatewayError(`link_${refusal}`, 404, message);
|
|
99
99
|
return json(publicGatewayErrorBody(error), error.status);
|
|
100
100
|
}
|
|
@@ -227,12 +227,20 @@ async function resolveFamilyStats(
|
|
|
227
227
|
if (resolved === null) return null;
|
|
228
228
|
const templateId = resolved.key.id;
|
|
229
229
|
const tenantId = resolved.key.tenantId;
|
|
230
|
-
|
|
231
|
-
|
|
230
|
+
// Recipient counts come back as one aggregate row: a family minted by the
|
|
231
|
+
// tenant can carry tens of thousands of children, and materializing their
|
|
232
|
+
// ids only to count them made every share-page view pay for the family's
|
|
233
|
+
// size. The fingerprint/quote keys the rollup needs are addressed by the
|
|
234
|
+
// template id (`familyApiKeyId`), which scopes in SQL without any id list.
|
|
235
|
+
const [counts] = await db
|
|
236
|
+
.select({
|
|
237
|
+
total: sql<number>`count(*)`,
|
|
238
|
+
active: sql<number>`sum(case when ${apiKeys.revokedAt} is null then 1 else 0 end)`,
|
|
239
|
+
})
|
|
232
240
|
.from(apiKeys)
|
|
233
241
|
.where(eq(apiKeys.parentKeyId, templateId));
|
|
234
|
-
const
|
|
235
|
-
const
|
|
242
|
+
const total = Number(counts?.total ?? 0);
|
|
243
|
+
const active = Number(counts?.active ?? 0);
|
|
236
244
|
// Rank only the models this link grants. A refused request still writes a
|
|
237
245
|
// telemetry row carrying the name the client asked for, so an unfiltered
|
|
238
246
|
// top-models table would list the very names the grant excludes. An
|
|
@@ -244,11 +252,16 @@ async function resolveFamilyStats(
|
|
|
244
252
|
configured !== null && configured.length > 0
|
|
245
253
|
? await modelsForShare(db, resolved.key)
|
|
246
254
|
: undefined;
|
|
247
|
-
return stats.getFamilyStats(
|
|
255
|
+
return stats.getFamilyStats(
|
|
256
|
+
tenantId,
|
|
257
|
+
{ templateKeyId: templateId, memberCount: total + 1 },
|
|
258
|
+
{ total, active },
|
|
259
|
+
allowedModels,
|
|
260
|
+
);
|
|
248
261
|
}
|
|
249
262
|
|
|
250
263
|
/** Creates the public enrollment page and one-time shared-key issuance route. */
|
|
251
|
-
export function createShareRouter(options: ShareRouterOptions):
|
|
264
|
+
export function createShareRouter(options: ShareRouterOptions): AnyElysia {
|
|
252
265
|
const { db, shareStore } = options;
|
|
253
266
|
|
|
254
267
|
return new Elysia()
|
|
@@ -379,6 +392,26 @@ export function createShareRouter(options: ShareRouterOptions): Elysia {
|
|
|
379
392
|
nameHint,
|
|
380
393
|
});
|
|
381
394
|
if (issued.kind === "link_unavailable") return notFound();
|
|
395
|
+
if (issued.kind === "slot_full") {
|
|
396
|
+
return json({
|
|
397
|
+
error: {
|
|
398
|
+
code: "template_slot_full",
|
|
399
|
+
message: "This template has reached its maximum number of recipients.",
|
|
400
|
+
},
|
|
401
|
+
}, 429);
|
|
402
|
+
}
|
|
403
|
+
if (issued.kind === "already_issued") {
|
|
404
|
+
// The recipient's IP already enrolled from this link. The original
|
|
405
|
+
// bearer secret is not stored server-side, so it cannot be returned;
|
|
406
|
+
// refuse the duplicate mint instead. The share page surfaces this as
|
|
407
|
+
// a clear message rather than minting an unbounded key set.
|
|
408
|
+
return json({
|
|
409
|
+
error: {
|
|
410
|
+
code: "already_issued",
|
|
411
|
+
message: "This address already has a key from this share link.",
|
|
412
|
+
},
|
|
413
|
+
}, 409);
|
|
414
|
+
}
|
|
382
415
|
set.status = 201;
|
|
383
416
|
return json({
|
|
384
417
|
key: generated.secret,
|
|
@@ -453,5 +486,5 @@ export function createShareRouter(options: ShareRouterOptions): Elysia {
|
|
|
453
486
|
};
|
|
454
487
|
}),
|
|
455
488
|
);
|
|
456
|
-
})
|
|
489
|
+
})
|
|
457
490
|
}
|
|
@@ -11,10 +11,19 @@
|
|
|
11
11
|
// masked addresses. No request or response bodies cross this boundary.
|
|
12
12
|
|
|
13
13
|
import { createHash } from "node:crypto";
|
|
14
|
+
import { eq } from "drizzle-orm";
|
|
14
15
|
import { maskClientIp } from "../../observability/redaction";
|
|
15
16
|
import { type TelemetryEventRecord } from "../../observability/event-log";
|
|
16
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
queryTelemetryEvents as querySqlEvents,
|
|
19
|
+
sumTelemetryTokens,
|
|
20
|
+
familyKpiTotals,
|
|
21
|
+
familyWindowRequests,
|
|
22
|
+
familyHourlyTelemetry,
|
|
23
|
+
familyIpBreakdown,
|
|
24
|
+
} from "../../observability/event-query";
|
|
17
25
|
import type { CartethyiaDatabase } from "../../persistence/db-handle";
|
|
26
|
+
import { apiKeys } from "../../persistence/schema";
|
|
18
27
|
import { isGatewayError } from "../../observability/telemetry-status";
|
|
19
28
|
import type { RedisClient } from "../../persistence/redis";
|
|
20
29
|
import { cachedRead } from "../shared/read-through-cache";
|
|
@@ -67,6 +76,12 @@ export interface ShareTopClientIp {
|
|
|
67
76
|
* the tool rather than the machine.
|
|
68
77
|
*/
|
|
69
78
|
readonly clientType: string | null;
|
|
79
|
+
/**
|
|
80
|
+
* True when this address made a request within {@link ACTIVE_IP_WINDOW_MS}.
|
|
81
|
+
* A recipient watching the link can tell which addresses are still using it
|
|
82
|
+
* versus ones that stopped days ago.
|
|
83
|
+
*/
|
|
84
|
+
readonly active: boolean;
|
|
70
85
|
}
|
|
71
86
|
|
|
72
87
|
export interface ShareFamilyStats {
|
|
@@ -81,6 +96,14 @@ export interface ShareFamilyStats {
|
|
|
81
96
|
/** Read-only family rollup for one share link. */
|
|
82
97
|
export interface ShareStatsPort {
|
|
83
98
|
/**
|
|
99
|
+
* The family is addressed by its template key (`templateKeyId`) plus the
|
|
100
|
+
* number of keys it covers at call time (`memberCount`), not by an id list:
|
|
101
|
+
* a family grows with every key the link mints, and enumerating its ids to
|
|
102
|
+
* build a query — or a cache key — scaled both with the family instead of
|
|
103
|
+
* with the request. `memberCount` keeps the cache key sensitive to minting:
|
|
104
|
+
* a newly issued child starts a fresh entry rather than being folded into
|
|
105
|
+
* the previous one for a full TTL.
|
|
106
|
+
*
|
|
84
107
|
* `allowedModels`, when supplied, restricts the top-models table to the names
|
|
85
108
|
* the link actually grants. A rejected request still writes a telemetry row
|
|
86
109
|
* (with the requested name), so without this the table would rank models the
|
|
@@ -90,7 +113,7 @@ export interface ShareStatsPort {
|
|
|
90
113
|
*/
|
|
91
114
|
getFamilyStats(
|
|
92
115
|
tenantId: string,
|
|
93
|
-
|
|
116
|
+
family: { readonly templateKeyId: string; readonly memberCount: number },
|
|
94
117
|
recipients: { readonly total: number; readonly active: number },
|
|
95
118
|
allowedModels?: readonly string[],
|
|
96
119
|
): Promise<ShareFamilyStats>;
|
|
@@ -98,6 +121,24 @@ export interface ShareStatsPort {
|
|
|
98
121
|
|
|
99
122
|
const HOUR_MS = 3_600_000;
|
|
100
123
|
|
|
124
|
+
/**
|
|
125
|
+
* How old an address's last request may be and still count as active on the
|
|
126
|
+
* share page. 24 hours matches the page's own "last 24 hours" framing: an IP
|
|
127
|
+
* that made a request since yesterday is "active", one that stopped before
|
|
128
|
+
* that window is "inactive".
|
|
129
|
+
*/
|
|
130
|
+
const ACTIVE_IP_WINDOW_MS = 24 * HOUR_MS;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Recency window for the client-IP ranking's *work set*: the GROUP BY only
|
|
134
|
+
* scans this slice of telemetry instead of the whole retained window every
|
|
135
|
+
* tick. Within the window, addresses are ranked by request volume (highest
|
|
136
|
+
* first), and older addresses fall out of the ranking entirely. 7 days keeps
|
|
137
|
+
* the ranking useful while staying orders of magnitude smaller than the
|
|
138
|
+
* retained window.
|
|
139
|
+
*/
|
|
140
|
+
const IP_WINDOW_MS = 7 * 24 * HOUR_MS;
|
|
141
|
+
|
|
101
142
|
/**
|
|
102
143
|
* Names the client behind a request from its User-Agent.
|
|
103
144
|
*
|
|
@@ -141,7 +182,7 @@ interface FamilyRollup {
|
|
|
141
182
|
totals: ShareFamilyTotals;
|
|
142
183
|
byHour: Map<string, { requests: number; tokens: number }>;
|
|
143
184
|
models: Map<string, { modelId: string; requests: number; tokens: number; rateSum: number; rateCount: number; ttfbSum: number; ttfbCount: number }>;
|
|
144
|
-
ips: Map<string, { ip: string; requests: number; tokens: number; lastSeenAt: string | null; userAgent: string | null }>;
|
|
185
|
+
ips: Map<string, { ip: string; requests: number; tokens: number; lastSeenAt: string | null; userAgent: string | null; active: boolean }>;
|
|
145
186
|
}
|
|
146
187
|
|
|
147
188
|
/**
|
|
@@ -150,7 +191,7 @@ interface FamilyRollup {
|
|
|
150
191
|
*/
|
|
151
192
|
function rollupFamily(
|
|
152
193
|
events: readonly TelemetryEventRecord[],
|
|
153
|
-
bounds: { dayStart: number; monthStart: number; lastHourStart: number; windowStart: number; allowed: ReadonlySet<string> | null },
|
|
194
|
+
bounds: { dayStart: number; monthStart: number; lastHourStart: number; windowStart: number; activeIpCutoff: number; allowed: ReadonlySet<string> | null },
|
|
154
195
|
): FamilyRollup {
|
|
155
196
|
const totals = { requests: 0, errors: 0, inputTokens: 0, outputTokens: 0, totalTokens: 0, lastHourRequests: 0, todayTokens: 0, monthTokens: 0 };
|
|
156
197
|
const byHour = new Map<string, { requests: number; tokens: number }>();
|
|
@@ -183,13 +224,14 @@ function rollupFamily(
|
|
|
183
224
|
models.set(event.requestedModel, model);
|
|
184
225
|
}
|
|
185
226
|
if (event.clientIp !== null) {
|
|
186
|
-
const ip = ips.get(event.clientIp) ?? { ip: event.clientIp, requests: 0, tokens: 0, lastSeenAt: null, userAgent: null };
|
|
227
|
+
const ip = ips.get(event.clientIp) ?? { ip: event.clientIp, requests: 0, tokens: 0, lastSeenAt: null, userAgent: null, active: false };
|
|
187
228
|
ip.requests += 1;
|
|
188
229
|
ip.tokens += tokens;
|
|
189
230
|
// Events arrive newest first, so the first one seen is the most recent.
|
|
190
231
|
if (ip.lastSeenAt === null) {
|
|
191
232
|
ip.lastSeenAt = event.createdAt;
|
|
192
233
|
ip.userAgent = event.userAgent;
|
|
234
|
+
ip.active = at >= bounds.activeIpCutoff;
|
|
193
235
|
}
|
|
194
236
|
ips.set(event.clientIp, ip);
|
|
195
237
|
}
|
|
@@ -231,32 +273,35 @@ const HOURS_WINDOW = 24;
|
|
|
231
273
|
/**
|
|
232
274
|
* Cache lifetime for one family's rollup.
|
|
233
275
|
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
276
|
+
* Longer than the SSE tick (30s) so most ticks are served from cache. The
|
|
277
|
+
* cold-compute path reads the family's telemetry windows (24h chart, 7d IP
|
|
278
|
+
* ranking), which on a busy family is the single most expensive thing the
|
|
279
|
+
* share page does; a 5-minute TTL caps that to once per family per 5
|
|
280
|
+
* minutes while the page still reads as live — KPI numbers move with
|
|
281
|
+
* traffic, not on a per-minute cadence.
|
|
239
282
|
*
|
|
240
283
|
* Declared here rather than imported from the router so this port does not
|
|
241
|
-
* depend on it. `SHARE_STATS_STREAM_INTERVAL_MS` must
|
|
242
|
-
*
|
|
284
|
+
* depend on it. `SHARE_STATS_STREAM_INTERVAL_MS` must stay below it, or the
|
|
285
|
+
* tick and cache drift apart and every tick bypasses the cache.
|
|
243
286
|
*/
|
|
244
|
-
export const SHARE_STATS_CACHE_TTL_SECONDS =
|
|
287
|
+
export const SHARE_STATS_CACHE_TTL_SECONDS = 300;
|
|
245
288
|
|
|
246
289
|
/**
|
|
247
|
-
* Cache key for one family's rollup.
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
290
|
+
* Cache key for one family's rollup. The family is named by its template key
|
|
291
|
+
* and the number of keys it covers — both change the payload — and
|
|
292
|
+
* `allowedModels` filters the model table, so all three are hashed in.
|
|
293
|
+
* (The key used to hash the sorted list of every family id; on a family with
|
|
294
|
+
* tens of thousands of minted children that built and sorted a multi-megabyte
|
|
295
|
+
* string on every page view, before the cache could even answer.)
|
|
251
296
|
*/
|
|
252
297
|
function shareFamilyStatsKey(
|
|
253
298
|
tenantId: string,
|
|
254
|
-
|
|
299
|
+
family: { readonly templateKeyId: string; readonly memberCount: number },
|
|
255
300
|
allowedModels: readonly string[] | undefined,
|
|
256
301
|
): string {
|
|
257
302
|
const models = allowedModels === undefined ? "*" : [...allowedModels].sort().join(",");
|
|
258
303
|
const hash = createHash("sha256")
|
|
259
|
-
.update(
|
|
304
|
+
.update(`${family.templateKeyId}#${family.memberCount}|${models}`)
|
|
260
305
|
.digest("hex");
|
|
261
306
|
return `share:v1:stats:${tenantId}:${hash}`;
|
|
262
307
|
}
|
|
@@ -271,12 +316,12 @@ interface ShareStatsPortDeps {
|
|
|
271
316
|
|
|
272
317
|
export function createShareStatsPort(deps: ShareStatsPortDeps): ShareStatsPort {
|
|
273
318
|
return {
|
|
274
|
-
async getFamilyStats(tenantId,
|
|
275
|
-
if (
|
|
319
|
+
async getFamilyStats(tenantId, family, recipients, allowedModels) {
|
|
320
|
+
if (family.templateKeyId.length === 0) return emptyStats(recipients);
|
|
276
321
|
const cached = await cachedRead<Omit<ShareFamilyStats, "recipients">>(
|
|
277
322
|
deps.redis,
|
|
278
|
-
shareFamilyStatsKey(tenantId,
|
|
279
|
-
() => computeFamilyStatsBody(deps.db, tenantId,
|
|
323
|
+
shareFamilyStatsKey(tenantId, family, allowedModels),
|
|
324
|
+
() => computeFamilyStatsBody(deps.db, tenantId, family.templateKeyId, allowedModels),
|
|
280
325
|
SHARE_STATS_CACHE_TTL_SECONDS,
|
|
281
326
|
);
|
|
282
327
|
return { ...cached, recipients };
|
|
@@ -288,7 +333,7 @@ export function createShareStatsPort(deps: ShareStatsPortDeps): ShareStatsPort {
|
|
|
288
333
|
async function computeFamilyStatsBody(
|
|
289
334
|
db: CartethyiaDatabase,
|
|
290
335
|
tenantId: string,
|
|
291
|
-
|
|
336
|
+
templateKeyId: string,
|
|
292
337
|
allowedModels: readonly string[] | undefined,
|
|
293
338
|
): Promise<Omit<ShareFamilyStats, "recipients">> {
|
|
294
339
|
// A share that grants a fixed set ranks only that set. Telemetry keeps the
|
|
@@ -303,21 +348,43 @@ async function computeFamilyStatsBody(
|
|
|
303
348
|
const hourStart = utcHourStart(now);
|
|
304
349
|
const lastHourStart = new Date(now.getTime() - HOUR_MS);
|
|
305
350
|
const windowStart = new Date(hourStart.getTime() - (HOURS_WINDOW - 1) * HOUR_MS);
|
|
351
|
+
// The IP breakdown is bounded to this recency window: a family's "who is
|
|
352
|
+
// using the link" answer only needs addresses that were active recently,
|
|
353
|
+
// and an unbounded GROUP BY over every retained event is the one remaining
|
|
354
|
+
// full-window scan on the share page.
|
|
355
|
+
const ipWindowStart = new Date(now.getTime() - IP_WINDOW_MS);
|
|
306
356
|
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
//
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
357
|
+
// All-time family KPI comes from the durable rollup
|
|
358
|
+
// (`telemetry_usage_totals`, charged at write time), not from re-aggregating
|
|
359
|
+
// the entire retained telemetry window on every tick: the share page's tick
|
|
360
|
+
// would otherwise read every event a busy family ever produced just to draw
|
|
361
|
+
// four numbers. Today's/month's spend and the last-hour count are bounded
|
|
362
|
+
// window reads over the time index, which stay small.
|
|
363
|
+
const [kpi, todayTokens, monthTokens, lastHourRequests, template] = await Promise.all([
|
|
364
|
+
familyKpiTotals(db, tenantId, templateKeyId),
|
|
365
|
+
sumTelemetryTokens(db, tenantId, { familyKeyId: templateKeyId, since: dayStart }),
|
|
366
|
+
sumTelemetryTokens(db, tenantId, { familyKeyId: templateKeyId, since: monthStart }),
|
|
367
|
+
familyWindowRequests(db, tenantId, templateKeyId, lastHourStart),
|
|
368
|
+
db
|
|
369
|
+
.select({ lifetime: apiKeys.lifetimeTokensConsumed })
|
|
370
|
+
.from(apiKeys)
|
|
371
|
+
.where(eq(apiKeys.id, templateKeyId))
|
|
372
|
+
.limit(1),
|
|
373
|
+
]);
|
|
374
|
+
// Cumulative family spend: the template's own counter plus the telemetry
|
|
375
|
+
// total. The counter is a manually-configured seed (an operator limit), not
|
|
376
|
+
// traffic, and it is never reset; azmetry rows for revoked/deleted children
|
|
377
|
+
// likewise stay counted, so the running total is monotonic and consistent
|
|
378
|
+
// across the overview, the share dialog, and this page.
|
|
379
|
+
const templateLifetime = Number(template[0]?.lifetime ?? 0);
|
|
380
|
+
|
|
381
|
+
// The ranked tables need real events, bounded to the 24-hour window. The
|
|
382
|
+
// totals and hourly chart are computed in SQL, so the page cap here only
|
|
383
|
+
// shapes the model/IP rankings — never the KPI numbers.
|
|
318
384
|
const page = await querySqlEvents(db, {
|
|
319
385
|
tenantId,
|
|
320
|
-
|
|
386
|
+
familyApiKeyId: templateKeyId,
|
|
387
|
+
since: windowStart,
|
|
321
388
|
limit: MAX_FAMILY_EVENTS,
|
|
322
389
|
});
|
|
323
390
|
const events = page.events;
|
|
@@ -326,12 +393,36 @@ async function computeFamilyStatsBody(
|
|
|
326
393
|
monthStart: monthStart.getTime(),
|
|
327
394
|
lastHourStart: lastHourStart.getTime(),
|
|
328
395
|
windowStart: windowStart.getTime(),
|
|
396
|
+
activeIpCutoff: now.getTime() - ACTIVE_IP_WINDOW_MS,
|
|
329
397
|
allowed: allowedSet,
|
|
330
398
|
});
|
|
399
|
+
rollup.totals = {
|
|
400
|
+
...rollup.totals,
|
|
401
|
+
// The template counter is the seed on the running total, not a distinct
|
|
402
|
+
// bucket: daily/monthly windows are telemetry-only (the counter has no
|
|
403
|
+
// time axis), so those keep their own sums.
|
|
404
|
+
totalTokens: templateLifetime + kpi.tokens,
|
|
405
|
+
todayTokens,
|
|
406
|
+
monthTokens,
|
|
407
|
+
// Lifetime counts come from the durable rollup, not the page: a family
|
|
408
|
+
// busier than the page cap must not freeze at 500 requests/errors.
|
|
409
|
+
requests: kpi.requests,
|
|
410
|
+
errors: kpi.errors,
|
|
411
|
+
lastHourRequests,
|
|
412
|
+
};
|
|
331
413
|
|
|
332
|
-
//
|
|
333
|
-
//
|
|
334
|
-
|
|
414
|
+
// The hourly chart is a SQL aggregate over the whole family window, so a
|
|
415
|
+
// busy family's bars stay true. Buckets are filled in JS because only hours
|
|
416
|
+
// that saw traffic are present; a chart drawn from sparse rows would
|
|
417
|
+
// silently compress the axis.
|
|
418
|
+
const bucketSeconds = HOUR_MS / 1000;
|
|
419
|
+
const sqlHourly = await familyHourlyTelemetry(db, tenantId, templateKeyId, windowStart, bucketSeconds);
|
|
420
|
+
const byHour = new Map(
|
|
421
|
+
sqlHourly.map((bucket) => [
|
|
422
|
+
new Date(bucket.at).toISOString(),
|
|
423
|
+
{ requests: bucket.requests, tokens: bucket.inputTokens + bucket.outputTokens },
|
|
424
|
+
]),
|
|
425
|
+
);
|
|
335
426
|
const hourly: ShareHourlyBucket[] = [];
|
|
336
427
|
for (let index = 0; index < HOURS_WINDOW; index += 1) {
|
|
337
428
|
const hour = new Date(windowStart.getTime() + index * HOUR_MS).toISOString();
|
|
@@ -339,6 +430,33 @@ async function computeFamilyStatsBody(
|
|
|
339
430
|
hourly.push({ hour, requests: bucket?.requests ?? 0, tokens: bucket?.tokens ?? 0 });
|
|
340
431
|
}
|
|
341
432
|
|
|
433
|
+
// Client IPs come from a SQL aggregate over a bounded recent window, ranked
|
|
434
|
+
// by request volume — so a family's "who is using the link most" answer
|
|
435
|
+
// stays cheap instead of scanning the whole retained telemetry on every
|
|
436
|
+
// tick. `clientType` is best-effort from the page's events (the most recent
|
|
437
|
+
// 24 hours), which carry the User-Agent.
|
|
438
|
+
const ipRows = await familyIpBreakdown(db, tenantId, templateKeyId, TOP_IPS_LIMIT, ipWindowStart);
|
|
439
|
+
const clientTypeByIp = new Map<string, string | null>();
|
|
440
|
+
for (const event of events) {
|
|
441
|
+
if (event.clientIp !== null && !clientTypeByIp.has(event.clientIp)) {
|
|
442
|
+
clientTypeByIp.set(event.clientIp, clientTypeFromUserAgent(event.userAgent));
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
const activeIpCutoff = now.getTime() - ACTIVE_IP_WINDOW_MS;
|
|
446
|
+
const clientIps = ipRows.map((row) => {
|
|
447
|
+
const lastSeenAt =
|
|
448
|
+
row.lastSeenAt instanceof Date ? row.lastSeenAt : new Date(String(row.lastSeenAt ?? ""));
|
|
449
|
+
const lastSeenMs = Number.isFinite(lastSeenAt.getTime()) ? lastSeenAt.getTime() : 0;
|
|
450
|
+
return {
|
|
451
|
+
ip: maskClientIp(row.ip),
|
|
452
|
+
requests: row.requests,
|
|
453
|
+
tokens: row.tokens,
|
|
454
|
+
lastSeenAt: lastSeenMs > 0 ? lastSeenAt.toISOString() : null,
|
|
455
|
+
clientType: clientTypeByIp.get(row.ip) ?? null,
|
|
456
|
+
active: lastSeenMs >= activeIpCutoff,
|
|
457
|
+
};
|
|
458
|
+
});
|
|
459
|
+
|
|
342
460
|
return {
|
|
343
461
|
totals: rollup.totals,
|
|
344
462
|
hourly,
|
|
@@ -352,15 +470,6 @@ async function computeFamilyStatsBody(
|
|
|
352
470
|
avgTokensPerSec: row.rateCount > 0 ? row.rateSum / row.rateCount : null,
|
|
353
471
|
avgTtfbMs: row.ttfbCount > 0 ? Math.round(row.ttfbSum / row.ttfbCount) : null,
|
|
354
472
|
})),
|
|
355
|
-
clientIps
|
|
356
|
-
.sort((a, b) => b.requests - a.requests)
|
|
357
|
-
.slice(0, TOP_IPS_LIMIT)
|
|
358
|
-
.map((row) => ({
|
|
359
|
-
ip: maskClientIp(row.ip),
|
|
360
|
-
requests: row.requests,
|
|
361
|
-
tokens: row.tokens,
|
|
362
|
-
lastSeenAt: row.lastSeenAt,
|
|
363
|
-
clientType: clientTypeFromUserAgent(row.userAgent),
|
|
364
|
-
})),
|
|
473
|
+
clientIps,
|
|
365
474
|
};
|
|
366
475
|
}
|