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,514 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Qoder legacy COSY chat builder & transport.
|
|
3
|
+
*
|
|
4
|
+
* `buildQoderRequest` translates one canonical request into the legacy
|
|
5
|
+
* `/algo/.../agent_chat_generation` body (and, by reuse, the messages/tools
|
|
6
|
+
* shared by the IDE and gRPC surfaces). `callQoder` signs and sends it over
|
|
7
|
+
* the COSY surface with the real client's headers (Bun UA, machine identity,
|
|
8
|
+
* `x-model-key/source`, SSE accept).
|
|
9
|
+
*/
|
|
10
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
11
|
+
import type {
|
|
12
|
+
CanonicalRequest,
|
|
13
|
+
ContentPart,
|
|
14
|
+
} from "../../../protocol/canonical-model";
|
|
15
|
+
import { toolResultParts, toolCallParts } from "../../../protocol/canonical-model";
|
|
16
|
+
import { reasoningEffortFromIntent } from "../../../protocol/reasoning";
|
|
17
|
+
import { resolveImageSource } from "../../../protocol/codecs";
|
|
18
|
+
import { getQoderVersion } from "../../operations/client-versions";
|
|
19
|
+
import { qoderDiscoveredMaxContexts, QODER_CN_MODEL_CONFIGS, QODER_INTL_MODEL_CONFIGS } from "./qoder-catalog";
|
|
20
|
+
import { transformQoderBody, qoderBuildCosyHeaders } from "./qoder-cosy";
|
|
21
|
+
import { qoderSessionId } from "./qoder-auth";
|
|
22
|
+
import { flattenCanonicalContent, qoderToolChoice, qoderTools } from "./qoder-body";
|
|
23
|
+
import type { QoderRealm } from "./qoder-realm";
|
|
24
|
+
import type { QoderAuth, QoderModelConfig } from "./qoder-types";
|
|
25
|
+
|
|
26
|
+
export interface QoderModeProfile {
|
|
27
|
+
readonly businessProduct: string;
|
|
28
|
+
readonly businessType: string;
|
|
29
|
+
readonly cosyScene: string;
|
|
30
|
+
readonly mirrorTopLevelSystem: boolean;
|
|
31
|
+
readonly sendBusinessHeaders: boolean;
|
|
32
|
+
readonly sendModelSourceHeaders: boolean;
|
|
33
|
+
readonly emptyAliyunUserType: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const MODERN_PROFILE: QoderModeProfile = {
|
|
37
|
+
businessProduct: "cli",
|
|
38
|
+
businessType: "agent",
|
|
39
|
+
cosyScene: "assistant",
|
|
40
|
+
mirrorTopLevelSystem: true,
|
|
41
|
+
sendBusinessHeaders: true,
|
|
42
|
+
sendModelSourceHeaders: true,
|
|
43
|
+
emptyAliyunUserType: true,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function qoderStableRecordId(modelId: string, messages: CanonicalRequest["messages"], maxTokens: number): string {
|
|
47
|
+
const h = createHash("sha256");
|
|
48
|
+
h.update("qoder-record\0");
|
|
49
|
+
h.update(modelId);
|
|
50
|
+
for (const m of messages) {
|
|
51
|
+
h.update("\0");
|
|
52
|
+
h.update(m.role);
|
|
53
|
+
const text = flattenCanonicalContent(m.content);
|
|
54
|
+
if (text) {
|
|
55
|
+
h.update("\0");
|
|
56
|
+
h.update(text);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
h.update(`\0mt=${maxTokens}`);
|
|
60
|
+
return h.digest("hex").slice(0, 16);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The widest selectable context window for one model key.
|
|
65
|
+
*
|
|
66
|
+
* The live list's `context_config` is the authority when discovery has run:
|
|
67
|
+
* it names every window the account can switch to, and the widest one is what
|
|
68
|
+
* a dispatch should send (`model_config.max_input_tokens`) so a conversation is
|
|
69
|
+
* never capped at the *currently active* window (often 180k) when the account
|
|
70
|
+
* can use 1M. Falls back to the static roster's stated window otherwise.
|
|
71
|
+
*/
|
|
72
|
+
function qoderMaxContextFor(modelId: string, modelConfig: QoderModelConfig, realm: QoderRealm): number {
|
|
73
|
+
const discovered = qoderDiscoveredMaxContexts(realm).get(modelId);
|
|
74
|
+
if (discovered !== undefined) return discovered;
|
|
75
|
+
return modelConfig.max_input_tokens;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function qoderRequestedMaxTokens(
|
|
79
|
+
request: CanonicalRequest,
|
|
80
|
+
modelConfig: QoderModelConfig,
|
|
81
|
+
realm: QoderRealm,
|
|
82
|
+
): number {
|
|
83
|
+
/**
|
|
84
|
+
* The ceiling is the roster's stated one. A fixed 32k fallback would clamp
|
|
85
|
+
* every model alike, so a request asking for more would be silently lowered
|
|
86
|
+
* no matter which model it named. A discovered row carries no
|
|
87
|
+
* `max_output_tokens` (upstream's model list reports none), so consult the
|
|
88
|
+
* static roster for the key before the 65_536 last resort — and keep that
|
|
89
|
+
* last resort only when neither the caller nor the roster states a limit.
|
|
90
|
+
*/
|
|
91
|
+
const configured =
|
|
92
|
+
typeof modelConfig.max_output_tokens === "number"
|
|
93
|
+
? modelConfig.max_output_tokens
|
|
94
|
+
: qoderRosterMaxOutput(modelConfig.id, realm) ?? 65_536;
|
|
95
|
+
const controls = request.generation_controls as Record<string, unknown>;
|
|
96
|
+
const requested =
|
|
97
|
+
typeof controls["max_tokens"] === "number"
|
|
98
|
+
? (controls["max_tokens"] as number)
|
|
99
|
+
: typeof controls["max_output_tokens"] === "number"
|
|
100
|
+
? (controls["max_output_tokens"] as number)
|
|
101
|
+
: null;
|
|
102
|
+
return typeof requested === "number" && requested > 0 ? Math.min(requested, configured) : configured;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The static roster's stated output ceiling for one key, when the roster states
|
|
107
|
+
* one. Dispatch builds a live-discovered model's config without an output limit
|
|
108
|
+
* (upstream's model list reports none); this lookup restores the roster's
|
|
109
|
+
* ceiling so such a model is not capped at the 65_536 last resort.
|
|
110
|
+
*/
|
|
111
|
+
function qoderRosterMaxOutput(modelId: string, realm: QoderRealm): number | undefined {
|
|
112
|
+
const configs = realm === "cn" ? QODER_CN_MODEL_CONFIGS : QODER_INTL_MODEL_CONFIGS;
|
|
113
|
+
return configs[modelId]?.max_output_tokens;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Whether this model needs the prior turn's reasoning echoed back.
|
|
118
|
+
*
|
|
119
|
+
* The DeepSeek family ties multi-turn consistency to `reasoning_content`: an
|
|
120
|
+
* assistant turn that thought must be replayed with that field or the upstream
|
|
121
|
+
* refuses the whole history. The upstream reference bridge keeps it for exactly
|
|
122
|
+
* these models and drops it as noise for every other one
|
|
123
|
+
* (`keep_reasoning=is_deepseek_model(...)`, and `reasoning_content` in
|
|
124
|
+
* `NOISE_KEYS` otherwise), and notes the symptom when it is missed: occasional
|
|
125
|
+
* failures that sometimes pass on retry.
|
|
126
|
+
*
|
|
127
|
+
* Matched by the upstream key and by the display name, because a caller may
|
|
128
|
+
* name either — the reference warns that matching only the "deepseek" prefix
|
|
129
|
+
* missed every request that used the key.
|
|
130
|
+
*/
|
|
131
|
+
function qoderKeepsReasoning(modelId: string): boolean {
|
|
132
|
+
const key = modelId.trim().toLowerCase();
|
|
133
|
+
if (key === "dmodel" || key === "dfmodel") return true;
|
|
134
|
+
return key.startsWith("deepseek");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The `reasoning_content` field for an assistant turn, when one is needed.
|
|
139
|
+
*
|
|
140
|
+
* Presence, not length, decides: a client that already knows the rule sends
|
|
141
|
+
* `""` on a thinking turn whose text is not readable back, and dropping that
|
|
142
|
+
* empty string re-triggers the complaint it was sent to avoid.
|
|
143
|
+
*
|
|
144
|
+
* DeepSeek multi-turn consistency also backfills the field on every assistant
|
|
145
|
+
* turn once the conversation carries a reasoning trace at all — the reference
|
|
146
|
+
* bridge does the same (`backfill_reasoning_content`), and notes the symptom
|
|
147
|
+
* when it is missed: occasional failures that sometimes pass on retry. An
|
|
148
|
+
* assistant turn with no reasoning and no trace gains nothing, so nothing is
|
|
149
|
+
* fabricated.
|
|
150
|
+
*/
|
|
151
|
+
function qoderReasoningEcho(
|
|
152
|
+
message: { content: readonly ContentPart[] },
|
|
153
|
+
modelId: string,
|
|
154
|
+
hasReasoningTrace = false,
|
|
155
|
+
): { reasoning_content: string } | Record<string, never> {
|
|
156
|
+
if (!qoderKeepsReasoning(modelId)) return {};
|
|
157
|
+
const reasoning = message.content.filter((part) => part.kind === "reasoning");
|
|
158
|
+
if (reasoning.length === 0) {
|
|
159
|
+
return hasReasoningTrace ? { reasoning_content: "" } : {};
|
|
160
|
+
}
|
|
161
|
+
const text = reasoning
|
|
162
|
+
.map((part) => (part.kind === "reasoning" ? (part.summary ?? part.payload ?? "") : ""))
|
|
163
|
+
.join("");
|
|
164
|
+
return { reasoning_content: text };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Whether this request sends its tool history as structured turns.
|
|
169
|
+
*
|
|
170
|
+
* Qoder's wire is OpenAI-compatible `tool_calls` deltas on every realm; the
|
|
171
|
+
* model is fed the same structured turns in both INTL and CN. Writing a past
|
|
172
|
+
* call as prose (`[assistant 请求调用工具] …`) contaminates the conversation
|
|
173
|
+
* history with a text echo the model starts imitating — the origin of the
|
|
174
|
+
* text-leak feedback loop. Structured history keeps the model seeing only real
|
|
175
|
+
* tool calls, so it never learns the prose marker.
|
|
176
|
+
*/
|
|
177
|
+
function qoderStructuredTools(_realm: QoderRealm): boolean {
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function buildQoderRequest(
|
|
182
|
+
modelId: string,
|
|
183
|
+
request: CanonicalRequest,
|
|
184
|
+
modelConfig: QoderModelConfig,
|
|
185
|
+
auth: QoderAuth,
|
|
186
|
+
realm: QoderRealm = "intl",
|
|
187
|
+
): Record<string, unknown> {
|
|
188
|
+
const allMessages = request.messages;
|
|
189
|
+
const systemParts: string[] = [];
|
|
190
|
+
if (request.system) {
|
|
191
|
+
for (const p of request.system) if (p.kind === "text") systemParts.push(p.text);
|
|
192
|
+
}
|
|
193
|
+
if (request.instructions) {
|
|
194
|
+
for (const p of request.instructions) if (p.kind === "text") systemParts.push(p.text);
|
|
195
|
+
}
|
|
196
|
+
for (const m of allMessages) {
|
|
197
|
+
if (m.role === "system" || m.role === "developer") {
|
|
198
|
+
const t = flattenCanonicalContent(m.content);
|
|
199
|
+
if (t) systemParts.push(t);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const systemPrompt = systemParts.join("\n\n").trim();
|
|
203
|
+
const qoderMessages: Array<Record<string, unknown>> = [];
|
|
204
|
+
const structuredTools = qoderStructuredTools(realm);
|
|
205
|
+
// DeepSeek multi-turn: once any turn in the conversation carries a reasoning
|
|
206
|
+
// trace, every assistant message must replay `reasoning_content` (see
|
|
207
|
+
// `qoderReasoningEcho`).
|
|
208
|
+
const hasReasoningTrace = allMessages.some((message) =>
|
|
209
|
+
message.content.some((part) => part.kind === "reasoning"),
|
|
210
|
+
);
|
|
211
|
+
if (MODERN_PROFILE.mirrorTopLevelSystem && systemPrompt.length > 0) {
|
|
212
|
+
qoderMessages.push({
|
|
213
|
+
role: "system",
|
|
214
|
+
content: systemPrompt,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
// Every tool result across the whole conversation, keyed by call id. An
|
|
218
|
+
// assistant `tool_calls` turn is only sent structured when every call has a
|
|
219
|
+
// matching result somewhere in the history — otherwise the upstream rejects
|
|
220
|
+
// the transcript ("An assistant message with 'tool_calls' must be followed by
|
|
221
|
+
// tool messages responding to each 'tool_call_id'"). Calls without a result
|
|
222
|
+
// are left out of the structured turn instead of poisoning it.
|
|
223
|
+
const toolResultsById = new Map<string, string>();
|
|
224
|
+
for (const msg of allMessages) {
|
|
225
|
+
for (const part of toolResultParts(msg)) {
|
|
226
|
+
if (typeof part.call_id === "string" && part.call_id.length > 0) {
|
|
227
|
+
toolResultsById.set(part.call_id, part.call_id);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// The tool calls that actually survive onto the wire. A call only makes it
|
|
232
|
+
// when every call on its assistant turn carries an id AND a matching result
|
|
233
|
+
// exists in the history. This is the single source of truth for both sides
|
|
234
|
+
// of the transcript: the assistant `tool_calls` emit and the `tool` result
|
|
235
|
+
// turns. If a result's call is not here, its `tool` turn is dropped too —
|
|
236
|
+
// leaving it would emit an orphan `tool` turn with no preceding call, which
|
|
237
|
+
// the upstream rejects ("Messages with role 'tool' must be a response to a
|
|
238
|
+
// preceding message with 'tool_calls'"). This is what a cancelled or
|
|
239
|
+
// compacted parallel call looks like, and it is the "insufficient tool
|
|
240
|
+
// messages" failure the client sees on the turn after.
|
|
241
|
+
const emittableCallIds = new Set<string>();
|
|
242
|
+
for (const msg of allMessages) {
|
|
243
|
+
if (msg.role !== "assistant") continue;
|
|
244
|
+
const calls = toolCallParts(msg);
|
|
245
|
+
if (calls.length === 0) continue;
|
|
246
|
+
const idsOk = calls.every(
|
|
247
|
+
(part) => typeof part.call_id === "string" && part.call_id.length > 0,
|
|
248
|
+
);
|
|
249
|
+
const everyCallHasResult = calls.every((part) =>
|
|
250
|
+
typeof part.call_id === "string" ? toolResultsById.has(part.call_id) : false,
|
|
251
|
+
);
|
|
252
|
+
if (idsOk && everyCallHasResult) {
|
|
253
|
+
for (const part of calls) emittableCallIds.add(part.call_id as string);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
for (const m of allMessages) {
|
|
257
|
+
if (m.role === "system" || m.role === "developer") continue;
|
|
258
|
+
// Answers may live in a `user` turn (the Messages ledger re-homes them),
|
|
259
|
+
// so the result scan is role-agnostic. Each result becomes its own `tool`
|
|
260
|
+
// turn carrying its `tool_call_id`; any non-result content on the same turn
|
|
261
|
+
// is emitted after, so nothing is duplicated or lost.
|
|
262
|
+
const results = structuredTools ? toolResultParts(m) : [];
|
|
263
|
+
if (results.length > 0) {
|
|
264
|
+
let emittedAnyTool = false;
|
|
265
|
+
for (const block of results) {
|
|
266
|
+
// Drop orphan results: their assistant `tool_calls` turn was filtered
|
|
267
|
+
// out (a sibling call has no result), and emitting the `tool` turn
|
|
268
|
+
// anyway would make the upstream reject the whole transcript.
|
|
269
|
+
if (!emittableCallIds.has(block.call_id)) continue;
|
|
270
|
+
emittedAnyTool = true;
|
|
271
|
+
const text = flattenCanonicalContent(
|
|
272
|
+
typeof block.content === "string"
|
|
273
|
+
? [{ kind: "text", text: block.content }]
|
|
274
|
+
: block.content,
|
|
275
|
+
);
|
|
276
|
+
qoderMessages.push({
|
|
277
|
+
role: "tool",
|
|
278
|
+
content: text,
|
|
279
|
+
tool_call_id: block.call_id,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
// Text that shared the turn with a result is emitted after it, minus the
|
|
283
|
+
// result itself. Re-emitting the result's own text here was doubling
|
|
284
|
+
// every tool answer: the `tool` turn above and a `user` turn with the
|
|
285
|
+
// same content, which doubled the payload for no reason.
|
|
286
|
+
const remaining = m.content.filter((part) => part.kind !== "toolResult");
|
|
287
|
+
const flat = flattenCanonicalContent(remaining);
|
|
288
|
+
if (flat.length === 0 && !emittedAnyTool) continue;
|
|
289
|
+
if (flat.length > 0) {
|
|
290
|
+
qoderMessages.push({
|
|
291
|
+
role: "user",
|
|
292
|
+
content: flat,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
// An assistant turn that called tools must carry the calls, not just its
|
|
298
|
+
// text. Falling through to the text-only emit dropped them, so the payload
|
|
299
|
+
// arrived as a `tool` result with no preceding call and the upstream
|
|
300
|
+
// rejected the whole transcript: "Messages with role 'tool' must be a
|
|
301
|
+
// response to a preceding message with 'tool_calls'".
|
|
302
|
+
const allCalls = m.content.filter((part) => part.kind === "toolCall");
|
|
303
|
+
const reasoningEcho = qoderReasoningEcho(m, modelId, hasReasoningTrace);
|
|
304
|
+
// Structured tool turns are only safe when every call carries an id AND a
|
|
305
|
+
// matching result exists in the history — the upstream rejects a transcript
|
|
306
|
+
// whose `tool_calls` is not fully answered by `tool` turns. The same set
|
|
307
|
+
// that gates the `tool` emit above decides this, so the two halves of the
|
|
308
|
+
// transcript can never disagree.
|
|
309
|
+
const calls =
|
|
310
|
+
structuredTools && allCalls.every((part) => emittableCallIds.has(part.call_id))
|
|
311
|
+
? allCalls
|
|
312
|
+
: [];
|
|
313
|
+
if (calls.length > 0) {
|
|
314
|
+
const textParts = m.content.filter((part) => part.kind !== "toolCall");
|
|
315
|
+
const flat = flattenCanonicalContent(textParts);
|
|
316
|
+
qoderMessages.push({
|
|
317
|
+
role: "assistant",
|
|
318
|
+
content: flat,
|
|
319
|
+
...reasoningEcho,
|
|
320
|
+
tool_calls: calls.map((part) => ({
|
|
321
|
+
id: part.call_id,
|
|
322
|
+
type: "function",
|
|
323
|
+
function: {
|
|
324
|
+
name: part.name,
|
|
325
|
+
arguments:
|
|
326
|
+
typeof part.arguments === "string"
|
|
327
|
+
? part.arguments
|
|
328
|
+
: part.arguments !== undefined && part.arguments !== null
|
|
329
|
+
? JSON.stringify(part.arguments)
|
|
330
|
+
: "",
|
|
331
|
+
},
|
|
332
|
+
})),
|
|
333
|
+
});
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
const role = m.role === "assistant" ? "assistant" : "user";
|
|
337
|
+
// Images must travel as structured OpenAI parts inside the message content —
|
|
338
|
+
// the upstream reads `image_url` parts there and not the top-level
|
|
339
|
+
// `image_urls` (mirroring `flatten_messages` in the reference bridge). A
|
|
340
|
+
// bare file_id reference degrades to a text note instead of being dropped.
|
|
341
|
+
const hasImages = m.content.some((part) => part.kind === "image");
|
|
342
|
+
if (hasImages) {
|
|
343
|
+
const parts: Array<Record<string, unknown>> = [];
|
|
344
|
+
for (const part of m.content) {
|
|
345
|
+
if (part.kind === "text") {
|
|
346
|
+
parts.push({ type: "text", text: part.text });
|
|
347
|
+
} else if (part.kind === "image") {
|
|
348
|
+
const source = resolveImageSource(part.payload);
|
|
349
|
+
if (source?.url !== undefined) {
|
|
350
|
+
parts.push({ type: "image_url", image_url: { url: source.url } });
|
|
351
|
+
} else {
|
|
352
|
+
parts.push({
|
|
353
|
+
type: "text",
|
|
354
|
+
text: source?.fileId !== undefined ? `[image: ${source.fileId}]` : "[image: unsupported source]",
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
qoderMessages.push({
|
|
360
|
+
role,
|
|
361
|
+
content: parts,
|
|
362
|
+
...(role === "assistant" ? qoderReasoningEcho(m, modelId, hasReasoningTrace) : {}),
|
|
363
|
+
});
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
const flat = flattenCanonicalContent(m.content);
|
|
367
|
+
qoderMessages.push({
|
|
368
|
+
role,
|
|
369
|
+
content: flat,
|
|
370
|
+
...(role === "assistant" ? qoderReasoningEcho(m, modelId, hasReasoningTrace) : {}),
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
const latestUserMessage = [...qoderMessages].reverse().find((m) => m["role"] === "user");
|
|
374
|
+
const latestUserText = typeof latestUserMessage?.["content"] === "string" ? (latestUserMessage["content"] as string) : "";
|
|
375
|
+
const maxTokens = qoderRequestedMaxTokens(request, modelConfig, realm);
|
|
376
|
+
const reasoning = modelConfig.is_reasoning === true;
|
|
377
|
+
// 1:1 with the IDE worker's `FTn` legacy builder, which sends reasoning
|
|
378
|
+
// effort as a top-level camelCase field (`reasoningEffort`), derived from
|
|
379
|
+
// the canonical intent when one is present. Omitted entirely when the
|
|
380
|
+
// caller asked for no reasoning, matching the reference's conditional send.
|
|
381
|
+
// When the caller sends no intent but the model needs the push to think
|
|
382
|
+
// (verified live: Sonus/Cantus stream no reasoning without it), use the
|
|
383
|
+
// roster's default effort.
|
|
384
|
+
//
|
|
385
|
+
// Only a reasoning-capable model carries the field. A non-reasoning model
|
|
386
|
+
// (e.g. `lite`) cannot think, so an effort is meaningless there; sending it
|
|
387
|
+
// would also assert a capability the model does not advertise. The upstream
|
|
388
|
+
// tolerates it today, but declaring reasoning on a model that has none is a
|
|
389
|
+
// latent contract violation, so the field is kept off non-reasoning models.
|
|
390
|
+
const reasoningEffort =
|
|
391
|
+
reasoning
|
|
392
|
+
? (reasoningEffortFromIntent(request.reasoning) ?? modelConfig.default_reasoning_effort)
|
|
393
|
+
: undefined;
|
|
394
|
+
const sessionId = qoderSessionId(auth.userId, modelId);
|
|
395
|
+
const recordId = qoderStableRecordId(modelId, allMessages, maxTokens);
|
|
396
|
+
return {
|
|
397
|
+
// 1:1 with the IDE worker's `FTn` legacy request builder, which always
|
|
398
|
+
// sends a top-level `model` equal to the resolved roster key (falling
|
|
399
|
+
// back to "auto" only when no key is known). The upstream honours
|
|
400
|
+
// `model_config.key` for actual dispatch and echoes a fixed "auto" in
|
|
401
|
+
// stream chunks, but the field is part of the wire shape the IDE sends.
|
|
402
|
+
model: modelId,
|
|
403
|
+
request_id: randomUUID(),
|
|
404
|
+
request_set_id: recordId,
|
|
405
|
+
chat_record_id: recordId,
|
|
406
|
+
session_id: sessionId,
|
|
407
|
+
source_session_id: "",
|
|
408
|
+
stream: true,
|
|
409
|
+
...(reasoningEffort === undefined ? {} : { reasoningEffort }),
|
|
410
|
+
...(MODERN_PROFILE.emptyAliyunUserType ? { aliyun_user_type: "" } : {}),
|
|
411
|
+
chat_task: "FREE_INPUT",
|
|
412
|
+
is_reply: true,
|
|
413
|
+
is_retry: false,
|
|
414
|
+
source: 1,
|
|
415
|
+
version: "3",
|
|
416
|
+
session_type: "qodercli",
|
|
417
|
+
agent_id: "agent_common",
|
|
418
|
+
task_id: "common",
|
|
419
|
+
code_language: "",
|
|
420
|
+
chat_prompt: MODERN_PROFILE.mirrorTopLevelSystem ? systemPrompt : "",
|
|
421
|
+
image_urls: null,
|
|
422
|
+
chat_context: {
|
|
423
|
+
chatPrompt: MODERN_PROFILE.mirrorTopLevelSystem ? systemPrompt : "",
|
|
424
|
+
imageUrls: null,
|
|
425
|
+
extra: { context: [], modelConfig: { key: modelId, is_reasoning: reasoning }, originalContent: { text: latestUserText } },
|
|
426
|
+
features: [],
|
|
427
|
+
// An object, as the reference sends it: `cc["text"]["text"] = prompt`.
|
|
428
|
+
text: { text: latestUserText },
|
|
429
|
+
},
|
|
430
|
+
model_config: {
|
|
431
|
+
key: modelId,
|
|
432
|
+
display_name: modelConfig.display_name,
|
|
433
|
+
// Declares the wire dialect for the tool calls. Without it the upstream
|
|
434
|
+
// does not read the OpenAI-shaped `tools` array the same way, and the
|
|
435
|
+
// model then emits a call with no arguments filled in.
|
|
436
|
+
format: "openai",
|
|
437
|
+
model: "",
|
|
438
|
+
api_key: "",
|
|
439
|
+
url: "",
|
|
440
|
+
is_vl: modelConfig.is_vl,
|
|
441
|
+
is_reasoning: modelConfig.is_reasoning,
|
|
442
|
+
// Send the widest selectable window from the live list's `context_config`
|
|
443
|
+
// (falling back to the static roster's stated window). Without this, the
|
|
444
|
+
// request would declare the *currently active* window — often 180k — even
|
|
445
|
+
// when the account can use 1M, capping the conversation the upstream sees.
|
|
446
|
+
max_input_tokens: qoderMaxContextFor(modelId, modelConfig, realm),
|
|
447
|
+
// Mirrors the roster's stated ceiling. A fixed 32k fallback would be sent
|
|
448
|
+
// for every model alike, so the request would declare a cap the catalog
|
|
449
|
+
// does not publish. A discovered model's config carries no output limit,
|
|
450
|
+
// so consult the roster for the key before the 65_536 last resort.
|
|
451
|
+
max_output_tokens: modelConfig.max_output_tokens ?? qoderRosterMaxOutput(modelId, realm) ?? 65_536,
|
|
452
|
+
source: modelConfig.source ?? "system",
|
|
453
|
+
},
|
|
454
|
+
messages: qoderMessages,
|
|
455
|
+
max_tokens: maxTokens,
|
|
456
|
+
// Always present, even when empty. Omitting the field makes Qoder fall back
|
|
457
|
+
// to the desktop agent's own toolset (Bash/Edit/...), which then leaks to a
|
|
458
|
+
// caller that declared none — the reference bridge sends `[]` for exactly
|
|
459
|
+
// that reason. Parallel calls are stated explicitly rather than left to the
|
|
460
|
+
// upstream default, so a caller expecting them is not served serial
|
|
461
|
+
// behaviour.
|
|
462
|
+
tools: qoderTools(request.tools),
|
|
463
|
+
parallel_tool_calls: true,
|
|
464
|
+
...(() => {
|
|
465
|
+
const choice = qoderToolChoice(request.tool_choice);
|
|
466
|
+
return choice === undefined ? {} : { tool_choice: choice };
|
|
467
|
+
})(),
|
|
468
|
+
// Passthrough knobs the IDE surface reads from `parameters` (worker `vyl`):
|
|
469
|
+
// parallel_tool_calls, tool_choice, top_p, seed, user, response_format, …
|
|
470
|
+
// Only include what the caller actually set.
|
|
471
|
+
parameters: (() => {
|
|
472
|
+
const out: Record<string, unknown> = {};
|
|
473
|
+
const choice = qoderToolChoice(request.tool_choice);
|
|
474
|
+
if (choice !== undefined) out["tool_choice"] = choice;
|
|
475
|
+
const parallel = request.generation_controls.parallel_tool_calls;
|
|
476
|
+
if (parallel !== undefined) out["parallel_tool_calls"] = parallel;
|
|
477
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
478
|
+
})(),
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
type QoderFetcher = (url: string, init: RequestInit) => Promise<Response>;
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Signs and sends one legacy COSY chat request. The body is alphabet-scrambled
|
|
486
|
+
* before signing; headers come from `qoderBuildCosyHeaders` plus the modern
|
|
487
|
+
* `x-model-key`/`x-model-source` and SSE accepts.
|
|
488
|
+
*/
|
|
489
|
+
export async function callQoder(
|
|
490
|
+
url: string,
|
|
491
|
+
body: Record<string, unknown>,
|
|
492
|
+
modelId: string,
|
|
493
|
+
auth: QoderAuth,
|
|
494
|
+
signal: AbortSignal,
|
|
495
|
+
fetcher: QoderFetcher,
|
|
496
|
+
version = getQoderVersion(),
|
|
497
|
+
): Promise<Response> {
|
|
498
|
+
const encoded = transformQoderBody(JSON.stringify(body));
|
|
499
|
+
const modelConfig = (body["model_config"] as Record<string, unknown> | null) ?? null;
|
|
500
|
+
return fetcher(url, {
|
|
501
|
+
method: "POST",
|
|
502
|
+
headers: {
|
|
503
|
+
...qoderBuildCosyHeaders(encoded, url, auth, version),
|
|
504
|
+
...(MODERN_PROFILE.sendModelSourceHeaders
|
|
505
|
+
? { "x-model-key": modelId, "x-model-source": typeof modelConfig?.["source"] === "string" ? (modelConfig["source"] as string) : "system" }
|
|
506
|
+
: {}),
|
|
507
|
+
Accept: "text/event-stream",
|
|
508
|
+
"Cache-Control": "no-cache",
|
|
509
|
+
"Accept-Encoding": "identity",
|
|
510
|
+
},
|
|
511
|
+
body: encoded as unknown as BodyInit,
|
|
512
|
+
signal,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Qoder COSY signing — AES-128-CBC identity encryption + RSA PKCS1 key +
|
|
3
|
+
* md5 chain, and the alphabet-scrambled body transform. This is the donor of
|
|
4
|
+
* the WASM `prepareInferRequest` output of the real client, and the only chat
|
|
5
|
+
* surface a free/lite account can use.
|
|
6
|
+
*
|
|
7
|
+
* Header values mirror live captures of the real client (v1.1.83,
|
|
8
|
+
* 2026-11-05): `User-Agent: Bun/1.4.2`, machine identity headers from the
|
|
9
|
+
* UMID (`cosy-machineid/token/type/code/os/hostname`), business headers,
|
|
10
|
+
* and the static `signature` md5 chain.
|
|
11
|
+
*/
|
|
12
|
+
import { constants, createCipheriv, createHash, publicEncrypt, randomUUID } from "node:crypto";
|
|
13
|
+
import { getQoderVersion } from "../../operations/client-versions";
|
|
14
|
+
import { qoderMachineHostname, qoderMachineOs } from "../../operations/cli-platform";
|
|
15
|
+
import { QODER_UA_CLI } from "./qoder-realm";
|
|
16
|
+
import type { QoderAuth } from "./qoder-types";
|
|
17
|
+
|
|
18
|
+
const QODER_APPCODE = "cosy";
|
|
19
|
+
const QODER_SIG_SECRET = "d2FyLCB3YXIgbmV2ZXIgY2hhbmdlcw==";
|
|
20
|
+
const QODER_STANDARD_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
21
|
+
const QODER_ALPHABET = "_doRTgHZBKcGVjlvpC,@aFSx#DPuNJme&i*MzLOEn)sUrthbf%Y^w.(kIQyXqWA!";
|
|
22
|
+
const QODER_RSA_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
|
|
23
|
+
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDA8iMH5c02LilrsERw9t6Pv5Nc
|
|
24
|
+
4k6Pz1EaDicBMpdpxKduSZu5OANqUq8er4GM95omAGIOPOh+Nx0spthYA2BqGz+l
|
|
25
|
+
6HRkPJ7S236FZz73In/KVuLnwI8JJ2CbuJap8kvheCCZpmAWpb/cPx/3Vr/J6I17
|
|
26
|
+
XcW+ML9FoCI6AOvOzwIDAQAB
|
|
27
|
+
-----END PUBLIC KEY-----`;
|
|
28
|
+
|
|
29
|
+
function qoderMd5(value: string | Uint8Array): string {
|
|
30
|
+
return createHash("md5").update(value).digest("hex");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Canonical compact JSON used by Qoder's COSY signing: keys sorted
|
|
35
|
+
* lexicographically, no whitespace, null/undefined rendered as empty string.
|
|
36
|
+
* The upstream verifies the signature against this exact serialization, so a
|
|
37
|
+
* payload built with plain `JSON.stringify` (insertion order) fails with
|
|
38
|
+
* `code 101 / Signature invalid`.
|
|
39
|
+
*/
|
|
40
|
+
function qoderJsonSortedCompact(mapping: Record<string, unknown>): string {
|
|
41
|
+
const parts: string[] = [];
|
|
42
|
+
for (const key of Object.keys(mapping).sort()) {
|
|
43
|
+
const value = mapping[key] === null || mapping[key] === undefined ? "" : mapping[key];
|
|
44
|
+
parts.push(`${JSON.stringify(key)}:${JSON.stringify(value)}`);
|
|
45
|
+
}
|
|
46
|
+
return `{${parts.join(",")}}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function qoderPathSignature(url: string): string {
|
|
50
|
+
const path = new URL(url).pathname;
|
|
51
|
+
return path.startsWith("/algo") ? path.slice(5) : path;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Qoder's own base64 alphabet transform for a request body: base64, rotate
|
|
56
|
+
* thirds, then map each character through the custom alphabet (`=` → `$`).
|
|
57
|
+
*/
|
|
58
|
+
export function transformQoderBody(plaintext: string): Uint8Array {
|
|
59
|
+
const base64 = Buffer.from(plaintext).toString("base64");
|
|
60
|
+
const third = Math.floor(base64.length / 3);
|
|
61
|
+
const reordered = base64.slice(base64.length - third) + base64.slice(third, base64.length - third) + base64.slice(0, third);
|
|
62
|
+
let encoded = "";
|
|
63
|
+
for (const char of reordered) {
|
|
64
|
+
if (char === "=") encoded += "$";
|
|
65
|
+
else encoded += QODER_ALPHABET[QODER_STANDARD_ALPHABET.indexOf(char)] ?? char;
|
|
66
|
+
}
|
|
67
|
+
return Buffer.from(encoded, "latin1");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Builds the COSY Bearer headers for one signed request. The identity payload
|
|
72
|
+
* is encrypted AES-128-CBC (random 16-byte key, IV = key), the key is RSA
|
|
73
|
+
* PKCS1-encrypted, and the signature is md5 over
|
|
74
|
+
* `payload\ncosyKey\ndate\nbody\nsigPath`.
|
|
75
|
+
*
|
|
76
|
+
* Machine identity headers mirror the live 1.1.83 capture: `cosy-machineid`
|
|
77
|
+
* (the account's machine id), `cosy-machinetoken/type/code` when a native
|
|
78
|
+
* UMID identity is bound (otherwise they fall back to the machine id / `5`),
|
|
79
|
+
* real `cosy-machineos`, and the sanitized hostname.
|
|
80
|
+
*/
|
|
81
|
+
export function qoderBuildCosyHeaders(
|
|
82
|
+
body: Uint8Array,
|
|
83
|
+
url: string,
|
|
84
|
+
auth: QoderAuth,
|
|
85
|
+
version = getQoderVersion(),
|
|
86
|
+
): Record<string, string> {
|
|
87
|
+
const key = randomUUID().replaceAll("-", "").slice(0, 16);
|
|
88
|
+
const identity = qoderJsonSortedCompact({
|
|
89
|
+
name: auth.userName,
|
|
90
|
+
aid: auth.userId,
|
|
91
|
+
uid: auth.userId,
|
|
92
|
+
yx_uid: "",
|
|
93
|
+
organization_id: "",
|
|
94
|
+
organization_name: "",
|
|
95
|
+
user_type: auth.userType,
|
|
96
|
+
security_oauth_token: auth.securityOauthToken,
|
|
97
|
+
refresh_token: auth.refreshToken,
|
|
98
|
+
});
|
|
99
|
+
const cipher = createCipheriv("aes-128-cbc", Buffer.from(key), Buffer.from(key));
|
|
100
|
+
const info = Buffer.concat([cipher.update(identity), cipher.final()]).toString("base64");
|
|
101
|
+
const cosyKey = publicEncrypt({ key: QODER_RSA_PUBLIC_KEY, padding: constants.RSA_PKCS1_PADDING }, Buffer.from(key)).toString("base64");
|
|
102
|
+
const payload = Buffer.from(
|
|
103
|
+
qoderJsonSortedCompact({ version: "v1", requestId: randomUUID(), info, cosyVersion: version, ideVersion: "" }),
|
|
104
|
+
).toString("base64");
|
|
105
|
+
const date = String(Math.floor(Date.now() / 1000));
|
|
106
|
+
const signature = qoderMd5(`${payload}\n${cosyKey}\n${date}\n${Buffer.from(body).toString("latin1")}\n${qoderPathSignature(url)}`);
|
|
107
|
+
return {
|
|
108
|
+
authorization: `Bearer COSY.${payload}.${signature}`,
|
|
109
|
+
"cosy-data-policy": "agree",
|
|
110
|
+
"cosy-machinetype": auth.machineType ?? "5",
|
|
111
|
+
"cosy-machineos": qoderMachineOs(),
|
|
112
|
+
"cosy-clienttype": "5",
|
|
113
|
+
"cosy-date": date,
|
|
114
|
+
"cosy-user": auth.userId,
|
|
115
|
+
"cosy-key": cosyKey,
|
|
116
|
+
"cosy-bodyhash": qoderMd5(body),
|
|
117
|
+
"cosy-bodylength": String(body.byteLength),
|
|
118
|
+
"cosy-sigpath": qoderPathSignature(url),
|
|
119
|
+
"cosy-machineid": auth.machineId,
|
|
120
|
+
...(auth.machineToken !== undefined && auth.machineToken.length > 0 ? { "cosy-machinetoken": auth.machineToken } : {}),
|
|
121
|
+
...(auth.machineCode !== undefined && auth.machineCode.length > 0 ? { "cosy-machinecode": auth.machineCode } : {}),
|
|
122
|
+
"cosy-machinehostname": qoderMachineHostname(),
|
|
123
|
+
"cosy-organization-id": "",
|
|
124
|
+
"cosy-organization-tags": "",
|
|
125
|
+
"x-request-id": randomUUID(),
|
|
126
|
+
"cosy-business-product": "cli",
|
|
127
|
+
"cosy-business-type": "agent",
|
|
128
|
+
"cosy-business-version": version,
|
|
129
|
+
"cosy-scene": "assistant",
|
|
130
|
+
"cosy-version": version,
|
|
131
|
+
"login-version": "v2",
|
|
132
|
+
appcode: QODER_APPCODE,
|
|
133
|
+
accept: "application/json",
|
|
134
|
+
"accept-encoding": "identity",
|
|
135
|
+
date: new Date().toUTCString(),
|
|
136
|
+
signature: qoderMd5(`${QODER_APPCODE}&${QODER_SIG_SECRET}&${new Date().toUTCString()}`),
|
|
137
|
+
"content-type": "application/json",
|
|
138
|
+
"user-agent": QODER_UA_CLI,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* A COSY signer for one already-resolved account identity.
|
|
144
|
+
*
|
|
145
|
+
* Discovery needs signed headers but lives in `qoder-catalog.ts`, which cannot
|
|
146
|
+
* import this module (the adapter imports its rosters — that would be a cycle).
|
|
147
|
+
* Exporting the signer keeps one signing implementation and lets discovery
|
|
148
|
+
* stay pure and injectable.
|
|
149
|
+
*/
|
|
150
|
+
export function qoderSignerFor(
|
|
151
|
+
auth: QoderAuth,
|
|
152
|
+
version = getQoderVersion(),
|
|
153
|
+
): (url: string, body: string) => Record<string, string> {
|
|
154
|
+
return (url: string, body: string): Record<string, string> =>
|
|
155
|
+
qoderBuildCosyHeaders(Buffer.from(body, "latin1"), url, auth, version);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Qoder's own base64 alphabet transform, for a request body. */
|
|
159
|
+
export function qoderEncodeBody(plaintext: Uint8Array | string): string {
|
|
160
|
+
const text = typeof plaintext === "string" ? plaintext : new TextDecoder().decode(plaintext);
|
|
161
|
+
return Buffer.from(transformQoderBody(text)).toString("latin1");
|
|
162
|
+
}
|