cartethyia 2.4.4 → 2.4.6
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 +4 -1
- package/README.md +18 -0
- package/dist/dashboard/assets/AccountCooldown-ChpdzxRH.js.gz +0 -0
- package/dist/dashboard/assets/{BannedPage-TDue2X0p.js → BannedPage-QhvbJB9o.js} +1 -1
- package/dist/dashboard/assets/BannedPage-QhvbJB9o.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-qaHXuiI5.js → CliToolDetailPage-Bcy150Jo.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-Bcy150Jo.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-BFuQBKez.js → CliToolsPage-DyNXefph.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-DyNXefph.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-Cak0ljnO.js +1 -0
- package/dist/dashboard/assets/CombosPage-Cak0ljnO.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-CATpAEOz.js +1 -0
- package/dist/dashboard/assets/ConsoleLogPage-CATpAEOz.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-BhglcqyR.js → CustomizationPage-D6jEU7nL.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-D6jEU7nL.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-CZkpn0a7.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-CZkpn0a7.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-CeqROJrT.js → LoginPage-BJlMVN4U.js} +1 -1
- package/dist/dashboard/assets/LoginPage-BJlMVN4U.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-Br3CXEau.js +1 -0
- package/dist/dashboard/assets/ModelPicker-Br3CXEau.js.gz +0 -0
- package/dist/dashboard/assets/{OverviewPage-CRXSAITY.js → OverviewPage-CQxXbo2g.js} +1 -1
- package/dist/dashboard/assets/OverviewPage-CQxXbo2g.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage--0RBNh99.js +3 -0
- package/dist/dashboard/assets/ProviderDetailPage--0RBNh99.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-paOCq8I0.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-1M-C3mbu.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-1M-C3mbu.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-CXiumA53.js +7 -0
- package/dist/dashboard/assets/ProxyPage-CXiumA53.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-BGDIfIYP.js +1 -0
- package/dist/dashboard/assets/QuotaPage-BGDIfIYP.js.gz +0 -0
- package/dist/dashboard/assets/{SettingsPage-YxRml8AT.js → SettingsPage-BHW4Zt06.js} +1 -1
- package/dist/dashboard/assets/SettingsPage-BHW4Zt06.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-VOYC_lIa.js +1 -0
- package/dist/dashboard/assets/SetupPage-VOYC_lIa.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-DpOTnGuH.js +1 -0
- package/dist/dashboard/assets/SortableList-DpOTnGuH.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-BNIDLiU6.js +31 -0
- package/dist/dashboard/assets/StudioPage-BNIDLiU6.js.gz +0 -0
- package/dist/dashboard/assets/ToolIcon-K1ckcDKg.js.gz +0 -0
- package/dist/dashboard/assets/{UsagePage-B6LA4ewF.js → UsagePage-BAFaSDQV.js} +14 -14
- package/dist/dashboard/assets/UsagePage-BAFaSDQV.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-BETfaMYZ.js +1 -0
- package/dist/dashboard/assets/api-keys-BETfaMYZ.js.gz +0 -0
- package/dist/dashboard/assets/app-BLCvGdzb.css +2 -0
- package/dist/dashboard/assets/app-BLCvGdzb.css.gz +0 -0
- package/dist/dashboard/assets/{app-BDJWMThu.js → app-Cjq7nrwu.js} +3 -3
- package/dist/dashboard/assets/app-Cjq7nrwu.js.gz +0 -0
- package/dist/dashboard/assets/arrow-left-CpBxkoxI.js.gz +0 -0
- package/dist/dashboard/assets/arrow-right-Dl5yKnp8.js.gz +0 -0
- package/dist/dashboard/assets/{assert-Bwh6DJTK.js → assert-7J6_LVD3.js} +1 -1
- package/dist/dashboard/assets/assert-7J6_LVD3.js.gz +0 -0
- package/dist/dashboard/assets/backup-file-gdKIlg8U.js.gz +0 -0
- package/dist/dashboard/assets/{badge-BwmwEOgA.js → badge-BTyGk_DL.js} +1 -1
- package/dist/dashboard/assets/badge-BTyGk_DL.js.gz +0 -0
- package/dist/dashboard/assets/{banner-BKBfKTqn.js → banner-bujTWYM4.js} +1 -1
- package/dist/dashboard/assets/banner-bujTWYM4.js.gz +0 -0
- package/dist/dashboard/assets/brain-Cwed_72z.js.gz +0 -0
- package/dist/dashboard/assets/{card-BTdbRFFg.js → card-B0Bnx5CA.js} +1 -1
- package/dist/dashboard/assets/card-B0Bnx5CA.js.gz +0 -0
- package/dist/dashboard/assets/check-D69FzdvC.js.gz +0 -0
- package/dist/dashboard/assets/chevron-right-BXIy5l3h.js.gz +0 -0
- package/dist/dashboard/assets/circle-alert-CntYHV5v.js.gz +0 -0
- package/dist/dashboard/assets/circle-check-BU3v_mFW.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-DE2ug812.js +1 -0
- package/dist/dashboard/assets/cli-tools-DE2ug812.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-R_rArH8S.js +1 -0
- package/dist/dashboard/assets/clipboard-button-R_rArH8S.js.gz +0 -0
- package/dist/dashboard/assets/cog-CdWhsajs.js.gz +0 -0
- package/dist/dashboard/assets/copy-ngmQ9zcZ.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-iocKA3jr.js → dialog-BWIP_EvW.js} +1 -1
- package/dist/dashboard/assets/dialog-BWIP_EvW.js.gz +0 -0
- package/dist/dashboard/assets/dialog-actions-B1axpEg7.js.gz +0 -0
- package/dist/dashboard/assets/download-A0SW9OCW.js.gz +0 -0
- package/dist/dashboard/assets/download-dRFo5_Wu.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-CDKOTZdY.js → drawer-BKcgVugF.js} +1 -1
- package/dist/dashboard/assets/drawer-BKcgVugF.js.gz +0 -0
- package/dist/dashboard/assets/entry-7dpgvpJ3.js +4 -0
- package/dist/dashboard/assets/entry-7dpgvpJ3.js.gz +0 -0
- package/dist/dashboard/assets/entry-CcWkGR4a.js +1 -0
- package/dist/dashboard/assets/entry-CcWkGR4a.js.gz +0 -0
- package/dist/dashboard/assets/entry-kTiTFrvh.js +1 -0
- package/dist/dashboard/assets/entry-kTiTFrvh.js.gz +0 -0
- package/dist/dashboard/assets/eye-C_hawfH4.js.gz +0 -0
- package/dist/dashboard/assets/eye-off-Bk5g2luF.js.gz +0 -0
- package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
- package/dist/dashboard/assets/gauge-BbIMy7MB.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-Booc5mNe.js +1 -0
- package/dist/dashboard/assets/github-badge-Booc5mNe.js.gz +0 -0
- package/dist/dashboard/assets/image-BPaouViG.js.gz +0 -0
- package/dist/dashboard/assets/{index-BA_ni9Ix.js → index-D70pChfn.js} +2 -2
- package/dist/dashboard/assets/index-D70pChfn.js.gz +0 -0
- package/dist/dashboard/assets/{inline-DCejSLlA.js → inline-D1TCqM2D.js} +1 -1
- package/dist/dashboard/assets/inline-D1TCqM2D.js.gz +0 -0
- package/dist/dashboard/assets/{input-Bn4_kNav.js → input-B9oyU93y.js} +1 -1
- package/dist/dashboard/assets/input-B9oyU93y.js.gz +0 -0
- package/dist/dashboard/assets/jsx-runtime-CXUMngI_.js.gz +0 -0
- package/dist/dashboard/assets/key-round-UkavsGxp.js.gz +0 -0
- package/dist/dashboard/assets/layers-2-glBbjl.js.gz +0 -0
- package/dist/dashboard/assets/{layout-Bv-NxqZ5.js → layout-BArzXirQ.js} +1 -1
- package/dist/dashboard/assets/layout-BArzXirQ.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-BLA0sldD.js → list-row-BVZzaoHF.js} +1 -1
- package/dist/dashboard/assets/list-row-BVZzaoHF.js.gz +0 -0
- package/dist/dashboard/assets/{live-YlifPYox.js → live-EmuIayqp.js} +1 -1
- package/dist/dashboard/assets/live-EmuIayqp.js.gz +0 -0
- package/dist/dashboard/assets/log-in-BmRjy60c.js.gz +0 -0
- package/dist/dashboard/assets/message-circle-CCxGwRvv.js.gz +0 -0
- package/dist/dashboard/assets/{model-bans-CVvHZ-qz.js → model-bans-CSPQ9xB0.js} +1 -1
- package/dist/dashboard/assets/model-bans-CSPQ9xB0.js.gz +0 -0
- package/dist/dashboard/assets/mutation-BkoLM5Ba.js +1 -0
- package/dist/dashboard/assets/mutation-BkoLM5Ba.js.gz +0 -0
- package/dist/dashboard/assets/numbers-CFL4_Dqc.js.gz +0 -0
- package/dist/dashboard/assets/pencil-768ysRAr.js.gz +0 -0
- package/dist/dashboard/assets/plus-DIdX_g-x.js.gz +0 -0
- package/dist/dashboard/assets/power-off-Yjw9GMJh.js +1 -0
- package/dist/dashboard/assets/power-off-Yjw9GMJh.js.gz +0 -0
- package/dist/dashboard/assets/privacy-mode-GM8bej85.js +1 -0
- package/dist/dashboard/assets/privacy-mode-GM8bej85.js.gz +0 -0
- package/dist/dashboard/assets/providers/agentrouter.svg.gz +0 -0
- package/dist/dashboard/assets/providers/aihubmix.svg.gz +0 -0
- package/dist/dashboard/assets/providers/alibaba-coding-plan.svg.gz +0 -0
- package/dist/dashboard/assets/providers/alibaba.svg.gz +0 -0
- package/dist/dashboard/assets/providers/antigravity.svg.gz +0 -0
- package/dist/dashboard/assets/providers/bai.svg.gz +0 -0
- package/dist/dashboard/assets/providers/blackbox.svg.gz +0 -0
- package/dist/dashboard/assets/providers/claude-code.svg.gz +0 -0
- package/dist/dashboard/assets/providers/exa.svg.gz +0 -0
- package/dist/dashboard/assets/providers/gmi-cloud.svg.gz +0 -0
- package/dist/dashboard/assets/providers/inferhub.svg.gz +0 -0
- package/dist/dashboard/assets/providers/muse.svg.gz +0 -0
- package/dist/dashboard/assets/providers/nous-research.svg.gz +0 -0
- package/dist/dashboard/assets/providers/openai-light.svg.gz +0 -0
- package/dist/dashboard/assets/providers/parallel.svg.gz +0 -0
- package/dist/dashboard/assets/providers/tokenharbor.svg.gz +0 -0
- package/dist/dashboard/assets/providers/zai-coding-plan.svg.gz +0 -0
- package/dist/dashboard/assets/{query-keys-ROf-g1Wi.js → query-keys-D7wX2vM5.js} +1 -1
- package/dist/dashboard/assets/query-keys-D7wX2vM5.js.gz +0 -0
- package/dist/dashboard/assets/quota-DZGyp0N5.js.gz +0 -0
- package/dist/dashboard/assets/refresh-cw-BqJFk4sU.js.gz +0 -0
- package/dist/dashboard/assets/rotate-ccw-CufcqITi.js.gz +0 -0
- package/dist/dashboard/assets/route-CNSNj9Zm.js.gz +0 -0
- package/dist/dashboard/assets/{routing-jntw9-Ui.js → routing-BExgxxBU.js} +1 -1
- package/dist/dashboard/assets/routing-BExgxxBU.js.gz +0 -0
- package/dist/dashboard/assets/scaling-nWE4Cx8f.js.gz +0 -0
- package/dist/dashboard/assets/search-JOKus9rb.js.gz +0 -0
- package/dist/dashboard/assets/{select-CGUuTQmj.js → select-C9mMoEpJ.js} +1 -1
- package/dist/dashboard/assets/select-C9mMoEpJ.js.gz +0 -0
- package/dist/dashboard/assets/settings-C4_Gn_wq.js +1 -0
- package/dist/dashboard/assets/settings-C4_Gn_wq.js.gz +0 -0
- package/dist/dashboard/assets/shield-check-DVS0Foqh.js.gz +0 -0
- package/dist/dashboard/assets/sliders-horizontal-ClG7dBeJ.js.gz +0 -0
- package/dist/dashboard/assets/sparkles-BRaRUxAU.js.gz +0 -0
- package/dist/dashboard/assets/{stack-C1kTcOmP.js → stack-Ca5HxI5i.js} +1 -1
- package/dist/dashboard/assets/stack-Ca5HxI5i.js.gz +0 -0
- package/dist/dashboard/assets/{state-DHKdg-pm.js → state-CzUPfLXo.js} +1 -1
- package/dist/dashboard/assets/state-CzUPfLXo.js.gz +0 -0
- package/dist/dashboard/assets/stream-retry-5QnVuSSo.js.gz +0 -0
- package/dist/dashboard/assets/{switch-DIDeAiz9.js → switch-DOUIky_i.js} +1 -1
- package/dist/dashboard/assets/switch-DOUIky_i.js.gz +0 -0
- package/dist/dashboard/assets/terminal-Bd4ee8TA.js +1 -0
- package/dist/dashboard/assets/terminal-Bd4ee8TA.js.gz +0 -0
- package/dist/dashboard/assets/toast-C76WrMhc.js +2 -0
- package/dist/dashboard/assets/toast-C76WrMhc.js.gz +0 -0
- package/dist/dashboard/assets/tokens-DDxMbSRL.js.gz +0 -0
- package/dist/dashboard/assets/trash-Cppn5c3R.js.gz +0 -0
- package/dist/dashboard/assets/use-clipboard-B0Sy03fE.js.gz +0 -0
- package/dist/dashboard/assets/use-presence-BSZEBx7U.js.gz +0 -0
- package/dist/dashboard/assets/use-reduced-motion-C4B0R6Sz.js.gz +0 -0
- package/dist/dashboard/assets/use-timeout-BwcBBRrT.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-DVmwOKU-.js → useMutation-D87FZwH0.js} +1 -1
- package/dist/dashboard/assets/useMutation-D87FZwH0.js.gz +0 -0
- package/dist/dashboard/assets/wrench-B6E1oEuY.js.gz +0 -0
- package/dist/dashboard/index.html +1 -1
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +123 -0
- package/migrations/full-postgres/0013_add_providers_tenant_id_index.sql +22 -0
- package/migrations/full-postgres/0014_add_provider_accounts_tenant_id_index.sql +21 -0
- package/migrations/lite-sqlite3/0000_baseline.sql +4 -2
- package/migrations/lite-sqlite3/0013_add_providers_tenant_id_index.sql +4 -0
- package/migrations/lite-sqlite3/0014_add_provider_accounts_tenant_id_index.sql +4 -0
- package/package.json +25 -30
- package/scripts/cli/{carte-launcher.cjs → bin/carte.cjs} +8 -8
- package/scripts/cli/{cli-launcher.cjs → bin/cartethyia.cjs} +167 -134
- package/scripts/cli/command/admin.ts +65 -0
- package/scripts/cli/command/console-menu.ts +227 -0
- package/scripts/cli/{doctor.ts → command/doctor.ts} +257 -258
- package/scripts/cli/{setup.ts → command/setup.ts} +8 -12
- package/scripts/cli/command/start.ts +13 -0
- package/scripts/cli/command/tray.ts +165 -0
- package/scripts/cli/command/update.ts +23 -0
- package/scripts/cli/{runtime-state.ts → gateway/drain-token.ts} +78 -78
- package/scripts/cli/gateway/spawn.ts +101 -0
- package/scripts/cli/gateway/stop.ts +51 -0
- package/scripts/cli/main.ts +94 -0
- package/scripts/cli/support/bun.ts +27 -0
- package/scripts/cli/support/env.ts +99 -0
- package/scripts/cli/support/platform.ts +64 -0
- package/scripts/cli/support/services.ts +88 -0
- package/scripts/cli/support/startup-signal.ts +23 -0
- package/scripts/cli/support/version.ts +37 -0
- package/scripts/cli/tray/hidden-window.vbs +28 -0
- package/scripts/cli/{tray.ts → tray/index.ts} +3 -3
- package/scripts/cli/{tray.ps1 → tray/windows.ps1} +107 -98
- package/scripts/cli/ui/background.ts +49 -0
- package/src/boot/app.ts +3 -1
- package/src/boot/dependencies.ts +36 -13
- package/src/config/providers.ts +27 -1
- package/src/config/spec.ts +11 -0
- package/src/console/backup/routes.ts +8 -0
- package/src/console/cli-tools/contracts.ts +6 -6
- package/src/console/console-router.ts +5 -3
- package/src/console/dashboard-assets.ts +81 -11
- package/src/console/observability/contracts.ts +54 -22
- package/src/console/observability/routes.ts +31 -6
- package/src/console/observability/store.ts +68 -28
- package/src/console/providers/catalog/public-model-store.ts +91 -60
- package/src/console/providers/catalog/store.ts +5 -1
- package/src/console/quota/account-quota-tenant.ts +9 -6
- package/src/console/response-compression.ts +69 -2
- package/src/console/routing/pools/probe-verdict.ts +72 -0
- package/src/console/share/share-router.ts +7 -4
- package/src/console/share/share-stats.ts +12 -5
- package/src/console/shared/read-through-cache.ts +133 -7
- package/src/console/updates/repository-updates.ts +39 -34
- package/src/network/outbound-fetch.ts +113 -25
- package/src/network/tls/bun-client-hello.ts +112 -0
- package/src/observability/event-log.ts +29 -8
- package/src/observability/metrics.ts +40 -13
- package/src/observability/runtime-metrics.ts +7 -0
- package/src/persistence/schema.ts +40 -2
- package/src/protocol/response/codex.ts +9 -75
- package/src/protocol/tool-identity.ts +43 -9
- package/src/providers/authentication/jwt-validator.ts +130 -187
- package/src/providers/authentication/oauth-flow-store.ts +16 -9
- package/src/providers/default-registry.ts +4 -0
- package/src/providers/discovery/catalog/catalog-format.ts +20 -0
- package/src/providers/discovery/models-dev-catalog.ts +39 -22
- package/src/providers/discovery/probe-wire.ts +29 -5
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +3 -2
- package/src/providers/integrations/buddy/buddy-cli-headers.ts +88 -0
- package/src/providers/integrations/buddy/buddy-conversation-headers.ts +4 -0
- package/src/providers/integrations/buddy/buddy-discovery-shared.ts +33 -1
- package/src/providers/integrations/buddy/buddy-identity.ts +46 -0
- package/src/providers/integrations/buddy/buddy-oauth-shared.ts +37 -1
- package/src/providers/integrations/buddy/buddy-usage-store.ts +40 -5
- package/src/providers/integrations/buddy/codebuddy/provider.ts +2 -2
- package/src/providers/integrations/buddy/codebuddy/shared.ts +106 -73
- package/src/providers/integrations/buddy/workbuddy/cn-provider.ts +5 -2
- package/src/providers/integrations/buddy/workbuddy/quota.ts +172 -159
- package/src/providers/integrations/buddy/workbuddy/shared.ts +200 -171
- package/src/providers/integrations/codex/codex.ts +5 -8
- package/src/providers/integrations/factory/factory-oauth.ts +7 -11
- package/src/providers/integrations/opencode/opencode-go-quota.ts +130 -0
- package/src/providers/integrations/opencode/provider.ts +42 -3
- package/src/providers/integrations/qoder/qoder-oauth.ts +3 -9
- package/src/providers/integrations/shared/install-id.ts +29 -2
- package/src/providers/operations/client-versions.ts +11 -1
- package/src/providers/operations/model-catalog-cache.ts +18 -3
- package/src/providers/operations/provider-catalog-service.ts +76 -0
- package/src/transport/dispatch/attempt-finalize.ts +7 -1
- package/src/transport/dispatch/attempt-loop.ts +9 -7
- package/src/transport/dispatch/empty-stream.ts +17 -26
- package/src/transport/failure-policy.ts +20 -0
- package/src/transport/routing/route-catalog.ts +42 -3
- package/src/transport/surface/chat/encode.ts +18 -31
- package/src/transport/surface/messages/stream.ts +3 -2
- package/src/transport/translation/tool-repair.ts +37 -16
- package/src/workers/account-identity-sweep.ts +14 -12
- package/src/workers/daily-checkin.ts +62 -0
- package/src/workers/quota-refresh-sweep.ts +27 -2
- package/dist/dashboard/assets/CombosPage-B7ETkfVH.js +0 -1
- package/dist/dashboard/assets/ConsoleLogPage-vkZBxMVo.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-DDkHfwpV.js +0 -1
- package/dist/dashboard/assets/ModelPicker-CAiPR5Vd.js +0 -1
- package/dist/dashboard/assets/ProviderDetailPage-cNAD_6uN.js +0 -3
- package/dist/dashboard/assets/ProvidersPage-Cv5tdxlx.js +0 -1
- package/dist/dashboard/assets/ProxyPage-CWvMfzjj.js +0 -7
- package/dist/dashboard/assets/QuotaPage-IYQOOHR4.js +0 -1
- package/dist/dashboard/assets/SetupPage-BPDNo3Ek.js +0 -1
- package/dist/dashboard/assets/SortableList-Bol1ASmI.js +0 -1
- package/dist/dashboard/assets/StudioPage-BE5DJONZ.js +0 -31
- package/dist/dashboard/assets/api-keys-CjIGQhe7.js +0 -1
- package/dist/dashboard/assets/app-Cjgo4-Lf.css +0 -2
- package/dist/dashboard/assets/cli-tools-DccaOfhX.js +0 -1
- package/dist/dashboard/assets/clipboard-button-DQmMikNQ.js +0 -1
- package/dist/dashboard/assets/entry-DTiWhIY-.js +0 -4
- package/dist/dashboard/assets/entry-Dm3PpDCO.js +0 -1
- package/dist/dashboard/assets/entry-keNDZ_Wz.js +0 -1
- package/dist/dashboard/assets/github-badge-BuAJr8oU.js +0 -1
- package/dist/dashboard/assets/mutation-BIizFZav.js +0 -1
- package/dist/dashboard/assets/privacy-mode-CQjbZ3xk.js +0 -1
- package/dist/dashboard/assets/settings-BAS6QbmJ.js +0 -1
- package/dist/dashboard/assets/terminal-C9mZ4Ugm.js +0 -1
- package/dist/dashboard/assets/toast-DKAdP2mo.js +0 -2
- package/scripts/cli/cli.ts +0 -766
- package/scripts/cli/version.ts +0 -66
- package/scripts/operations/probe-egress.ts +0 -335
- package/scripts/operations/restart.ts +0 -120
- package/scripts/shared/env.ts +0 -269
- /package/{tray-assets → scripts/cli/tray/assets}/cartethyia.ico +0 -0
- /package/{tray-assets → scripts/cli/tray/assets}/cartethyia.png +0 -0
- /package/scripts/cli/{menu.ts → ui/prompt.ts} +0 -0
- /package/scripts/cli/{wordmark.ts → ui/wordmark.ts} +0 -0
|
@@ -1,171 +1,200 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
3
|
-
|
|
4
|
-
import type { OpenAICompatibleAdapterConfig } from "../../../adapters/compatible-adapter";
|
|
5
|
-
import { withBearerAuthentication } from "../../../adapters/compatible-adapter";
|
|
6
|
-
import type { CanonicalRequest } from "../../../../protocol/canonical-model";
|
|
7
|
-
import type { ProviderDispatchContext, ProviderDispatchTarget, ProviderId } from "../../../provider-contracts";
|
|
8
|
-
import { resolveInboundSessionId } from "../../../operations/session-resolution";
|
|
9
|
-
import { buddyConversationHeaders } from "../buddy-conversation-headers";
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
*
|
|
20
|
-
* `/
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
export
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
import type { OpenAICompatibleAdapterConfig } from "../../../adapters/compatible-adapter";
|
|
5
|
+
import { withBearerAuthentication } from "../../../adapters/compatible-adapter";
|
|
6
|
+
import type { CanonicalRequest } from "../../../../protocol/canonical-model";
|
|
7
|
+
import type { ProviderDispatchContext, ProviderDispatchTarget, ProviderId } from "../../../provider-contracts";
|
|
8
|
+
import { resolveInboundSessionId } from "../../../operations/session-resolution";
|
|
9
|
+
import { buddyConversationHeaders } from "../buddy-conversation-headers";
|
|
10
|
+
import { buddyCliHeaders, buddyCliTraceHeaders } from "../buddy-cli-headers";
|
|
11
|
+
import { buddyUserId, resolveBuddyIdentity } from "../buddy-identity";
|
|
12
|
+
import {
|
|
13
|
+
buildWorkBuddyUserAgent,
|
|
14
|
+
getWorkBuddyClientVersion,
|
|
15
|
+
getWorkBuddyCliVersion,
|
|
16
|
+
} from "../../../operations/client-versions";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* WorkBuddy serves chat from a versioned path on a base URL that carries no
|
|
20
|
+
* version segment (billing uses the unversioned `/billing/…` path), so both the
|
|
21
|
+
* adapter config and the static catalog rows must state it. The generic
|
|
22
|
+
* `/chat/completions` default would join into
|
|
23
|
+
* `https://www.workbuddy.ai/chat/completions`, which that upstream answers with
|
|
24
|
+
* a 405 HTML error page.
|
|
25
|
+
*/
|
|
26
|
+
export const WORKBUDDY_CHAT_PATH = "/v2/chat/completions" as const;
|
|
27
|
+
|
|
28
|
+
export {
|
|
29
|
+
buildWorkBuddyUserAgent,
|
|
30
|
+
getWorkBuddyClientVersion,
|
|
31
|
+
getWorkBuddyCliVersion,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** WorkBuddy international gateway domain (Origin/Referer/X-Domain). */
|
|
35
|
+
export const WORKBUDDY_DOMAIN = "www.workbuddy.ai";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Stable per-account device identifiers.
|
|
39
|
+
*
|
|
40
|
+
* WorkBuddy's desktop client presents one fixed virtual device per account.
|
|
41
|
+
* Deriving `X-Machine-ID` / `X-Session-ID` from the account id (sha256, 36 hex)
|
|
42
|
+
* keeps them constant across restarts while staying distinct per account, so
|
|
43
|
+
* multiple pooled accounts are not correlated by a missing or drifting device
|
|
44
|
+
* fingerprint. Mirrors the reference gateway's `wb2a:<purpose>:<uid>` scheme.
|
|
45
|
+
*/
|
|
46
|
+
function deriveAccountStableId(accountId: string, purpose: string): string {
|
|
47
|
+
const sum = createHash("sha256").update(`wb2a:${purpose}:${accountId}`).digest("hex");
|
|
48
|
+
return sum.slice(0, 36);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Per-request WorkBuddy chat headers — the exact desktop-client contract.
|
|
53
|
+
*
|
|
54
|
+
* The international gateway gates chat on the desktop `User-Agent` (platform
|
|
55
|
+
* brand `WorkBuddy AI`; the wrong brand trips 403 code 11140), plus the
|
|
56
|
+
* `X-CodeBuddy-Request` risk gate and the account-scoped device headers.
|
|
57
|
+
* The background client-version monitor keeps this value fresh; dispatch only
|
|
58
|
+
* reads the cached value and never waits on a version endpoint.
|
|
59
|
+
*
|
|
60
|
+
* `cliIdentity` selects the reference CLI contract instead. The two are not
|
|
61
|
+
* variations of one set: the CLI drops every browser header and the device
|
|
62
|
+
* token, and adds the agent envelope. Left unset, nothing changes.
|
|
63
|
+
*/
|
|
64
|
+
export async function workbuddyHeaders(
|
|
65
|
+
context?: ProviderDispatchContext,
|
|
66
|
+
request?: CanonicalRequest,
|
|
67
|
+
domain = WORKBUDDY_DOMAIN,
|
|
68
|
+
clientVersion = getWorkBuddyClientVersion(),
|
|
69
|
+
cliIdentity?: boolean,
|
|
70
|
+
): Promise<Record<string, string>> {
|
|
71
|
+
const conversation = buddyConversationHeaders({
|
|
72
|
+
...(resolveInboundSessionId(context, request) === undefined
|
|
73
|
+
? {}
|
|
74
|
+
: { conversationId: resolveInboundSessionId(context, request) }),
|
|
75
|
+
});
|
|
76
|
+
const accountId = context?.credential.account_id;
|
|
77
|
+
if (resolveBuddyIdentity(context, cliIdentity) === "cli") {
|
|
78
|
+
// The reference CLI: issued by the OpenAI Node SDK, so no browser header
|
|
79
|
+
// and no device token, and the machine/session device pair does not
|
|
80
|
+
// exist — only `X-User-Id`, which is `anonymous` without a uid.
|
|
81
|
+
return {
|
|
82
|
+
accept: "application/json",
|
|
83
|
+
"user-agent": `CLI/${getWorkBuddyCliVersion()} CodeBuddy/${getWorkBuddyCliVersion()}`,
|
|
84
|
+
"x-domain": domain,
|
|
85
|
+
"x-agent-purpose": "conversation",
|
|
86
|
+
"x-ide-name": "CLI",
|
|
87
|
+
"x-ide-type": "CLI",
|
|
88
|
+
"x-ide-version": getWorkBuddyCliVersion(),
|
|
89
|
+
"x-product": "SaaS",
|
|
90
|
+
"x-no-enterprise-id": "1",
|
|
91
|
+
...conversation,
|
|
92
|
+
...buddyCliTraceHeaders(conversation),
|
|
93
|
+
...buddyCliHeaders({ userId: buddyUserId(context) }),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const headers: Record<string, string> = {
|
|
97
|
+
accept: "application/json, text/event-stream",
|
|
98
|
+
"user-agent": buildWorkBuddyUserAgent(clientVersion, getWorkBuddyCliVersion()),
|
|
99
|
+
origin: `https://${domain}`,
|
|
100
|
+
referer: `https://${domain}/`,
|
|
101
|
+
"accept-language": "en-US",
|
|
102
|
+
"x-requested-with": "XMLHttpRequest",
|
|
103
|
+
"x-codebuddy-request": "1",
|
|
104
|
+
"x-domain": domain,
|
|
105
|
+
"x-agent-purpose": "conversation",
|
|
106
|
+
"x-ide-name": "WorkBuddy",
|
|
107
|
+
"x-ide-type": "WorkBuddy",
|
|
108
|
+
"x-ide-version": clientVersion,
|
|
109
|
+
// Both references send the literal `SaaS` here. `WorkBuddy` was a value
|
|
110
|
+
// this repo invented; the desktop product name belongs in `x-ide-name`,
|
|
111
|
+
// and the CN test below compares the two realms against each other rather
|
|
112
|
+
// than to a hard-coded string.
|
|
113
|
+
"x-product": "SaaS",
|
|
114
|
+
// Personal international accounts carry no enterprise id: declare it
|
|
115
|
+
// explicitly so upstream risk control does not treat the omission as
|
|
116
|
+
// suspicious.
|
|
117
|
+
"x-no-enterprise-id": "1",
|
|
118
|
+
};
|
|
119
|
+
// The conversation/tracing family the upstream's channel check looks for. A
|
|
120
|
+
// live request answered `400 · 11-128 "conversation id missing"` while carrying
|
|
121
|
+
// only `x-session-id`; the aggregation key below is the one the reference
|
|
122
|
+
// marks mandatory.
|
|
123
|
+
Object.assign(headers, conversation);
|
|
124
|
+
if (accountId) {
|
|
125
|
+
headers["x-user-id"] = accountId;
|
|
126
|
+
headers["x-machine-id"] = deriveAccountStableId(accountId, "machine");
|
|
127
|
+
headers["x-session-id"] = deriveAccountStableId(accountId, "session");
|
|
128
|
+
}
|
|
129
|
+
const deviceToken = readBuddyDeviceToken(context);
|
|
130
|
+
if (deviceToken !== undefined) headers["x-device-token"] = deviceToken;
|
|
131
|
+
return headers;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Desktop risk token, sent only when the account actually has one.
|
|
136
|
+
*
|
|
137
|
+
* The account's own `auth_state` wins. When it carries none, a file the
|
|
138
|
+
* desktop client writes (`CARTETHYIA_DEVICE_TOKEN_FILE`, default
|
|
139
|
+
* `/app/data/device_token`) is read instead — cached for five minutes, and
|
|
140
|
+
* ignored when it is larger than 1 KB, because a token file that size is not
|
|
141
|
+
* a token. A missing file is the normal case and reads as "no token".
|
|
142
|
+
*/
|
|
143
|
+
const DEVICE_TOKEN_FILE_MAX_BYTES = 1024;
|
|
144
|
+
const DEVICE_TOKEN_FILE_TTL_MS = 5 * 60_000;
|
|
145
|
+
let deviceTokenFileCache: { readonly value: string | undefined; readonly readAt: number } | undefined;
|
|
146
|
+
|
|
147
|
+
export function readBuddyDeviceToken(context?: ProviderDispatchContext): string | undefined {
|
|
148
|
+
const state = context?.credential.auth_state;
|
|
149
|
+
const value = state?.["deviceToken"] ?? state?.["device_token"];
|
|
150
|
+
if (typeof value === "string" && value.trim().length > 0) return value.trim();
|
|
151
|
+
return readDeviceTokenFile();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function readDeviceTokenFile(): string | undefined {
|
|
155
|
+
const now = Date.now();
|
|
156
|
+
if (deviceTokenFileCache !== undefined && now - deviceTokenFileCache.readAt < DEVICE_TOKEN_FILE_TTL_MS) {
|
|
157
|
+
return deviceTokenFileCache.value;
|
|
158
|
+
}
|
|
159
|
+
const path = process.env["CARTETHYIA_DEVICE_TOKEN_FILE"] ?? "/app/data/device_token";
|
|
160
|
+
let value: string | undefined;
|
|
161
|
+
try {
|
|
162
|
+
const raw = readFileSync(path);
|
|
163
|
+
if (raw.byteLength > 0 && raw.byteLength <= DEVICE_TOKEN_FILE_MAX_BYTES) {
|
|
164
|
+
const text = raw.toString("utf8").trim();
|
|
165
|
+
if (text.length > 0) value = text;
|
|
166
|
+
}
|
|
167
|
+
} catch {
|
|
168
|
+
value = undefined;
|
|
169
|
+
}
|
|
170
|
+
deviceTokenFileCache = { value, readAt: now };
|
|
171
|
+
return value;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Shared OpenAI-compatible chat-adapter config for WorkBuddy: bearer auth
|
|
176
|
+
* (api_key and oauth both use `Authorization: Bearer`), `/v2/chat/completions`
|
|
177
|
+
* endpoint, per-request desktop-client headers, and the payload hook.
|
|
178
|
+
*/
|
|
179
|
+
export function workbuddyAdapterConfig(args: {
|
|
180
|
+
providerId: ProviderId;
|
|
181
|
+
baseUrl: string;
|
|
182
|
+
domain?: string;
|
|
183
|
+
prePayload: (
|
|
184
|
+
payload: Record<string, unknown>,
|
|
185
|
+
request: CanonicalRequest,
|
|
186
|
+
candidate: ProviderDispatchTarget,
|
|
187
|
+
) => void;
|
|
188
|
+
fetchImpl?: typeof fetch;
|
|
189
|
+
cliIdentity?: boolean;
|
|
190
|
+
}): OpenAICompatibleAdapterConfig {
|
|
191
|
+
return withBearerAuthentication({
|
|
192
|
+
provider_id: args.providerId,
|
|
193
|
+
base_url: args.baseUrl,
|
|
194
|
+
endpoint_paths_by_wire_family: { chat: WORKBUDDY_CHAT_PATH },
|
|
195
|
+
buildExtraHeaders: (context, request) =>
|
|
196
|
+
workbuddyHeaders(context, request, args.domain ?? WORKBUDDY_DOMAIN, undefined, args.cliIdentity),
|
|
197
|
+
prePayload: args.prePayload,
|
|
198
|
+
...(args.fetchImpl ? { fetchImpl: args.fetchImpl } : {}),
|
|
199
|
+
});
|
|
200
|
+
}
|
|
@@ -31,7 +31,11 @@ import { decodeSseEvents } from "../../../protocol/sse-decoder";
|
|
|
31
31
|
import { imageCreatedSeconds, imagePrompt, responsesImageResult } from "../../../protocol/image-generation";
|
|
32
32
|
import { isRecord, joinUrl } from "../../../protocol/codecs";
|
|
33
33
|
import { postUpstreamJson } from "../../../protocol/upstream/post-json";
|
|
34
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
MIN_RETRY_BUDGET_MS,
|
|
36
|
+
parseUpstreamBackoff,
|
|
37
|
+
sleep as defaultSleep,
|
|
38
|
+
} from "../../../transport/failure-policy";
|
|
35
39
|
|
|
36
40
|
/**
|
|
37
41
|
* Sleep used by the 429 capacity-wait loop, overridable in tests.
|
|
@@ -277,13 +281,6 @@ const CODEX_429_MAX_WAIT_MS = 60_000;
|
|
|
277
281
|
/** Zero-delay 429 guard. Grows from 50ms, doubling per wait. */
|
|
278
282
|
const CODEX_429_MIN_WAIT_MS = 50;
|
|
279
283
|
|
|
280
|
-
/**
|
|
281
|
-
* The attempt-loop floor: a retry repeats the whole pre-dispatch phase, so it
|
|
282
|
-
* needs at least this much budget left on top of the upstream call itself.
|
|
283
|
-
* Mirrors `MIN_RETRY_BUDGET_MS` in `src/transport/dispatch/attempt-loop.ts`.
|
|
284
|
-
*/
|
|
285
|
-
const MIN_RETRY_BUDGET_MS = 15_000;
|
|
286
|
-
|
|
287
284
|
/** The `x-codex-*` window headers, in seconds or epoch seconds. */
|
|
288
285
|
function codexWindowResetMs(res: Response): number | undefined {
|
|
289
286
|
for (const header of ["x-codex-primary-reset-at", "x-codex-secondary-reset-at"]) {
|
|
@@ -29,6 +29,7 @@ import type {
|
|
|
29
29
|
} from "../../authentication/oauth-flow-store";
|
|
30
30
|
import {
|
|
31
31
|
devicePollBackoff,
|
|
32
|
+
decodeJwtPayload,
|
|
32
33
|
nonEmptyTrimmedString,
|
|
33
34
|
readJsonResponse,
|
|
34
35
|
record,
|
|
@@ -71,19 +72,14 @@ export type { FactoryDroidEnvelope } from "../../../security/credential-codec";
|
|
|
71
72
|
* Repeating it on every refresh would add a JWKS fetch to the dispatch path for
|
|
72
73
|
* no additional guarantee: the signature cannot change without the payload
|
|
73
74
|
* changing, and the payload is what this reads.
|
|
75
|
+
*
|
|
76
|
+
* Delegates to the shared decoder rather than hand-rolling a base64url pass:
|
|
77
|
+
* `atob` decodes to a Latin-1 string, so a claim carrying multi-byte UTF-8
|
|
78
|
+
* came back as mojibake. `Buffer.from(segment, "base64url")` round-trips the
|
|
79
|
+
* original bytes.
|
|
74
80
|
*/
|
|
75
81
|
function jwtClaims(token: string): Record<string, unknown> | undefined {
|
|
76
|
-
|
|
77
|
-
const payload = segments[1];
|
|
78
|
-
if (segments.length < 2 || payload === undefined || payload.length === 0) return undefined;
|
|
79
|
-
try {
|
|
80
|
-
const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
81
|
-
const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), "=");
|
|
82
|
-
const parsed: unknown = JSON.parse(atob(padded));
|
|
83
|
-
return record(parsed);
|
|
84
|
-
} catch {
|
|
85
|
-
return undefined;
|
|
86
|
-
}
|
|
82
|
+
return decodeJwtPayload(token);
|
|
87
83
|
}
|
|
88
84
|
|
|
89
85
|
/** Expiry from the token's own `exp` claim, or the fallback when absent. */
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getJson,
|
|
3
|
+
type FetchLike,
|
|
4
|
+
type ProviderQuotaResult,
|
|
5
|
+
type ProviderQuotaWindow,
|
|
6
|
+
} from "../../quota/quota-contracts";
|
|
7
|
+
import type { QuotaCollectionContext } from "../../quota/quota-support";
|
|
8
|
+
|
|
9
|
+
const SOURCE = "opencode-go";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The Go plan's `/v1/usage` answer.
|
|
13
|
+
*
|
|
14
|
+
* `usage` carries exactly three windows in the reference implementation —
|
|
15
|
+
* `rolling`, `weekly`, `monthly` — each with a `percent` and a `resetsAt`.
|
|
16
|
+
* The shape is read defensively: an unknown window name is still reported
|
|
17
|
+
* rather than dropped, since a new window is more useful to an operator than
|
|
18
|
+
* a silently narrower view.
|
|
19
|
+
*/
|
|
20
|
+
interface GoUsageWindow {
|
|
21
|
+
readonly percent?: number | null;
|
|
22
|
+
readonly resetsAt?: string | null;
|
|
23
|
+
readonly status?: string | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface GoUsagePayload {
|
|
27
|
+
readonly usage?: Readonly<Record<string, GoUsageWindow | undefined>> | null;
|
|
28
|
+
readonly plan?: string | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Windows whose meaning the reference names, in the order it lists them. */
|
|
32
|
+
const KNOWN_WINDOWS: readonly (readonly [string, string])[] = [
|
|
33
|
+
["rolling", "Rolling"],
|
|
34
|
+
["weekly", "Weekly"],
|
|
35
|
+
["monthly", "Monthly"],
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
function labelFor(kind: string): string {
|
|
39
|
+
for (const [name, label] of KNOWN_WINDOWS) {
|
|
40
|
+
if (name === kind) return label;
|
|
41
|
+
}
|
|
42
|
+
return kind.charAt(0).toUpperCase() + kind.slice(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function resetIso(value: string | null | undefined): string | null {
|
|
46
|
+
if (typeof value !== "string" || value.trim() === "") return null;
|
|
47
|
+
const parsed = Date.parse(value);
|
|
48
|
+
if (!Number.isFinite(parsed)) return null;
|
|
49
|
+
return new Date(parsed).toISOString();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* One window, or `undefined` when the upstream omitted what makes it usable.
|
|
54
|
+
*
|
|
55
|
+
* A window with no percentage is not a zero-percent window — it is an absent
|
|
56
|
+
* measurement, and reporting it as `0` would tell the operator the plan is
|
|
57
|
+
* exhausted when the upstream simply did not say.
|
|
58
|
+
*/
|
|
59
|
+
function windowFor(
|
|
60
|
+
kind: string,
|
|
61
|
+
raw: GoUsageWindow | undefined,
|
|
62
|
+
): ProviderQuotaWindow | undefined {
|
|
63
|
+
if (!raw) return undefined;
|
|
64
|
+
const percent = typeof raw.percent === "number" && Number.isFinite(raw.percent)
|
|
65
|
+
? Math.min(100, Math.max(0, raw.percent))
|
|
66
|
+
: null;
|
|
67
|
+
if (percent === null) return undefined;
|
|
68
|
+
return {
|
|
69
|
+
kind,
|
|
70
|
+
label: labelFor(kind),
|
|
71
|
+
// The endpoint reports subscription-wide percentages, never per-key or
|
|
72
|
+
// per-model balances. Every window of one subscription reads the same
|
|
73
|
+
// budget, so adding them would double-count: they are independent, and
|
|
74
|
+
// the lowest gates the account.
|
|
75
|
+
accounting: "independent",
|
|
76
|
+
usedPercent: percent,
|
|
77
|
+
remainingPercent: 100 - percent,
|
|
78
|
+
resetsAt: resetIso(raw.resetsAt),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function parseWindows(payload: GoUsagePayload): readonly ProviderQuotaWindow[] {
|
|
83
|
+
const usage = payload.usage;
|
|
84
|
+
if (!usage) return [];
|
|
85
|
+
const windows: ProviderQuotaWindow[] = [];
|
|
86
|
+
const seen = new Set<string>();
|
|
87
|
+
// Known windows first, in their documented order, so the dashboard is
|
|
88
|
+
// stable across refreshes; anything novel is appended.
|
|
89
|
+
for (const [name] of KNOWN_WINDOWS) {
|
|
90
|
+
seen.add(name);
|
|
91
|
+
const window = windowFor(name, usage[name]);
|
|
92
|
+
if (window) windows.push(window);
|
|
93
|
+
}
|
|
94
|
+
for (const [name, raw] of Object.entries(usage)) {
|
|
95
|
+
if (seen.has(name)) continue;
|
|
96
|
+
const window = windowFor(name, raw);
|
|
97
|
+
if (window) windows.push(window);
|
|
98
|
+
}
|
|
99
|
+
return windows;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Fetches the OpenCode Go plan's subscription quota from `/v1/usage`.
|
|
104
|
+
*
|
|
105
|
+
* The anonymous free tier has no usage surface — it is rate-limited by egress
|
|
106
|
+
* IP, not by an account balance — so this is the keyed `opencode-go` tier
|
|
107
|
+
* only. The endpoint reports percentages for a *shared* subscription: several
|
|
108
|
+
* Go keys can draw on one plan, so no window here may be summed against
|
|
109
|
+
* another account's.
|
|
110
|
+
*/
|
|
111
|
+
export async function fetchOpenCodeGoQuota(
|
|
112
|
+
credential: string,
|
|
113
|
+
fetcher: FetchLike,
|
|
114
|
+
_context?: QuotaCollectionContext,
|
|
115
|
+
): Promise<ProviderQuotaResult> {
|
|
116
|
+
const token = credential.trim();
|
|
117
|
+
const payload = (await getJson(
|
|
118
|
+
`https://opencode.ai/zen/go/v1/usage`,
|
|
119
|
+
{
|
|
120
|
+
authorization: `Bearer ${token}`,
|
|
121
|
+
accept: "application/json",
|
|
122
|
+
"x-opencode-client": "cli",
|
|
123
|
+
},
|
|
124
|
+
fetcher,
|
|
125
|
+
)) as GoUsagePayload;
|
|
126
|
+
const windows = parseWindows(payload);
|
|
127
|
+
const plan =
|
|
128
|
+
(typeof payload.plan === "string" && payload.plan.trim()) || "OpenCode Go";
|
|
129
|
+
return { source: SOURCE, plan, windows, error: null };
|
|
130
|
+
}
|
|
@@ -6,6 +6,7 @@ import type { ProviderDispatchContext } from "../../provider-contracts";
|
|
|
6
6
|
import { PROVIDER_COMPATIBILITY_PROFILES } from "../../provider-metadata";
|
|
7
7
|
import { defineModel } from "../../provider-model-definition";
|
|
8
8
|
import type { ApiKeyProviderSpec } from "../../configured-provider";
|
|
9
|
+
import { resolveOpenCodeUnavailableFree } from "../../../config/providers";
|
|
9
10
|
import { buildOpenCodeHeaders } from "./fingerprint";
|
|
10
11
|
import { OPENCODE_TOOL_DEFINITIONS } from "./tool-definitions";
|
|
11
12
|
/**
|
|
@@ -65,6 +66,28 @@ const UNAVAILABLE_FREE_ZEN_IDS: readonly string[] = [
|
|
|
65
66
|
"space-bunny-free",
|
|
66
67
|
];
|
|
67
68
|
|
|
69
|
+
/**
|
|
70
|
+
* The unavailable list, overridable without a code change.
|
|
71
|
+
*
|
|
72
|
+
* These ids are what upstream *currently* refuses, measured 2026-10-14 — and
|
|
73
|
+
* the reference gateway takes the opposite approach entirely: it trusts
|
|
74
|
+
* metadata (`pricing.go` marks a model free when its cost is 0 and it is not
|
|
75
|
+
* deprecated) and hardcodes no ids at all. Ours is more precise while the
|
|
76
|
+
* measurements hold, and worse the moment upstream changes, because a new
|
|
77
|
+
* refusal needs a deploy.
|
|
78
|
+
*
|
|
79
|
+
* `CARTETHYIA_OPENCODE_UNAVAILABLE_FREE` lets an operator correct that
|
|
80
|
+
* immediately: a comma-separated list that *replaces* the bundled one, or the
|
|
81
|
+
* single token `none` to trust the listing completely. The bundled ids remain
|
|
82
|
+
* the default so an unconfigured install behaves exactly as it does today.
|
|
83
|
+
*/
|
|
84
|
+
function unavailableFreeZenIds(): readonly string[] {
|
|
85
|
+
const override = resolveOpenCodeUnavailableFree();
|
|
86
|
+
if (override === undefined) return UNAVAILABLE_FREE_ZEN_IDS;
|
|
87
|
+
if (override === "none") return [];
|
|
88
|
+
return override;
|
|
89
|
+
}
|
|
90
|
+
|
|
68
91
|
/**
|
|
69
92
|
* Zen ids served by the native System One decision endpoint, not the chat wire.
|
|
70
93
|
*
|
|
@@ -93,7 +116,7 @@ export function isSystemoneZenModel(modelId: string): boolean {
|
|
|
93
116
|
* fail a probe.
|
|
94
117
|
*/
|
|
95
118
|
export function isFreeTierZenModel(modelId: string): boolean {
|
|
96
|
-
if (
|
|
119
|
+
if (unavailableFreeZenIds().includes(modelId)) return false;
|
|
97
120
|
return modelId.endsWith("-free") || KNOWN_FREE_ZEN_IDS.includes(modelId);
|
|
98
121
|
}
|
|
99
122
|
|
|
@@ -108,6 +131,17 @@ export function isFreeTierZenModel(modelId: string): boolean {
|
|
|
108
131
|
*/
|
|
109
132
|
const ANONYMOUS_ZEN_BEARER = "public";
|
|
110
133
|
|
|
134
|
+
/**
|
|
135
|
+
* The `Accept` a free-tier dispatch advertises.
|
|
136
|
+
*
|
|
137
|
+
* What OpenCode 2.x's AI SDK puts on the wire, and what the reference gateway
|
|
138
|
+
* sends (`upstream.go:1293`). The gate has only ever been seen accepting the
|
|
139
|
+
* narrower `text/event-stream`, so nothing here fixes a live failure — this is
|
|
140
|
+
* the value a real client produces, which is the only defensible choice when
|
|
141
|
+
* the gate tightens.
|
|
142
|
+
*/
|
|
143
|
+
const FREE_TIER_ACCEPT = "application/json, text/event-stream";
|
|
144
|
+
|
|
111
145
|
function opencodeDesktopHeaders(
|
|
112
146
|
context: ProviderDispatchContext,
|
|
113
147
|
request?: CanonicalRequest,
|
|
@@ -124,8 +158,13 @@ function opencodeDesktopHeaders(
|
|
|
124
158
|
...(freeModel ? { authorization: `Bearer ${ANONYMOUS_ZEN_BEARER}` } : {}),
|
|
125
159
|
// The free-tier gate refuses non-streaming dispatches with 403
|
|
126
160
|
// FreeTierError, so free models always advertise SSE regardless of the
|
|
127
|
-
// client's own Accept; billed models keep the JSON default.
|
|
128
|
-
|
|
161
|
+
// client's own Accept; billed models keep the JSON default. The value is
|
|
162
|
+
// what OpenCode 2.x's AI SDK sends, not the narrower
|
|
163
|
+
// `text/event-stream` we used to claim — the gate always accepted the
|
|
164
|
+
// narrow one, so this is alignment with the real client rather than a
|
|
165
|
+
// fix for a live failure. It costs nothing: `stream: true` is forced on
|
|
166
|
+
// the body either way, so the upstream answers SSE regardless.
|
|
167
|
+
...(freeModel ? { accept: FREE_TIER_ACCEPT } : {}),
|
|
129
168
|
};
|
|
130
169
|
}
|
|
131
170
|
/**
|