cartethyia 2.7.9 → 2.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +6 -5
- package/dist/dashboard/assets/{BannedPage-BxoG1MrL.js → BannedPage-Dq530awD.js} +1 -1
- package/dist/dashboard/assets/BannedPage-Dq530awD.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-ixwgKk53.js → CliToolDetailPage-DZzaS-7S.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-DZzaS-7S.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-BmttNV3U.js → CliToolsPage-DaZZ3cqP.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-DaZZ3cqP.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-BE3qwwfC.js → CombosPage-BlC_xdkX.js} +1 -1
- package/dist/dashboard/assets/CombosPage-BlC_xdkX.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-CRZdmMIX.js → ConsoleLogPage-D-rqeVtG.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-D-rqeVtG.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-Dv_FJ6Er.js → CustomizationPage-7xT5yg2E.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-7xT5yg2E.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-Clq8E9cW.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-Clq8E9cW.js.gz +0 -0
- package/dist/dashboard/assets/{InjectSystemPromptPanel-BOoO8rpz.js → InjectSystemPromptPanel-6Teoq9Se.js} +2 -2
- package/dist/dashboard/assets/InjectSystemPromptPanel-6Teoq9Se.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-ChJuNNVN.js → LoginPage-CZuI0MAR.js} +1 -1
- package/dist/dashboard/assets/LoginPage-CZuI0MAR.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-BZAJtPg1.js → ModelPicker-C1hNTlkr.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-C1hNTlkr.js.gz +0 -0
- package/dist/dashboard/assets/{OverviewPage-BE40s3u5.js → OverviewPage-0yfhSI1C.js} +1 -1
- package/dist/dashboard/assets/OverviewPage-0yfhSI1C.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-xqTxdQ4c.js +8 -0
- package/dist/dashboard/assets/ProviderDetailPage-xqTxdQ4c.js.gz +0 -0
- package/dist/dashboard/assets/{ProviderIcon-CQ6bPUf_.js → ProviderIcon-Ddhm1InL.js} +1 -1
- package/dist/dashboard/assets/ProviderIcon-Ddhm1InL.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-C3fYuu6i.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-C3fYuu6i.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-CHtJtZ3u.js +9 -0
- package/dist/dashboard/assets/ProxyPage-CHtJtZ3u.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-D1RV_2BX.js +1 -0
- package/dist/dashboard/assets/QuotaPage-D1RV_2BX.js.gz +0 -0
- package/dist/dashboard/assets/{SettingsPage-C5HPWqEv.js → SettingsPage-BzZAvYSX.js} +1 -1
- package/dist/dashboard/assets/SettingsPage-BzZAvYSX.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-CFNcATJJ.js → SetupPage--s-EfEIL.js} +1 -1
- package/dist/dashboard/assets/SetupPage--s-EfEIL.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-YoD4tY5l.js → StudioPage-BjXFGSK4.js} +1 -1
- package/dist/dashboard/assets/StudioPage-BjXFGSK4.js.gz +0 -0
- package/dist/dashboard/assets/{UsagePage-DxjaAhb_.js → UsagePage-CP8F8BN7.js} +1 -1
- package/dist/dashboard/assets/UsagePage-CP8F8BN7.js.gz +0 -0
- package/dist/dashboard/assets/{api-keys-BTlkq_Uk.js → api-keys-2hc12Uz2.js} +1 -1
- package/dist/dashboard/assets/api-keys-2hc12Uz2.js.gz +0 -0
- package/dist/dashboard/assets/app-DGXbNfAR.css +2 -0
- package/dist/dashboard/assets/app-DGXbNfAR.css.gz +0 -0
- package/dist/dashboard/assets/assert-BMcj1xZE.js +1 -0
- package/dist/dashboard/assets/assert-BMcj1xZE.js.gz +0 -0
- package/dist/dashboard/assets/{badge-DdbT_7Am.js → badge-DkRUuVfz.js} +1 -1
- package/dist/dashboard/assets/badge-DkRUuVfz.js.gz +0 -0
- package/dist/dashboard/assets/{banner-CsGN-nbm.js → banner-CDjW841Q.js} +1 -1
- package/dist/dashboard/assets/banner-CDjW841Q.js.gz +0 -0
- package/dist/dashboard/assets/{card-mgZ_TLLZ.js → card-Bto5UlL9.js} +1 -1
- package/dist/dashboard/assets/card-Bto5UlL9.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-A1-v8-03.js → cli-tools-DaH6g9hL.js} +1 -1
- package/dist/dashboard/assets/cli-tools-DaH6g9hL.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-CJ4Zty89.js → clipboard-button-BYKBCzjk.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-BYKBCzjk.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-B5L2jPQ_.js → dialog-DL0Zftwv.js} +1 -1
- package/dist/dashboard/assets/dialog-DL0Zftwv.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-DkjUouAj.js → drawer-Dpx_UXaS.js} +1 -1
- package/dist/dashboard/assets/drawer-Dpx_UXaS.js.gz +0 -0
- package/dist/dashboard/assets/{entry-B-LVLV6o.js → entry-BV7-1NoM.js} +1 -1
- package/dist/dashboard/assets/entry-BV7-1NoM.js.gz +0 -0
- package/dist/dashboard/assets/{entry-CAhdYl3B.js → entry-CGvdNU6r.js} +1 -1
- package/dist/dashboard/assets/entry-CGvdNU6r.js.gz +0 -0
- package/dist/dashboard/assets/entry-DScKYvZj.js +4 -0
- package/dist/dashboard/assets/entry-DScKYvZj.js.gz +0 -0
- package/dist/dashboard/assets/{formatters-CrsdLVsn.js → formatters-BS3DZlZj.js} +1 -1
- package/dist/dashboard/assets/formatters-BS3DZlZj.js.gz +0 -0
- package/dist/dashboard/assets/{index-BU64Lqvp.js → index-SQnvvTWX.js} +2 -2
- package/dist/dashboard/assets/index-SQnvvTWX.js.gz +0 -0
- package/dist/dashboard/assets/{inline-B2aDLyyg.js → inline-Dl-cuLK0.js} +1 -1
- package/dist/dashboard/assets/inline-Dl-cuLK0.js.gz +0 -0
- package/dist/dashboard/assets/{input-BCVkdR9d.js → input-Bfx0EzRc.js} +1 -1
- package/dist/dashboard/assets/input-Bfx0EzRc.js.gz +0 -0
- package/dist/dashboard/assets/{layout-DNcAdMK-.js → layout-BxhbQ_JS.js} +1 -1
- package/dist/dashboard/assets/layout-BxhbQ_JS.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-DgrIH0hw.js → list-row-D49Y4ptn.js} +1 -1
- package/dist/dashboard/assets/list-row-D49Y4ptn.js.gz +0 -0
- package/dist/dashboard/assets/{live-Cae01iu8.js → live-CbVTdX9V.js} +1 -1
- package/dist/dashboard/assets/live-CbVTdX9V.js.gz +0 -0
- package/dist/dashboard/assets/{network-CRib7H_2.js → network-JGvDveof.js} +1 -1
- package/dist/dashboard/assets/network-JGvDveof.js.gz +0 -0
- package/dist/dashboard/assets/{numbers-BKQE8Xpz.js → numbers-Dw6kiew-.js} +1 -1
- package/dist/dashboard/assets/numbers-Dw6kiew-.js.gz +0 -0
- package/dist/dashboard/assets/{pagination-ecPvG_8g.js → pagination-iUoI8GeE.js} +1 -1
- package/dist/dashboard/assets/pagination-iUoI8GeE.js.gz +0 -0
- package/dist/dashboard/assets/providers/sensenova-intl.png +0 -0
- package/dist/dashboard/assets/providers/sensenova.png +0 -0
- package/dist/dashboard/assets/providers/zro.svg +3 -0
- package/dist/dashboard/assets/providers/zro.svg.gz +0 -0
- package/dist/dashboard/assets/quota-DiPD6UKH.js +1 -0
- package/dist/dashboard/assets/quota-DiPD6UKH.js.gz +0 -0
- package/dist/dashboard/assets/{routing-Ckm1UXuC.js → routing-8jZr-3WE.js} +1 -1
- package/dist/dashboard/assets/routing-8jZr-3WE.js.gz +0 -0
- package/dist/dashboard/assets/{select-ChoFWyKz.js → select-oO1FxMVb.js} +2 -2
- package/dist/dashboard/assets/select-oO1FxMVb.js.gz +0 -0
- package/dist/dashboard/assets/{settings-CKjFPlKW.js → settings-CLr3kdIx.js} +1 -1
- package/dist/dashboard/assets/settings-CLr3kdIx.js.gz +0 -0
- package/dist/dashboard/assets/{stack-DWCwU3fh.js → stack-Y3pDteZr.js} +1 -1
- package/dist/dashboard/assets/stack-Y3pDteZr.js.gz +0 -0
- package/dist/dashboard/assets/{state-Cg3VL5J_.js → state-CArX_Iak.js} +1 -1
- package/dist/dashboard/assets/state-CArX_Iak.js.gz +0 -0
- package/dist/dashboard/assets/{switch-Db70BMix.js → switch-DwlwmDO-.js} +1 -1
- package/dist/dashboard/assets/switch-DwlwmDO-.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-BqdE0LuM.js → useMutation-DJ9GYEgf.js} +1 -1
- package/dist/dashboard/assets/useMutation-DJ9GYEgf.js.gz +0 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +54 -53
- package/migrations/full-postgres/0046_add_accounts_quota_plan_fetched_at.sql +7 -0
- package/migrations/full-postgres/0047_add_provider_device_flow_proxy.sql +17 -0
- package/migrations/lite-sqlite3/0046_add_accounts_quota_plan_fetched_at.sql +4 -0
- package/migrations/lite-sqlite3/0047_add_provider_device_flow_proxy.sql +19 -0
- package/package.json +1 -1
- package/src/boot/app.ts +11 -14
- package/src/boot/dependencies.ts +5 -7
- package/src/boot/flush.ts +20 -0
- package/src/boot/lifecycle.ts +7 -5
- package/src/config/providers.ts +6 -5
- package/src/config/routing.ts +26 -0
- package/src/config/spec-metadata.ts +72 -0
- package/src/config/spec.ts +28 -8
- package/src/console/api-keys/contracts.ts +12 -1
- package/src/console/api-keys/routes.ts +40 -28
- package/src/console/auth/account-reset.ts +7 -9
- package/src/console/backup/contracts.ts +4 -0
- package/src/console/backup/export-endpoint.ts +7 -5
- package/src/console/backup/nine-router.ts +71 -10
- package/src/console/backup/service.ts +8 -1
- package/src/console/cli-tools/service.ts +6 -12
- package/src/console/console-router-guards.ts +23 -16
- package/src/console/console-router.ts +1 -1
- package/src/console/domain-registration.ts +7 -4
- package/src/console/inferhub/client.ts +1 -1
- package/src/console/inferhub/normalize.ts +24 -28
- package/src/console/inferhub/types.ts +3 -0
- package/src/console/ip-bans/routes.ts +7 -13
- package/src/console/observability/live.ts +2 -2
- package/src/console/observability/store.ts +0 -53
- package/src/console/providers/catalog/contracts.ts +9 -3
- package/src/console/providers/catalog/model-operations.ts +2 -3
- package/src/console/providers/catalog/provider-operations.ts +1 -1
- package/src/console/providers/catalog/routes.ts +3 -2
- package/src/console/providers/catalog/store.ts +1914 -1902
- package/src/console/providers/detail/routes.ts +1 -0
- package/src/console/providers/detail/store.ts +4 -0
- package/src/console/providers/oauth/callback-listener.ts +30 -11
- package/src/console/providers/oauth/device-egress.ts +123 -0
- package/src/console/providers/oauth/routes.ts +123 -16
- package/src/console/quota/account-quota-global.ts +14 -24
- package/src/console/quota/account-quota-shared.ts +49 -0
- package/src/console/quota/account-quota-tenant.ts +11 -23
- package/src/console/quota/account-test-egress.ts +2 -2
- package/src/console/quota/daily-growth-job.ts +1 -1
- package/src/console/quota/quota-view.ts +19 -3
- package/src/console/routing/pools/contracts.ts +1 -1
- package/src/console/routing/pools/operations.ts +32 -47
- package/src/console/routing/pools/probe-verdict.ts +2 -2
- package/src/console/routing/pools/relay-deploy.ts +1 -1
- package/src/console/routing/pools/store.ts +4 -4
- package/src/console/shared/errors.ts +33 -6
- package/src/main.ts +2 -3
- package/src/network/client.ts +67 -0
- package/src/network/egress/bidi-session.ts +322 -0
- package/src/network/egress/dial.ts +201 -0
- package/src/network/egress/factory.ts +191 -0
- package/src/network/{outbound-fetch.ts → egress/fetch.ts} +195 -29
- package/src/network/egress/tls-profile.ts +123 -0
- package/src/network/egress/ws.ts +175 -0
- package/src/network/pool/{byte-accounting.ts → accounting.ts} +3 -2
- package/src/network/{types.ts → pool/agent-config.ts} +1 -1
- package/src/network/pool/agent.ts +2 -2
- package/src/network/{pool-cooldown-store.ts → pool/cooldown-store.ts} +95 -20
- package/src/network/{pool-health-machine.ts → pool/health.ts} +205 -6
- package/src/network/pool/resolver.ts +10 -849
- package/src/network/pool/selector.ts +184 -41
- package/src/network/ssrf.ts +7 -7
- package/src/network/{retry.ts → util/retry.ts} +1 -1
- package/src/network/{rate-limit-verdict.ts → verdict.ts} +1 -1
- package/src/observability/event-log.ts +7 -361
- package/src/observability/performance-metrics.ts +9 -0
- package/src/observability/runtime-metrics.ts +14 -9
- package/src/observability/telemetry-buffer.ts +16 -26
- package/src/observability/telemetry-reconciliation.ts +9 -37
- package/src/observability/telemetry-row.ts +125 -0
- package/src/persistence/db-mode.ts +4 -1
- package/src/persistence/dialect.ts +18 -0
- package/src/persistence/migrate.ts +6 -3
- package/src/persistence/schema.ts +17 -0
- package/src/persistence/ttl-cache.ts +21 -3
- package/src/protocol/codecs.ts +59 -2
- package/src/protocol/dsml.ts +34 -86
- package/src/protocol/markup-stream-parser.ts +195 -0
- package/src/protocol/reasoning-text.ts +19 -0
- package/src/protocol/request/chat.ts +10 -10
- package/src/protocol/request/gemini.ts +8 -2
- package/src/protocol/request/messages.ts +34 -22
- package/src/protocol/response/chat.ts +12 -20
- package/src/protocol/response/messages.ts +34 -7
- package/src/protocol/response/omp-tool-call-markup.ts +43 -86
- package/src/protocol/response/responses.ts +18 -2
- package/src/protocol/stop-reason.ts +23 -1
- package/src/protocol/think-split.ts +112 -0
- package/src/protocol/translation/dialect/qwen.ts +26 -58
- package/src/protocol/translation/quirks.ts +15 -0
- package/src/protocol/upstream/send-claude-messages.ts +2 -2
- package/src/providers/adapters/compatible-adapter.ts +76 -12
- package/src/providers/authentication/oauth-client.ts +40 -6
- package/src/providers/authentication/oauth-device-flow.ts +105 -11
- package/src/providers/authentication/oauth-flow-store.ts +82 -1
- package/src/providers/authentication/oauth-refresh-service.ts +766 -758
- package/src/providers/configured-provider.ts +148 -0
- package/src/providers/default-registry.ts +137 -36
- package/src/providers/discovery/discovery-types.ts +7 -0
- package/src/providers/discovery/probe-phases.ts +20 -6
- package/src/providers/discovery/probing-service.ts +22 -2
- package/src/providers/integrations/alibabacp/alibabacp-models.ts +1 -2
- package/src/providers/integrations/{dashscope-models.ts → alibabacp/dashscope-models.ts} +2 -2
- package/src/providers/integrations/antigravity/antigravity-protocol.ts +16 -1
- package/src/providers/integrations/buddy/account.ts +969 -0
- package/src/providers/integrations/buddy/capabilities.ts +12 -12
- package/src/providers/integrations/buddy/{buddy-catalog-shared.ts → catalog.ts} +198 -136
- package/src/providers/integrations/buddy/{buddy-chat-shared.ts → chat.ts} +176 -20
- package/src/providers/integrations/buddy/codebuddy/index.ts +429 -0
- package/src/providers/integrations/buddy/codebuddy/shared.ts +41 -26
- package/src/providers/integrations/buddy/headers.ts +311 -0
- package/src/providers/integrations/buddy/lifecycle.ts +1018 -0
- package/src/providers/integrations/buddy/{buddy-media.ts → media.ts} +1 -1
- package/src/providers/integrations/claude/claude-quota.ts +14 -17
- package/src/providers/integrations/cline/cline-oauth.ts +10 -5
- package/src/providers/integrations/cline/cline.ts +5 -6
- package/src/providers/integrations/clixad/clixad-oauth.ts +98 -0
- package/src/providers/integrations/clixad/clixad-quota.ts +132 -0
- package/src/providers/integrations/clixad/provider.ts +145 -0
- package/src/providers/integrations/codex/codex-oauth.ts +8 -5
- package/src/providers/integrations/commandcode/quota.ts +22 -28
- package/src/providers/integrations/cursor/cursor-auth.ts +1 -9
- package/src/providers/integrations/cursor/cursor-exec.ts +9 -2
- package/src/providers/integrations/cursor/cursor-hardening.ts +279 -0
- package/src/providers/integrations/cursor/cursor-http2.ts +605 -6
- package/src/providers/integrations/cursor/cursor-request.ts +26 -12
- package/src/providers/integrations/cursor/cursor-shared.ts +8 -5
- package/src/providers/integrations/cursor/cursor.ts +574 -110
- package/src/providers/integrations/cursor/protocol/messages.ts +22 -29
- package/src/providers/integrations/cursor/protocol/protobuf-schema.ts +37 -0
- package/src/providers/integrations/deepai/deepai-catalog.ts +149 -0
- package/src/providers/integrations/deepai/deepai-common.ts +145 -0
- package/src/providers/integrations/deepai/deepai-oauth.ts +129 -0
- package/src/providers/integrations/deepai/deepai-quota.ts +97 -0
- package/src/providers/integrations/deepai/provider.ts +42 -0
- package/src/providers/integrations/devin/devin-auth.ts +7 -0
- package/src/providers/integrations/devin/protocol/devin-proto.ts +1 -27
- package/src/providers/integrations/factory/factory-oauth.ts +33 -51
- package/src/providers/integrations/freebuff/assets/official-system-manager.txt +88 -0
- package/src/providers/integrations/freebuff/assets/official-system-worker.txt +57 -0
- package/src/providers/integrations/freebuff/assets/official-tools.json +1070 -0
- package/src/providers/integrations/freebuff/freebuff-catalog.ts +120 -0
- package/src/providers/integrations/freebuff/freebuff-constants.ts +1233 -0
- package/src/providers/integrations/freebuff/freebuff-discovery.ts +108 -0
- package/src/providers/integrations/freebuff/freebuff-oauth.ts +240 -0
- package/src/providers/integrations/freebuff/freebuff-quota.ts +260 -0
- package/src/providers/integrations/freebuff/freebuff-virtual.ts +89 -0
- package/src/providers/integrations/freebuff/freebuff.ts +682 -0
- package/src/providers/integrations/freepi/freepi-catalog.ts +5 -0
- package/src/providers/integrations/freepi/freepi-discovery.ts +2 -2
- package/src/providers/integrations/freepi/freepi-headers.ts +2 -2
- package/src/providers/integrations/freepi/freepi-protocol.ts +11 -2
- package/src/providers/integrations/freepi/freepi-realm.ts +0 -3
- package/src/providers/integrations/freepi/freepi-session.ts +158 -0
- package/src/providers/integrations/freepi/freepi.ts +80 -54
- package/src/providers/integrations/github/github-discovery.ts +30 -13
- package/src/providers/integrations/github/github-oauth.ts +217 -44
- package/src/providers/integrations/github/github-quota.ts +22 -5
- package/src/providers/integrations/github/github.ts +40 -32
- package/src/providers/integrations/grok/grok-oauth.ts +52 -49
- package/src/providers/integrations/grok/grok-ratelimit-store.ts +10 -17
- package/src/providers/integrations/grok/grok-turn-index.ts +11 -16
- package/src/providers/integrations/hermes/hermes-oauth.ts +3 -3
- package/src/providers/integrations/{hermes.ts → hermes/hermes.ts} +42 -13
- package/src/providers/integrations/hermes/index.ts +2 -0
- package/src/providers/integrations/kilo/kilo-oauth.ts +4 -4
- package/src/providers/integrations/kilo/kilo.ts +1 -1
- package/src/providers/integrations/kimi/kimi-oauth.ts +3 -3
- package/src/providers/integrations/kiro/kiro-catalog.ts +17 -1
- package/src/providers/integrations/kiro/kiro-discovery.ts +10 -12
- package/src/providers/integrations/kiro/kiro-oauth.ts +2 -2
- package/src/providers/integrations/kiro/kiro-quota.ts +8 -4
- package/src/providers/integrations/kiro/kiro.ts +15 -19
- package/src/providers/integrations/muse/muse-oauth.ts +5 -5
- package/src/providers/integrations/ollamacloud/index.ts +2 -0
- package/src/providers/integrations/{ollamacloud-models.ts → ollamacloud/models.ts} +2 -2
- package/src/providers/integrations/{ollamacloud-quota.ts → ollamacloud/quota.ts} +3 -3
- package/src/providers/integrations/opencode/fingerprint.ts +1 -1
- package/src/providers/integrations/opencode/opencode-go-quota.ts +2 -8
- package/src/providers/integrations/openrouter/provider.ts +1 -1
- package/src/providers/integrations/perch/perch-credential.ts +25 -0
- package/src/providers/integrations/perch/perch-protocol.ts +0 -6
- package/src/providers/integrations/perch/perch-quota.ts +2 -16
- package/src/providers/integrations/perch/perch.ts +6 -16
- package/src/providers/integrations/qoder/claim-qoder-credits.ts +19 -60
- package/src/providers/integrations/qoder/qoder-adapter.ts +20 -125
- package/src/providers/integrations/qoder/qoder-auth.ts +109 -30
- package/src/providers/integrations/qoder/qoder-catalog.ts +21 -6
- package/src/providers/integrations/qoder/qoder-chat.ts +32 -29
- package/src/providers/integrations/qoder/qoder-checkin.ts +5 -5
- package/src/providers/integrations/qoder/qoder-cosy.ts +37 -9
- package/src/providers/integrations/qoder/qoder-oauth.ts +9 -2
- package/src/providers/integrations/qoder/qoder-realm.ts +13 -36
- package/src/providers/integrations/qoder/qoder-sse.ts +447 -68
- package/src/providers/integrations/qoder/qoder-types.ts +9 -0
- package/src/providers/integrations/qoder/qoder.ts +3 -26
- package/src/providers/integrations/sensenova/provider.ts +144 -0
- package/src/providers/integrations/trae/trae-protocol.ts +9 -5
- package/src/providers/integrations/trae/trae-quota.ts +21 -29
- package/src/providers/integrations/trae/trae-realm.ts +38 -0
- package/src/providers/integrations/trae/trae.ts +7 -24
- package/src/providers/integrations/vercel/provider.ts +117 -0
- package/src/providers/integrations/vercel/vercel-quota.ts +96 -0
- package/src/providers/integrations/xai/xai-oauth.ts +5 -5
- package/src/providers/integrations/zcode-start-plan/gifts.ts +1 -1
- package/src/providers/integrations/zcode-start-plan/oauth.ts +3 -3
- package/src/providers/integrations/zcode-start-plan/provider.ts +226 -23
- package/src/providers/integrations/zcode-start-plan/quota.ts +19 -2
- package/src/providers/integrations/zro/zro.ts +205 -0
- package/src/providers/operations/account-error-classification.ts +199 -307
- package/src/providers/operations/account-health-service.ts +82 -1
- package/src/providers/operations/account-refresh-status.ts +36 -37
- package/src/providers/operations/account-tier.ts +15 -16
- package/src/providers/operations/client-version-resolver.ts +1 -1
- package/src/providers/operations/client-versions.ts +100 -81
- package/src/providers/operations/cooldown-rules.ts +11 -16
- package/src/providers/operations/oauth-refresh-lead.ts +46 -22
- package/src/providers/operations/{model-catalog-cache.ts → provider-caches.ts} +68 -16
- package/src/providers/operations/provider-catalog-service.ts +1 -1
- package/src/providers/operations/provider-credential-service.ts +118 -51
- package/src/providers/operations/provider-rules/github.ts +48 -0
- package/src/providers/operations/provider-rules/grok.ts +33 -0
- package/src/providers/operations/provider-rules/inception.ts +49 -0
- package/src/providers/operations/provider-rules/index.ts +32 -0
- package/src/providers/operations/provider-rules/perch.ts +37 -0
- package/src/providers/operations/provider-rules/qoder.ts +70 -0
- package/src/providers/operations/provider-rules/trae.ts +81 -0
- package/src/providers/operations/session-resolution.ts +17 -1
- package/src/providers/provider-contracts.ts +6 -4
- package/src/providers/{provider-credential-resolver.ts → provider-credentials.ts} +40 -3
- package/src/providers/provider-metadata.ts +70 -10
- package/src/providers/provider-probe.ts +15 -1
- package/src/providers/provider-registry.ts +42 -33
- package/src/providers/quota/quota-contracts.ts +2 -0
- package/src/providers/quota/quota-limit-windows.ts +16 -5
- package/src/providers/quota/quota-support.ts +1 -1
- package/src/providers/quota/quota-window-parser.ts +90 -18
- package/src/providers/quota/refresh.ts +100 -21
- package/src/security/api-key-auth-cache.ts +1 -1
- package/src/security/api-key-auth.ts +6 -2
- package/src/security/ip-boundary.ts +1 -1
- package/src/transport/dispatch/attempt-finalize.ts +15 -14
- package/src/transport/dispatch/attempt-loop.ts +73 -19
- package/src/transport/dispatch/generation.ts +2 -8
- package/src/transport/dispatch/handler-deps.ts +1 -1
- package/src/transport/dispatch/leases.ts +1 -1
- package/src/transport/dispatch/native-shared.ts +25 -0
- package/src/transport/dispatch/proxy-request.ts +8 -46
- package/src/transport/dispatch/responses-compact.ts +2 -8
- package/src/transport/dispatch/retry-policy.ts +1 -1
- package/src/transport/dispatch/streaming/finalize.ts +10 -7
- package/src/transport/dispatch/streaming/stream.ts +9 -37
- package/src/transport/dispatch/systemone.ts +2 -8
- package/src/transport/dispatch/token-count.ts +2 -6
- package/src/transport/dispatch/upstream.ts +63 -0
- package/src/transport/drain-endpoint.ts +31 -4
- package/src/transport/failure-policy.ts +16 -30
- package/src/transport/guards/body-policy.ts +7 -1
- package/src/transport/guards/error-lifecycle.ts +13 -1
- package/src/transport/guards/request-context.ts +22 -14
- package/src/transport/request/preparer.ts +81 -71
- package/src/transport/routing/bounded-lru.ts +14 -0
- package/src/transport/routing/drain-reserve.ts +129 -0
- package/src/transport/routing/plan-gate.ts +7 -1
- package/src/transport/routing/round-robin.ts +60 -0
- package/src/transport/routing/route-catalog.ts +123 -111
- package/src/transport/routing/route-model.ts +51 -9
- package/src/transport/routing/router.ts +393 -32
- package/src/transport/shutdown-notice.ts +1 -1
- package/src/transport/surface/adapters.ts +11 -2
- package/src/transport/surface/chat/adapter.ts +2 -2
- package/src/transport/surface/chat/encode.ts +47 -4
- package/src/transport/surface/chat/parse.ts +2 -12
- package/src/transport/surface/completion.ts +44 -33
- package/src/transport/surface/content-parts.ts +21 -0
- package/src/transport/surface/messages/adapter.ts +7 -1
- package/src/transport/surface/messages/encode.ts +11 -3
- package/src/transport/surface/messages/parse.ts +2 -1
- package/src/transport/surface/messages/stream.ts +33 -17
- package/src/transport/surface/responses/encode.ts +22 -5
- package/src/transport/surface/responses/parse.ts +3 -11
- package/src/workers/buddy-growth-tasks.ts +8 -8
- package/src/workers/daily-checkin.ts +4 -4
- package/src/workers/quota-refresh-sweep.ts +1 -1
- package/src/workers/zcode-gift-claim.ts +1 -1
- package/dist/dashboard/assets/BannedPage-BxoG1MrL.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-ixwgKk53.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-BmttNV3U.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-BE3qwwfC.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-CRZdmMIX.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-Dv_FJ6Er.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js.gz +0 -0
- package/dist/dashboard/assets/InjectSystemPromptPanel-BOoO8rpz.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-ChJuNNVN.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-BZAJtPg1.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-BE40s3u5.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js +0 -8
- package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-CQ6bPUf_.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-BJ87Pp0U.js +0 -9
- package/dist/dashboard/assets/ProxyPage-BJ87Pp0U.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-gkSyRAKa.js +0 -1
- package/dist/dashboard/assets/QuotaPage-gkSyRAKa.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-C5HPWqEv.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-CFNcATJJ.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-YoD4tY5l.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-DxjaAhb_.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-BTlkq_Uk.js.gz +0 -0
- package/dist/dashboard/assets/app-BX2nnKqs.css +0 -2
- package/dist/dashboard/assets/app-BX2nnKqs.css.gz +0 -0
- package/dist/dashboard/assets/assert-CndthXFQ.js +0 -1
- package/dist/dashboard/assets/assert-CndthXFQ.js.gz +0 -0
- package/dist/dashboard/assets/badge-DdbT_7Am.js.gz +0 -0
- package/dist/dashboard/assets/banner-CsGN-nbm.js.gz +0 -0
- package/dist/dashboard/assets/card-mgZ_TLLZ.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-A1-v8-03.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-CJ4Zty89.js.gz +0 -0
- package/dist/dashboard/assets/dialog-B5L2jPQ_.js.gz +0 -0
- package/dist/dashboard/assets/drawer-DkjUouAj.js.gz +0 -0
- package/dist/dashboard/assets/entry-B-LVLV6o.js.gz +0 -0
- package/dist/dashboard/assets/entry-CAhdYl3B.js.gz +0 -0
- package/dist/dashboard/assets/entry-D2xD0Tmd.js +0 -4
- package/dist/dashboard/assets/entry-D2xD0Tmd.js.gz +0 -0
- package/dist/dashboard/assets/formatters-CrsdLVsn.js.gz +0 -0
- package/dist/dashboard/assets/index-BU64Lqvp.js.gz +0 -0
- package/dist/dashboard/assets/inline-B2aDLyyg.js.gz +0 -0
- package/dist/dashboard/assets/input-BCVkdR9d.js.gz +0 -0
- package/dist/dashboard/assets/layout-DNcAdMK-.js.gz +0 -0
- package/dist/dashboard/assets/list-row-DgrIH0hw.js.gz +0 -0
- package/dist/dashboard/assets/live-Cae01iu8.js.gz +0 -0
- package/dist/dashboard/assets/network-CRib7H_2.js.gz +0 -0
- package/dist/dashboard/assets/numbers-BKQE8Xpz.js.gz +0 -0
- package/dist/dashboard/assets/pagination-ecPvG_8g.js.gz +0 -0
- package/dist/dashboard/assets/quota-DJ1QlAOq.js +0 -1
- package/dist/dashboard/assets/quota-DJ1QlAOq.js.gz +0 -0
- package/dist/dashboard/assets/routing-Ckm1UXuC.js.gz +0 -0
- package/dist/dashboard/assets/select-ChoFWyKz.js.gz +0 -0
- package/dist/dashboard/assets/settings-CKjFPlKW.js.gz +0 -0
- package/dist/dashboard/assets/stack-DWCwU3fh.js.gz +0 -0
- package/dist/dashboard/assets/state-Cg3VL5J_.js.gz +0 -0
- package/dist/dashboard/assets/switch-Db70BMix.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-BqdE0LuM.js.gz +0 -0
- package/src/network/pool-health.ts +0 -210
- package/src/providers/bundled-discovery-providers.ts +0 -98
- package/src/providers/configured-openai-providers.ts +0 -120
- package/src/providers/integrations/buddy/buddy-checkin.ts +0 -521
- package/src/providers/integrations/buddy/buddy-cli-headers.ts +0 -76
- package/src/providers/integrations/buddy/buddy-conversation-headers.ts +0 -130
- package/src/providers/integrations/buddy/buddy-discovery-shared.ts +0 -234
- package/src/providers/integrations/buddy/buddy-growth-shared.ts +0 -496
- package/src/providers/integrations/buddy/buddy-identity.ts +0 -300
- package/src/providers/integrations/buddy/buddy-oauth-shared.ts +0 -341
- package/src/providers/integrations/buddy/buddy-quota-shared.ts +0 -417
- package/src/providers/integrations/buddy/buddy-server-tools.ts +0 -240
- package/src/providers/integrations/buddy/buddy-usage-shared.ts +0 -221
- package/src/providers/integrations/buddy/buddy-usage-store.ts +0 -181
- package/src/providers/integrations/buddy/codebuddy/cn-catalog.ts +0 -37
- package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +0 -185
- package/src/providers/integrations/buddy/codebuddy/oauth.ts +0 -47
- package/src/providers/integrations/buddy/codebuddy/provider.ts +0 -175
- package/src/providers/integrations/buddy/codebuddy/quota.ts +0 -154
- package/src/providers/integrations/qoder/chat-proto.ts +0 -328
- package/src/providers/integrations/qoder/qoder-grpc.ts +0 -871
- package/src/providers/integrations/qoder/qoder-ide.ts +0 -588
- package/src/providers/operations/model-discovery-cache.ts +0 -34
- package/src/providers/operations/probe-fetch.ts +0 -15
- package/src/providers/operations/provider-version-cache.ts +0 -30
- package/src/providers/provider-capabilities.ts +0 -33
- package/src/providers/provider-credential-envelope.ts +0 -43
- /package/dist/dashboard/assets/{app-B7kIWFm5.js → app-C0gTXEoN.js} +0 -0
- /package/dist/dashboard/assets/{app-B7kIWFm5.js.gz → app-C0gTXEoN.js.gz} +0 -0
- /package/src/network/{http2-bidi.ts → bidi.ts} +0 -0
- /package/src/network/pool/{pool-scripts.ts → scripts.ts} +0 -0
- /package/src/network/pool/{pool-state.ts → state.ts} +0 -0
- /package/src/network/{backoff.ts → util/backoff.ts} +0 -0
- /package/src/network/{deduplication.ts → util/dedupe.ts} +0 -0
- /package/src/network/{ip-utils.ts → util/ip.ts} +0 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Freebuff live model discovery — TypeScript, Freebuff provider.
|
|
2
|
+
//
|
|
3
|
+
// `GET /api/v1/freebuff/models` with catalog protocol 1 + desktop client. The
|
|
4
|
+
// response rows carry `m-<hex>` keys, `legacyDigests`, display names, and an
|
|
5
|
+
// ephemeral handle (`fbm1.*`). Discovery maps rows to ModelDefinitions: a key
|
|
6
|
+
// with a caller-facing virtual name is published under that name (see
|
|
7
|
+
// `freebuff-virtual.ts`), and an unnamed key keeps its raw id. The handle is
|
|
8
|
+
// NOT stored (it is single-fetch ephemeral and re-resolved per dispatch).
|
|
9
|
+
|
|
10
|
+
import type { ModelDefinition } from "../../provider-model-definition";
|
|
11
|
+
import { defineModel } from "../../provider-model-definition";
|
|
12
|
+
import type { ProviderModelDiscovery } from "../../discovery/discovery-types";
|
|
13
|
+
import {
|
|
14
|
+
FREEBUFF_API_HOST,
|
|
15
|
+
FREEBUFF_CATALOG_PROTOCOL_HEADER,
|
|
16
|
+
FREEBUFF_CATALOG_PROTOCOL_VERSION,
|
|
17
|
+
FREEBUFF_CHAT_COMPLETIONS_PATH,
|
|
18
|
+
FREEBUFF_CLIENT_HEADER,
|
|
19
|
+
FREEBUFF_CLIENT_HEADER_VALUE,
|
|
20
|
+
} from "./freebuff";
|
|
21
|
+
import { FREEBUFF_VIRTUAL_MODELS } from "./freebuff-virtual";
|
|
22
|
+
|
|
23
|
+
const KNOWN_PREMIUM_KEYS: ReadonlySet<string> = new Set([
|
|
24
|
+
"m-127314bb39", // DeepSeek V4.1 Flash Fast
|
|
25
|
+
"m-5a5d0e255e", // GPT-6 Luna
|
|
26
|
+
"m-ffcbc2bcf3", // Muse Spark 1.3
|
|
27
|
+
"m-7d6e1b3ae0", // GPT-6.1 Terra
|
|
28
|
+
"m-5b6a604eb6", // Claude Haiku 5.5 · 100k
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
const KNOWN_FREE_KEYS: ReadonlySet<string> = new Set([
|
|
32
|
+
"m-096e75164d", // DeepSeek Fast
|
|
33
|
+
"m-8db1ccbba6", // DeepSeek
|
|
34
|
+
"m-61c26317cc", // DeepSeek Slow
|
|
35
|
+
"m-9a7e098cc1", // Solar Pro 4
|
|
36
|
+
"m-69307952f8", // Solar Mini 4
|
|
37
|
+
"m-0921d81ce7", // Crouton Alpha
|
|
38
|
+
"m-02244092cb", // Glyph Cluster
|
|
39
|
+
"m-ddc687c50f", // Nemotron 3.5 Lightning
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
/** Virtual id by roster key: a key with a known name publishes under that name. */
|
|
43
|
+
const VIRTUAL_ID_BY_KEY: ReadonlyMap<string, string> = new Map(
|
|
44
|
+
FREEBUFF_VIRTUAL_MODELS.map((virtual) => [virtual.target, virtual.id]),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Live discovery over the static floor: `GET /api/v1/freebuff/models` answers
|
|
49
|
+
* with the account's current roster. A failed fetch returns the static roster
|
|
50
|
+
* (offline fallback) rather than raising.
|
|
51
|
+
*/
|
|
52
|
+
export function freebuffModelDiscovery(): ProviderModelDiscovery {
|
|
53
|
+
return async ({ credential, fetcher }) => {
|
|
54
|
+
if (credential.trim().length === 0) return null;
|
|
55
|
+
const fetchFn = fetcher ?? globalThis.fetch;
|
|
56
|
+
const res = await fetchFn(`${FREEBUFF_API_HOST}/api/v1/freebuff/models`, {
|
|
57
|
+
method: "GET",
|
|
58
|
+
headers: {
|
|
59
|
+
Authorization: `Bearer ${credential.trim()}`,
|
|
60
|
+
[FREEBUFF_CATALOG_PROTOCOL_HEADER]: FREEBUFF_CATALOG_PROTOCOL_VERSION,
|
|
61
|
+
[FREEBUFF_CLIENT_HEADER]: FREEBUFF_CLIENT_HEADER_VALUE,
|
|
62
|
+
},
|
|
63
|
+
signal: AbortSignal.timeout(15_000),
|
|
64
|
+
});
|
|
65
|
+
if (!res.ok) return null;
|
|
66
|
+
const body: unknown = await res.json().catch(() => null);
|
|
67
|
+
if (typeof body !== "object" || body === null || !Array.isArray((body as { rows?: unknown }).rows)) return null;
|
|
68
|
+
const rows = (body as { rows: unknown[] }).rows;
|
|
69
|
+
const models: ModelDefinition[] = [];
|
|
70
|
+
for (const row of rows) {
|
|
71
|
+
if (typeof row !== "object" || row === null) continue;
|
|
72
|
+
const r = row as Record<string, unknown>;
|
|
73
|
+
const key = typeof r["key"] === "string" ? r["key"] : undefined;
|
|
74
|
+
const access = typeof r["access"] === "string" ? r["access"] : undefined;
|
|
75
|
+
const premium = r["premium"] === true;
|
|
76
|
+
if (!key) continue;
|
|
77
|
+
const isPremium = premium || access === "locked" || KNOWN_PREMIUM_KEYS.has(key);
|
|
78
|
+
const isFree = KNOWN_FREE_KEYS.has(key) || access === "open";
|
|
79
|
+
// The server publishes the real context window, modality, and reasoning
|
|
80
|
+
// efforts per row — use them verbatim rather than a hardcoded guess.
|
|
81
|
+
const ctx = typeof r["contextWindow"] === "number" && r["contextWindow"] > 0 ? r["contextWindow"] : undefined;
|
|
82
|
+
const multimodal = r["multimodal"] === true;
|
|
83
|
+
const efforts = Array.isArray(r["efforts"]) && r["efforts"].every((e) => typeof e === "string")
|
|
84
|
+
? (r["efforts"] as string[])
|
|
85
|
+
: undefined;
|
|
86
|
+
models.push(
|
|
87
|
+
defineModel({
|
|
88
|
+
// A known roster key publishes under its caller-facing virtual name;
|
|
89
|
+
// an unnamed key keeps its raw id (still reachable, never renamed).
|
|
90
|
+
id: VIRTUAL_ID_BY_KEY.get(key) ?? key,
|
|
91
|
+
providerId: "freebuff",
|
|
92
|
+
wireFamily: "chat",
|
|
93
|
+
endpoint: FREEBUFF_CHAT_COMPLETIONS_PATH,
|
|
94
|
+
...(ctx !== undefined ? { ctx } : {}),
|
|
95
|
+
...(multimodal ? { vision: true } : {}),
|
|
96
|
+
reasoning: true,
|
|
97
|
+
...(efforts !== undefined ? { reasoningEfforts: efforts } : {}),
|
|
98
|
+
toolCall: true,
|
|
99
|
+
...(isFree ? { free: true, freeTier: true } : {}),
|
|
100
|
+
...(isPremium ? { proOnly: true } : {}),
|
|
101
|
+
}),
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
return models.length > 0 ? models : null;
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export { defineModel };
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// Freebuff OAuth client — TypeScript, Freebuff provider.
|
|
2
|
+
//
|
|
3
|
+
// Freebuff's login is a code flow, not standard OAuth2:
|
|
4
|
+
// 1. `POST /api/auth/cli/code` with `{fingerprintId}` → `{loginUrl,
|
|
5
|
+
// fingerprintHash, expiresAt, expiresInMs}`.
|
|
6
|
+
// 2. The operator opens `loginUrl` in a browser and signs in.
|
|
7
|
+
// 3. `GET /api/auth/cli/status?fingerprintId=...&fingerprintHash=...&expiresAt=...`
|
|
8
|
+
// → `{user:{authToken, id, email, name}}` once signed in.
|
|
9
|
+
//
|
|
10
|
+
// This maps onto the gateway's device-flow shape: `startDeviceAuth` issues the
|
|
11
|
+
// code (verificationUri = loginUrl), and `pollDeviceAuth` polls the status.
|
|
12
|
+
// On success we mint the Ed25519 device key (`POST /api/v1/freebuff/device-keys`)
|
|
13
|
+
// and persist `{accountId, device}` in `auth_state` so dispatch can sign.
|
|
14
|
+
|
|
15
|
+
import type {
|
|
16
|
+
OAuthDeviceFlowContext,
|
|
17
|
+
OAuthDevicePollResult,
|
|
18
|
+
OAuthDeviceStartResult,
|
|
19
|
+
OAuthExchangeResult,
|
|
20
|
+
OAuthImportInput,
|
|
21
|
+
OAuthLoginClient,
|
|
22
|
+
} from "../../authentication/oauth-flow-store";
|
|
23
|
+
import { isRecord } from "../../../protocol/codecs";
|
|
24
|
+
import { GatewayError } from "../../../protocol/gateway-error";
|
|
25
|
+
import {
|
|
26
|
+
FREEBUFF_API_HOST,
|
|
27
|
+
FREEBUFF_CLIENT_HEADER_VALUE,
|
|
28
|
+
FREEBUFF_MODEL_CATALOG_PATH,
|
|
29
|
+
FREEBUFF_CATALOG_PROTOCOL_HEADER,
|
|
30
|
+
FREEBUFF_CATALOG_PROTOCOL_VERSION,
|
|
31
|
+
FREEBUFF_CLIENT_HEADER,
|
|
32
|
+
freebuffMintDeviceKey,
|
|
33
|
+
} from "./freebuff";
|
|
34
|
+
|
|
35
|
+
const CODE_PATH = "/api/auth/cli/code";
|
|
36
|
+
const STATUS_PATH = "/api/auth/cli/status";
|
|
37
|
+
|
|
38
|
+
interface CodeIssued {
|
|
39
|
+
readonly loginUrl: string;
|
|
40
|
+
readonly fingerprintId: string;
|
|
41
|
+
readonly fingerprintHash: string;
|
|
42
|
+
readonly expiresAt: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class FreebuffOAuthClient implements OAuthLoginClient {
|
|
46
|
+
readonly supportsDeviceCode = true;
|
|
47
|
+
readonly supportsBrowserCode = false;
|
|
48
|
+
// No operator fields: the device flow issues a code and the account
|
|
49
|
+
// identity (email/name) comes back in the poll response — asking for an
|
|
50
|
+
// email up front is both unnecessary and wrong.
|
|
51
|
+
readonly deviceLoginFields = [] as const;
|
|
52
|
+
readonly importFields = [
|
|
53
|
+
{
|
|
54
|
+
key: "token",
|
|
55
|
+
label: "Freebuff access token (from the Desktop/CLI login)",
|
|
56
|
+
placeholder: "Paste the bearer token…",
|
|
57
|
+
secret: true,
|
|
58
|
+
required: true,
|
|
59
|
+
},
|
|
60
|
+
] as const;
|
|
61
|
+
|
|
62
|
+
private readonly fetchFn: typeof fetch;
|
|
63
|
+
|
|
64
|
+
constructor(fetchFn: typeof fetch = globalThis.fetch) {
|
|
65
|
+
this.fetchFn = fetchFn;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async startDeviceAuth(_context?: OAuthDeviceFlowContext): Promise<OAuthDeviceStartResult> {
|
|
69
|
+
const fingerprintId = crypto.randomUUID();
|
|
70
|
+
let res: Response;
|
|
71
|
+
try {
|
|
72
|
+
res = await this.fetchFn(`${FREEBUFF_API_HOST}${CODE_PATH}`, {
|
|
73
|
+
method: "POST",
|
|
74
|
+
headers: { "content-type": "application/json" },
|
|
75
|
+
body: JSON.stringify({ fingerprintId }),
|
|
76
|
+
signal: AbortSignal.timeout(15_000),
|
|
77
|
+
});
|
|
78
|
+
} catch (error) {
|
|
79
|
+
// Transient network/timeout failure — retry once on the same route. The
|
|
80
|
+
// donor's login path does the same (freebuff-proxy: fetchLoginUpstream)
|
|
81
|
+
// because a single jitter would otherwise bounce the whole login flow.
|
|
82
|
+
const err = error as { name?: string; code?: string };
|
|
83
|
+
const transient =
|
|
84
|
+
err?.name === "AbortError" ||
|
|
85
|
+
err?.name === "TypeError" ||
|
|
86
|
+
err?.code === "ECONNRESET" ||
|
|
87
|
+
err?.code === "ETIMEDOUT" ||
|
|
88
|
+
err?.code === "ENOTFOUND" ||
|
|
89
|
+
err?.code === "ECONNREFUSED" ||
|
|
90
|
+
err?.code === "EPIPE";
|
|
91
|
+
if (!transient) throw error;
|
|
92
|
+
res = await this.fetchFn(`${FREEBUFF_API_HOST}${CODE_PATH}`, {
|
|
93
|
+
method: "POST",
|
|
94
|
+
headers: { "content-type": "application/json" },
|
|
95
|
+
body: JSON.stringify({ fingerprintId }),
|
|
96
|
+
signal: AbortSignal.timeout(15_000),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
if (!res.ok) {
|
|
100
|
+
const text = await res.text().catch(() => "");
|
|
101
|
+
throw new GatewayError("platform_unavailable", 502, `Freebuff code issue failed (HTTP ${res.status}): ${text.slice(0, 200)} (request {requestId}).`, {}, "upstream");
|
|
102
|
+
}
|
|
103
|
+
const body: unknown = await res.json().catch(() => null);
|
|
104
|
+
if (!isRecord(body)) {
|
|
105
|
+
throw new GatewayError("platform_unavailable", 502, "Freebuff code issue returned an invalid body (request {requestId}).", {}, "upstream");
|
|
106
|
+
}
|
|
107
|
+
const loginUrl = typeof body["loginUrl"] === "string" ? body["loginUrl"] : "";
|
|
108
|
+
const fingerprintHash = typeof body["fingerprintHash"] === "string" ? body["fingerprintHash"] : "";
|
|
109
|
+
// The server reports expiresAt as an epoch-millisecond number; keep it as
|
|
110
|
+
// the raw value so the poll query matches what the donor sends (a string
|
|
111
|
+
// coercion of a numeric field was silently dropping it, leaving the poll
|
|
112
|
+
// URL incomplete — the upstream then answers 401 forever).
|
|
113
|
+
const expiresAtRaw = body["expiresAt"];
|
|
114
|
+
const expiresAt = typeof expiresAtRaw === "number" ? String(expiresAtRaw) : typeof expiresAtRaw === "string" ? expiresAtRaw : "";
|
|
115
|
+
if (!loginUrl || !fingerprintHash) {
|
|
116
|
+
throw new GatewayError("platform_unavailable", 502, "Freebuff code issue returned no login URL (request {requestId}).", {}, "upstream");
|
|
117
|
+
}
|
|
118
|
+
const state: CodeIssued = { loginUrl, fingerprintId, fingerprintHash, expiresAt };
|
|
119
|
+
return {
|
|
120
|
+
verificationUri: loginUrl,
|
|
121
|
+
userCode: fingerprintId.slice(0, 8),
|
|
122
|
+
deviceAuthId: fingerprintId,
|
|
123
|
+
intervalSeconds: 3,
|
|
124
|
+
expiresInSeconds: 3600,
|
|
125
|
+
providerState: JSON.stringify(state),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async pollDeviceAuth(deviceAuthId: string, context?: OAuthDeviceFlowContext): Promise<OAuthDevicePollResult> {
|
|
130
|
+
let state: CodeIssued;
|
|
131
|
+
try {
|
|
132
|
+
state = JSON.parse(context?.providerState ?? "") as CodeIssued;
|
|
133
|
+
} catch {
|
|
134
|
+
state = { loginUrl: "", fingerprintId: deviceAuthId, fingerprintHash: "", expiresAt: "" };
|
|
135
|
+
}
|
|
136
|
+
const params = new URLSearchParams({
|
|
137
|
+
fingerprintId: state.fingerprintId || deviceAuthId,
|
|
138
|
+
});
|
|
139
|
+
if (state.fingerprintHash) params.set("fingerprintHash", state.fingerprintHash);
|
|
140
|
+
if (state.expiresAt) params.set("expiresAt", state.expiresAt);
|
|
141
|
+
let res: Response;
|
|
142
|
+
try {
|
|
143
|
+
res = await this.fetchFn(`${FREEBUFF_API_HOST}${STATUS_PATH}?${params.toString()}`, {
|
|
144
|
+
method: "GET",
|
|
145
|
+
signal: AbortSignal.timeout(15_000),
|
|
146
|
+
});
|
|
147
|
+
} catch {
|
|
148
|
+
// Transient poll failure — the donor keeps polling through these; a
|
|
149
|
+
// dropped poll must not kill the flow.
|
|
150
|
+
return { status: "pending", retryAfterSeconds: 3 };
|
|
151
|
+
}
|
|
152
|
+
if (res.status === 401 || res.status === 404) {
|
|
153
|
+
// 401 is "not authorized yet" — the status endpoint refuses before the
|
|
154
|
+
// browser sign-in completes. Treat it as pending, never as a failure
|
|
155
|
+
// (mirrors freebuff-proxy: loginStatus -> 401 => { pending: true }).
|
|
156
|
+
return { status: "pending", retryAfterSeconds: 3 };
|
|
157
|
+
}
|
|
158
|
+
if (!res.ok) {
|
|
159
|
+
return { status: "pending", retryAfterSeconds: 3 };
|
|
160
|
+
}
|
|
161
|
+
const body: unknown = await res.json().catch(() => null);
|
|
162
|
+
if (!isRecord(body)) return { status: "pending", retryAfterSeconds: 3 };
|
|
163
|
+
const user = body["user"];
|
|
164
|
+
if (!isRecord(user)) return { status: "pending", retryAfterSeconds: 3 };
|
|
165
|
+
const authToken = typeof user["authToken"] === "string" ? user["authToken"] : "";
|
|
166
|
+
if (!authToken) return { status: "pending", retryAfterSeconds: 3 };
|
|
167
|
+
const accountId = typeof user["id"] === "string" ? user["id"] : undefined;
|
|
168
|
+
const email = typeof user["email"] === "string" ? user["email"] : undefined;
|
|
169
|
+
const name = typeof user["name"] === "string" ? user["name"] : undefined;
|
|
170
|
+
const device = await freebuffMintDeviceKey(authToken, {
|
|
171
|
+
fetchFn: this.fetchFn,
|
|
172
|
+
...(accountId !== undefined ? { accountId } : {}),
|
|
173
|
+
});
|
|
174
|
+
const label = name ?? email;
|
|
175
|
+
return {
|
|
176
|
+
status: "complete",
|
|
177
|
+
result: {
|
|
178
|
+
access: authToken,
|
|
179
|
+
refresh: "",
|
|
180
|
+
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
|
|
181
|
+
...(label ? { accountLabel: label } : {}),
|
|
182
|
+
auth_state: { ...(accountId !== undefined ? { accountId } : {}), device },
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async importCredential(input: OAuthImportInput): Promise<OAuthExchangeResult> {
|
|
188
|
+
const token = input.fields["token"]?.trim();
|
|
189
|
+
if (!token) {
|
|
190
|
+
throw new GatewayError("invalid_request", 400, "Freebuff token is required (request {requestId}).", { providerId: "freebuff" });
|
|
191
|
+
}
|
|
192
|
+
// Validate by fetching /me.
|
|
193
|
+
const res = await this.fetchFn(`${FREEBUFF_API_HOST}/api/v1/me?fields=id,email,name`, {
|
|
194
|
+
method: "GET",
|
|
195
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
196
|
+
signal: AbortSignal.timeout(15_000),
|
|
197
|
+
});
|
|
198
|
+
if (!res.ok) {
|
|
199
|
+
throw new GatewayError("authentication_failed", 401, "Freebuff token is invalid (request {requestId}).", {}, "upstream");
|
|
200
|
+
}
|
|
201
|
+
const body: unknown = await res.json().catch(() => null);
|
|
202
|
+
const accountId = isRecord(body) && typeof body["id"] === "string" ? body["id"] : undefined;
|
|
203
|
+
const email = isRecord(body) && typeof body["email"] === "string" ? body["email"] : undefined;
|
|
204
|
+
const name = isRecord(body) && typeof body["name"] === "string" ? body["name"] : undefined;
|
|
205
|
+
const device = await freebuffMintDeviceKey(token, {
|
|
206
|
+
fetchFn: this.fetchFn,
|
|
207
|
+
...(accountId !== undefined ? { accountId } : {}),
|
|
208
|
+
});
|
|
209
|
+
const label = name ?? email ?? input.accountLabel;
|
|
210
|
+
return {
|
|
211
|
+
access: token,
|
|
212
|
+
refresh: "",
|
|
213
|
+
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
|
|
214
|
+
...(label ? { accountLabel: label } : {}),
|
|
215
|
+
auth_state: { ...(accountId !== undefined ? { accountId } : {}), device },
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const client = new FreebuffOAuthClient();
|
|
221
|
+
|
|
222
|
+
/** Config imported by the registry; device key lives in auth_state. */
|
|
223
|
+
export const freebuffOAuthClient: OAuthLoginClient = client;
|
|
224
|
+
|
|
225
|
+
// Re-export a helper so the catalog probe and other modules can validate a
|
|
226
|
+
// pasted token without constructing the client.
|
|
227
|
+
export async function freebuffValidateToken(token: string, fetchFn: typeof fetch = globalThis.fetch): Promise<string | undefined> {
|
|
228
|
+
const res = await fetchFn(`${FREEBUFF_API_HOST}${FREEBUFF_MODEL_CATALOG_PATH}`, {
|
|
229
|
+
method: "GET",
|
|
230
|
+
headers: {
|
|
231
|
+
Authorization: `Bearer ${token}`,
|
|
232
|
+
[FREEBUFF_CATALOG_PROTOCOL_HEADER]: FREEBUFF_CATALOG_PROTOCOL_VERSION,
|
|
233
|
+
[FREEBUFF_CLIENT_HEADER]: FREEBUFF_CLIENT_HEADER_VALUE,
|
|
234
|
+
},
|
|
235
|
+
signal: AbortSignal.timeout(15_000),
|
|
236
|
+
});
|
|
237
|
+
if (!res.ok) return undefined;
|
|
238
|
+
const body: unknown = await res.json().catch(() => null);
|
|
239
|
+
return isRecord(body) && typeof body["fetchId"] === "string" ? body["fetchId"] : undefined;
|
|
240
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// Freebuff quota collector — TypeScript, Freebuff provider.
|
|
2
|
+
//
|
|
3
|
+
// Freebuff's billing surface is session-scoped: freebucks (balance, daily
|
|
4
|
+
// limit/spent/reset) and per-model rate limits are reported by the session
|
|
5
|
+
// endpoint, not by a static billing API. The collector reads the current
|
|
6
|
+
// session read-only (`GET /api/v1/freebuff/session`) using the account's
|
|
7
|
+
// device identity — it never performs an admission, so a quota refresh cannot
|
|
8
|
+
// spend a session, lock a model purchase, or trip the upstream's gates.
|
|
9
|
+
//
|
|
10
|
+
// Session-active windows: Freebuff locks each purchased session to one model
|
|
11
|
+
// until `expiresAt` (desktopPurchases). The session read returns those
|
|
12
|
+
// purchases, so the collector emits one `session_active` window per purchase,
|
|
13
|
+
// giving the provider-detail page its countdown pill ("Session · <model> ·
|
|
14
|
+
// Expires in Nh Nm").
|
|
15
|
+
//
|
|
16
|
+
// When the account carries no device identity (auth_state), the session read
|
|
17
|
+
// cannot be signed and is skipped; the collector then returns an empty quota
|
|
18
|
+
// rather than manufacturing a probe.
|
|
19
|
+
|
|
20
|
+
import type { QuotaFetcher } from "../../quota/quota-support";
|
|
21
|
+
import type { ProviderQuotaResult, ProviderQuotaWindow } from "../../quota/quota-contracts";
|
|
22
|
+
import { isRecord } from "../../../protocol/codecs";
|
|
23
|
+
import {
|
|
24
|
+
FREEBUFF_API_HOST,
|
|
25
|
+
FREEBUFF_CLIENT_HEADER,
|
|
26
|
+
FREEBUFF_CLIENT_HEADER_VALUE,
|
|
27
|
+
FREEBUFF_INSTALL_ID_HEADER,
|
|
28
|
+
FREEBUFF_SESSION_PATH,
|
|
29
|
+
FREEBUFF_ACTING_USER_HEADER,
|
|
30
|
+
FREEBUFF_ADMISSION_USER_AGENT,
|
|
31
|
+
freebuffSignRequest,
|
|
32
|
+
} from "./freebuff";
|
|
33
|
+
import type { FreebuffAuthState, FreebuffDeviceIdentity } from "./freebuff";
|
|
34
|
+
import { freebuffVirtualIdFor } from "./freebuff-virtual";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Parses `desktopPurchases` from the session body. Returns the active
|
|
38
|
+
* purchases (model + expiresAt) carried by the entitlement surface.
|
|
39
|
+
*/
|
|
40
|
+
function parsePurchases(body: Record<string, unknown>): Array<{ model: string; expiresAt: string }> {
|
|
41
|
+
const purchases = body["desktopPurchases"];
|
|
42
|
+
if (!Array.isArray(purchases)) return [];
|
|
43
|
+
const result: Array<{ model: string; expiresAt: string }> = [];
|
|
44
|
+
for (const raw of purchases) {
|
|
45
|
+
if (!isRecord(raw)) continue;
|
|
46
|
+
const model = typeof raw["model"] === "string" ? raw["model"] : undefined;
|
|
47
|
+
const expiresAt = typeof raw["expiresAt"] === "string" ? raw["expiresAt"] : undefined;
|
|
48
|
+
if (!model || !expiresAt) continue;
|
|
49
|
+
// The upstream reports the `m-<hex>` roster key; the catalog publishes the
|
|
50
|
+
// caller-facing virtual name, so the window must carry the same id a route
|
|
51
|
+
// candidate does (`filterCandidateQuotaWindows` matches modelIds).
|
|
52
|
+
result.push({ model: freebuffVirtualIdFor(model) ?? model, expiresAt });
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function buildSessionWindows(purchases: Array<{ model: string; expiresAt: string }>): ProviderQuotaWindow[] {
|
|
58
|
+
const now = Date.now();
|
|
59
|
+
const windows: ProviderQuotaWindow[] = [];
|
|
60
|
+
for (const purchase of purchases) {
|
|
61
|
+
const expiresMs = new Date(purchase.expiresAt).getTime();
|
|
62
|
+
if (!Number.isFinite(expiresMs) || expiresMs <= now) continue;
|
|
63
|
+
const remainingMs = expiresMs - now;
|
|
64
|
+
const remainingMinutes = Math.max(1, Math.ceil(remainingMs / 60_000));
|
|
65
|
+
const remainingPercent = Math.min(100, Math.max(0, Math.round((remainingMs / 3_600_000) * 100)));
|
|
66
|
+
windows.push({
|
|
67
|
+
kind: "session_active",
|
|
68
|
+
label: "Session active",
|
|
69
|
+
accounting: "independent",
|
|
70
|
+
usedPercent: null,
|
|
71
|
+
remainingPercent,
|
|
72
|
+
modelIds: [purchase.model],
|
|
73
|
+
resetsAt: purchase.expiresAt,
|
|
74
|
+
remaining: remainingMinutes,
|
|
75
|
+
recurring: false,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return windows;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Normalizes the stored auth_state into a usable device identity. */
|
|
82
|
+
function resolveDeviceIdentity(authState: unknown): FreebuffDeviceIdentity | undefined {
|
|
83
|
+
if (!isRecord(authState)) return undefined;
|
|
84
|
+
const device = authState["device"];
|
|
85
|
+
if (!isRecord(device)) return undefined;
|
|
86
|
+
const identity = device as Record<string, unknown>;
|
|
87
|
+
if (
|
|
88
|
+
typeof identity["version"] !== "number" ||
|
|
89
|
+
typeof identity["publicKey"] !== "string" ||
|
|
90
|
+
typeof identity["privateKey"] !== "string" ||
|
|
91
|
+
typeof identity["keyId"] !== "string" ||
|
|
92
|
+
typeof identity["installId"] !== "string"
|
|
93
|
+
) {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
version: identity["version"] as 1,
|
|
98
|
+
publicKey: identity["publicKey"] as string,
|
|
99
|
+
privateKey: identity["privateKey"] as string,
|
|
100
|
+
keyId: identity["keyId"] as string,
|
|
101
|
+
installId: identity["installId"] as string,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function buildSessionHeaders(
|
|
106
|
+
token: string,
|
|
107
|
+
device: FreebuffDeviceIdentity,
|
|
108
|
+
accountId: string | undefined,
|
|
109
|
+
fetchId: string,
|
|
110
|
+
): Promise<Record<string, string>> {
|
|
111
|
+
// The session read takes the same device signature as every other desktop
|
|
112
|
+
// request. The fetchId keeps the signature's payload unique per request.
|
|
113
|
+
return freebuffSignRequest(device, {
|
|
114
|
+
method: "GET",
|
|
115
|
+
path: FREEBUFF_SESSION_PATH,
|
|
116
|
+
fetchId,
|
|
117
|
+
}).then((signed) => ({
|
|
118
|
+
Authorization: `Bearer ${token}`,
|
|
119
|
+
[FREEBUFF_CLIENT_HEADER]: FREEBUFF_CLIENT_HEADER_VALUE,
|
|
120
|
+
[FREEBUFF_INSTALL_ID_HEADER]: device.installId,
|
|
121
|
+
...(accountId === undefined ? {} : { [FREEBUFF_ACTING_USER_HEADER]: accountId }),
|
|
122
|
+
"user-agent": FREEBUFF_ADMISSION_USER_AGENT,
|
|
123
|
+
...signed,
|
|
124
|
+
}));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export const fetchFreebuffQuota: QuotaFetcher = async (credential, fetcher, context) => {
|
|
128
|
+
const token = credential.trim();
|
|
129
|
+
const fetchFn = fetcher ?? globalThis.fetch;
|
|
130
|
+
const authState: FreebuffAuthState | undefined = isRecord(context?.auth_state)
|
|
131
|
+
? (context.auth_state as FreebuffAuthState)
|
|
132
|
+
: undefined;
|
|
133
|
+
const device = resolveDeviceIdentity(authState);
|
|
134
|
+
if (!device) {
|
|
135
|
+
return {
|
|
136
|
+
source: "freebuff",
|
|
137
|
+
plan: null,
|
|
138
|
+
windows: [],
|
|
139
|
+
error: "Freebuff quota check requires the account's device identity (re-login to register one).",
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const accountId =
|
|
143
|
+
authState !== undefined && typeof authState["accountId"] === "string"
|
|
144
|
+
? authState["accountId"]
|
|
145
|
+
: context?.accountId;
|
|
146
|
+
const fetchId = crypto.randomUUID();
|
|
147
|
+
let headers: Record<string, string>;
|
|
148
|
+
try {
|
|
149
|
+
headers = await buildSessionHeaders(token, device, accountId, fetchId);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
return {
|
|
152
|
+
source: "freebuff",
|
|
153
|
+
plan: null,
|
|
154
|
+
windows: [],
|
|
155
|
+
error: error instanceof Error ? error.message : "Failed to sign the Freebuff session request",
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
let res: Response;
|
|
159
|
+
try {
|
|
160
|
+
res = await fetchFn(`${FREEBUFF_API_HOST}${FREEBUFF_SESSION_PATH}`, {
|
|
161
|
+
method: "GET",
|
|
162
|
+
headers,
|
|
163
|
+
signal: AbortSignal.timeout(15_000),
|
|
164
|
+
});
|
|
165
|
+
} catch (error) {
|
|
166
|
+
return {
|
|
167
|
+
source: "freebuff",
|
|
168
|
+
plan: null,
|
|
169
|
+
windows: [],
|
|
170
|
+
error: error instanceof Error ? error.message : String(error),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
const body: unknown = await res.json().catch(() => null);
|
|
174
|
+
if (!res.ok || !isRecord(body)) {
|
|
175
|
+
return {
|
|
176
|
+
source: "freebuff",
|
|
177
|
+
plan: null,
|
|
178
|
+
windows: [],
|
|
179
|
+
error: `Freebuff quota check failed (HTTP ${res.status})`,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
// The session surface carries the entitlement numbers and the active
|
|
183
|
+
// purchases. A malformed body yields empty windows; never fail the fetch.
|
|
184
|
+
const windows = buildQuotaWindows(body);
|
|
185
|
+
const purchases = parsePurchases(body);
|
|
186
|
+
const sessionWindows = buildSessionWindows(purchases);
|
|
187
|
+
const plan = typeof body["accessTier"] === "string" ? body["accessTier"] : null;
|
|
188
|
+
return {
|
|
189
|
+
source: "freebuff",
|
|
190
|
+
plan,
|
|
191
|
+
windows: [...windows, ...sessionWindows],
|
|
192
|
+
error: null,
|
|
193
|
+
};
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
function buildQuotaWindows(body: Record<string, unknown>): ProviderQuotaResult["windows"] {
|
|
197
|
+
const windows: ProviderQuotaWindow[] = [];
|
|
198
|
+
const freebucks = body["freebucks"];
|
|
199
|
+
if (isRecord(freebucks)) {
|
|
200
|
+
const balance = typeof freebucks["balance"] === "number" ? freebucks["balance"] : null;
|
|
201
|
+
const daily = freebucks["daily"];
|
|
202
|
+
if (isRecord(daily)) {
|
|
203
|
+
const limit = typeof daily["limit"] === "number" ? daily["limit"] : null;
|
|
204
|
+
const spent = typeof daily["spent"] === "number" ? daily["spent"] : null;
|
|
205
|
+
const remaining = typeof daily["remaining"] === "number" ? daily["remaining"] : null;
|
|
206
|
+
const resetAt = typeof daily["resetAt"] === "string" ? daily["resetAt"] : null;
|
|
207
|
+
windows.push({
|
|
208
|
+
kind: "freebucks_daily",
|
|
209
|
+
label: "Freebucks daily",
|
|
210
|
+
accounting: "pooled",
|
|
211
|
+
usedPercent: limit && spent !== null ? Math.round((spent / limit) * 100) : null,
|
|
212
|
+
remainingPercent: limit && remaining !== null ? Math.round((remaining / limit) * 100) : null,
|
|
213
|
+
resetsAt: resetAt,
|
|
214
|
+
used: spent,
|
|
215
|
+
limit,
|
|
216
|
+
remaining,
|
|
217
|
+
recurring: true,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
if (balance !== null) {
|
|
221
|
+
windows.push({
|
|
222
|
+
kind: "freebucks_balance",
|
|
223
|
+
label: "Freebucks balance",
|
|
224
|
+
accounting: "pooled",
|
|
225
|
+
usedPercent: null,
|
|
226
|
+
remainingPercent: null,
|
|
227
|
+
resetsAt: null,
|
|
228
|
+
remaining: balance,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// Rate limits are reported per model in `rateLimitsByModel` (a map keyed by
|
|
233
|
+
// model id).
|
|
234
|
+
const rateLimitsByModel = body["rateLimitsByModel"];
|
|
235
|
+
if (isRecord(rateLimitsByModel)) {
|
|
236
|
+
for (const [modelId, raw] of Object.entries(rateLimitsByModel)) {
|
|
237
|
+
if (!isRecord(raw)) continue;
|
|
238
|
+
const limit = typeof raw["limit"] === "number" ? raw["limit"] : null;
|
|
239
|
+
const recentCount = typeof raw["recentCount"] === "number" ? raw["recentCount"] : null;
|
|
240
|
+
const resetAt = typeof raw["resetAt"] === "string" ? raw["resetAt"] : null;
|
|
241
|
+
if (limit === null) continue;
|
|
242
|
+
windows.push({
|
|
243
|
+
kind: "rate_limit",
|
|
244
|
+
label: "Daily session rate limit",
|
|
245
|
+
accounting: "independent",
|
|
246
|
+
// Publish the caller-facing virtual name, matching the catalog and the
|
|
247
|
+
// routing candidate id (`filterCandidateQuotaWindows`).
|
|
248
|
+
modelIds: [freebuffVirtualIdFor(modelId) ?? modelId],
|
|
249
|
+
usedPercent: recentCount !== null && limit > 0 ? Math.round((recentCount / limit) * 100) : null,
|
|
250
|
+
remainingPercent: recentCount !== null && limit > 0 ? Math.round(((limit - recentCount) / limit) * 100) : null,
|
|
251
|
+
resetsAt: resetAt,
|
|
252
|
+
used: recentCount,
|
|
253
|
+
limit,
|
|
254
|
+
remaining: recentCount !== null ? limit - recentCount : null,
|
|
255
|
+
recurring: true,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return windows;
|
|
260
|
+
}
|