cartethyia 2.7.4 → 2.7.5
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/README.md +17 -5
- package/dist/dashboard/assets/{BannedPage-ChG5svFb.js → BannedPage-Cj15GsMp.js} +1 -1
- package/dist/dashboard/assets/BannedPage-Cj15GsMp.js.gz +0 -0
- package/dist/dashboard/assets/ClaimGiftsButton-D83pBJ6J.js +1 -0
- package/dist/dashboard/assets/ClaimGiftsButton-D83pBJ6J.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-BIcWQun7.js → CliToolDetailPage-DZKmVuJN.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-DZKmVuJN.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-Bf2VelDe.js → CliToolsPage-BIccZmeL.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-BIccZmeL.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-DEEKkYD-.js +1 -0
- package/dist/dashboard/assets/CombosPage-DEEKkYD-.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-D5rol2j2.js +1 -0
- package/dist/dashboard/assets/ConsoleLogPage-D5rol2j2.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-ggeMSjte.js → CustomizationPage-BKj_r0rp.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-BKj_r0rp.js.gz +0 -0
- package/dist/dashboard/assets/InjectSystemPromptPanel-B5nZvmf0.js +656 -0
- package/dist/dashboard/assets/InjectSystemPromptPanel-B5nZvmf0.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-U6AKaJcF.js → LoginPage-DkXb6dx-.js} +1 -1
- package/dist/dashboard/assets/LoginPage-DkXb6dx-.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-BMS2t8BA.js → ModelPicker-DnQPSIA8.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-DnQPSIA8.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-Ci2wQKUz.js +1 -0
- package/dist/dashboard/assets/OverviewPage-Ci2wQKUz.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-odlgYrOg.js +5 -0
- package/dist/dashboard/assets/ProviderDetailPage-odlgYrOg.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-EfeloBn4.js +1 -0
- package/dist/dashboard/assets/ProviderIcon-EfeloBn4.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-CqFBQEB8.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-CqFBQEB8.js.gz +0 -0
- package/dist/dashboard/assets/{ProxyPage-lsrx7NYg.js → ProxyPage-5SnUSy3j.js} +1 -1
- package/dist/dashboard/assets/ProxyPage-5SnUSy3j.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-CmnMDzhP.js +1 -0
- package/dist/dashboard/assets/QuotaPage-CmnMDzhP.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-BJtRZ4qD.js +1 -0
- package/dist/dashboard/assets/SettingsPage-BJtRZ4qD.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-f0DE1h0-.js → SetupPage-WTa0sktC.js} +1 -1
- package/dist/dashboard/assets/SetupPage-WTa0sktC.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-Cq5tW7hW.js → StudioPage-BfAb5-o_.js} +3 -3
- package/dist/dashboard/assets/StudioPage-BfAb5-o_.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-BNzeZ2Vk.js +56 -0
- package/dist/dashboard/assets/UsagePage-BNzeZ2Vk.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-l29JROPv.js +1 -0
- package/dist/dashboard/assets/api-keys-l29JROPv.js.gz +0 -0
- package/dist/dashboard/assets/app--PJWq8_C.css +2 -0
- package/dist/dashboard/assets/app--PJWq8_C.css.gz +0 -0
- package/dist/dashboard/assets/assert-C0K-c4eo.js +1 -0
- package/dist/dashboard/assets/assert-C0K-c4eo.js.gz +0 -0
- package/dist/dashboard/assets/{badge-D9CS6dZk.js → badge-DfbhQU4k.js} +1 -1
- package/dist/dashboard/assets/badge-DfbhQU4k.js.gz +0 -0
- package/dist/dashboard/assets/{banner-DympBNq4.js → banner-BllT4LcF.js} +1 -1
- package/dist/dashboard/assets/banner-BllT4LcF.js.gz +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/{card-ibRWU_uT.js → card-CtkX66Wd.js} +1 -1
- package/dist/dashboard/assets/card-CtkX66Wd.js.gz +0 -0
- package/dist/dashboard/assets/chevron-left-Bf6CaTSJ.js +1 -0
- package/dist/dashboard/assets/chevron-left-Bf6CaTSJ.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-CQXVMn4P.js +1 -0
- package/dist/dashboard/assets/cli-tools-CQXVMn4P.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-DJm-9pz7.js → clipboard-button-BQqBQd2n.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-BQqBQd2n.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-8fiE6tec.js → dialog-BEK7KP0H.js} +1 -1
- package/dist/dashboard/assets/dialog-BEK7KP0H.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-CqGYeOrd.js → drawer-BrTEZ8zu.js} +1 -1
- package/dist/dashboard/assets/drawer-BrTEZ8zu.js.gz +0 -0
- package/dist/dashboard/assets/entry-CHWxbS56.js +1 -0
- package/dist/dashboard/assets/entry-CHWxbS56.js.gz +0 -0
- package/dist/dashboard/assets/{entry-BSAsYsmV.js → entry-EBHJ3DrE.js} +1 -1
- package/dist/dashboard/assets/entry-EBHJ3DrE.js.gz +0 -0
- package/dist/dashboard/assets/entry-LCS85hiq.js +4 -0
- package/dist/dashboard/assets/entry-LCS85hiq.js.gz +0 -0
- package/dist/dashboard/assets/{formatters-D-MTJxMr.js → formatters-BAACA727.js} +1 -1
- package/dist/dashboard/assets/formatters-BAACA727.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-CPK47xQx.js → github-badge-YSCE4UC2.js} +1 -1
- package/dist/dashboard/assets/github-badge-YSCE4UC2.js.gz +0 -0
- package/dist/dashboard/assets/{index-DBNPOfhV.js → index-FQlj0HUT.js} +2 -2
- package/dist/dashboard/assets/index-FQlj0HUT.js.gz +0 -0
- package/dist/dashboard/assets/{inline-BT0zntEr.js → inline-C4gU5iOL.js} +1 -1
- package/dist/dashboard/assets/inline-C4gU5iOL.js.gz +0 -0
- package/dist/dashboard/assets/{input-Da2ZViWg.js → input-BHHX_Tmm.js} +1 -1
- package/dist/dashboard/assets/input-BHHX_Tmm.js.gz +0 -0
- package/dist/dashboard/assets/{layout-BXNeLw6M.js → layout-CajrYUnD.js} +1 -1
- package/dist/dashboard/assets/layout-CajrYUnD.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-BycOrrIV.js → list-row-Bx2I7XUe.js} +1 -1
- package/dist/dashboard/assets/list-row-Bx2I7XUe.js.gz +0 -0
- package/dist/dashboard/assets/live-DNNqRbj2.js +1 -0
- package/dist/dashboard/assets/live-DNNqRbj2.js.gz +0 -0
- package/dist/dashboard/assets/network-C5UBACrJ.js +1 -0
- package/dist/dashboard/assets/network-C5UBACrJ.js.gz +0 -0
- package/dist/dashboard/assets/numbers-CUWAZK4J.js +2 -0
- package/dist/dashboard/assets/numbers-CUWAZK4J.js.gz +0 -0
- package/dist/dashboard/assets/pagination-Xsi9Qx04.js +1 -0
- package/dist/dashboard/assets/pagination-Xsi9Qx04.js.gz +0 -0
- package/dist/dashboard/assets/quota-DvQbD_sr.js +1 -0
- package/dist/dashboard/assets/quota-DvQbD_sr.js.gz +0 -0
- package/dist/dashboard/assets/routing-in0f_IfK.js +1 -0
- package/dist/dashboard/assets/routing-in0f_IfK.js.gz +0 -0
- package/dist/dashboard/assets/{select-BIYIBkMU.js → select-BLULNqTp.js} +1 -1
- package/dist/dashboard/assets/select-BLULNqTp.js.gz +0 -0
- package/dist/dashboard/assets/settings-CcGczj6I.js +1 -0
- package/dist/dashboard/assets/settings-CcGczj6I.js.gz +0 -0
- package/dist/dashboard/assets/shield-BMQNxORh.js +1 -0
- package/dist/dashboard/assets/shield-BMQNxORh.js.gz +0 -0
- package/dist/dashboard/assets/{stack-DfgnFDWY.js → stack-D2lIR0tg.js} +1 -1
- package/dist/dashboard/assets/stack-D2lIR0tg.js.gz +0 -0
- package/dist/dashboard/assets/{state-CkfdYWel.js → state-DTARjfiW.js} +1 -1
- package/dist/dashboard/assets/state-DTARjfiW.js.gz +0 -0
- package/dist/dashboard/assets/{switch-SYm237HG.js → switch-c0oPSnqW.js} +1 -1
- package/dist/dashboard/assets/switch-c0oPSnqW.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-BQqCRNIi.js → useMutation-m8WGNPrz.js} +1 -1
- package/dist/dashboard/assets/useMutation-m8WGNPrz.js.gz +0 -0
- package/dist/dashboard/console-backgrounds.jpg +0 -0
- package/dist/dashboard/console-backgrounds_initial.jpg +0 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +56 -55
- package/migrations/full-postgres/0036_system_prompt_presets_refs.sql +10 -0
- package/migrations/full-postgres/0037_share_concurrency_scope.sql +3 -0
- package/migrations/full-postgres/0038_add_accounts_quota_snapshot.sql +7 -0
- package/migrations/full-postgres/0039_add_telemetry_provider_model_indexes.sql +13 -0
- package/migrations/full-postgres/0040_add_models_dialect.sql +3 -0
- package/migrations/full-postgres/0041_add_daily_checkin_ledger_account_fk.sql +7 -0
- package/migrations/lite-sqlite3/0036_system_prompt_presets_refs.sql +14 -0
- package/migrations/lite-sqlite3/0037_share_concurrency_scope.sql +3 -0
- package/migrations/lite-sqlite3/0038_add_accounts_quota_snapshot.sql +4 -0
- package/migrations/lite-sqlite3/0039_add_telemetry_provider_model_indexes.sql +6 -0
- package/migrations/lite-sqlite3/0040_add_models_dialect.sql +3 -0
- package/migrations/lite-sqlite3/0041_add_daily_checkin_ledger_account_fk.sql +26 -0
- package/package.json +4 -6
- package/scripts/cli/update.ts +15 -41
- package/src/boot/app.ts +58 -2
- package/src/boot/dependencies.ts +786 -732
- package/src/boot/lifecycle.ts +11 -12
- package/src/config/cooldowns.ts +34 -55
- package/src/config/network.ts +1 -1
- package/src/config/package-paths.ts +1 -1
- package/src/config/providers.ts +13 -14
- package/src/config/resources.ts +0 -5
- package/src/config/runtime.ts +2 -5
- package/src/config/spec.ts +26 -6
- package/src/console/api-keys/ban-constants.ts +38 -0
- package/src/console/api-keys/contracts.ts +71 -62
- package/src/console/api-keys/routes.ts +48 -12
- package/src/console/auth/access.ts +4 -4
- package/src/console/auth/account-reset.ts +1 -1
- package/src/console/auth/audit-recorder.ts +1 -1
- package/src/console/auth/lockout.ts +7 -3
- package/src/console/auth/row-coercion.ts +57 -12
- package/src/console/auth/session-resolver.ts +1 -1
- package/src/console/auth/session-service.ts +8 -2
- package/src/console/auth/session.ts +37 -31
- package/src/console/auth/setup-service.ts +4 -3
- package/src/console/auth/user-store.ts +1 -1
- package/src/console/backup/9router-map.ts +0 -3
- package/src/console/backup/contracts.ts +9 -9
- package/src/console/backup/export-endpoint.ts +5 -6
- package/src/console/backup/nine-router.ts +1 -7
- package/src/console/backup/routes.ts +29 -33
- package/src/console/backup/service.ts +468 -468
- package/src/console/backup/store.ts +47 -29
- package/src/console/backup/validate.ts +11 -1
- package/src/console/cli-tools/contracts.ts +7 -7
- package/src/console/cli-tools/injectors/cline.ts +110 -110
- package/src/console/cli-tools/injectors/codex.ts +181 -181
- package/src/console/cli-tools/injectors/copilot.ts +108 -108
- package/src/console/cli-tools/injectors/driver.ts +247 -247
- package/src/console/cli-tools/injectors/jcode.ts +115 -115
- package/src/console/cli-tools/injectors/openclaw.ts +161 -161
- package/src/console/cli-tools/injectors/opencode.ts +153 -153
- package/src/console/cli-tools/routes.ts +1 -1
- package/src/console/cli-tools/service.ts +1 -1
- package/src/console/cli-tools/store.ts +2 -2
- package/src/console/console-router.ts +32 -0
- package/src/console/dashboard-assets.ts +9 -9
- package/src/console/domain-registration.ts +16 -2
- package/src/console/inferhub/client.ts +0 -7
- package/src/console/inferhub/key-store.ts +1 -1
- package/src/console/inferhub/normalize.ts +2 -2
- package/src/console/inferhub/routes.ts +1 -1
- package/src/console/inferhub/types.ts +1 -1
- package/src/console/observability/contracts.ts +1 -1
- package/src/console/observability/live.ts +31 -18
- package/src/console/observability/logs.ts +1 -1
- package/src/console/observability/sse.ts +62 -8
- package/src/console/observability/store.ts +20 -4
- package/src/console/providers/catalog/catalog-projections.ts +1 -1
- package/src/console/providers/catalog/contracts.ts +39 -0
- package/src/console/providers/catalog/model-operations.ts +1 -1
- package/src/console/providers/catalog/provider-operations.ts +95 -17
- package/src/console/providers/catalog/public-model-store.ts +0 -0
- package/src/console/providers/catalog/routes.ts +27 -3
- package/src/console/providers/catalog/store.ts +147 -8
- package/src/console/providers/detail/routes.ts +1 -1
- package/src/console/providers/oauth/callback-listener.ts +100 -18
- package/src/console/providers/oauth/routes.ts +92 -13
- package/src/console/quota/account-quota-global.ts +41 -3
- package/src/console/quota/account-quota-shared.ts +10 -3
- package/src/console/quota/account-quota-tenant.ts +259 -66
- package/src/console/quota/daily-growth-job.ts +308 -0
- package/src/console/quota/native-identity.ts +2 -2
- package/src/console/quota/quota-view.ts +1 -1
- package/src/console/remote-support/routes.ts +1 -1
- package/src/console/remote-support/service.ts +2 -2
- package/src/console/routing/model/contracts.ts +1 -1
- package/src/console/routing/pools/contracts.ts +2 -2
- package/src/console/routing/pools/import-parse.ts +3 -3
- package/src/console/routing/pools/relay-deploy.ts +3 -3
- package/src/console/settings/contracts.ts +29 -2
- package/src/console/settings/store.ts +20 -0
- package/src/console/share/share-router.ts +100 -32
- package/src/console/share/share-stats.ts +4 -4
- package/src/console/share/share-usage.ts +1 -1
- package/src/console/shared/errors.ts +2 -2
- package/src/console/shared/object.ts +18 -0
- package/src/console/shared/read-through-cache.ts +4 -4
- package/src/console/shared/transaction.ts +14 -0
- package/src/console/system-prompt/contracts.ts +89 -12
- package/src/console/system-prompt/routes.ts +98 -45
- package/src/console/updates/apply-update.ts +244 -0
- package/src/console/updates/routes.ts +252 -97
- package/src/main.ts +11 -5
- package/src/network/http2-bidi.ts +1 -1
- package/src/network/ip-utils.ts +298 -0
- package/src/network/outbound-fetch.ts +1 -1
- package/src/network/pool/agent.ts +7 -6
- package/src/network/pool/byte-accounting.ts +1 -1
- package/src/network/pool/resolver.ts +16 -12
- package/src/network/pool/selector.ts +150 -136
- package/src/network/pool-health-machine.ts +1 -1
- package/src/network/rate-limit-verdict.ts +1 -1
- package/src/network/retry.ts +1 -1
- package/src/network/ssrf.ts +20 -71
- package/src/observability/event-log.ts +2 -2
- package/src/observability/event-query.ts +59 -291
- package/src/observability/log-sanitize.ts +1 -1
- package/src/observability/metrics.ts +4 -4
- package/src/observability/payload-capture.ts +6 -5
- package/src/observability/payload-store.ts +1 -1
- package/src/observability/runtime-metrics.ts +1 -1
- package/src/observability/telemetry-buffer.ts +12 -3
- package/src/observability/telemetry-reconciliation.ts +3 -3
- package/src/observability/token-speed.ts +1 -1
- package/src/persistence/connection-url.ts +1 -1
- package/src/persistence/db-handle.ts +1 -1
- package/src/persistence/db-pg.ts +2 -2
- package/src/persistence/db-sqlite.ts +3 -3
- package/src/persistence/dialect.ts +5 -1
- package/src/persistence/readiness.ts +1 -1
- package/src/persistence/redis.ts +1 -1
- package/src/persistence/schema.ts +113 -40
- package/src/persistence/shared-cache.ts +2 -2
- package/src/persistence/sort-order.ts +1 -1
- package/src/persistence/stores/api-key-store.ts +10 -1
- package/src/persistence/stores/share-store.ts +20 -8
- package/src/persistence/system-prompt-store.ts +63 -18
- package/src/persistence/tenant-preferences.ts +1 -1
- package/src/persistence/ttl-cache.ts +3 -3
- package/src/persistence/usage-flush.ts +7 -2
- package/src/protocol/canonical-model.ts +1 -1
- package/src/protocol/codecs.ts +6 -10
- package/src/protocol/dsml.ts +3 -49
- package/src/protocol/image-clamp.ts +2 -2
- package/src/protocol/reasoning-text.ts +1 -1
- package/src/protocol/reasoning.ts +7 -52
- package/src/protocol/registry.ts +50 -15
- package/src/protocol/request/chat.ts +46 -19
- package/src/protocol/request/codex.ts +1 -1
- package/src/protocol/request/messages.ts +46 -14
- package/src/protocol/request/responses.ts +19 -10
- package/src/protocol/response/chat.ts +25 -10
- package/src/protocol/response/codex.ts +1 -1
- package/src/protocol/response/messages.ts +1 -1
- package/src/protocol/response/omp-tool-call-markup.ts +1 -1
- package/src/protocol/response/responses.ts +1 -1
- package/src/protocol/sse-decoder.ts +50 -1
- package/src/protocol/stop-reason.ts +2 -2
- package/src/protocol/think-split.ts +347 -0
- package/src/protocol/translation/attachment-integrity.ts +3 -3
- package/src/protocol/translation/cache-controls.ts +8 -20
- package/src/protocol/translation/capabilities.ts +6 -6
- package/src/protocol/translation/dialect/deepseek.ts +10 -0
- package/src/protocol/translation/dialect/inband.ts +39 -0
- package/src/protocol/translation/dialect/model-thinking.ts +89 -0
- package/src/protocol/translation/dialect/qwen.ts +114 -0
- package/src/protocol/translation/dialect/resolver.ts +83 -0
- package/src/protocol/translation/dialect/rules.ts +93 -0
- package/src/protocol/translation/dialect/thinking-ladder.ts +245 -0
- package/src/protocol/translation/dialect/types.ts +58 -0
- package/src/protocol/translation/quirks.ts +18 -4
- package/src/protocol/translation/thinking.ts +12 -252
- package/src/protocol/translation/tool-id.ts +3 -3
- package/src/protocol/translation/tool-repair.ts +1 -1
- package/src/protocol/upstream/post-json.ts +6 -1
- package/src/providers/adapters/compatible-adapter.ts +42 -3
- package/src/providers/authentication/jwt-validator.ts +0 -5
- package/src/providers/authentication/oauth-client.ts +254 -253
- package/src/providers/authentication/oauth-flow-store.ts +42 -7
- package/src/providers/bundled-discovery-providers.ts +6 -30
- package/src/providers/configured-openai-providers.ts +82 -97
- package/src/providers/default-registry.ts +23 -35
- package/src/providers/discovery/catalog/catalog-format.ts +13 -2
- package/src/providers/discovery/discovery-types.ts +0 -1
- package/src/providers/discovery/openai-model-discovery.ts +2 -2
- package/src/providers/discovery/probe-phases.ts +48 -13
- package/src/providers/discovery/probing-service.ts +6 -8
- package/src/providers/integrations/agentrouter/agentrouter.ts +12 -2
- package/src/providers/integrations/antigravity/antigravity-oauth.ts +0 -1
- package/src/providers/integrations/antigravity/antigravity-protocol.ts +0 -14
- package/src/providers/integrations/antigravity/antigravity.ts +2 -2
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +1 -1
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +20 -4
- package/src/providers/integrations/buddy/buddy-cli-headers.ts +19 -52
- package/src/providers/integrations/buddy/buddy-identity.ts +86 -0
- package/src/providers/integrations/buddy/buddy-oauth-shared.ts +1 -1
- package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +1 -2
- package/src/providers/integrations/buddy/codebuddy/provider.ts +4 -15
- package/src/providers/integrations/buddy/codebuddy/shared.ts +30 -9
- package/src/providers/integrations/buddy/workbuddy/provider.ts +5 -9
- package/src/providers/integrations/buddy/workbuddy/shared.ts +21 -3
- package/src/providers/integrations/claude/claude-cch.ts +148 -148
- package/src/providers/integrations/claude/fallback-credit.ts +3 -7
- package/src/providers/integrations/cline/cline.ts +1 -1
- package/src/providers/integrations/codex/codex-device-code.ts +101 -101
- package/src/providers/integrations/codex/codex-oauth.ts +0 -1
- package/src/providers/integrations/commandcode/commandcode.ts +75 -115
- package/src/providers/integrations/commandcode/quota.ts +131 -51
- package/src/providers/integrations/cursor/cursor-http2.ts +0 -3
- package/src/providers/integrations/cursor/cursor-quota.ts +2 -6
- package/src/providers/integrations/cursor/cursor-request.ts +0 -87
- package/src/providers/integrations/cursor/cursor-shared.ts +0 -4
- package/src/providers/integrations/cursor/cursor.ts +38 -5
- package/src/providers/integrations/cursor/protocol/framing.ts +0 -56
- package/src/providers/integrations/cursor/protocol/messages.ts +0 -96
- package/src/providers/integrations/factory/factory.ts +9 -9
- package/src/providers/integrations/github/github-discovery.ts +55 -10
- package/src/providers/integrations/github/github-oauth.ts +6 -5
- package/src/providers/integrations/github/github.ts +41 -14
- package/src/providers/integrations/grok/grok-oauth.ts +261 -261
- package/src/providers/integrations/grok/grok-quota-frame.ts +1 -1
- package/src/providers/integrations/grok/grok-quota.ts +16 -7
- package/src/providers/integrations/grok/grok-ratelimit-store.ts +0 -5
- package/src/providers/integrations/grok/grok-ratelimit.ts +3 -0
- package/src/providers/integrations/grok/grok.ts +86 -36
- package/src/providers/integrations/kilo/kilo-oauth.ts +1 -1
- package/src/providers/integrations/kiro/kiro-cache.ts +31 -0
- package/src/providers/integrations/kiro/kiro-catalog.ts +31 -5
- package/src/providers/integrations/kiro/kiro-discovery.ts +47 -43
- package/src/providers/integrations/kiro/kiro-machine-id.ts +6 -3
- package/src/providers/integrations/kiro/kiro-oauth.ts +523 -255
- package/src/providers/integrations/kiro/kiro-quota.ts +35 -47
- package/src/providers/integrations/kiro/kiro-request.ts +8 -11
- package/src/providers/integrations/kiro/kiro-stream.ts +13 -0
- package/src/providers/integrations/kiro/kiro.ts +81 -68
- package/src/providers/integrations/muse/muse-oauth.ts +0 -1
- package/src/providers/integrations/muse/muse-quota.ts +0 -4
- package/src/providers/integrations/opencode/fingerprint.ts +7 -5
- package/src/providers/integrations/opencode/provider.ts +17 -24
- package/src/providers/integrations/qoder/qoder-catalog.ts +85 -8
- package/src/providers/integrations/qoder/qoder-oauth.ts +165 -25
- package/src/providers/integrations/qoder/qoder-quota.ts +48 -6
- package/src/providers/integrations/qoder/qoder-realm.ts +10 -31
- package/src/providers/integrations/qoder/qoder-sse.ts +159 -0
- package/src/providers/integrations/qoder/qoder-umid.ts +21 -25
- package/src/providers/integrations/qoder/qoder-virtual.ts +12 -0
- package/src/providers/integrations/qoder/qoder.ts +354 -62
- package/src/providers/integrations/shared/connect.ts +0 -54
- package/src/providers/integrations/shared/hash.ts +15 -0
- package/src/providers/integrations/shared/session-cache.ts +136 -0
- package/src/providers/integrations/trae/trae-cache.ts +7 -87
- package/src/providers/integrations/trae/trae-catalog.ts +1 -6
- package/src/providers/integrations/trae/trae-protocol.ts +17 -8
- package/src/providers/integrations/trae/trae-realm.ts +0 -7
- package/src/providers/integrations/trae/trae.ts +53 -10
- package/src/providers/integrations/xai/xai-oauth.ts +5 -5
- package/src/providers/integrations/zcode-start-plan/gifts.ts +265 -0
- package/src/providers/integrations/zcode-start-plan/oauth.ts +154 -0
- package/src/providers/integrations/zcode-start-plan/provider.ts +131 -0
- package/src/providers/integrations/zcode-start-plan/quota.ts +152 -0
- package/src/providers/integrations/zcode-start-plan/zcode_prompt.json +10 -0
- package/src/providers/operations/account-error-classification.ts +131 -116
- package/src/providers/operations/account-health-service.ts +31 -39
- package/src/providers/operations/account-reset-service.ts +58 -1
- package/src/providers/operations/cli-platform.ts +31 -5
- package/src/providers/operations/client-version-resolver.ts +4 -6
- package/src/providers/operations/client-versions.ts +10 -7
- package/src/providers/operations/provider-catalog-seeder.ts +2 -0
- package/src/providers/operations/provider-catalog-service.ts +18 -2
- package/src/providers/operations/provider-credential-service.ts +19 -2
- package/src/providers/provider-contracts.ts +18 -7
- package/src/providers/provider-metadata.ts +17 -2
- package/src/providers/provider-model-definition.ts +22 -1
- package/src/providers/provider-probe.ts +4 -2
- package/src/providers/provider-usage.ts +81 -16
- package/src/providers/quota/cache.ts +29 -0
- package/src/providers/quota/quota-contracts.ts +4 -0
- package/src/providers/quota/refresh.ts +27 -4
- package/src/providers/quota/snapshot.ts +94 -0
- package/src/providers/search/search-provider.ts +1 -3
- package/src/providers/search/search-providers.ts +2 -1
- package/src/security/abuse.ts +4 -4
- package/src/security/admission/service.ts +2 -2
- package/src/security/admission/ttl.ts +61 -61
- package/src/security/api-key-auth.ts +16 -4
- package/src/security/client-router-fingerprint.ts +4 -4
- package/src/security/credential-codec.ts +3 -3
- package/src/security/csrf.ts +2 -2
- package/src/security/ip-boundary.ts +16 -189
- package/src/security/model-access-rule.ts +1 -1
- package/src/security/outbound-headers.ts +18 -0
- package/src/shared/invoke-guard.ts +47 -0
- package/src/transport/contracts.ts +13 -4
- package/src/transport/dispatch/attempt-finalize.ts +1 -1
- package/src/transport/dispatch/capture-policy.ts +34 -37
- package/src/transport/dispatch/empty-stream.ts +23 -20
- package/src/transport/dispatch/generation.ts +25 -45
- package/src/transport/dispatch/native-shared.ts +127 -0
- package/src/transport/dispatch/proxy-request.ts +3 -1
- package/src/transport/dispatch/responses-compact.ts +25 -46
- package/src/transport/dispatch/streaming/encoder.ts +18 -72
- package/src/transport/dispatch/streaming/finalize.ts +2 -2
- package/src/transport/dispatch/streaming/stream.ts +7 -3
- package/src/transport/dispatch/streaming/thinking-loop.ts +1 -1
- package/src/transport/dispatch/systemone.ts +20 -48
- package/src/transport/dispatch/token-count.ts +41 -38
- package/src/transport/dispatch/websearch.ts +19 -45
- package/src/transport/drain-endpoint.ts +1 -1
- package/src/transport/failure-policy.ts +6 -0
- package/src/transport/guards/auth-guard.ts +0 -6
- package/src/transport/guards/body-policy.ts +3 -3
- package/src/transport/guards/headroom-guard.ts +1 -1
- package/src/transport/guards/pipeline.ts +1 -1
- package/src/transport/guards/request-context.ts +12 -17
- package/src/transport/request/preparer.ts +286 -7
- package/src/transport/request/rtk/compress-request.ts +3 -3
- package/src/transport/request/rtk/rtk-constants.ts +2 -2
- package/src/transport/request/rtk/rtk-filters.ts +8 -8
- package/src/transport/request/state.ts +1 -13
- package/src/transport/request/system-prompt-injection.ts +74 -11
- package/src/transport/request/tenant-preferences.ts +22 -0
- package/src/transport/routing/candidate-index.ts +2 -2
- package/src/transport/routing/round-robin.ts +1 -1
- package/src/transport/routing/route-catalog.ts +17 -18
- package/src/transport/routing/route-model.ts +126 -33
- package/src/transport/routing/router.ts +152 -37
- package/src/transport/shutdown-notice.ts +2 -2
- package/src/transport/surface/adapters.ts +3 -3
- package/src/transport/surface/chat/encode.ts +7 -7
- package/src/transport/surface/chat/parse.ts +7 -7
- package/src/transport/surface/completion.ts +2 -2
- package/src/transport/surface/content-parts.ts +1 -1
- package/src/transport/surface/dialects.ts +2 -2
- package/src/transport/surface/messages/parse.ts +3 -3
- package/src/transport/surface/responses/adapter.ts +1 -1
- package/src/transport/surface/responses/errors.ts +0 -9
- package/src/transport/vision-handoff-cache.ts +58 -0
- package/src/transport/vision-handoff.ts +376 -0
- package/src/workers/account-identity-sweep.ts +2 -2
- package/src/workers/buddy-growth-tasks.ts +392 -346
- package/src/workers/daily-checkin.ts +102 -49
- package/src/workers/maintenance-lease.ts +2 -35
- package/src/workers/oauth-refresh-sweep.ts +8 -1
- package/src/workers/quota-refresh-sweep.ts +120 -82
- package/src/workers/scheduler.ts +67 -4
- package/src/workers/sweep-runner.ts +24 -5
- package/src/workers/zcode-gift-claim.ts +108 -0
- package/dist/dashboard/assets/AccountCooldown-Bjo92Qat.js +0 -1
- package/dist/dashboard/assets/AccountCooldown-Bjo92Qat.js.gz +0 -0
- package/dist/dashboard/assets/BannedPage-ChG5svFb.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-BIcWQun7.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-Bf2VelDe.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-DCgm5g6g.js +0 -1
- package/dist/dashboard/assets/CombosPage-DCgm5g6g.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-a8mD0QF1.js +0 -1
- package/dist/dashboard/assets/ConsoleLogPage-a8mD0QF1.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-ggeMSjte.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-DzSWzE_j.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-DzSWzE_j.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-U6AKaJcF.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-BMS2t8BA.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-D2D4IiHl.js +0 -1
- package/dist/dashboard/assets/OverviewPage-D2D4IiHl.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-CrNf7j_X.js +0 -5
- package/dist/dashboard/assets/ProviderDetailPage-CrNf7j_X.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js +0 -1
- package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-BSDbquc1.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-BSDbquc1.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-lsrx7NYg.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-L0skzML8.js +0 -1
- package/dist/dashboard/assets/QuotaPage-L0skzML8.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-CmAbjRHW.js +0 -1
- package/dist/dashboard/assets/SettingsPage-CmAbjRHW.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-f0DE1h0-.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-ClzpwyET.js +0 -1
- package/dist/dashboard/assets/SortableList-ClzpwyET.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-Cq5tW7hW.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-Dfc9rWQn.js +0 -56
- package/dist/dashboard/assets/UsagePage-Dfc9rWQn.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-C38iXoO4.js +0 -1
- package/dist/dashboard/assets/api-keys-C38iXoO4.js.gz +0 -0
- package/dist/dashboard/assets/app-a159k0ub.css +0 -2
- package/dist/dashboard/assets/app-a159k0ub.css.gz +0 -0
- package/dist/dashboard/assets/assert-DbRA8EG2.js +0 -15
- package/dist/dashboard/assets/assert-DbRA8EG2.js.gz +0 -0
- package/dist/dashboard/assets/badge-D9CS6dZk.js.gz +0 -0
- package/dist/dashboard/assets/banner-DympBNq4.js.gz +0 -0
- package/dist/dashboard/assets/card-ibRWU_uT.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-63qBGaRw.js +0 -1
- package/dist/dashboard/assets/cli-tools-63qBGaRw.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-DJm-9pz7.js.gz +0 -0
- package/dist/dashboard/assets/dialog-8fiE6tec.js.gz +0 -0
- package/dist/dashboard/assets/drawer-CqGYeOrd.js.gz +0 -0
- package/dist/dashboard/assets/entry-BSAsYsmV.js.gz +0 -0
- package/dist/dashboard/assets/entry-CiIQULZD.js +0 -4
- package/dist/dashboard/assets/entry-CiIQULZD.js.gz +0 -0
- package/dist/dashboard/assets/entry-Daz0lJ-W.js +0 -1
- package/dist/dashboard/assets/entry-Daz0lJ-W.js.gz +0 -0
- package/dist/dashboard/assets/formatters-D-MTJxMr.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-CPK47xQx.js.gz +0 -0
- package/dist/dashboard/assets/index-DBNPOfhV.js.gz +0 -0
- package/dist/dashboard/assets/inline-BT0zntEr.js.gz +0 -0
- package/dist/dashboard/assets/input-Da2ZViWg.js.gz +0 -0
- package/dist/dashboard/assets/layout-BXNeLw6M.js.gz +0 -0
- package/dist/dashboard/assets/list-row-BycOrrIV.js.gz +0 -0
- package/dist/dashboard/assets/live-DCQNZpF7.js +0 -1
- package/dist/dashboard/assets/live-DCQNZpF7.js.gz +0 -0
- package/dist/dashboard/assets/network-9Yj2ZyLo.js +0 -1
- package/dist/dashboard/assets/network-9Yj2ZyLo.js.gz +0 -0
- package/dist/dashboard/assets/numbers-CEagj6ZZ.js +0 -2
- package/dist/dashboard/assets/numbers-CEagj6ZZ.js.gz +0 -0
- package/dist/dashboard/assets/quota-Caw7wqci.js +0 -1
- package/dist/dashboard/assets/quota-Caw7wqci.js.gz +0 -0
- package/dist/dashboard/assets/routing-CO9zCUus.js +0 -1
- package/dist/dashboard/assets/routing-CO9zCUus.js.gz +0 -0
- package/dist/dashboard/assets/select-BIYIBkMU.js.gz +0 -0
- package/dist/dashboard/assets/settings-jS3-PNSA.js +0 -1
- package/dist/dashboard/assets/settings-jS3-PNSA.js.gz +0 -0
- package/dist/dashboard/assets/stack-DfgnFDWY.js.gz +0 -0
- package/dist/dashboard/assets/state-CkfdYWel.js.gz +0 -0
- package/dist/dashboard/assets/stream-retry-5QnVuSSo.js +0 -1
- package/dist/dashboard/assets/stream-retry-5QnVuSSo.js.gz +0 -0
- package/dist/dashboard/assets/switch-SYm237HG.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-BQqCRNIi.js.gz +0 -0
- package/src/network/response-headers.ts +0 -39
- package/src/protocol/response/dsml.ts +0 -38
- package/src/providers/integrations/xiaomi-mimo/think-stream.ts +0 -229
- /package/dist/dashboard/assets/{app-BayTTy01.js → app-Bgd3zANQ.js} +0 -0
- /package/dist/dashboard/assets/{app-BayTTy01.js.gz → app-Bgd3zANQ.js.gz} +0 -0
- /package/dist/dashboard/assets/brand/{og_bansos.webp → og_bansos_initial.webp} +0 -0
- /package/dist/dashboard/assets/brand/{og_image.webp → og_image_initial.webp} +0 -0
|
@@ -11,6 +11,7 @@ export interface OpenAiUsage {
|
|
|
11
11
|
};
|
|
12
12
|
}
|
|
13
13
|
import { modelsDevCatalog } from "./discovery/models-dev-catalog";
|
|
14
|
+
import { resolveCacheMode } from "../config/providers";
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Extract cost multipliers from catalog model cost JSONB.
|
|
@@ -125,6 +126,36 @@ export function repriceUsage(
|
|
|
125
126
|
: { ...usage, estimated_cost: estimatedCost };
|
|
126
127
|
}
|
|
127
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Simplified cache accounting: when the provider reported a cache hit, the
|
|
131
|
+
* whole prompt counts as cached — request input ber-cache = cache-eligible →
|
|
132
|
+
* input = cache. The cache-write volume is already inside the all-in input
|
|
133
|
+
* total on cache-aware shapes, so it stays separate and is not double-counted
|
|
134
|
+
* as a hit. A reported `0` or an absent report stay a cache miss. In `real`
|
|
135
|
+
* mode the provider's reported split is kept as-is.
|
|
136
|
+
*
|
|
137
|
+
* Exported so the hand-built record producers (Cursor, Kiro) apply the same
|
|
138
|
+
* rule as `normalizeUsage`.
|
|
139
|
+
*/
|
|
140
|
+
export function applyCacheMode(usage: UsageRecord): UsageRecord {
|
|
141
|
+
if (resolveCacheMode() !== "simplified") return usage;
|
|
142
|
+
if (
|
|
143
|
+
typeof usage.cached_input_tokens === "number" &&
|
|
144
|
+
usage.cached_input_tokens > 0 &&
|
|
145
|
+
usage.input_tokens > 0
|
|
146
|
+
) {
|
|
147
|
+
const writes =
|
|
148
|
+
typeof usage.cache_write_tokens === "number" ? Math.max(0, usage.cache_write_tokens) : 0;
|
|
149
|
+
const cached = Math.min(usage.input_tokens, Math.max(0, usage.input_tokens - writes));
|
|
150
|
+
return {
|
|
151
|
+
...usage,
|
|
152
|
+
cached_input_tokens: cached,
|
|
153
|
+
uncached_input_tokens: 0,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
return usage;
|
|
157
|
+
}
|
|
158
|
+
|
|
128
159
|
/**
|
|
129
160
|
* Normalize provider-specific usage data into canonical UsageRecord.
|
|
130
161
|
* Handles Anthropic (cache_read_input_tokens, cache_creation_input_tokens, output_tokens_details.thinking_tokens)
|
|
@@ -250,6 +281,21 @@ export function normalizeUsage(input: Record<string, unknown>): UsageRecord {
|
|
|
250
281
|
input_tokens += cache_write_tokens;
|
|
251
282
|
}
|
|
252
283
|
|
|
284
|
+
// Simplified cache accounting: a reported hit makes the whole prompt count as
|
|
285
|
+
// cached — request input ber-cache = cache-eligible → input = cache. The
|
|
286
|
+
// cache-write volume is already inside the all-in input total on cache-aware
|
|
287
|
+
// shapes, so it stays separate and is not double-counted as a hit. Applied
|
|
288
|
+
// before pricing so the estimated cost reads the same simplified split as the
|
|
289
|
+
// record.
|
|
290
|
+
if (typeof cached_input_tokens === "number" && cached_input_tokens > 0 && input_tokens > 0) {
|
|
291
|
+
if (resolveCacheMode() === "simplified") {
|
|
292
|
+
const writes =
|
|
293
|
+
typeof cache_write_tokens === "number" ? Math.max(0, cache_write_tokens) : 0;
|
|
294
|
+
cached_input_tokens = Math.min(input_tokens, Math.max(0, input_tokens - writes));
|
|
295
|
+
uncached_input_tokens = 0;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
253
299
|
// Handle reasoning tokens: Anthropic and OpenAI both use output_tokens_details
|
|
254
300
|
let reasoning_tokens: number | "unavailable" = "unavailable";
|
|
255
301
|
const details: NonNullable<UsageRecord["details"]> = {};
|
|
@@ -436,28 +482,47 @@ export function usageToMessagesWire(usage: UsageRecord | undefined): Record<stri
|
|
|
436
482
|
* and event shapes — only the key lookup lives here, so a new upstream key
|
|
437
483
|
* is added once and every reader picks it up.
|
|
438
484
|
*/
|
|
439
|
-
export const REASONING_TEXT_KEYS: readonly string[] = [
|
|
485
|
+
export const REASONING_TEXT_KEYS: readonly string[] = [
|
|
486
|
+
"reasoning_content",
|
|
487
|
+
"reasoning",
|
|
488
|
+
"reasoning_summary",
|
|
489
|
+
"reasoning_summary_text",
|
|
490
|
+
"reasoning_text",
|
|
491
|
+
];
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Extracts readable text from one reasoning value. Accepts a plain string and
|
|
495
|
+
* the object shapes upstreams use for reasoning deltas: `{text}`,
|
|
496
|
+
* `{summary}`, `{delta}` (Responses-style), or an array of parts.
|
|
497
|
+
*/
|
|
498
|
+
function reasoningTextValue(value: unknown): string | undefined {
|
|
499
|
+
if (typeof value === "string") return value;
|
|
500
|
+
if (Array.isArray(value)) {
|
|
501
|
+
let text = "";
|
|
502
|
+
for (const part of value) {
|
|
503
|
+
const piece = reasoningTextValue(part);
|
|
504
|
+
if (piece !== undefined) text += piece;
|
|
505
|
+
}
|
|
506
|
+
return text.length > 0 ? text : undefined;
|
|
507
|
+
}
|
|
508
|
+
if (typeof value !== "object" || value === null) return undefined;
|
|
509
|
+
const rec = value as Record<string, unknown>;
|
|
510
|
+
for (const key of ["text", "summary", "delta", "content"]) {
|
|
511
|
+
const piece = reasoningTextValue(rec[key]);
|
|
512
|
+
if (piece !== undefined) return piece;
|
|
513
|
+
}
|
|
514
|
+
return undefined;
|
|
515
|
+
}
|
|
440
516
|
|
|
441
517
|
/** First string found under a known reasoning-text key, or undefined. */
|
|
442
518
|
export function readReasoningText(source: Record<string, unknown>): string | undefined {
|
|
443
519
|
for (const key of REASONING_TEXT_KEYS) {
|
|
444
|
-
const
|
|
445
|
-
if (
|
|
520
|
+
const text = reasoningTextValue(source[key]);
|
|
521
|
+
if (text !== undefined) return text;
|
|
446
522
|
}
|
|
447
523
|
// Qwen/MiniMax-family `reasoning_details[]` parts carrying text fragments.
|
|
448
|
-
const details = source["reasoning_details"];
|
|
449
|
-
if (
|
|
450
|
-
const text = details
|
|
451
|
-
.map((part) =>
|
|
452
|
-
typeof part === "string"
|
|
453
|
-
? part
|
|
454
|
-
: typeof (part as Record<string, unknown>)?.["text"] === "string"
|
|
455
|
-
? ((part as Record<string, unknown>)["text"] as string)
|
|
456
|
-
: "",
|
|
457
|
-
)
|
|
458
|
-
.join("");
|
|
459
|
-
if (text.length > 0) return text;
|
|
460
|
-
}
|
|
524
|
+
const details = reasoningTextValue(source["reasoning_details"]);
|
|
525
|
+
if (details !== undefined) return details;
|
|
461
526
|
return undefined;
|
|
462
527
|
}
|
|
463
528
|
|
|
@@ -318,6 +318,35 @@ export async function invalidateQuotaCache(
|
|
|
318
318
|
}
|
|
319
319
|
}
|
|
320
320
|
|
|
321
|
+
/**
|
|
322
|
+
* Drops many accounts from one lens in a single pass.
|
|
323
|
+
*
|
|
324
|
+
* The batch-delete route clears several accounts' quota caches at once. The
|
|
325
|
+
* in-process tier is swept once (one `keys()` pass), and Redis is cleared with
|
|
326
|
+
* one pipeline, so the cost does not scale with per-account round trips.
|
|
327
|
+
*/
|
|
328
|
+
export async function invalidateQuotaCaches(
|
|
329
|
+
lens: string,
|
|
330
|
+
accountIds: readonly string[],
|
|
331
|
+
redis: RedisClient | undefined,
|
|
332
|
+
): Promise<void> {
|
|
333
|
+
if (accountIds.length === 0) return;
|
|
334
|
+
const keys = accountIds.map((accountId) => quotaKey(lens, accountId));
|
|
335
|
+
const keySet = new Set(keys);
|
|
336
|
+
for (const key of inMemoryQuotaCache.keys()) {
|
|
337
|
+
if (keySet.has(key)) inMemoryQuotaCache.delete(key);
|
|
338
|
+
}
|
|
339
|
+
if (redis === undefined) return;
|
|
340
|
+
try {
|
|
341
|
+
const pipeline = redis.pipeline();
|
|
342
|
+
for (const key of keys) pipeline.del(key);
|
|
343
|
+
await pipeline.exec().catch(() => undefined);
|
|
344
|
+
} catch (error) {
|
|
345
|
+
metrics.quota_cache_invalidate_failed.inc(1);
|
|
346
|
+
log.warn("quota cache batch invalidate failed", { lens, error: String(error) });
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
321
350
|
/**
|
|
322
351
|
* Drops one account from EVERY cache lens. Global accounts are shared, but a
|
|
323
352
|
* release-N writer may have written the same account under a per-tenant lens,
|
|
@@ -18,6 +18,10 @@ export interface ProviderQuotaWindow {
|
|
|
18
18
|
readonly remaining?: number | null;
|
|
19
19
|
/** False means the timestamp is a hard expiry rather than a recurring reset. */
|
|
20
20
|
readonly recurring?: boolean;
|
|
21
|
+
/** Exact routing model IDs this grant serves; absent means account-wide. */
|
|
22
|
+
readonly modelIds?: readonly string[];
|
|
23
|
+
/** Activation instant: future grants remain visible but cannot fund requests. */
|
|
24
|
+
readonly startsAt?: string | null;
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
export interface ProviderQuotaResult {
|
|
@@ -15,8 +15,21 @@ import { fetchProviderQuota } from "./quota-support";
|
|
|
15
15
|
import { type FetchLike, type ProviderQuotaResult } from "./quota-contracts";
|
|
16
16
|
import type { ProviderId } from "../provider-contracts";
|
|
17
17
|
import type { ProviderRegistry } from "../provider-registry";
|
|
18
|
-
import {
|
|
18
|
+
import { quotaLens, setCachedQuota } from "./cache";
|
|
19
|
+
import { persistQuotaSnapshot } from "./snapshot";
|
|
19
20
|
import { asJsonbRecord } from "../../shared/coerce";
|
|
21
|
+
import { KIRO_DEFAULT_LABELS } from "../integrations/kiro/kiro-oauth";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The stock Kiro fallback labels as a SQL literal list. Built once from the
|
|
25
|
+
* canonical set (`KIRO_DEFAULT_LABELS`) and escaped for the single-quote SQL
|
|
26
|
+
* literal; the values are compile-time constants, so this fragment is static.
|
|
27
|
+
*/
|
|
28
|
+
const KIRO_DEFAULT_LABEL_IN = sql.raw(
|
|
29
|
+
[...KIRO_DEFAULT_LABELS]
|
|
30
|
+
.map((label) => `'${label.replace(/'/g, "''")}'`)
|
|
31
|
+
.join(", "),
|
|
32
|
+
);
|
|
20
33
|
|
|
21
34
|
/** Why an account quota refresh failed; `null` means the fetch succeeded. */
|
|
22
35
|
export type QuotaRefreshFailureCode = "missing_credential" | "quota_fetch_failed";
|
|
@@ -52,7 +65,7 @@ export interface QuotaRefreshDeps {
|
|
|
52
65
|
*/
|
|
53
66
|
readonly resolveCredential: (providerId: ProviderId, accountId: string) => Promise<string>;
|
|
54
67
|
/** Marks a stored api_key credential with the upstream-distinguishable envelope. */
|
|
55
|
-
readonly markApiKeyCredential?: (providerId: string, credential: string) => string
|
|
68
|
+
readonly markApiKeyCredential?: (providerId: string, credential: string) => Promise<string>;
|
|
56
69
|
/** Invalidates the route snapshot when a stamped figure may exclude an account. */
|
|
57
70
|
readonly snapshotInvalidator?: { invalidate(): unknown } | undefined;
|
|
58
71
|
}
|
|
@@ -219,7 +232,7 @@ export interface QuotaRefreshTarget {
|
|
|
219
232
|
|
|
220
233
|
/** Cache lens an account's quota belongs under. */
|
|
221
234
|
export function targetLens(target: QuotaRefreshTarget): string {
|
|
222
|
-
return target.tenantId
|
|
235
|
+
return quotaLens(target.tenantId);
|
|
223
236
|
}
|
|
224
237
|
|
|
225
238
|
const TARGET_COLUMNS = {
|
|
@@ -352,7 +365,7 @@ async function runQuotaRefresh(
|
|
|
352
365
|
// collector to the key-only probe, and OAuth credentials carry no key.
|
|
353
366
|
const marked =
|
|
354
367
|
target.credentialKind === "api_key"
|
|
355
|
-
? (deps.markApiKeyCredential?.(target.providerId, credential) ?? credential)
|
|
368
|
+
? (await deps.markApiKeyCredential?.(target.providerId, credential) ?? credential)
|
|
356
369
|
: credential;
|
|
357
370
|
try {
|
|
358
371
|
const authState = asJsonbRecord(target.authState);
|
|
@@ -399,6 +412,15 @@ async function runQuotaRefresh(
|
|
|
399
412
|
log.warn(`[quota] failed to record plan for account=${target.accountId}`, error as Error);
|
|
400
413
|
}
|
|
401
414
|
}
|
|
415
|
+
// Persist the whole successful read, not just the plan. The row survives a
|
|
416
|
+
// restart that empties the in-process cache, so a cold console open renders
|
|
417
|
+
// the last known balance instead of nothing (and the sweep refreshes it). A
|
|
418
|
+
// failed fetch leaves the previous snapshot in place — same reasoning as the
|
|
419
|
+
// plan column: failure is evidence about the network/credential, not that
|
|
420
|
+
// the account's balance changed.
|
|
421
|
+
if (quota.error === null) {
|
|
422
|
+
await persistQuotaSnapshot(deps.db, target.accountId, quota);
|
|
423
|
+
}
|
|
402
424
|
// A quota surface that names the account adopts the identity — but only over
|
|
403
425
|
// a default label. An operator's custom rename is never overwritten, and the
|
|
404
426
|
// CASE keeps the read and the write in one statement so a concurrent rename
|
|
@@ -416,6 +438,7 @@ async function runQuotaRefresh(
|
|
|
416
438
|
label: sql`CASE
|
|
417
439
|
WHEN lower(trim(${providerAccounts.label})) = lower(trim(${providerAccounts.providerId})) THEN ${quota.accountLabel}
|
|
418
440
|
WHEN ${providerAccounts.label} LIKE '%|%' THEN ${quota.accountLabel}
|
|
441
|
+
WHEN ${providerAccounts.label} IN (${KIRO_DEFAULT_LABEL_IN}) THEN ${quota.accountLabel}
|
|
419
442
|
ELSE ${providerAccounts.label}
|
|
420
443
|
END`,
|
|
421
444
|
})
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persisted quota snapshots.
|
|
3
|
+
*
|
|
4
|
+
* The in-process quota cache is a short TTL and empty on a cold start, so
|
|
5
|
+
* right after a restart the console would render no balance for every account
|
|
6
|
+
* until the sweep re-reads them — while Buddy's usage snapshots survived via a
|
|
7
|
+
* table. This is the generic counterpart: every provider's last successful
|
|
8
|
+
* quota read is persisted on the account row, and the console falls back to it
|
|
9
|
+
* when the cache is cold. A failed fetch leaves the previous snapshot in place,
|
|
10
|
+
* mirroring `quota_plan`.
|
|
11
|
+
*/
|
|
12
|
+
import { eq, inArray } from "drizzle-orm";
|
|
13
|
+
import type { CartethyiaDatabase } from "../../persistence/db-handle";
|
|
14
|
+
import { providerAccounts, type ProviderQuotaResultSnapshot } from "../../persistence/schema";
|
|
15
|
+
import type { ProviderQuotaResult } from "./quota-contracts";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Parses the `quota_snapshot` column. Postgres' jsonb arrives as an object;
|
|
19
|
+
* SQLite's text-backed column arrives as a JSON string — accept both.
|
|
20
|
+
*/
|
|
21
|
+
function parseSnapshot(value: unknown): ProviderQuotaResultSnapshot | null {
|
|
22
|
+
if (value === null || value === undefined) return null;
|
|
23
|
+
let root: unknown = value;
|
|
24
|
+
if (typeof value === "string") {
|
|
25
|
+
try {
|
|
26
|
+
root = JSON.parse(value);
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (typeof root !== "object" || root === null || Array.isArray(root)) return null;
|
|
32
|
+
const rec = root as Record<string, unknown>;
|
|
33
|
+
const { quota, fetchedAt } = rec;
|
|
34
|
+
if (typeof quota !== "object" || quota === null || typeof fetchedAt !== "string") return null;
|
|
35
|
+
return { quota: quota as ProviderQuotaResult, fetchedAt };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Stores one successful quota read on the account row. Never throws. */
|
|
39
|
+
export async function persistQuotaSnapshot(
|
|
40
|
+
db: CartethyiaDatabase,
|
|
41
|
+
accountId: string,
|
|
42
|
+
quota: ProviderQuotaResult,
|
|
43
|
+
fetchedAt: Date = new Date(),
|
|
44
|
+
): Promise<void> {
|
|
45
|
+
try {
|
|
46
|
+
await db
|
|
47
|
+
.update(providerAccounts)
|
|
48
|
+
.set({ quotaSnapshot: { quota, fetchedAt: fetchedAt.toISOString() } })
|
|
49
|
+
.where(eq(providerAccounts.id, accountId));
|
|
50
|
+
} catch {
|
|
51
|
+
// A snapshot that cannot be stored is a stale display, not an error the
|
|
52
|
+
// caller must handle.
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Reads one account's last successful quota snapshot, if any. */
|
|
57
|
+
export async function readQuotaSnapshot(
|
|
58
|
+
db: CartethyiaDatabase,
|
|
59
|
+
accountId: string,
|
|
60
|
+
): Promise<ProviderQuotaResultSnapshot | null> {
|
|
61
|
+
try {
|
|
62
|
+
const rows = await db
|
|
63
|
+
.select({ quotaSnapshot: providerAccounts.quotaSnapshot })
|
|
64
|
+
.from(providerAccounts)
|
|
65
|
+
.where(eq(providerAccounts.id, accountId))
|
|
66
|
+
.limit(1);
|
|
67
|
+
return parseSnapshot(rows[0]?.quotaSnapshot);
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Batch read for one page render: one query for every account. */
|
|
74
|
+
export async function readQuotaSnapshots(
|
|
75
|
+
db: CartethyiaDatabase,
|
|
76
|
+
accountIds: readonly string[],
|
|
77
|
+
): Promise<Map<string, ProviderQuotaResultSnapshot>> {
|
|
78
|
+
const found = new Map<string, ProviderQuotaResultSnapshot>();
|
|
79
|
+
if (accountIds.length === 0) return found;
|
|
80
|
+
try {
|
|
81
|
+
const rows = await db
|
|
82
|
+
.select({ id: providerAccounts.id, quotaSnapshot: providerAccounts.quotaSnapshot })
|
|
83
|
+
.from(providerAccounts)
|
|
84
|
+
.where(inArray(providerAccounts.id, [...accountIds]));
|
|
85
|
+
for (const row of rows) {
|
|
86
|
+
const snapshot = parseSnapshot(row.quotaSnapshot);
|
|
87
|
+
if (snapshot) found.set(row.id, snapshot);
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
// A read failure yields a partial map; callers already treat a missing
|
|
91
|
+
// entry as "no snapshot".
|
|
92
|
+
}
|
|
93
|
+
return found;
|
|
94
|
+
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { asRecord } from "../../shared/coerce";
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* Declarative web-search provider adapters.
|
|
5
3
|
*
|
|
@@ -161,4 +159,4 @@ function jsonFromEventStream(raw: string): unknown {
|
|
|
161
159
|
|
|
162
160
|
}
|
|
163
161
|
|
|
164
|
-
export {
|
|
162
|
+
export { stringField };
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* normalization, which is what makes a provider's wire local to one spec.
|
|
10
10
|
*/
|
|
11
11
|
import type { SearchParams, SearchProviderSpec, SearchRequestInit } from "./search-provider";
|
|
12
|
-
import {
|
|
12
|
+
import { stringField } from "./search-provider";
|
|
13
|
+
import { asRecord } from "../../shared/coerce";
|
|
13
14
|
|
|
14
15
|
function domainFilterParams(filter: readonly string[] | undefined): {
|
|
15
16
|
includes: string[];
|
package/src/security/abuse.ts
CHANGED
|
@@ -57,7 +57,7 @@ const BAN_ROUTE_KEY = "#ban";
|
|
|
57
57
|
* `banThreshold` is both the identity-wide escalation threshold and the
|
|
58
58
|
* highest count either ring must keep observable.
|
|
59
59
|
*/
|
|
60
|
-
|
|
60
|
+
interface IpAbuseAttempt {
|
|
61
61
|
readonly identity: string;
|
|
62
62
|
readonly route: string;
|
|
63
63
|
readonly now: number;
|
|
@@ -67,7 +67,7 @@ export interface IpAbuseAttempt {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
/** What one admission step decided. */
|
|
70
|
-
|
|
70
|
+
interface IpAbuseOutcome {
|
|
71
71
|
/** The identity was already banned; nothing was incremented. */
|
|
72
72
|
readonly banned: boolean;
|
|
73
73
|
/** Attempts now in this route's window, including this one. `0` when banned. */
|
|
@@ -115,13 +115,13 @@ export interface IpAbuseStore {
|
|
|
115
115
|
checkAndRecord(attempt: IpAbuseAttempt): Promise<IpAbuseOutcome>;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
|
|
118
|
+
interface IpAbuseProtectionServiceOptions {
|
|
119
119
|
readonly maxRequestsPerWindow?: number;
|
|
120
120
|
readonly banThreshold?: number;
|
|
121
121
|
readonly banDurationMs?: number;
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
|
|
124
|
+
interface InMemoryIpAbuseStoreOptions {
|
|
125
125
|
readonly windowMs?: number;
|
|
126
126
|
readonly clock?: () => number;
|
|
127
127
|
/** Distinct (identity, route) keys tracked before the oldest is evicted. */
|
|
@@ -12,7 +12,7 @@ import { admissionMetricLabel, reasonToGatewayError } from "./reasons";
|
|
|
12
12
|
|
|
13
13
|
/** Persists reconciled lifetime token consumption for an API key. Injected so
|
|
14
14
|
* the in-memory admission service stays decoupled from Drizzle/Postgres. */
|
|
15
|
-
|
|
15
|
+
type LifetimeUsagePersister = (input: {
|
|
16
16
|
apiKeyId: string;
|
|
17
17
|
delta: number;
|
|
18
18
|
}) => Promise<void>;
|
|
@@ -23,7 +23,7 @@ export type LifetimeUsagePersister = (input: {
|
|
|
23
23
|
* counter store only when it is about to seed a missing lifetime counter, so
|
|
24
24
|
* the ≤3s-stale snapshot value cannot freeze a low baseline in place. Injected
|
|
25
25
|
* so the admission module stays decoupled from Drizzle/Postgres. */
|
|
26
|
-
|
|
26
|
+
type FreshLifetimeConsumedReader = (apiKeyId: string) => Promise<number | undefined>;
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
export class ApiKeyAdmissionService {
|
|
@@ -1,61 +1,61 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Lease and counter lifetimes, and the calendar bucket keys they key on.
|
|
3
|
-
*
|
|
4
|
-
* These live together because they are one arithmetic problem: a counter
|
|
5
|
-
* must outlive the lease that charges it, and a lease key must outlive the
|
|
6
|
-
* horizon the sweeper reads. Splitting them across files is how those
|
|
7
|
-
* invariants silently break, so the reasoning stays in one place.
|
|
8
|
-
*/
|
|
9
|
-
export const LEASE_TTL_MS = 3600_000;
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* The lease key's own Redis TTL is deliberately LONGER than the `expires_at`
|
|
13
|
-
* horizon the lease hash records.
|
|
14
|
-
*
|
|
15
|
-
* `expires_at` is the moment the sweeper becomes allowed to reclaim the lease.
|
|
16
|
-
* If the key TTL equalled that horizon, Redis would evict the hash at exactly
|
|
17
|
-
* the moment `reapIfExpired` wanted to read it: `HGET state` would return nil,
|
|
18
|
-
* the sweep would skip it, and a crash between reserve and release would strand
|
|
19
|
-
* the held concurrency / tenant-concurrency slot for as long as the counter
|
|
20
|
-
* keeps being refreshed — every later reserve re-arms the counter's own TTL, so
|
|
21
|
-
* a key still in use never recovers its leaked slot. The grace window keeps the
|
|
22
|
-
* hash readable for several sweep intervals past the horizon, so the recovery
|
|
23
|
-
* path actually runs.
|
|
24
|
-
*
|
|
25
|
-
* Exported so the invariant is asserted directly: a lease key must outlive the
|
|
26
|
-
* horizon it records by more than one sweep interval (`sweepLeases` runs every
|
|
27
|
-
* 30s, wired in `runtime/dependencies.ts`).
|
|
28
|
-
*/
|
|
29
|
-
const LEASE_TTL_GRACE_SECONDS = 120;
|
|
30
|
-
/** Seconds after which a lease hash is reapable (`expires_at` horizon). */
|
|
31
|
-
export const LEASE_REAP_HORIZON_SECONDS = Math.ceil(LEASE_TTL_MS / 1000);
|
|
32
|
-
/** Seconds the lease hash itself lives; must exceed the reap horizon. */
|
|
33
|
-
export const LEASE_KEY_TTL_SECONDS = LEASE_REAP_HORIZON_SECONDS + LEASE_TTL_GRACE_SECONDS;
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* TTLs for the token-budget counters, in seconds.
|
|
37
|
-
*
|
|
38
|
-
* Daily lives just past two days (a day bucket plus slack to cover a request
|
|
39
|
-
* straddling midnight); monthly and lifetime share one 35-day window — long
|
|
40
|
-
* enough that a month bucket is never evicted mid-month, and it is the *only*
|
|
41
|
-
* thing bounding `admission:lifetime:*`, which has no natural bucket to expire
|
|
42
|
-
* on. Every writer below must therefore carry its own TTL: a plain `SET`
|
|
43
|
-
* without one creates a key that outlives every bucket it feeds.
|
|
44
|
-
*/
|
|
45
|
-
export const DAILY_COUNTER_TTL_SECONDS = 172800;
|
|
46
|
-
export const MONTHLY_COUNTER_TTL_SECONDS = 3024000;
|
|
47
|
-
export const LIFETIME_COUNTER_TTL_SECONDS = 3024000;
|
|
48
|
-
/** RPM window plus slack; concurrency slots are held for the request duration. */
|
|
49
|
-
export const RPM_WINDOW_TTL_SECONDS = 65;
|
|
50
|
-
// Must exceed the lease's own lifetime (`LEASE_TTL_MS` + grace), otherwise a
|
|
51
|
-
// request held longer than this lets its concurrency counter expire underneath
|
|
52
|
-
// a still-live lease and a later reserve INCRs from zero. Derived rather than
|
|
53
|
-
// hard-coded so raising the lease TTL cannot silently reopen the hole.
|
|
54
|
-
export const CONCURRENCY_TTL_SECONDS = LEASE_KEY_TTL_SECONDS;
|
|
55
|
-
|
|
56
|
-
export function dailyBucket(now: number): string {
|
|
57
|
-
return new Date(now).toISOString().slice(0, 10);
|
|
58
|
-
}
|
|
59
|
-
export function monthlyBucket(now: number): string {
|
|
60
|
-
return new Date(now).toISOString().slice(0, 7);
|
|
61
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Lease and counter lifetimes, and the calendar bucket keys they key on.
|
|
3
|
+
*
|
|
4
|
+
* These live together because they are one arithmetic problem: a counter
|
|
5
|
+
* must outlive the lease that charges it, and a lease key must outlive the
|
|
6
|
+
* horizon the sweeper reads. Splitting them across files is how those
|
|
7
|
+
* invariants silently break, so the reasoning stays in one place.
|
|
8
|
+
*/
|
|
9
|
+
export const LEASE_TTL_MS = 3600_000;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The lease key's own Redis TTL is deliberately LONGER than the `expires_at`
|
|
13
|
+
* horizon the lease hash records.
|
|
14
|
+
*
|
|
15
|
+
* `expires_at` is the moment the sweeper becomes allowed to reclaim the lease.
|
|
16
|
+
* If the key TTL equalled that horizon, Redis would evict the hash at exactly
|
|
17
|
+
* the moment `reapIfExpired` wanted to read it: `HGET state` would return nil,
|
|
18
|
+
* the sweep would skip it, and a crash between reserve and release would strand
|
|
19
|
+
* the held concurrency / tenant-concurrency slot for as long as the counter
|
|
20
|
+
* keeps being refreshed — every later reserve re-arms the counter's own TTL, so
|
|
21
|
+
* a key still in use never recovers its leaked slot. The grace window keeps the
|
|
22
|
+
* hash readable for several sweep intervals past the horizon, so the recovery
|
|
23
|
+
* path actually runs.
|
|
24
|
+
*
|
|
25
|
+
* Exported so the invariant is asserted directly: a lease key must outlive the
|
|
26
|
+
* horizon it records by more than one sweep interval (`sweepLeases` runs every
|
|
27
|
+
* 30s, wired in `runtime/dependencies.ts`).
|
|
28
|
+
*/
|
|
29
|
+
const LEASE_TTL_GRACE_SECONDS = 120;
|
|
30
|
+
/** Seconds after which a lease hash is reapable (`expires_at` horizon). */
|
|
31
|
+
export const LEASE_REAP_HORIZON_SECONDS = Math.ceil(LEASE_TTL_MS / 1000);
|
|
32
|
+
/** Seconds the lease hash itself lives; must exceed the reap horizon. */
|
|
33
|
+
export const LEASE_KEY_TTL_SECONDS = LEASE_REAP_HORIZON_SECONDS + LEASE_TTL_GRACE_SECONDS;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* TTLs for the token-budget counters, in seconds.
|
|
37
|
+
*
|
|
38
|
+
* Daily lives just past two days (a day bucket plus slack to cover a request
|
|
39
|
+
* straddling midnight); monthly and lifetime share one 35-day window — long
|
|
40
|
+
* enough that a month bucket is never evicted mid-month, and it is the *only*
|
|
41
|
+
* thing bounding `admission:lifetime:*`, which has no natural bucket to expire
|
|
42
|
+
* on. Every writer below must therefore carry its own TTL: a plain `SET`
|
|
43
|
+
* without one creates a key that outlives every bucket it feeds.
|
|
44
|
+
*/
|
|
45
|
+
export const DAILY_COUNTER_TTL_SECONDS = 172800;
|
|
46
|
+
export const MONTHLY_COUNTER_TTL_SECONDS = 3024000;
|
|
47
|
+
export const LIFETIME_COUNTER_TTL_SECONDS = 3024000;
|
|
48
|
+
/** RPM window plus slack; concurrency slots are held for the request duration. */
|
|
49
|
+
export const RPM_WINDOW_TTL_SECONDS = 65;
|
|
50
|
+
// Must exceed the lease's own lifetime (`LEASE_TTL_MS` + grace), otherwise a
|
|
51
|
+
// request held longer than this lets its concurrency counter expire underneath
|
|
52
|
+
// a still-live lease and a later reserve INCRs from zero. Derived rather than
|
|
53
|
+
// hard-coded so raising the lease TTL cannot silently reopen the hole.
|
|
54
|
+
export const CONCURRENCY_TTL_SECONDS = LEASE_KEY_TTL_SECONDS;
|
|
55
|
+
|
|
56
|
+
export function dailyBucket(now: number): string {
|
|
57
|
+
return new Date(now).toISOString().slice(0, 10);
|
|
58
|
+
}
|
|
59
|
+
export function monthlyBucket(now: number): string {
|
|
60
|
+
return new Date(now).toISOString().slice(0, 7);
|
|
61
|
+
}
|
|
@@ -118,7 +118,7 @@ function freezeList(
|
|
|
118
118
|
return arr;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
|
|
121
|
+
function freezeSnapshot(s: ApiKeyAuthorizationSnapshot): ApiKeyAuthorizationSnapshot {
|
|
122
122
|
const model_list = freezeList(s.model_list);
|
|
123
123
|
const model_access_mode: ApiKeyModelAccessMode =
|
|
124
124
|
s.model_access_mode === "blacklist" ? "blacklist" : "whitelist";
|
|
@@ -363,6 +363,12 @@ export async function resolveApiKeyAuthorization(
|
|
|
363
363
|
// keep the issued row values forever. Reading the parent here makes denylist /
|
|
364
364
|
// one-time / recurring edits apply on the next cache miss.
|
|
365
365
|
const policy = parent ?? row;
|
|
366
|
+
// Share-template concurrency scope decides whether the family shares one
|
|
367
|
+
// admission counter (default `family`) or each recipient has its own
|
|
368
|
+
// (`per_key`). In `family` mode the child participates in the parent's
|
|
369
|
+
// counter namespace; in `per_key` mode the child is its own admission
|
|
370
|
+
// identity and carries its own cap.
|
|
371
|
+
const perKeyConcurrency = parent !== undefined && row.concurrencyScope === "per_key";
|
|
366
372
|
// Only a `share` parent can have children, and children are the only thing
|
|
367
373
|
// the SUM below can find. Every other key ran an aggregate that was certain
|
|
368
374
|
// to return 0 — one extra Postgres round-trip on the auth path for nothing.
|
|
@@ -381,8 +387,10 @@ export async function resolveApiKeyAuthorization(
|
|
|
381
387
|
api_key_id: row.id,
|
|
382
388
|
tenant_id: row.tenantId,
|
|
383
389
|
// Family quota / concurrency share one admission counter namespace so two
|
|
384
|
-
// recipients cannot each burn a full lifetime/daily budget.
|
|
385
|
-
|
|
390
|
+
// recipients cannot each burn a full lifetime/daily budget. In `per_key`
|
|
391
|
+
// concurrency mode the child is its own admission identity so its cap and
|
|
392
|
+
// counter are independent.
|
|
393
|
+
...(parent && !perKeyConcurrency ? { admission_identity: parent.id } : {}),
|
|
386
394
|
...(policy.modelList ? { model_list: policy.modelList as string[] } : {}),
|
|
387
395
|
model_access_mode: policy.modelAccessMode ?? "whitelist",
|
|
388
396
|
...(policy.clientRouterDenylist
|
|
@@ -393,7 +401,11 @@ export async function resolveApiKeyAuthorization(
|
|
|
393
401
|
...(policy.monthlyTokenLimit != null ? { monthly_tokens: policy.monthlyTokenLimit } : {}),
|
|
394
402
|
...(policy.lifetimeTokenBudget != null ? { lifetime_token_budget: policy.lifetimeTokenBudget } : {}),
|
|
395
403
|
lifetime_tokens_consumed: consumed,
|
|
396
|
-
|
|
404
|
+
// `per_key` mode: the child carries its own cap (copied at issue); family
|
|
405
|
+
// mode inherits the parent's cap.
|
|
406
|
+
...((perKeyConcurrency ? row.maxConcurrentRequests : policy.maxConcurrentRequests) != null
|
|
407
|
+
? { max_concurrent: perKeyConcurrency ? row.maxConcurrentRequests : policy.maxConcurrentRequests }
|
|
408
|
+
: {}),
|
|
397
409
|
scopes,
|
|
398
410
|
});
|
|
399
411
|
|
|
@@ -21,7 +21,7 @@ import { getHeader as readRecordHeader } from "../protocol/canonical-model";
|
|
|
21
21
|
* `id` is the stable token an operator stores in a key's denylist, so it is
|
|
22
22
|
* part of the persisted contract and must not be renamed casually.
|
|
23
23
|
*/
|
|
24
|
-
|
|
24
|
+
interface ClientRouterDefinition {
|
|
25
25
|
readonly id: string;
|
|
26
26
|
/** Human label for the dashboard. */
|
|
27
27
|
readonly label: string;
|
|
@@ -40,7 +40,7 @@ export interface ClientRouterDefinition {
|
|
|
40
40
|
* safe for a uniquely named header; a header whose value distinguishes it must
|
|
41
41
|
* name a matcher instead.
|
|
42
42
|
*/
|
|
43
|
-
|
|
43
|
+
interface ClientRouterSignal {
|
|
44
44
|
/** Header name, lowercase. */
|
|
45
45
|
readonly field: string;
|
|
46
46
|
/** Human description shown when the signal matches. */
|
|
@@ -114,14 +114,14 @@ export const CLIENT_ROUTERS: readonly ClientRouterDefinition[] = [
|
|
|
114
114
|
const LEGACY_CLIENT_ROUTER_IDS: Readonly<Record<string, string>> = { omniroute: "9router" };
|
|
115
115
|
|
|
116
116
|
/** A signal that matched, with the product it belongs to. */
|
|
117
|
-
|
|
117
|
+
interface MatchedClientSignal {
|
|
118
118
|
readonly field: string;
|
|
119
119
|
readonly description: string;
|
|
120
120
|
/** The observed value, capped, for the operator's audit trail. */
|
|
121
121
|
readonly value: string;
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
|
|
124
|
+
interface ClientRouterMatch {
|
|
125
125
|
readonly routerId: string;
|
|
126
126
|
readonly label: string;
|
|
127
127
|
readonly signals: readonly MatchedClientSignal[];
|
|
@@ -22,7 +22,7 @@ import { isRecord } from "../protocol/codecs";
|
|
|
22
22
|
const TEXT_DECODER = new TextDecoder();
|
|
23
23
|
|
|
24
24
|
/** Decodes a stored secret: UTF-8 bytes or a string, trimmed. */
|
|
25
|
-
|
|
25
|
+
function decodeSecretText(secret: Uint8Array | string): string {
|
|
26
26
|
const raw = typeof secret === "string" ? secret : TEXT_DECODER.decode(secret);
|
|
27
27
|
return raw.trim();
|
|
28
28
|
}
|
|
@@ -48,7 +48,7 @@ function fieldString(record: Record<string, unknown>, key: string): string {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
/** `{accessToken, orgId?}` — the Kilo Code envelope (org scoping is optional). */
|
|
51
|
-
|
|
51
|
+
interface KiloEnvelope {
|
|
52
52
|
readonly accessToken: string;
|
|
53
53
|
/** Organization the gateway scopes requests to; absent when the profile omits one. */
|
|
54
54
|
readonly orgId?: string;
|
|
@@ -79,7 +79,7 @@ export function decodeKiloCredential(secret: Uint8Array | string): KiloEnvelope
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
/** `{oauthAccessToken, apiKey}` — the Muse Code (Meta) envelope. */
|
|
82
|
-
|
|
82
|
+
interface MuseEnvelope {
|
|
83
83
|
readonly oauthAccessToken: string;
|
|
84
84
|
readonly apiKey: string;
|
|
85
85
|
}
|