cartethyia 2.7.5 → 2.7.7
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 +8 -3
- package/README.md +26 -2
- package/dist/dashboard/assets/BannedPage-B05aF8Dg.js +1 -0
- package/dist/dashboard/assets/BannedPage-B05aF8Dg.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-CLUhmjUy.js +3 -0
- package/dist/dashboard/assets/CliToolDetailPage-CLUhmjUy.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-CQmrqqrk.js +1 -0
- package/dist/dashboard/assets/CliToolsPage-CQmrqqrk.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-CHG_1Wr3.js +1 -0
- package/dist/dashboard/assets/CombosPage-CHG_1Wr3.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-D5rol2j2.js → ConsoleLogPage-DPgbklad.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-DPgbklad.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-BKj_r0rp.js → CustomizationPage-pnfAqr_-.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-pnfAqr_-.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-DGJuU2A6.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-DGJuU2A6.js.gz +0 -0
- package/dist/dashboard/assets/{InjectSystemPromptPanel-B5nZvmf0.js → InjectSystemPromptPanel-C-e2AMz6.js} +245 -2
- package/dist/dashboard/assets/InjectSystemPromptPanel-C-e2AMz6.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-J8wPbmfT.js +1 -0
- package/dist/dashboard/assets/LoginPage-J8wPbmfT.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-DnQPSIA8.js → ModelPicker-C6WlpU5E.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-C6WlpU5E.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-BSQnF2Lp.js +1 -0
- package/dist/dashboard/assets/OverviewPage-BSQnF2Lp.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-DK_e0FJz.js +7 -0
- package/dist/dashboard/assets/ProviderDetailPage-DK_e0FJz.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js +1 -0
- package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-D_a3r6Gc.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-D_a3r6Gc.js.gz +0 -0
- package/dist/dashboard/assets/{ProxyPage-5SnUSy3j.js → ProxyPage-BBTRBAQ3.js} +1 -1
- package/dist/dashboard/assets/ProxyPage-BBTRBAQ3.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-B2WqMSG1.js +1 -0
- package/dist/dashboard/assets/QuotaPage-B2WqMSG1.js.gz +0 -0
- package/dist/dashboard/assets/{SettingsPage-BJtRZ4qD.js → SettingsPage-D7xp5Ij_.js} +1 -1
- package/dist/dashboard/assets/SettingsPage-D7xp5Ij_.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-LND6_Ss7.js +1 -0
- package/dist/dashboard/assets/SetupPage-LND6_Ss7.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-BfAb5-o_.js → StudioPage-BzNnm6Bi.js} +4 -4
- package/dist/dashboard/assets/StudioPage-BzNnm6Bi.js.gz +0 -0
- package/dist/dashboard/assets/ToolIcon-CfeLA-Z_.js +1 -0
- package/dist/dashboard/assets/ToolIcon-CfeLA-Z_.js.gz +0 -0
- package/dist/dashboard/assets/{UsagePage-BNzeZ2Vk.js → UsagePage-C1TbL24N.js} +13 -13
- package/dist/dashboard/assets/UsagePage-C1TbL24N.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-KvX83mRe.js +1 -0
- package/dist/dashboard/assets/api-keys-KvX83mRe.js.gz +0 -0
- package/dist/dashboard/assets/app-DRnQLhsc.js +9 -0
- package/dist/dashboard/assets/app-DRnQLhsc.js.gz +0 -0
- package/dist/dashboard/assets/app-mweRcsKp.css +2 -0
- package/dist/dashboard/assets/app-mweRcsKp.css.gz +0 -0
- package/dist/dashboard/assets/{assert-C0K-c4eo.js → assert-CidQVoNo.js} +1 -1
- package/dist/dashboard/assets/assert-CidQVoNo.js.gz +0 -0
- package/dist/dashboard/assets/{badge-DfbhQU4k.js → badge-BaX2GTrn.js} +1 -1
- package/dist/dashboard/assets/badge-BaX2GTrn.js.gz +0 -0
- package/dist/dashboard/assets/{banner-BllT4LcF.js → banner-DFH4Bkte.js} +1 -1
- package/dist/dashboard/assets/banner-DFH4Bkte.js.gz +0 -0
- package/dist/dashboard/assets/{card-CtkX66Wd.js → card-CxDw0lK6.js} +1 -1
- package/dist/dashboard/assets/card-CxDw0lK6.js.gz +0 -0
- package/dist/dashboard/assets/circle-question-mark-Cz4un8sH.js +1 -0
- package/dist/dashboard/assets/circle-question-mark-Cz4un8sH.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-CQXVMn4P.js → cli-tools-Bwv5-zGZ.js} +1 -1
- package/dist/dashboard/assets/cli-tools-Bwv5-zGZ.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-BQqBQd2n.js → clipboard-button-DfS1ntiq.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-DfS1ntiq.js.gz +0 -0
- package/dist/dashboard/assets/dialog-BK-NRr5u.js +1 -0
- package/dist/dashboard/assets/dialog-BK-NRr5u.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-BrTEZ8zu.js → drawer-QqPul6sC.js} +1 -1
- package/dist/dashboard/assets/drawer-QqPul6sC.js.gz +0 -0
- package/dist/dashboard/assets/entry-BMlYJVFj.js +1 -0
- package/dist/dashboard/assets/entry-BMlYJVFj.js.gz +0 -0
- package/dist/dashboard/assets/entry-Dmx7gBiN.js +1 -0
- package/dist/dashboard/assets/entry-Dmx7gBiN.js.gz +0 -0
- package/dist/dashboard/assets/entry-vqGb8yVz.js +4 -0
- package/dist/dashboard/assets/entry-vqGb8yVz.js.gz +0 -0
- package/dist/dashboard/assets/formatters-DP-5E01-.js +1 -0
- package/dist/dashboard/assets/formatters-DP-5E01-.js.gz +0 -0
- package/dist/dashboard/assets/house-h9AZlJuT.js +1 -0
- package/dist/dashboard/assets/house-h9AZlJuT.js.gz +0 -0
- package/dist/dashboard/assets/{index-FQlj0HUT.js → index-D36vCZht.js} +2 -2
- package/dist/dashboard/assets/index-D36vCZht.js.gz +0 -0
- package/dist/dashboard/assets/{inline-C4gU5iOL.js → inline-CHJQwxLa.js} +1 -1
- package/dist/dashboard/assets/inline-CHJQwxLa.js.gz +0 -0
- package/dist/dashboard/assets/{input-BHHX_Tmm.js → input-BlmlizVV.js} +1 -1
- package/dist/dashboard/assets/input-BlmlizVV.js.gz +0 -0
- package/dist/dashboard/assets/{layout-CajrYUnD.js → layout-Bz93I-yE.js} +1 -1
- package/dist/dashboard/assets/layout-Bz93I-yE.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-Bx2I7XUe.js → list-row-Ml16KCfu.js} +1 -1
- package/dist/dashboard/assets/list-row-Ml16KCfu.js.gz +0 -0
- package/dist/dashboard/assets/{live-DNNqRbj2.js → live-7_QpnYGu.js} +1 -1
- package/dist/dashboard/assets/live-7_QpnYGu.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-Rr0TIvcj.js → mutation-GU7XdEjz.js} +1 -1
- package/dist/dashboard/assets/mutation-GU7XdEjz.js.gz +0 -0
- package/dist/dashboard/assets/{network-C5UBACrJ.js → network-CuMDaTzT.js} +1 -1
- package/dist/dashboard/assets/network-CuMDaTzT.js.gz +0 -0
- package/dist/dashboard/assets/numbers-CoqpaDMx.js +2 -0
- package/dist/dashboard/assets/numbers-CoqpaDMx.js.gz +0 -0
- package/dist/dashboard/assets/{pagination-Xsi9Qx04.js → pagination-B5aOoweZ.js} +1 -1
- package/dist/dashboard/assets/pagination-B5aOoweZ.js.gz +0 -0
- package/dist/dashboard/assets/providers/zai.webp +0 -0
- package/dist/dashboard/assets/providers/zcode.webp +0 -0
- package/dist/dashboard/assets/{query-keys-BloKtaXJ.js → query-keys-YSoQOUh6.js} +1 -1
- package/dist/dashboard/assets/{query-keys-BloKtaXJ.js.gz → query-keys-YSoQOUh6.js.gz} +0 -0
- package/dist/dashboard/assets/quota-DJ1QlAOq.js +1 -0
- package/dist/dashboard/assets/quota-DJ1QlAOq.js.gz +0 -0
- package/dist/dashboard/assets/{routing-in0f_IfK.js → routing-fnCsxgNO.js} +1 -1
- package/dist/dashboard/assets/routing-fnCsxgNO.js.gz +0 -0
- package/dist/dashboard/assets/{select-BLULNqTp.js → select-C9Ssle9b.js} +2 -2
- package/dist/dashboard/assets/select-C9Ssle9b.js.gz +0 -0
- package/dist/dashboard/assets/{settings-CcGczj6I.js → settings-BxslsErn.js} +1 -1
- package/dist/dashboard/assets/settings-BxslsErn.js.gz +0 -0
- package/dist/dashboard/assets/{stack-D2lIR0tg.js → stack-j7cXjgYX.js} +1 -1
- package/dist/dashboard/assets/stack-j7cXjgYX.js.gz +0 -0
- package/dist/dashboard/assets/{state-DTARjfiW.js → state-DHmEnLa2.js} +1 -1
- package/dist/dashboard/assets/state-DHmEnLa2.js.gz +0 -0
- package/dist/dashboard/assets/{switch-c0oPSnqW.js → switch-DmZcyTIC.js} +1 -1
- package/dist/dashboard/assets/switch-DmZcyTIC.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-m8WGNPrz.js → useMutation-CbCP4w1o.js} +1 -1
- package/dist/dashboard/assets/useMutation-CbCP4w1o.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 +57 -57
- package/migrations/full-postgres/0042_add_telemetry_degenerate_stop.sql +14 -0
- package/migrations/full-postgres/0043_add_provider_custom_user_agent.sql +14 -0
- package/migrations/lite-sqlite3/0042_add_telemetry_degenerate_stop.sql +12 -0
- package/migrations/lite-sqlite3/0043_add_provider_custom_user_agent.sql +10 -0
- package/package.json +3 -1
- package/src/boot/dependencies.ts +10 -2
- package/src/config/providers.ts +13 -7
- package/src/config/security.ts +1 -1
- package/src/config/spec.ts +31 -17
- package/src/config/usage.ts +27 -0
- package/src/console/api-keys/routes.ts +19 -23
- package/src/console/cli-tools/cartethyia-websearch-plugin.ts +186 -0
- package/src/console/cli-tools/contracts.ts +39 -295
- package/src/console/cli-tools/fs-ops.ts +29 -0
- package/src/console/cli-tools/injectors/claude.ts +3 -3
- package/src/console/cli-tools/injectors/codex.ts +6 -6
- package/src/console/cli-tools/injectors/driver.ts +8 -105
- package/src/console/cli-tools/injectors/kilo.ts +146 -21
- package/src/console/cli-tools/injectors/opencode.ts +68 -17
- package/src/console/cli-tools/routes.ts +6 -0
- package/src/console/cli-tools/service.ts +7 -6
- package/src/console/observability/contracts.ts +6 -0
- package/src/console/observability/store.ts +3 -0
- package/src/console/providers/catalog/contracts.ts +4 -0
- package/src/console/providers/catalog/routes.ts +1 -0
- package/src/console/providers/detail/routes.ts +8 -0
- package/src/console/providers/detail/store.ts +4 -0
- package/src/console/quota/account-quota-tenant.ts +74 -0
- package/src/console/share/share-stats.ts +7 -9
- package/src/observability/event-log.ts +7 -0
- package/src/observability/event-query.ts +2 -0
- package/src/observability/telemetry-buffer.ts +9 -1
- package/src/persistence/boot.ts +13 -4
- package/src/persistence/schema.ts +15 -1
- package/src/persistence/stores/api-key-store.ts +19 -1
- package/src/protocol/canonical-model.ts +7 -0
- package/src/protocol/dsml.ts +5 -76
- package/src/protocol/request/gemini.ts +20 -1
- package/src/protocol/tool-identity.ts +15 -1
- package/src/providers/adapters/compatible-adapter.ts +29 -1
- package/src/providers/default-registry.ts +20 -4
- package/src/providers/discovery/discovery-types.ts +18 -0
- package/src/providers/discovery/probe-phases.ts +45 -0
- package/src/providers/discovery/probing-service.ts +41 -6
- package/src/providers/integrations/antigravity/antigravity-protocol.ts +108 -16
- package/src/providers/integrations/antigravity/antigravity-quota.ts +18 -6
- package/src/providers/integrations/antigravity/antigravity.ts +44 -11
- package/src/providers/integrations/codex/codex.ts +28 -1
- package/src/providers/integrations/cursor/cursor.ts +2 -3
- package/src/providers/integrations/github/github-discovery.ts +15 -2
- package/src/providers/integrations/github/github-oauth.ts +178 -9
- package/src/providers/integrations/github/github.ts +95 -14
- package/src/providers/integrations/kiro/kiro.ts +1 -2
- package/src/providers/integrations/qoder/chat-proto.ts +328 -0
- package/src/providers/integrations/qoder/claim-qoder-credits.ts +258 -0
- package/src/providers/integrations/qoder/qoder-adapter.ts +275 -0
- package/src/providers/integrations/qoder/qoder-auth.ts +201 -0
- package/src/providers/integrations/qoder/qoder-body.ts +78 -0
- package/src/providers/integrations/qoder/qoder-catalog.ts +14 -4
- package/src/providers/integrations/qoder/qoder-chat.ts +514 -0
- package/src/providers/integrations/qoder/qoder-cosy.ts +162 -0
- package/src/providers/integrations/qoder/qoder-grpc.ts +667 -0
- package/src/providers/integrations/qoder/qoder-ide.ts +584 -0
- package/src/providers/integrations/qoder/qoder-oauth.ts +4 -4
- package/src/providers/integrations/qoder/qoder-quota.ts +1 -1
- package/src/providers/integrations/qoder/qoder-realm.ts +40 -4
- package/src/providers/integrations/qoder/qoder-types.ts +51 -0
- package/src/providers/integrations/qoder/qoder-umid.ts +123 -8
- package/src/providers/integrations/qoder/qoder.ts +147 -1830
- package/src/providers/integrations/trae/trae.ts +2 -2
- package/src/providers/operations/account-error-classification.ts +67 -17
- package/src/providers/operations/byok-wire-profile.ts +19 -15
- package/src/providers/provider-contracts.ts +7 -0
- package/src/providers/provider-usage.ts +24 -55
- package/src/security/abuse.ts +5 -5
- package/src/security/admission/service.ts +11 -4
- package/src/security/api-key-auth.ts +5 -3
- package/src/transport/dispatch/attempt-finalize.ts +69 -21
- package/src/transport/dispatch/attempt-loop.ts +22 -0
- package/src/transport/dispatch/upstream.ts +4 -2
- package/src/transport/failure-policy.ts +15 -1
- package/src/transport/guards/error-lifecycle.ts +4 -0
- package/src/transport/request/state.ts +6 -0
- package/src/transport/routing/route-catalog.ts +8 -0
- package/src/transport/routing/route-model.ts +4 -0
- package/src/transport/routing/router.ts +8 -110
- package/src/transport/surface/reasoning-summary.ts +9 -14
- package/src/transport/vision-handoff.ts +83 -28
- package/dist/dashboard/assets/BannedPage-Cj15GsMp.js +0 -1
- package/dist/dashboard/assets/BannedPage-Cj15GsMp.js.gz +0 -0
- package/dist/dashboard/assets/ClaimGiftsButton-D83pBJ6J.js +0 -1
- package/dist/dashboard/assets/ClaimGiftsButton-D83pBJ6J.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-DZKmVuJN.js +0 -1
- package/dist/dashboard/assets/CliToolDetailPage-DZKmVuJN.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-BIccZmeL.js +0 -1
- package/dist/dashboard/assets/CliToolsPage-BIccZmeL.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-DEEKkYD-.js +0 -1
- package/dist/dashboard/assets/CombosPage-DEEKkYD-.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-D5rol2j2.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-BKj_r0rp.js.gz +0 -0
- package/dist/dashboard/assets/InjectSystemPromptPanel-B5nZvmf0.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-DkXb6dx-.js +0 -1
- package/dist/dashboard/assets/LoginPage-DkXb6dx-.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-DnQPSIA8.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-Ci2wQKUz.js +0 -1
- package/dist/dashboard/assets/OverviewPage-Ci2wQKUz.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-odlgYrOg.js +0 -5
- package/dist/dashboard/assets/ProviderDetailPage-odlgYrOg.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-EfeloBn4.js +0 -1
- package/dist/dashboard/assets/ProviderIcon-EfeloBn4.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-CqFBQEB8.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-CqFBQEB8.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-5SnUSy3j.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-CmnMDzhP.js +0 -1
- package/dist/dashboard/assets/QuotaPage-CmnMDzhP.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-BJtRZ4qD.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-WTa0sktC.js +0 -1
- package/dist/dashboard/assets/SetupPage-WTa0sktC.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-BfAb5-o_.js.gz +0 -0
- package/dist/dashboard/assets/ToolIcon-K1ckcDKg.js +0 -1
- package/dist/dashboard/assets/ToolIcon-K1ckcDKg.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-BNzeZ2Vk.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-l29JROPv.js +0 -1
- package/dist/dashboard/assets/api-keys-l29JROPv.js.gz +0 -0
- package/dist/dashboard/assets/app--PJWq8_C.css +0 -2
- package/dist/dashboard/assets/app--PJWq8_C.css.gz +0 -0
- package/dist/dashboard/assets/app-Bgd3zANQ.js +0 -9
- package/dist/dashboard/assets/app-Bgd3zANQ.js.gz +0 -0
- package/dist/dashboard/assets/assert-C0K-c4eo.js.gz +0 -0
- package/dist/dashboard/assets/badge-DfbhQU4k.js.gz +0 -0
- package/dist/dashboard/assets/banner-BllT4LcF.js.gz +0 -0
- package/dist/dashboard/assets/card-CtkX66Wd.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-CQXVMn4P.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-BQqBQd2n.js.gz +0 -0
- package/dist/dashboard/assets/dialog-BEK7KP0H.js +0 -1
- package/dist/dashboard/assets/dialog-BEK7KP0H.js.gz +0 -0
- package/dist/dashboard/assets/drawer-BrTEZ8zu.js.gz +0 -0
- package/dist/dashboard/assets/entry-CHWxbS56.js +0 -1
- package/dist/dashboard/assets/entry-CHWxbS56.js.gz +0 -0
- package/dist/dashboard/assets/entry-EBHJ3DrE.js +0 -1
- package/dist/dashboard/assets/entry-EBHJ3DrE.js.gz +0 -0
- package/dist/dashboard/assets/entry-LCS85hiq.js +0 -4
- package/dist/dashboard/assets/entry-LCS85hiq.js.gz +0 -0
- package/dist/dashboard/assets/formatters-BAACA727.js +0 -1
- package/dist/dashboard/assets/formatters-BAACA727.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-YSCE4UC2.js +0 -1
- package/dist/dashboard/assets/github-badge-YSCE4UC2.js.gz +0 -0
- package/dist/dashboard/assets/index-FQlj0HUT.js.gz +0 -0
- package/dist/dashboard/assets/inline-C4gU5iOL.js.gz +0 -0
- package/dist/dashboard/assets/input-BHHX_Tmm.js.gz +0 -0
- package/dist/dashboard/assets/layout-CajrYUnD.js.gz +0 -0
- package/dist/dashboard/assets/list-row-Bx2I7XUe.js.gz +0 -0
- package/dist/dashboard/assets/live-DNNqRbj2.js.gz +0 -0
- package/dist/dashboard/assets/mutation-Rr0TIvcj.js.gz +0 -0
- package/dist/dashboard/assets/network-C5UBACrJ.js.gz +0 -0
- package/dist/dashboard/assets/numbers-CUWAZK4J.js +0 -2
- package/dist/dashboard/assets/numbers-CUWAZK4J.js.gz +0 -0
- package/dist/dashboard/assets/pagination-Xsi9Qx04.js.gz +0 -0
- package/dist/dashboard/assets/providers/zai-coding-plan.svg +0 -6
- package/dist/dashboard/assets/providers/zai-coding-plan.svg.gz +0 -0
- package/dist/dashboard/assets/quota-DvQbD_sr.js +0 -1
- package/dist/dashboard/assets/quota-DvQbD_sr.js.gz +0 -0
- package/dist/dashboard/assets/routing-in0f_IfK.js.gz +0 -0
- package/dist/dashboard/assets/select-BLULNqTp.js.gz +0 -0
- package/dist/dashboard/assets/settings-CcGczj6I.js.gz +0 -0
- package/dist/dashboard/assets/stack-D2lIR0tg.js.gz +0 -0
- package/dist/dashboard/assets/state-DTARjfiW.js.gz +0 -0
- package/dist/dashboard/assets/switch-c0oPSnqW.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-m8WGNPrz.js.gz +0 -0
- package/src/console/cli-tools/injectors/cline.ts +0 -110
- package/src/console/cli-tools/injectors/copilot.ts +0 -108
- package/src/console/cli-tools/injectors/cowork.ts +0 -64
- package/src/console/cli-tools/injectors/deepseek-tui.ts +0 -85
- package/src/console/cli-tools/injectors/droid.ts +0 -96
- package/src/console/cli-tools/injectors/grok-build.ts +0 -70
- package/src/console/cli-tools/injectors/hermes.ts +0 -101
- package/src/console/cli-tools/injectors/jcode.ts +0 -115
- package/src/console/cli-tools/injectors/openclaw.ts +0 -161
- package/src/providers/integrations/qoder/qoder-text-tools.ts +0 -277
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Qoder adapter — transport selection, credential resolution, dispatch.
|
|
3
|
+
*
|
|
4
|
+
* Surface contract (2026-11-05):
|
|
5
|
+
* - `grpc` (default, production): `model.chat.ChatService/ChatCompletionStream`
|
|
6
|
+
* — the only proven carrier of real 1M-token context (HTTP IDE caps at
|
|
7
|
+
* ~256 KiB). Serves billed rows; a free-tier row (`lite`) is refused there
|
|
8
|
+
* (`quota exceeded` 116) and must use legacy COSY.
|
|
9
|
+
* - `cosy` (free/lite + forced legacy): `/algo/.../agent_chat_generation` —
|
|
10
|
+
* the only surface that accepts free-tier rows. Signing is the COSY Bearer
|
|
11
|
+
* scheme (AES/RSA + alphabet-scrambled body).
|
|
12
|
+
* - `ide` (`/model/v1/chat/completions`): **probe/testing only — never
|
|
13
|
+
* production.** It is the only surface that echoes strategy metadata
|
|
14
|
+
* (`raw_usage` → provider_model/target) and it streams immediately, but its
|
|
15
|
+
* ~256 KiB body cap cannot carry the 1M window the roster declares. Dispatch
|
|
16
|
+
* reaches it only when `probe_surface: "ide"` is set (discovery probes) or
|
|
17
|
+
* the option `surface: "ide"` is forced explicitly for local testing.
|
|
18
|
+
*/
|
|
19
|
+
import { randomUUID } from "node:crypto";
|
|
20
|
+
import { GatewayError } from "../../../protocol/gateway-error";
|
|
21
|
+
import type { CanonicalEvent, CanonicalRequest } from "../../../protocol/canonical-model";
|
|
22
|
+
import {
|
|
23
|
+
assertWireFamily,
|
|
24
|
+
readCredentialSecret,
|
|
25
|
+
type ProviderAdapter,
|
|
26
|
+
type ProviderDispatchContext,
|
|
27
|
+
type ProviderDispatchTarget,
|
|
28
|
+
type ProviderId,
|
|
29
|
+
} from "../../provider-contracts";
|
|
30
|
+
import { abortGatewayError, createUpstreamDeadlineLifecycle } from "../../operations/upstream-deadline";
|
|
31
|
+
import { getQoderVersion } from "../../operations/client-versions";
|
|
32
|
+
import { qoderMachineHostname, qoderMachineOs } from "../../operations/cli-platform";
|
|
33
|
+
import { mapUpstreamHttpError } from "../../../transport/failure-policy";
|
|
34
|
+
import { QODER_CN_MODEL_CONFIGS, QODER_INTL_MODEL_CONFIGS, qoderDiscoveredMaxContexts } from "./qoder-catalog";
|
|
35
|
+
import { isQoderVirtualModel, qoderDispatchModelId } from "./qoder-virtual";
|
|
36
|
+
import { isQoderPat, qoderOAuthAuth, qoderSessionId, resolveQoderAuth } from "./qoder-auth";
|
|
37
|
+
import { buildQoderRequest, callQoder } from "./qoder-chat";
|
|
38
|
+
import { buildQoderGrpcRequest, qoderGrpcMetadata, qoderGrpcToCanonicalEvents } from "./qoder-grpc";
|
|
39
|
+
import { buildQoderIdeRequest, qoderBodyToCanonicalEvents } from "./qoder-ide";
|
|
40
|
+
import {
|
|
41
|
+
QODER_CHAT_PATH,
|
|
42
|
+
QODER_PROVIDER_ID,
|
|
43
|
+
qoderIdeChatUrl,
|
|
44
|
+
qoderInferenceCandidates,
|
|
45
|
+
type QoderRealm,
|
|
46
|
+
} from "./qoder-realm";
|
|
47
|
+
import type { QoderModelConfig } from "./qoder-types";
|
|
48
|
+
|
|
49
|
+
interface QoderAdapterOptions {
|
|
50
|
+
readonly fetch?: typeof fetch;
|
|
51
|
+
/** Which deployment this adapter dispatches to. Defaults to intl. */
|
|
52
|
+
readonly realm?: QoderRealm;
|
|
53
|
+
/** Bundled provider id, so the adapter's own id matches the route's. */
|
|
54
|
+
readonly providerId?: string;
|
|
55
|
+
/**
|
|
56
|
+
* Which chat transport to dispatch on.
|
|
57
|
+
*
|
|
58
|
+
* `grpc` (default) uses the gRPC model-server surface
|
|
59
|
+
* (`model.chat.ChatService/ChatCompletionStream`) — the only proven carrier
|
|
60
|
+
* of real 1M-token context (the HTTP IDE surface caps at ~256 KiB). gRPC
|
|
61
|
+
* serves billed models; a free-tier row (`lite`) is refused there
|
|
62
|
+
* (`quota exceeded` 116) and must use the legacy COSY surface.
|
|
63
|
+
*
|
|
64
|
+
* `cosy` forces only the legacy surface (`/algo/.../agent_chat_generation`)
|
|
65
|
+
* — the free-tier path and the pre-gRPC behavior.
|
|
66
|
+
*
|
|
67
|
+
* `ide` forces the modern `/model/v1` surface — **probe/testing only, never
|
|
68
|
+
* production** (see module doc).
|
|
69
|
+
*/
|
|
70
|
+
readonly surface?: "grpc" | "cosy" | "ide";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Resolves the transport for one dispatch. Exported for tests. */
|
|
74
|
+
export function resolveQoderTransport(
|
|
75
|
+
surface: "grpc" | "cosy" | "ide" | undefined,
|
|
76
|
+
modelConfig: QoderModelConfig,
|
|
77
|
+
): "grpc" | "cosy" | "ide" {
|
|
78
|
+
const chosen = surface ?? "grpc";
|
|
79
|
+
// `ide` is an explicit testing override — never auto-selected, and it does
|
|
80
|
+
// not apply the free-tier rule (a forced IDE call is a test call).
|
|
81
|
+
if (chosen === "ide") return "ide";
|
|
82
|
+
// Free-tier rows (`lite`) are refused on gRPC with `quota exceeded` (116);
|
|
83
|
+
// they must use the legacy COSY surface.
|
|
84
|
+
if (chosen === "grpc" && modelConfig.is_free === true) return "cosy";
|
|
85
|
+
return chosen;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The widest selectable context window for one model key (from the live list's
|
|
90
|
+
* `context_config`; falls back to the static roster's stated window).
|
|
91
|
+
*/
|
|
92
|
+
function qoderMaxContextFor(modelId: string, modelConfig: QoderModelConfig, realm: QoderRealm): number {
|
|
93
|
+
const discovered = qoderDiscoveredMaxContexts(realm).get(modelId);
|
|
94
|
+
if (discovered !== undefined) return discovered;
|
|
95
|
+
return modelConfig.max_input_tokens;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
class QoderAdapter implements ProviderAdapter {
|
|
99
|
+
readonly provider_id: ProviderId;
|
|
100
|
+
private readonly realm: QoderRealm;
|
|
101
|
+
private readonly surface: "grpc" | "cosy" | "ide";
|
|
102
|
+
private readonly fetchFn: typeof fetch;
|
|
103
|
+
|
|
104
|
+
constructor(options: QoderAdapterOptions = {}) {
|
|
105
|
+
this.provider_id = (options.providerId ?? QODER_PROVIDER_ID) as ProviderId;
|
|
106
|
+
this.realm = options.realm ?? "intl";
|
|
107
|
+
this.surface = options.surface ?? "grpc";
|
|
108
|
+
this.fetchFn = options.fetch ?? globalThis.fetch;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The static roster for this adapter's realm.
|
|
113
|
+
*
|
|
114
|
+
* A single merged table advertised CN-only keys to global accounts and vice
|
|
115
|
+
* versa, so a request was routed to a deployment that refuses it.
|
|
116
|
+
*/
|
|
117
|
+
#modelConfigs(): Record<string, QoderModelConfig> {
|
|
118
|
+
return (this.realm === "cn" ? QODER_CN_MODEL_CONFIGS : QODER_INTL_MODEL_CONFIGS) as Record<
|
|
119
|
+
string,
|
|
120
|
+
QoderModelConfig
|
|
121
|
+
>;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async *dispatch(
|
|
125
|
+
request: CanonicalRequest,
|
|
126
|
+
candidate: ProviderDispatchTarget,
|
|
127
|
+
context: ProviderDispatchContext,
|
|
128
|
+
): AsyncIterable<CanonicalEvent> {
|
|
129
|
+
if (candidate.wire_family !== "chat") {
|
|
130
|
+
assertWireFamily(candidate, "chat", "qoder");
|
|
131
|
+
}
|
|
132
|
+
const secret = readCredentialSecret(context.credential, "Qoder credential is required");
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* A caller-facing virtual name is translated to its roster key here.
|
|
136
|
+
*
|
|
137
|
+
* The translation has to happen before the config lookup and before the
|
|
138
|
+
* request body: the body carries the roster key, and looking up the
|
|
139
|
+
* virtual id in the config table would fall through to the synthetic
|
|
140
|
+
* default and silently dispatch under-declared capabilities.
|
|
141
|
+
*/
|
|
142
|
+
const requestedModelId = candidate.model_id || request.model;
|
|
143
|
+
if (isQoderVirtualModel(requestedModelId) && !this.#modelConfigs()[qoderDispatchModelId(requestedModelId)]) {
|
|
144
|
+
throw new GatewayError(
|
|
145
|
+
"model_not_found",
|
|
146
|
+
404,
|
|
147
|
+
`Qoder virtual model ${requestedModelId} resolves to a roster key this realm does not serve (request {requestId}). Try another model — this one's off the menu 📚`,
|
|
148
|
+
{ virtual_model: requestedModelId, target: qoderDispatchModelId(requestedModelId) },
|
|
149
|
+
"upstream",
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
const modelId = qoderDispatchModelId(requestedModelId);
|
|
153
|
+
const modelConfig = this.#modelConfigs()[modelId] ?? {
|
|
154
|
+
id: modelId,
|
|
155
|
+
display_name: modelId,
|
|
156
|
+
max_input_tokens: 180_000,
|
|
157
|
+
is_vl: true,
|
|
158
|
+
is_reasoning: false,
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const lifecycle = createUpstreamDeadlineLifecycle(context);
|
|
162
|
+
|
|
163
|
+
// Outbound fetch respects SSRF binding when provided
|
|
164
|
+
const outboundFetch: typeof fetch = (context.outbound_fetch as unknown as typeof fetch) ?? this.fetchFn;
|
|
165
|
+
|
|
166
|
+
const fetcher: (url: string, init: RequestInit) => Promise<Response> = (url, init) =>
|
|
167
|
+
outboundFetch(url, { ...init, signal: lifecycle.signal } as RequestInit) as Promise<Response>;
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
const version = getQoderVersion();
|
|
171
|
+
// Dual mode like Cline: a pasted PAT is exchanged for a job token per
|
|
172
|
+
// request, while an OAuth device token is used directly with the
|
|
173
|
+
// identity the login persisted.
|
|
174
|
+
const auth = isQoderPat(secret)
|
|
175
|
+
? await resolveQoderAuth({ secret, authState: context.credential.auth_state, signal: lifecycle.signal, fetcher, realm: this.realm })
|
|
176
|
+
: qoderOAuthAuth(secret, context.credential.auth_state, context.credential.account_id);
|
|
177
|
+
|
|
178
|
+
// Transport selection — see module doc for the surface contract.
|
|
179
|
+
const forceIde = this.surface === "ide" || candidate.probe_surface === "ide";
|
|
180
|
+
const useGrpc = !forceIde && resolveQoderTransport(this.surface, modelConfig) === "grpc";
|
|
181
|
+
|
|
182
|
+
if (forceIde) {
|
|
183
|
+
const ideUrl = qoderIdeChatUrl(this.realm);
|
|
184
|
+
const ideBody = buildQoderIdeRequest(modelId, request, modelConfig, auth, this.realm, version);
|
|
185
|
+
// Header fingerprint mirrors the desktop client's IDE surface (`PFn` /
|
|
186
|
+
// `Hc`): X-Request-ID normalized to uppercase, Cosy-Version, real
|
|
187
|
+
// machine OS, and the sanitized hostname on infer SSE requests. The
|
|
188
|
+
// User-Agent is the real IDE client's (`Bun/1.4.4`, live capture) and
|
|
189
|
+
// `X-Session-ID` matches the body's session id (sha256 userId:model).
|
|
190
|
+
const ideHostname = qoderMachineHostname();
|
|
191
|
+
const res = await fetcher(ideUrl, {
|
|
192
|
+
method: "POST",
|
|
193
|
+
headers: {
|
|
194
|
+
Authorization: `Bearer ${auth.securityOauthToken}`,
|
|
195
|
+
"Content-Type": "application/json",
|
|
196
|
+
Accept: "text/event-stream",
|
|
197
|
+
"User-Agent": "Bun/1.4.4",
|
|
198
|
+
"X-Request-ID": randomUUID().toUpperCase(),
|
|
199
|
+
"X-Session-ID": qoderSessionId(auth.userId, modelId),
|
|
200
|
+
"Cosy-Version": version,
|
|
201
|
+
"Cosy-ClientType": "5",
|
|
202
|
+
"Cosy-MachineOS": qoderMachineOs(),
|
|
203
|
+
...(ideHostname.length > 0 ? { "Cosy-MachineHostname": ideHostname } : {}),
|
|
204
|
+
},
|
|
205
|
+
body: JSON.stringify(ideBody),
|
|
206
|
+
});
|
|
207
|
+
if (!res.ok) throw await mapUpstreamHttpError(res, "qoder");
|
|
208
|
+
if (!res.body) throw new GatewayError("platform_unavailable", 502, "Qoder returned empty body (request {requestId}). The upstream came back empty-handed — retry 🌙", {}, "upstream");
|
|
209
|
+
lifecycle.release();
|
|
210
|
+
yield* qoderBodyToCanonicalEvents(res.body as ReadableStream<Uint8Array>, lifecycle.signal, modelId);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (useGrpc) {
|
|
215
|
+
// The gRPC request mirrors the HTTP surfaces' OpenAI-shaped body, so
|
|
216
|
+
// the shared builder supplies the messages, tool declarations, and
|
|
217
|
+
// tool-choice knobs both surfaces expect. Without the `tools` array
|
|
218
|
+
// the upstream sees no toolset and answers with a DSML text block (or
|
|
219
|
+
// claims no tool exists) instead of a structured `tool_calls`.
|
|
220
|
+
const legacy = buildQoderRequest(modelId, request, modelConfig, auth, this.realm);
|
|
221
|
+
const grpcRequest = buildQoderGrpcRequest(
|
|
222
|
+
modelId,
|
|
223
|
+
request,
|
|
224
|
+
auth.userId,
|
|
225
|
+
qoderMaxContextFor(modelId, modelConfig, this.realm),
|
|
226
|
+
{
|
|
227
|
+
messages: legacy.messages as Array<Record<string, unknown>>,
|
|
228
|
+
tools: legacy.tools as Array<Record<string, unknown>>,
|
|
229
|
+
...(legacy.parameters === undefined
|
|
230
|
+
? {}
|
|
231
|
+
: { parameters: legacy.parameters as Record<string, unknown> }),
|
|
232
|
+
},
|
|
233
|
+
);
|
|
234
|
+
const grpcMetadata = qoderGrpcMetadata(auth);
|
|
235
|
+
lifecycle.release();
|
|
236
|
+
yield* qoderGrpcToCanonicalEvents(grpcRequest, grpcMetadata, this.realm, lifecycle.signal);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const cosyUrl = candidate.endpoint_path?.startsWith("http")
|
|
241
|
+
? candidate.endpoint_path
|
|
242
|
+
: `${qoderInferenceCandidates(this.realm, auth.securityOauthToken)[0] ?? ""}${QODER_CHAT_PATH}`;
|
|
243
|
+
const cosyRequest = () =>
|
|
244
|
+
callQoder(cosyUrl, buildQoderRequest(modelId, request, modelConfig, auth, this.realm), modelId, auth, lifecycle.signal, fetcher, version);
|
|
245
|
+
const response = await cosyRequest();
|
|
246
|
+
if (!response.ok) throw await mapUpstreamHttpError(response, "qoder");
|
|
247
|
+
if (!response.body) throw new GatewayError("platform_unavailable", 502, "Qoder returned empty body (request {requestId}). The upstream came back empty-handed — retry 🌙", {}, "upstream");
|
|
248
|
+
// Headers arrived: the pre-stream deadline has served its purpose.
|
|
249
|
+
// From here the gateway stall/first-chunk watchdog (propagated via
|
|
250
|
+
// context.abort_signal) governs the body. Keeping this timer armed
|
|
251
|
+
// would kill healthy long streams at the stale pre-stream deadline.
|
|
252
|
+
lifecycle.release();
|
|
253
|
+
yield* qoderBodyToCanonicalEvents(response.body as ReadableStream<Uint8Array>, lifecycle.signal, request.model);
|
|
254
|
+
} catch (err: unknown) {
|
|
255
|
+
const abortError = abortGatewayError(lifecycle, err, context.abort_signal);
|
|
256
|
+
if (abortError) throw abortError;
|
|
257
|
+
throw err;
|
|
258
|
+
} finally {
|
|
259
|
+
lifecycle.release();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function createQoderAdapter(options: QoderAdapterOptions = {}): ProviderAdapter {
|
|
265
|
+
return new QoderAdapter(options);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Adapter for the China deployment.
|
|
270
|
+
*
|
|
271
|
+
* Same wire, different gateway host and roster — see `qoder-realm.ts`.
|
|
272
|
+
*/
|
|
273
|
+
export function createQoderCnAdapter(options: QoderAdapterOptions = {}): ProviderAdapter {
|
|
274
|
+
return new QoderAdapter({ ...options, realm: "cn", providerId: "qodercn" });
|
|
275
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Qoder credential resolution & PAT→jobToken exchange.
|
|
3
|
+
*
|
|
4
|
+
* Device tokens (`dt-…`) from OAuth login are used directly; a PAT
|
|
5
|
+
* (`pt-…`) must be exchanged for a short-lived job token (`jt-…`) on the
|
|
6
|
+
* realm's job-token endpoint before it can authenticate any surface.
|
|
7
|
+
*
|
|
8
|
+
* The exchange is the modern v1 endpoint (`/api/v1/jobToken/exchange`) with
|
|
9
|
+
* a plain JSON body — verified live (2026-11-05): `POST
|
|
10
|
+
* center.qoder.sh/api/v1/jobToken/exchange` with `{personal_token, machine_id}`
|
|
11
|
+
* returns `{token: jt-…, refresh_token: jrt-…, expires_at}`. The old v3
|
|
12
|
+
* scrambled-COSY endpoint is retired. `X-Request-ID` is sent uppercase and
|
|
13
|
+
* the User-Agent is `qoder/<version>` — both mirror the real client.
|
|
14
|
+
*
|
|
15
|
+
* The v1 response does not include the account's user id, so `userId`
|
|
16
|
+
* falls back to the machine id (decision 2026-11-05). That is sufficient
|
|
17
|
+
* for Bearer surfaces (gRPC / IDE); COSY signing (free/lite PAT) uses it
|
|
18
|
+
* as the identity placeholder until the user id is sourced another way.
|
|
19
|
+
*/
|
|
20
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
21
|
+
import { GatewayError } from "../../../protocol/gateway-error";
|
|
22
|
+
import { mapUpstreamHttpError } from "../../../transport/failure-policy";
|
|
23
|
+
import { getQoderVersion } from "../../operations/client-versions";
|
|
24
|
+
import { qoderMachineOs } from "../../operations/cli-platform";
|
|
25
|
+
import { parseQoderOAuthState } from "./qoder-oauth";
|
|
26
|
+
import { QODER_REALMS, type QoderRealm } from "./qoder-realm";
|
|
27
|
+
import type { QoderAuth } from "./qoder-types";
|
|
28
|
+
|
|
29
|
+
/** A PAT (`pt-…`) must be exchanged for a job token before use. */
|
|
30
|
+
export function isQoderPat(secret: string): boolean {
|
|
31
|
+
return secret.startsWith("pt-");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Stable per-account machine id for OAuth dispatches. Derived from the
|
|
36
|
+
* account id (not minted per request) so pooled accounts keep one device
|
|
37
|
+
* fingerprint across restarts instead of looking like a new device every
|
|
38
|
+
* time, while staying distinct per account.
|
|
39
|
+
*/
|
|
40
|
+
export function qoderMachineIdForAccount(accountId: string): string {
|
|
41
|
+
const hex = createHash("sha256").update(`qoder-machine-v1:${accountId}`).digest("hex");
|
|
42
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Builds dispatch auth from an OAuth device token. The token is used
|
|
47
|
+
* directly as the COSY identity's oauth token; the user id and machine id
|
|
48
|
+
* come from the `auth_state` the login persisted. Fails closed with a
|
|
49
|
+
* reconnect prompt when the identity is absent (e.g. an account created
|
|
50
|
+
* before `auth_state` existed, or an import that dropped the field) instead
|
|
51
|
+
* of signing with a guessed id. The message names the missing field so an
|
|
52
|
+
* operator can fix the export instead of re-logging blind.
|
|
53
|
+
*/
|
|
54
|
+
export function qoderOAuthAuth(
|
|
55
|
+
secret: string,
|
|
56
|
+
authState: unknown,
|
|
57
|
+
accountId: string | undefined,
|
|
58
|
+
): QoderAuth {
|
|
59
|
+
const state = parseQoderOAuthState(authState);
|
|
60
|
+
if (state === undefined) {
|
|
61
|
+
throw new GatewayError(
|
|
62
|
+
"authentication_failed",
|
|
63
|
+
401,
|
|
64
|
+
"Qoder OAuth account is missing its user identity (request {requestId}) (auth_state.userId/machineId absent or empty — re-login, or re-export with authState included); sign in again. Wrong key, right door — check the credential 🔑",
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
userId: state.userId,
|
|
69
|
+
userName: "",
|
|
70
|
+
userType: "personal_standard",
|
|
71
|
+
securityOauthToken: secret,
|
|
72
|
+
refreshToken: "",
|
|
73
|
+
machineId: state.machineId || (accountId ? qoderMachineIdForAccount(accountId) : randomUUID()),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const QODER_MACHINE_ID_TTL_MS = 60 * 60 * 1000;
|
|
78
|
+
const QODER_MAX_MACHINE_IDS = 1_024;
|
|
79
|
+
const qoderMachineIds = new Map<string, { id: string; expiresAt: number }>();
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Stable pseudo-device id per PAT for the whole hour (pool-friendly): a PAT
|
|
83
|
+
* keeps one machine id across requests within the TTL instead of minting a
|
|
84
|
+
* fresh device each time.
|
|
85
|
+
*/
|
|
86
|
+
function qoderMachineIdFromPat(pat: string): string {
|
|
87
|
+
const key = createHash("sha256").update(pat).digest("hex");
|
|
88
|
+
const now = Date.now();
|
|
89
|
+
const existing = qoderMachineIds.get(key);
|
|
90
|
+
if (existing && existing.expiresAt > now) return existing.id;
|
|
91
|
+
const id = randomUUID();
|
|
92
|
+
qoderMachineIds.delete(key);
|
|
93
|
+
qoderMachineIds.set(key, { id, expiresAt: now + QODER_MACHINE_ID_TTL_MS });
|
|
94
|
+
for (const [candidate, value] of qoderMachineIds) {
|
|
95
|
+
if (value.expiresAt <= now) qoderMachineIds.delete(candidate);
|
|
96
|
+
}
|
|
97
|
+
while (qoderMachineIds.size > QODER_MAX_MACHINE_IDS) {
|
|
98
|
+
const oldest = qoderMachineIds.keys().next();
|
|
99
|
+
if (oldest.done) break;
|
|
100
|
+
qoderMachineIds.delete(oldest.value);
|
|
101
|
+
}
|
|
102
|
+
return id;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Stable 32-hex session id for one user+model conversation. Mirrors the
|
|
107
|
+
* live IDE surface (`X-Session-ID` = sha256(userId:model), 32 hex) and is
|
|
108
|
+
* shared by the HTTP and gRPC transports so body `session_id` matches the
|
|
109
|
+
* transport-level session header.
|
|
110
|
+
*/
|
|
111
|
+
export function qoderSessionId(userId: string, modelId: string): string {
|
|
112
|
+
return createHash("sha256").update(`qoder-session:${userId}:${modelId}`).digest("hex").slice(0, 32);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
type QoderFetcher = (url: string, init: RequestInit) => Promise<Response>;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Exchanges a PAT (`pt-…`) for a short-lived job token (`jt-…`) using the
|
|
119
|
+
* modern v1 endpoint. Verified live (2026-11-05): `POST
|
|
120
|
+
* center.qoder.sh/api/v1/jobToken/exchange` with a plain JSON body returns
|
|
121
|
+
* `{token: jt-…, refresh_token: jrt-…, expires_at}`. `X-Request-ID` is
|
|
122
|
+
* sent uppercase and the User-Agent is `qoder/<version>` — both mirror the
|
|
123
|
+
* real client.
|
|
124
|
+
*
|
|
125
|
+
* The v1 response does not include the account's user id, so `userId`
|
|
126
|
+
* falls back to the machine id (decision 2026-11-05). That is sufficient
|
|
127
|
+
* for Bearer surfaces (gRPC / IDE); COSY signing (free/lite PAT) uses it
|
|
128
|
+
* as the identity placeholder until the user id is sourced another way.
|
|
129
|
+
*/
|
|
130
|
+
async function exchangeQoderPat(
|
|
131
|
+
pat: string,
|
|
132
|
+
signal: AbortSignal,
|
|
133
|
+
fetcher: QoderFetcher,
|
|
134
|
+
version = getQoderVersion(),
|
|
135
|
+
realm: QoderRealm = "intl",
|
|
136
|
+
): Promise<QoderAuth> {
|
|
137
|
+
const machineId = qoderMachineIdFromPat(pat);
|
|
138
|
+
const response = await fetcher(QODER_REALMS[realm].jobTokenUrl, {
|
|
139
|
+
method: "POST",
|
|
140
|
+
headers: {
|
|
141
|
+
"Content-Type": "application/json",
|
|
142
|
+
Accept: "application/json",
|
|
143
|
+
"User-Agent": `qoder/${version}`,
|
|
144
|
+
"X-Request-ID": randomUUID().toUpperCase(),
|
|
145
|
+
"Cosy-ClientType": "5",
|
|
146
|
+
"Cosy-Version": version,
|
|
147
|
+
"Cosy-MachineOS": qoderMachineOs(),
|
|
148
|
+
},
|
|
149
|
+
body: JSON.stringify({ personal_token: pat, machine_id: machineId }),
|
|
150
|
+
signal,
|
|
151
|
+
});
|
|
152
|
+
if (!response.ok) throw await mapUpstreamHttpError(response, "qoder");
|
|
153
|
+
let responseBody: unknown;
|
|
154
|
+
try {
|
|
155
|
+
responseBody = await response.json();
|
|
156
|
+
} catch {
|
|
157
|
+
throw new GatewayError("platform_unavailable", 502, "Qoder PAT exchange returned invalid JSON (request {requestId}). The upstream came back empty-handed — retry 🌙", {}, "upstream");
|
|
158
|
+
}
|
|
159
|
+
if (typeof responseBody !== "object" || responseBody === null || Array.isArray(responseBody)) {
|
|
160
|
+
throw new GatewayError("platform_unavailable", 502, "Qoder PAT exchange returned unexpected response (request {requestId}). The upstream came back empty-handed — retry 🌙", {}, "upstream");
|
|
161
|
+
}
|
|
162
|
+
const rec = responseBody as Record<string, unknown>;
|
|
163
|
+
const token = typeof rec["token"] === "string" ? (rec["token"] as string) : "";
|
|
164
|
+
if (!token) {
|
|
165
|
+
throw new GatewayError("platform_unavailable", 502, "Qoder PAT exchange returned incomplete credentials (request {requestId}). The upstream came back empty-handed — retry 🌙", {}, "upstream");
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
userId: machineId,
|
|
169
|
+
userName: typeof rec["name"] === "string" ? (rec["name"] as string) : "",
|
|
170
|
+
userType: typeof rec["userType"] === "string" ? (rec["userType"] as string) : "personal_standard",
|
|
171
|
+
securityOauthToken: token,
|
|
172
|
+
refreshToken: typeof rec["refresh_token"] === "string" ? (rec["refresh_token"] as string) : "",
|
|
173
|
+
machineId,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Resolves the dispatch identity for one Qoder credential.
|
|
179
|
+
*
|
|
180
|
+
* Shared by the adapter and by discovery: both need the same `QoderAuth` the
|
|
181
|
+
* COSY signature is built from, and duplicating the PAT-vs-OAuth branch would
|
|
182
|
+
* let the two drift.
|
|
183
|
+
*/
|
|
184
|
+
export async function resolveQoderAuth(input: {
|
|
185
|
+
readonly secret: string;
|
|
186
|
+
readonly authState: unknown;
|
|
187
|
+
readonly signal: AbortSignal;
|
|
188
|
+
readonly fetcher: (url: string, init: RequestInit) => Promise<Response>;
|
|
189
|
+
/** Which deployment mints the job token; a PAT is realm-bound. */
|
|
190
|
+
readonly realm?: QoderRealm;
|
|
191
|
+
}): Promise<QoderAuth> {
|
|
192
|
+
const realm = input.realm ?? "intl";
|
|
193
|
+
if (isQoderPat(input.secret)) {
|
|
194
|
+
return await exchangeQoderPat(input.secret, input.signal, input.fetcher, getQoderVersion(), realm);
|
|
195
|
+
}
|
|
196
|
+
// Discovery is handed only the secret and the persisted state, so the
|
|
197
|
+
// account id is read from that state — the same place `qoderOAuthAuth` reads
|
|
198
|
+
// it — rather than being a separate field callers can pass wrongly.
|
|
199
|
+
const state = parseQoderOAuthState(input.authState);
|
|
200
|
+
return qoderOAuthAuth(input.secret, input.authState, state?.userId);
|
|
201
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared canonical→Qoder message helpers.
|
|
3
|
+
*
|
|
4
|
+
* Kept in a separate module so both transports (`qoder.ts` HTTP and
|
|
5
|
+
* `qoder-grpc.ts`) build and flatten message content the same way.
|
|
6
|
+
*/
|
|
7
|
+
import type { ContentPart, ToolChoice, ToolDefinition } from "../../../protocol/canonical-model";
|
|
8
|
+
|
|
9
|
+
/** Joins text/tool-result content of one canonical message into plain text. */
|
|
10
|
+
export function flattenCanonicalContent(content: readonly ContentPart[]): string {
|
|
11
|
+
return content
|
|
12
|
+
.filter((b) => b.kind === "text" || b.kind === "toolResult")
|
|
13
|
+
.map((b) => {
|
|
14
|
+
if (b.kind === "text") return b.text;
|
|
15
|
+
if (b.kind === "toolResult") return typeof b.content === "string" ? b.content : JSON.stringify(b.content);
|
|
16
|
+
return "";
|
|
17
|
+
})
|
|
18
|
+
.join("\n");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Projects the canonical tools onto Qoder's chat-completions `tools` array
|
|
23
|
+
* (the same OpenAI-shaped declaration both the HTTP and the gRPC surface
|
|
24
|
+
* expect).
|
|
25
|
+
*
|
|
26
|
+
* Without this the upstream is never told a tool exists, so a model that wants
|
|
27
|
+
* one improvises markup in its prose (`<Read file_path="..." />`) instead of
|
|
28
|
+
* emitting a structured call — the call is then neither runnable nor
|
|
29
|
+
* recoverable, because it never was a call on either side.
|
|
30
|
+
*
|
|
31
|
+
* Only `function` tools are representable upstream. Anything else is dropped
|
|
32
|
+
* rather than forwarded: Qoder rejects an unknown variant during
|
|
33
|
+
* deserialization and fails the whole turn, which is worse than omitting a
|
|
34
|
+
* tool the model could not have used anyway.
|
|
35
|
+
*/
|
|
36
|
+
export function qoderTools(
|
|
37
|
+
tools: readonly ToolDefinition[] | undefined,
|
|
38
|
+
): Array<Record<string, unknown>> {
|
|
39
|
+
const out: Array<Record<string, unknown>> = [];
|
|
40
|
+
for (const tool of tools ?? []) {
|
|
41
|
+
if (tool.tool_type !== undefined && tool.tool_type !== "function") continue;
|
|
42
|
+
// The upstream requires `tools[].function` to be an object. A flat
|
|
43
|
+
// declaration naming the tool at the top level is rejected outright with
|
|
44
|
+
// "'function' is a required property, expected an object", which fails the
|
|
45
|
+
// whole request rather than just this tool.
|
|
46
|
+
out.push({
|
|
47
|
+
type: "function",
|
|
48
|
+
function: {
|
|
49
|
+
name: tool.name,
|
|
50
|
+
...(tool.description === undefined ? { description: "" } : { description: tool.description }),
|
|
51
|
+
parameters: tool.jsonSchema ?? { type: "object", properties: {} },
|
|
52
|
+
...(tool.strict === undefined ? {} : { strict: tool.strict }),
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Projects the canonical tool choice onto the upstream's field.
|
|
61
|
+
*
|
|
62
|
+
* The upstream only accepts a string here (both surfaces). Given the object
|
|
63
|
+
* form `{type:"function",function:{name}}` it ignores the field, and the model
|
|
64
|
+
* then calls the tool with no arguments filled in.
|
|
65
|
+
*
|
|
66
|
+
* `allowed_tools` has no chat-completions equivalent, so it collapses to the
|
|
67
|
+
* direction it already names rather than being dropped: a caller that asked for
|
|
68
|
+
* `required` must not silently get `auto`.
|
|
69
|
+
*/
|
|
70
|
+
export function qoderToolChoice(choice: ToolChoice | undefined): unknown {
|
|
71
|
+
if (choice === undefined) return undefined;
|
|
72
|
+
if (typeof choice === "string") return choice;
|
|
73
|
+
if (choice.type === "tool" || choice.type === "custom") {
|
|
74
|
+
const name = choice.name.trim();
|
|
75
|
+
return name.length > 0 ? name : "auto";
|
|
76
|
+
}
|
|
77
|
+
return choice.mode;
|
|
78
|
+
}
|
|
@@ -126,7 +126,13 @@ export const QODER_CN_MODEL_CONFIGS: Readonly<Record<string, QoderModelConfig>>
|
|
|
126
126
|
* `qmodel` carries a 1M window. That is why the rosters are not merged.
|
|
127
127
|
*/
|
|
128
128
|
export const QODER_INTL_MODEL_CONFIGS: Readonly<Record<string, QoderModelConfig>> = {
|
|
129
|
-
|
|
129
|
+
// Every row declares its *maximum selectable* window (1M), not the currently
|
|
130
|
+
// active one: Qoder publishes a `context_config` of selectable windows per
|
|
131
|
+
// row (1M/400K/200K/…), and the adapter dispatches the widest
|
|
132
|
+
// (`qoderMaxContextFor`). The live list's `max_input_tokens` only names the
|
|
133
|
+
// currently-selected window (often 180K/200K), which is not a ceiling —
|
|
134
|
+
// declaring that would cap a conversation a 1M-capable account could run.
|
|
135
|
+
auto: { id: "auto", display_name: "Auto", max_input_tokens: 1000000, max_output_tokens: 64_000, is_vl: true, is_reasoning: false },
|
|
130
136
|
ultimate: { id: "ultimate", display_name: "Ultimate", max_input_tokens: 1000000, max_output_tokens: 128_000, is_vl: true, is_free: false, is_reasoning: true },
|
|
131
137
|
// `fast` is what the live roster reports today; `performance` is the older
|
|
132
138
|
// key for the same slot. Both are kept so a hand-added model id written
|
|
@@ -134,14 +140,18 @@ export const QODER_INTL_MODEL_CONFIGS: Readonly<Record<string, QoderModelConfig>
|
|
|
134
140
|
// discovery supersedes it on the first successful fetch.
|
|
135
141
|
fast: { id: "fast", display_name: "Fast", max_input_tokens: 1000000, max_output_tokens: 64_000, is_vl: true, is_reasoning: false },
|
|
136
142
|
performance: { id: "performance", display_name: "Performance", max_input_tokens: 1000000, max_output_tokens: 64_000, is_vl: true, is_reasoning: false },
|
|
137
|
-
efficient: { id: "efficient", display_name: "Efficient", max_input_tokens:
|
|
143
|
+
efficient: { id: "efficient", display_name: "Efficient", max_input_tokens: 1000000, max_output_tokens: 64_000, is_vl: true, is_reasoning: false },
|
|
138
144
|
// Sonus/Cantus need an explicit effort on the IDE surface; without it the
|
|
139
145
|
// upstream streams text only, no reasoning (verified live). DeepSeek-family
|
|
140
146
|
// models think by default and are left untouched.
|
|
141
147
|
smodel: { id: "smodel", display_name: "Sonus", max_input_tokens: 1000000, max_output_tokens: 128_000, is_vl: true, is_free: false, is_reasoning: true, default_reasoning_effort: "high" },
|
|
142
148
|
cmodel: { id: "cmodel", display_name: "Cantus", max_input_tokens: 1000000, max_output_tokens: 128_000, is_vl: true, is_free: false, is_reasoning: true, default_reasoning_effort: "high" },
|
|
143
149
|
qmodel_38max: { id: "qmodel_38max", display_name: "Qwen3.8-Max", max_input_tokens: 1000000, max_output_tokens: 131_072, is_vl: true, is_reasoning: true },
|
|
144
|
-
|
|
150
|
+
// `qfmodel` is billed, not free: upstream's live `is_free: true` for this
|
|
151
|
+
// slot is a plan-tier flag, not a zero-cost tier — a free account gets
|
|
152
|
+
// `quota exceeded` (116) when dispatching it. The only truly free slot is
|
|
153
|
+
// `lite`. Do not re-add `is_free` here.
|
|
154
|
+
qfmodel: { id: "qfmodel", display_name: "Qwen3.8-Flash", max_input_tokens: 1000000, max_output_tokens: 131_072, is_vl: true, is_reasoning: true },
|
|
145
155
|
qmodel_latest: { id: "qmodel_latest", display_name: "Qwen3.7-Max", max_input_tokens: 1000000, max_output_tokens: 131_072, is_vl: true, is_reasoning: false },
|
|
146
156
|
qmodel: { id: "qmodel", display_name: "Qwen3.7-Plus", max_input_tokens: 1000000, max_output_tokens: 131_072, is_vl: true, is_reasoning: false },
|
|
147
157
|
kmodel_latest: { id: "kmodel_latest", display_name: "Kimi-K3.1-Pro", max_input_tokens: 1000000, max_output_tokens: 131_072, is_vl: true, is_free: false, is_reasoning: false },
|
|
@@ -152,7 +162,7 @@ export const QODER_INTL_MODEL_CONFIGS: Readonly<Record<string, QoderModelConfig>
|
|
|
152
162
|
// Live roster (2026-11-01) reports dfmodel as billed (is_free false); it was
|
|
153
163
|
// previously marked free. Display names are corrected from the same capture.
|
|
154
164
|
dfmodel: { id: "dfmodel", display_name: "DeepSeek-Flash", max_input_tokens: 1000000, max_output_tokens: 384_000, is_vl: true, is_free: false, is_reasoning: true },
|
|
155
|
-
mmodel: { id: "mmodel", display_name: "MiniMax-M3.1-Flash", max_input_tokens: 1000000, max_output_tokens: 512_000, is_vl: true, is_free: false, is_reasoning:
|
|
165
|
+
mmodel: { id: "mmodel", display_name: "MiniMax-M3.1-Flash", max_input_tokens: 1000000, max_output_tokens: 512_000, is_vl: true, is_free: false, is_reasoning: true },
|
|
156
166
|
// `lite` is the free-tier plan package: same 1M window and multimodal input
|
|
157
167
|
// as `qmodel` (Qwen3.7-Plus), capped at 64k output, free tier.
|
|
158
168
|
lite: { id: "lite", display_name: "Lite-Qwen-3.7-Plus", max_input_tokens: 1000000, max_output_tokens: 65_536, is_vl: true, is_free: true, is_reasoning: false },
|