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,667 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Qoder gRPC transport — `model.chat.ChatService/ChatCompletionStream`.
|
|
3
|
+
*
|
|
4
|
+
* This is the only proven surface for real 1M-token context: the HTTP IDE
|
|
5
|
+
* surface caps at ~256 KiB (~60K tokens), while gRPC accepts bodies up to
|
|
6
|
+
* ~5.5M–6M chars (~1.3M–1.4M tokens) — verified live 2026-11-05, see
|
|
7
|
+
* `documentation/research/qoder/live/2026-11-05-qoder-payload-limits.md`.
|
|
8
|
+
*
|
|
9
|
+
* gRPC only serves billed models: a free-tier row (`lite`) answers
|
|
10
|
+
* `quota exceeded` (116) on this surface, so free-tier dispatch must use the
|
|
11
|
+
* legacy COSY surface instead (see `qoder-adapter.ts` transport selection).
|
|
12
|
+
*
|
|
13
|
+
* The proto is vendored at `vendor/qoder-proto/chat.proto` (from the unpacked
|
|
14
|
+
* desktop client, version 1.1.81, `qoder-worker-runtime.mjs`).
|
|
15
|
+
*/
|
|
16
|
+
import { randomUUID } from "node:crypto";
|
|
17
|
+
import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
|
|
18
|
+
import { tmpdir } from "node:os";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
import * as grpc from "@grpc/grpc-js";
|
|
21
|
+
import * as protoLoader from "@grpc/proto-loader";
|
|
22
|
+
import { GatewayError } from "../../../protocol/gateway-error";
|
|
23
|
+
import type { CanonicalEvent, CanonicalRequest, CanonicalStopReason } from "../../../protocol/canonical-model";
|
|
24
|
+
import { usageFromProvider } from "../../provider-usage";
|
|
25
|
+
import { getQoderVersion } from "../../operations/client-versions";
|
|
26
|
+
import { qoderMachineOs, qoderMachineHostname } from "../../operations/cli-platform";
|
|
27
|
+
import { qoderSessionId } from "./qoder-auth";
|
|
28
|
+
import { flattenCanonicalContent } from "./qoder-body";
|
|
29
|
+
import { qoderGrpcHost } from "./qoder-realm";
|
|
30
|
+
import type { QoderAuth, QoderRealm } from "./qoder-types";
|
|
31
|
+
// The proto is embedded as a generated TS source (see `chat-proto.ts`), so
|
|
32
|
+
// the gRPC surface works from source, dist, and the standalone binary alike —
|
|
33
|
+
// the proto loader only reads from a path, so the string is materialized once
|
|
34
|
+
// to a temp file per process.
|
|
35
|
+
import { CHAT_PROTO_SOURCE } from "./chat-proto";
|
|
36
|
+
|
|
37
|
+
const PROTO_PATH = materializeChatProto();
|
|
38
|
+
|
|
39
|
+
function materializeChatProto(): string {
|
|
40
|
+
const dir = mkdtempSync(join(tmpdir(), "cartethyia-qoder-grpc-"));
|
|
41
|
+
const path = join(dir, "chat.proto");
|
|
42
|
+
writeFileSync(path, CHAT_PROTO_SOURCE);
|
|
43
|
+
// Best-effort cleanup; a leftover temp file is harmless but avoid keeping it.
|
|
44
|
+
process.on("exit", () => {
|
|
45
|
+
try {
|
|
46
|
+
rmSync(dir, { recursive: true, force: true });
|
|
47
|
+
} catch {
|
|
48
|
+
// ignore
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
return path;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let packageDefinitionCache: ReturnType<typeof protoLoader.loadSync> | null = null;
|
|
55
|
+
let grpcClientCache: any = null;
|
|
56
|
+
|
|
57
|
+
function loadProtoDefinition() {
|
|
58
|
+
if (packageDefinitionCache === null) {
|
|
59
|
+
packageDefinitionCache = protoLoader.loadSync(PROTO_PATH, {
|
|
60
|
+
keepCase: true,
|
|
61
|
+
longs: String,
|
|
62
|
+
enums: String,
|
|
63
|
+
defaults: false,
|
|
64
|
+
oneofs: true,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return packageDefinitionCache;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Loads (once) the gRPC service client. */
|
|
71
|
+
export function qoderGrpcClient(): any {
|
|
72
|
+
if (grpcClientCache !== null) return grpcClientCache;
|
|
73
|
+
const proto = grpc.loadPackageDefinition(loadProtoDefinition()) as Record<string, any>;
|
|
74
|
+
const model = proto.model as Record<string, any> | undefined;
|
|
75
|
+
grpcClientCache = model?.chat?.ChatService;
|
|
76
|
+
if (grpcClientCache === undefined) {
|
|
77
|
+
throw new Error("Qoder gRPC proto did not resolve model.chat.ChatService");
|
|
78
|
+
}
|
|
79
|
+
return grpcClientCache;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Channel options for the Qoder gRPC surface — mirrors the desktop client
|
|
84
|
+
* (`NDl` in the unpacked worker): disable HTTP proxy, pin the authority /
|
|
85
|
+
* SSL target name to the model-server host, and raise the message length
|
|
86
|
+
* limits to 32 MiB. grpc-js defaults to a 4 MiB receive cap, which aborts
|
|
87
|
+
* large streams that the upstream serves fine.
|
|
88
|
+
*/
|
|
89
|
+
export function qoderGrpcChannelOptions(host: string): Record<string, number | string> {
|
|
90
|
+
return {
|
|
91
|
+
"grpc.enable_http_proxy": 0,
|
|
92
|
+
"grpc.default_authority": host,
|
|
93
|
+
"grpc.ssl_target_name_override": host,
|
|
94
|
+
"grpc.max_receive_message_length": 32 * 1024 * 1024,
|
|
95
|
+
"grpc.max_send_message_length": 32 * 1024 * 1024,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Maps one canonical message onto the proto `ChatMessage` shape. */
|
|
100
|
+
function mapMessage(message: CanonicalRequest["messages"][number]): Record<string, unknown> {
|
|
101
|
+
const text = flattenCanonicalContent(message.content);
|
|
102
|
+
const out: Record<string, unknown> = { role: message.role };
|
|
103
|
+
if (text.length > 0) out.text_content = text;
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Maps one already-translated OpenAI-shaped message (from `buildQoderRequest`)
|
|
109
|
+
* onto the proto `ChatMessage` shape — including structured tool history.
|
|
110
|
+
*
|
|
111
|
+
* The gRPC surface is a protobuf mirror of the same OpenAI-shaped body the
|
|
112
|
+
* HTTP IDE surface sends, so the structured transcript (assistant
|
|
113
|
+
* `tool_calls`, `tool` result turns) is mapped field-for-field instead of
|
|
114
|
+
* being flattened to text.
|
|
115
|
+
*/
|
|
116
|
+
function toProtoChatMessage(msg: Record<string, unknown>): Record<string, unknown> {
|
|
117
|
+
const out: Record<string, unknown> = { role: msg["role"] };
|
|
118
|
+
const content = msg["content"];
|
|
119
|
+
if (typeof content === "string" && content.length > 0) out.text_content = content;
|
|
120
|
+
const toolCalls = msg["tool_calls"];
|
|
121
|
+
if (Array.isArray(toolCalls) && toolCalls.length > 0) {
|
|
122
|
+
out.tool_calls = toolCalls.map((tc) => {
|
|
123
|
+
const fn = (tc?.["function"] ?? {}) as Record<string, unknown>;
|
|
124
|
+
const callId = tc?.["call_id"];
|
|
125
|
+
const call = {
|
|
126
|
+
id: typeof tc["id"] === "string" && (tc["id"] as string).length > 0
|
|
127
|
+
? { value: tc["id"] }
|
|
128
|
+
: undefined,
|
|
129
|
+
// The desktop client's `jDl` also carries `call_id` (StringValue)
|
|
130
|
+
// when the source message has one — preserve it for full tool-turn
|
|
131
|
+
// fidelity.
|
|
132
|
+
call_id: typeof callId === "string" && (callId as string).length > 0
|
|
133
|
+
? { value: callId }
|
|
134
|
+
: undefined,
|
|
135
|
+
// `arguments` is a `google.protobuf.Value`; a JSON string maps to
|
|
136
|
+
// `{ stringValue: … }` (camelCase), matching the client's `Qat()`.
|
|
137
|
+
type: { value: "function" },
|
|
138
|
+
function: {
|
|
139
|
+
name: typeof fn["name"] === "string" ? (fn["name"] as string) : "",
|
|
140
|
+
arguments: {
|
|
141
|
+
stringValue:
|
|
142
|
+
typeof fn["arguments"] === "string"
|
|
143
|
+
? (fn["arguments"] as string)
|
|
144
|
+
: fn["arguments"] !== undefined && fn["arguments"] !== null
|
|
145
|
+
? JSON.stringify(fn["arguments"])
|
|
146
|
+
: "",
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
const outCall: Record<string, unknown> = {};
|
|
151
|
+
if (call.id !== undefined) outCall.id = call.id;
|
|
152
|
+
if (call.call_id !== undefined) outCall.call_id = call.call_id;
|
|
153
|
+
outCall.type = call.type;
|
|
154
|
+
outCall.function = call.function;
|
|
155
|
+
return outCall;
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
const toolCallId = msg["tool_call_id"];
|
|
159
|
+
if (typeof toolCallId === "string" && toolCallId.length > 0) {
|
|
160
|
+
out.tool_call_id = { value: toolCallId };
|
|
161
|
+
}
|
|
162
|
+
const reasoning = msg["reasoning_content"];
|
|
163
|
+
if (typeof reasoning === "string" && reasoning.length > 0) {
|
|
164
|
+
out.reasoning_content = { value: reasoning };
|
|
165
|
+
}
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Converts a plain JSON value into a `google.protobuf.Value` wire shape —
|
|
171
|
+
* mirroring the desktop client's `Qat()` helper. A JSON object becomes a
|
|
172
|
+
* `Struct` (`{ fields: Map<string, Value> }`), arrays become `listValue`,
|
|
173
|
+
* scalars map to `stringValue`/`numberValue`/`boolValue`/`nullValue`.
|
|
174
|
+
*/
|
|
175
|
+
function toProtoValue(value: unknown): Record<string, unknown> {
|
|
176
|
+
if (value === null || value === undefined) return { nullValue: 0 };
|
|
177
|
+
if (typeof value === "number") return { numberValue: value };
|
|
178
|
+
if (typeof value === "string") return { stringValue: value };
|
|
179
|
+
if (typeof value === "boolean") return { boolValue: value };
|
|
180
|
+
if (Array.isArray(value)) return { listValue: { values: value.map(toProtoValue) } };
|
|
181
|
+
if (typeof value === "object") {
|
|
182
|
+
const fields: Record<string, unknown> = {};
|
|
183
|
+
for (const [k, v] of Object.entries(value)) fields[k] = toProtoValue(v);
|
|
184
|
+
return { structValue: { fields } };
|
|
185
|
+
}
|
|
186
|
+
return { stringValue: String(value) };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Converts a plain JSON object into a `google.protobuf.Struct` wire shape —
|
|
191
|
+
* the desktop client's `oBA()` helper. Struct fields must be wrapped as
|
|
192
|
+
* `Value`s, not forwarded raw; a raw JSON object inside a Struct is rejected
|
|
193
|
+
* at serialization time ("object expected").
|
|
194
|
+
*/
|
|
195
|
+
function toProtoStruct(obj: Record<string, unknown>): Record<string, unknown> {
|
|
196
|
+
const fields: Record<string, unknown> = {};
|
|
197
|
+
for (const [k, v] of Object.entries(obj)) fields[k] = toProtoValue(v);
|
|
198
|
+
return { fields };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Converts a decoded `google.protobuf.Value` back to a plain JS value — the
|
|
203
|
+
* inverse of {@link toProtoValue}. The tagged union key (`stringValue`,
|
|
204
|
+
* `numberValue`, `boolValue`, `nullValue`, `structValue`, `listValue`) picks
|
|
205
|
+
* the payload; `kind` is metadata and ignored.
|
|
206
|
+
*/
|
|
207
|
+
function fromProtoValue(value: unknown): unknown {
|
|
208
|
+
if (typeof value !== "object" || value === null) return value;
|
|
209
|
+
const v = value as Record<string, unknown>;
|
|
210
|
+
if ("stringValue" in v) return v.stringValue;
|
|
211
|
+
if ("numberValue" in v) return v.numberValue;
|
|
212
|
+
if ("boolValue" in v) return v.boolValue;
|
|
213
|
+
if ("nullValue" in v) return null;
|
|
214
|
+
if ("structValue" in v) return fromProtoStruct(v.structValue);
|
|
215
|
+
if ("listValue" in v) {
|
|
216
|
+
const inner = v.listValue as Record<string, unknown> | undefined;
|
|
217
|
+
const values = inner?.values;
|
|
218
|
+
return Array.isArray(values) ? values.map(fromProtoValue) : [];
|
|
219
|
+
}
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Converts a decoded `google.protobuf.Struct` back to a plain JS object — the
|
|
225
|
+
* inverse of {@link toProtoStruct}. The upstream gRPC response carries
|
|
226
|
+
* `delta.tool_calls` as repeated Structs, and each element's fields are
|
|
227
|
+
* `Value` tagged unions (not raw JSON).
|
|
228
|
+
*/
|
|
229
|
+
export function fromProtoStruct(struct: unknown): Record<string, unknown> {
|
|
230
|
+
if (typeof struct !== "object" || struct === null) return {};
|
|
231
|
+
const fields = (struct as Record<string, unknown>).fields;
|
|
232
|
+
if (typeof fields !== "object" || fields === null) return {};
|
|
233
|
+
const out: Record<string, unknown> = {};
|
|
234
|
+
for (const [k, v] of Object.entries(fields as Record<string, unknown>)) {
|
|
235
|
+
const decoded = fromProtoValue(v);
|
|
236
|
+
if (decoded !== undefined) out[k] = decoded;
|
|
237
|
+
}
|
|
238
|
+
return out;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** One decoded `delta.tool_calls` element, in the shape the mapper consumes. */
|
|
242
|
+
export interface QoderProtoToolFragment {
|
|
243
|
+
/** Trimmed declared call id, or undefined when the delta omits it. */
|
|
244
|
+
readonly id?: string;
|
|
245
|
+
/** Wire `index` (0-based), or undefined when omitted. */
|
|
246
|
+
readonly index?: number;
|
|
247
|
+
/** Trimmed tool name, or undefined on continuation chunks. */
|
|
248
|
+
readonly name?: string;
|
|
249
|
+
/** Raw argument fragment string, or undefined when the delta has none. */
|
|
250
|
+
readonly args?: string;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** A tool-call delta ready to become a canonical event (donor `$Dl` mirror). */
|
|
254
|
+
export interface QoderDrainedToolCall {
|
|
255
|
+
readonly callId: string;
|
|
256
|
+
readonly index: number;
|
|
257
|
+
readonly name: string | undefined;
|
|
258
|
+
readonly arguments: string;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Maps a decoded `delta.tool_calls` element to a canonical `tool_call_delta`
|
|
263
|
+
* exactly like the donor's `$Dl` — pass-through: `id`, `index`, `name`, and
|
|
264
|
+
* the raw `arguments` string fragment are forwarded as received, with no
|
|
265
|
+
* buffering, splitting, or JSON repair. The client stitches fragments by
|
|
266
|
+
* `index`/`id` itself (mirroring the donor's StreamAdapter `vTe`, which only
|
|
267
|
+
* tracks args for state and forwards each `partial_json` fragment verbatim).
|
|
268
|
+
*/
|
|
269
|
+
export class QoderToolCallBuffer {
|
|
270
|
+
/** Consume one decoded tool-call delta element and emit its canonical event. */
|
|
271
|
+
consume(frag: QoderProtoToolFragment): QoderDrainedToolCall {
|
|
272
|
+
const { id, index, name, args } = frag;
|
|
273
|
+
const callId = id ?? (index !== undefined ? `call_${String(index)}` : `call_anon`);
|
|
274
|
+
return {
|
|
275
|
+
callId,
|
|
276
|
+
index: index ?? 0,
|
|
277
|
+
name,
|
|
278
|
+
arguments: args ?? "",
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Stitching state for continuation fragments (per-stream). */
|
|
284
|
+
export interface QoderToolFragmentState {
|
|
285
|
+
/**
|
|
286
|
+
* The call id of the most recently announced tool call. Continuation
|
|
287
|
+
* fragments (which echo `id: ""` and a non-distinct `index: 0`) attach to
|
|
288
|
+
* this call — verified live (2026-11-05): upstream announces one call at a
|
|
289
|
+
* time (`id` + empty args), then streams that call's arguments as several
|
|
290
|
+
* small fragments before announcing the next call. The index is always 0,
|
|
291
|
+
* so it cannot distinguish calls.
|
|
292
|
+
*/
|
|
293
|
+
lastSeenToolId?: string;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Resolves the call id for one wire fragment.
|
|
298
|
+
*
|
|
299
|
+
* - A fragment carrying a real `id` announces a (new) tool call: it becomes
|
|
300
|
+
* the current active call and is returned.
|
|
301
|
+
* - A continuation fragment (`id: ""`) belongs to the current active call —
|
|
302
|
+
* the upstream streams arguments in fragments right after the announce, and
|
|
303
|
+
* each parallel call's argument fragments arrive before the next announce.
|
|
304
|
+
*
|
|
305
|
+
* Without this, each continuation gets a fresh fallback id and the client
|
|
306
|
+
* sees an empty-arguments call plus orphaned fragments (regression found live
|
|
307
|
+
* 2026-11-05 via opencode→CarteLocal).
|
|
308
|
+
*/
|
|
309
|
+
export function resolveQoderToolFragmentId(
|
|
310
|
+
state: QoderToolFragmentState,
|
|
311
|
+
declaredId: string | undefined,
|
|
312
|
+
): string | undefined {
|
|
313
|
+
if (declaredId !== undefined && declaredId.length > 0) {
|
|
314
|
+
state.lastSeenToolId = declaredId;
|
|
315
|
+
return declaredId;
|
|
316
|
+
}
|
|
317
|
+
return state.lastSeenToolId;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Maps the shared OpenAI-shaped `tools` array (from `qoderTools`) onto the
|
|
322
|
+
* proto `Tool` shape — field-for-field matching the desktop client's `YDl()`
|
|
323
|
+
* mapper.
|
|
324
|
+
*
|
|
325
|
+
* Wrapper-typed fields (`StringValue description`, `BoolValue strict`) are
|
|
326
|
+
* sent as `{ value: … }`; `parameters` is a `Struct` so each value is wrapped
|
|
327
|
+
* by `toProtoStruct`. Undefined fields are dropped entirely (client `X6`).
|
|
328
|
+
*/
|
|
329
|
+
function toProtoTools(tools: Array<Record<string, unknown>>): Array<Record<string, unknown>> {
|
|
330
|
+
return tools.map((tool) => {
|
|
331
|
+
const fn = (tool["function"] ?? {}) as Record<string, unknown>;
|
|
332
|
+
const out: Record<string, unknown> = {
|
|
333
|
+
type: typeof tool["type"] === "string" ? (tool["type"] as string) : "function",
|
|
334
|
+
function: {
|
|
335
|
+
name: typeof fn["name"] === "string" ? (fn["name"] as string) : "",
|
|
336
|
+
parameters: toProtoStruct((fn["parameters"] as Record<string, unknown> | undefined) ?? { type: "object", properties: {} }),
|
|
337
|
+
},
|
|
338
|
+
};
|
|
339
|
+
const description = out["function"] as Record<string, unknown>;
|
|
340
|
+
if (typeof fn["description"] === "string" && (fn["description"] as string).length > 0) {
|
|
341
|
+
description["description"] = { value: fn["description"] };
|
|
342
|
+
}
|
|
343
|
+
if (typeof fn["strict"] === "boolean") {
|
|
344
|
+
description["strict"] = { value: fn["strict"] };
|
|
345
|
+
}
|
|
346
|
+
return out;
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Builds the gRPC `ChatCompletionRequest` from a canonical request.
|
|
352
|
+
*
|
|
353
|
+
* The request mirrors the OpenAI-shaped body the HTTP IDE surface sends: the
|
|
354
|
+
* messages, tool declarations, and tool-choice knobs come from
|
|
355
|
+
* `buildQoderRequest` (passed as `legacy`) so both transports share the exact
|
|
356
|
+
* same translation — including the structured tool history and the `tools`
|
|
357
|
+
* array. Without `tools` the upstream sees no toolset and answers with a DSML
|
|
358
|
+
* text block (or claims no tool exists) instead of a structured `tool_calls`.
|
|
359
|
+
*/
|
|
360
|
+
export function buildQoderGrpcRequest(
|
|
361
|
+
modelId: string,
|
|
362
|
+
request: CanonicalRequest,
|
|
363
|
+
userId: string,
|
|
364
|
+
_contextWindow = 1_000_000,
|
|
365
|
+
legacy?: {
|
|
366
|
+
readonly messages: Array<Record<string, unknown>>;
|
|
367
|
+
readonly tools?: Array<Record<string, unknown>>;
|
|
368
|
+
readonly parameters?: Record<string, unknown>;
|
|
369
|
+
},
|
|
370
|
+
): Record<string, unknown> {
|
|
371
|
+
const sessionId = qoderSessionId(userId, modelId);
|
|
372
|
+
const messages = legacy === undefined ? request.messages.map(mapMessage) : legacy.messages.map(toProtoChatMessage);
|
|
373
|
+
const tools = legacy === undefined ? [] : toProtoTools(legacy.tools ?? []);
|
|
374
|
+
const toolChoice = legacy?.parameters?.["tool_choice"];
|
|
375
|
+
const parallel = legacy?.parameters?.["parallel_tool_calls"];
|
|
376
|
+
// Donor mirror (`QFn`): `max_tokens` is sent only when the caller set it —
|
|
377
|
+
// the donor wraps the caller's value in `{value: …}` and omits the field
|
|
378
|
+
// entirely otherwise. Hard-coding 65536 turned a caller request into a
|
|
379
|
+
// different model behavior than the reference desktop client.
|
|
380
|
+
const maxTokens = legacy?.parameters?.["max_tokens"];
|
|
381
|
+
const contextLength = legacy?.parameters?.["context_length"];
|
|
382
|
+
return {
|
|
383
|
+
model: modelId,
|
|
384
|
+
messages,
|
|
385
|
+
// `stream` is a plain `bool` in the proto (field 6) — not a wrapper.
|
|
386
|
+
stream: true,
|
|
387
|
+
stream_options: { include_usage: { value: true } },
|
|
388
|
+
...(maxTokens === undefined ? {} : { max_tokens: { value: Number(maxTokens) } }),
|
|
389
|
+
// `tools` is repeated; an empty array serializes to nothing on the wire,
|
|
390
|
+
// so it is safe to always include. The upstream only ever sees a
|
|
391
|
+
// declaration when one exists.
|
|
392
|
+
tools,
|
|
393
|
+
// Mirror the donor's `QFn`: `parallel_tool_calls` is sent only when the
|
|
394
|
+
// caller set it (the donor wraps the caller's parameter in `{value: …}`).
|
|
395
|
+
...(parallel === undefined ? {} : { parallel_tool_calls: { value: Boolean(parallel) } }),
|
|
396
|
+
// `tool_choice` is a `google.protobuf.Value`; a string choice maps to
|
|
397
|
+
// `{ stringValue: … }` (camelCase), matching the desktop client's `Qat()`.
|
|
398
|
+
// gRPC rejects `tool_choice: "required"` outright (verified live
|
|
399
|
+
// 2026-11-05 model matrix: 400/406/422 on most models — dmodel/dfmodel/
|
|
400
|
+
// qmodel_38max/mmodel/cmodel). Omit it and let the neutral prompt carry the
|
|
401
|
+
// intent; `auto`/named choices are still sent.
|
|
402
|
+
...(toolChoice !== undefined && toolChoice !== "required" ? { tool_choice: { stringValue: String(toolChoice) } } : {}),
|
|
403
|
+
metadata: {
|
|
404
|
+
context: {
|
|
405
|
+
request_id: randomUUID(),
|
|
406
|
+
session_id: sessionId,
|
|
407
|
+
task_id: "common",
|
|
408
|
+
request_set_id: randomUUID(),
|
|
409
|
+
client_type: "5",
|
|
410
|
+
source_session_id: "",
|
|
411
|
+
...(contextLength === undefined ? {} : { context_length: String(contextLength) }),
|
|
412
|
+
},
|
|
413
|
+
business: {
|
|
414
|
+
product: "cli",
|
|
415
|
+
type: "agent",
|
|
416
|
+
version: getQoderVersion(),
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/** gRPC metadata headers mirroring the desktop client's identity headers. */
|
|
423
|
+
export function qoderGrpcMetadata(auth: QoderAuth, modelId = ""): grpc.Metadata {
|
|
424
|
+
const metadata = new grpc.Metadata();
|
|
425
|
+
metadata.set("authorization", `Bearer ${auth.securityOauthToken}`);
|
|
426
|
+
metadata.set("x-request-id", randomUUID());
|
|
427
|
+
metadata.set("x-session-id", qoderSessionId(auth.userId, modelId));
|
|
428
|
+
// The desktop client's gRPC surface sends exactly this set (bundle
|
|
429
|
+
// `bDl`/`patchCompletionStream`): version, client type, real machine OS,
|
|
430
|
+
// and the sanitized hostname. It does NOT send `cosy-machinetype` /
|
|
431
|
+
// `cosy-machinetoken` on gRPC (those are HTTP-only identity headers), and
|
|
432
|
+
// sending a bogus device token here is a candidate for a server-side
|
|
433
|
+
// mid-stream reset — see documentation/proposals/qoder-grpc-wire-alignment.md.
|
|
434
|
+
const version = getQoderVersion();
|
|
435
|
+
metadata.set("cosy-version", version);
|
|
436
|
+
metadata.set("cosy-clienttype", "5");
|
|
437
|
+
metadata.set("cosy-machineos", qoderMachineOs());
|
|
438
|
+
const hostname = qoderMachineHostname();
|
|
439
|
+
if (hostname.length > 0) metadata.set("cosy-machinehostname", hostname);
|
|
440
|
+
return metadata;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** Maps a proto `finish_reason` to the canonical stop reason. */
|
|
444
|
+
function mapFinishReason(raw: string | undefined): CanonicalStopReason | undefined {
|
|
445
|
+
switch (raw) {
|
|
446
|
+
case "stop":
|
|
447
|
+
return "stop";
|
|
448
|
+
case "tool_calls":
|
|
449
|
+
case "function_call":
|
|
450
|
+
return "tool_use";
|
|
451
|
+
case "length":
|
|
452
|
+
return "length";
|
|
453
|
+
default:
|
|
454
|
+
return undefined;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Streams one gRPC `ChatCompletionStream` call into canonical events.
|
|
460
|
+
*
|
|
461
|
+
* `signal` aborts the call; the gRPC status is mapped to a GatewayError when
|
|
462
|
+
* the call fails before any chunk.
|
|
463
|
+
*/
|
|
464
|
+
export async function* qoderGrpcToCanonicalEvents(
|
|
465
|
+
request: Record<string, unknown>,
|
|
466
|
+
metadata: grpc.Metadata,
|
|
467
|
+
realm: QoderRealm,
|
|
468
|
+
signal: AbortSignal,
|
|
469
|
+
): AsyncGenerator<CanonicalEvent> {
|
|
470
|
+
const ChatService = qoderGrpcClient();
|
|
471
|
+
const host = qoderGrpcHost(realm);
|
|
472
|
+
const client = new ChatService(`${host}:443`, grpc.credentials.createSsl(), qoderGrpcChannelOptions(host));
|
|
473
|
+
let sequence = 0;
|
|
474
|
+
const nextSeq = () => {
|
|
475
|
+
sequence += 1;
|
|
476
|
+
return sequence;
|
|
477
|
+
};
|
|
478
|
+
try {
|
|
479
|
+
const stream = client.ChatCompletionStream(request, metadata);
|
|
480
|
+
let aborted = false;
|
|
481
|
+
// Once a terminal event is emitted the turn is over: any later content,
|
|
482
|
+
// reasoning, or tool-call fragments must be dropped — strict OpenAI
|
|
483
|
+
// clients reject content after the finish reason. A second terminal is
|
|
484
|
+
// skipped too (the upstream can re-emit the finish frame).
|
|
485
|
+
let finished = false;
|
|
486
|
+
|
|
487
|
+
const abortHandler = () => {
|
|
488
|
+
aborted = true;
|
|
489
|
+
stream.cancel();
|
|
490
|
+
};
|
|
491
|
+
signal.addEventListener("abort", abortHandler, { once: true });
|
|
492
|
+
|
|
493
|
+
try {
|
|
494
|
+
let echoedModel: string | undefined;
|
|
495
|
+
// Tool-call deltas arrive as fragments on `delta.tool_calls` (same
|
|
496
|
+
// OpenAI shape as the HTTP surfaces). Each decoded element maps to one
|
|
497
|
+
// canonical `tool_call_delta` immediately, pass-through — the donor
|
|
498
|
+
// forwards fragments verbatim and the client stitches them by
|
|
499
|
+
// `index`/`id` itself.
|
|
500
|
+
const toolCallBuffer = new QoderToolCallBuffer();
|
|
501
|
+
/** Stitching state for continuation fragments. */
|
|
502
|
+
const fragmentState: QoderToolFragmentState = {};
|
|
503
|
+
for await (const chunk of stream) {
|
|
504
|
+
if (aborted || signal.aborted) break;
|
|
505
|
+
// The upstream names the model it actually ran in the chunk's `model`
|
|
506
|
+
// field. Surface it once as `response_start` so probes and telemetry
|
|
507
|
+
// can report the real echoed model (the gRPC surface has no HTTP body
|
|
508
|
+
// for `readReportedModel` to inspect).
|
|
509
|
+
if (echoedModel === undefined) {
|
|
510
|
+
if (typeof chunk.model === "string" && chunk.model.trim().length > 0) {
|
|
511
|
+
echoedModel = chunk.model.trim();
|
|
512
|
+
}
|
|
513
|
+
yield {
|
|
514
|
+
type: "response_start",
|
|
515
|
+
sequence_number: nextSeq(),
|
|
516
|
+
...(echoedModel === undefined ? {} : { model: echoedModel }),
|
|
517
|
+
} satisfies CanonicalEvent as CanonicalEvent;
|
|
518
|
+
}
|
|
519
|
+
const choices = chunk.choices ?? [];
|
|
520
|
+
for (const choice of choices) {
|
|
521
|
+
const delta = choice.delta ?? {};
|
|
522
|
+
if (!finished && typeof delta.content === "string" && delta.content.length > 0) {
|
|
523
|
+
yield {
|
|
524
|
+
type: "content_delta",
|
|
525
|
+
sequence_number: nextSeq(),
|
|
526
|
+
content: { kind: "text", text: delta.content },
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
if (!finished && typeof delta.reasoning_content === "string" && delta.reasoning_content.length > 0) {
|
|
530
|
+
yield {
|
|
531
|
+
type: "content_delta",
|
|
532
|
+
sequence_number: nextSeq(),
|
|
533
|
+
content: { kind: "reasoning", payload: delta.reasoning_content },
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
// `delta.tool_calls` is a repeated `google.protobuf.Struct` — each
|
|
537
|
+
// element arrives as a Struct-wrapped Value union (`{fields: {k:
|
|
538
|
+
// {stringValue/numberValue/...}}}`), NOT the plain OpenAI shape.
|
|
539
|
+
// Convert each element back to `{id, index, type, function}` before
|
|
540
|
+
// applying the same empty-id/continuation semantics as the HTTP
|
|
541
|
+
// surfaces: `id` only appears on the first chunk, so fragments
|
|
542
|
+
// rejoin the first-seen id per wire `index`.
|
|
543
|
+
const toolCalls = delta.tool_calls;
|
|
544
|
+
if (!finished && Array.isArray(toolCalls) && toolCalls.length > 0) {
|
|
545
|
+
for (const rawTc of toolCalls) {
|
|
546
|
+
const tc = fromProtoStruct(rawTc);
|
|
547
|
+
const fn = (tc["function"] ?? {}) as Record<string, unknown>;
|
|
548
|
+
const index = typeof tc["index"] === "number" ? (tc["index"] as number) : undefined;
|
|
549
|
+
const declaredId =
|
|
550
|
+
typeof tc["id"] === "string" && (tc["id"] as string).trim().length > 0
|
|
551
|
+
? (tc["id"] as string).trim()
|
|
552
|
+
: undefined;
|
|
553
|
+
const fragName =
|
|
554
|
+
typeof fn["name"] === "string" && fn["name"].trim().length > 0
|
|
555
|
+
? (fn["name"] as string).trim()
|
|
556
|
+
: undefined;
|
|
557
|
+
const fragArgs =
|
|
558
|
+
typeof fn["arguments"] === "string"
|
|
559
|
+
? (fn["arguments"] as string)
|
|
560
|
+
: fn["arguments"] !== undefined && fn["arguments"] !== null
|
|
561
|
+
? JSON.stringify(fn["arguments"])
|
|
562
|
+
: undefined;
|
|
563
|
+
// Continuation fragments (`id: ""`) attach to the current active
|
|
564
|
+
// call — the upstream announces one call at a time (id + empty
|
|
565
|
+
// args) and then streams that call's arguments as several small
|
|
566
|
+
// fragments, with the index always 0. Without this, each
|
|
567
|
+
// fragment gets a fresh fallback id and the client sees an
|
|
568
|
+
// empty-arguments call plus orphaned fragments.
|
|
569
|
+
const pinnedId = resolveQoderToolFragmentId(fragmentState, declaredId);
|
|
570
|
+
const call = toolCallBuffer.consume({
|
|
571
|
+
...(pinnedId !== undefined ? { id: pinnedId } : {}),
|
|
572
|
+
...(index !== undefined ? { index } : {}),
|
|
573
|
+
...(fragName !== undefined ? { name: fragName } : {}),
|
|
574
|
+
...(fragArgs !== undefined && fragArgs.length > 0 ? { args: fragArgs } : {}),
|
|
575
|
+
});
|
|
576
|
+
yield {
|
|
577
|
+
type: "tool_call_delta",
|
|
578
|
+
sequence_number: nextSeq(),
|
|
579
|
+
call_id: call.callId,
|
|
580
|
+
index: call.index,
|
|
581
|
+
...(call.name === undefined ? {} : { name: call.name }),
|
|
582
|
+
arguments_delta: call.arguments,
|
|
583
|
+
} satisfies CanonicalEvent as CanonicalEvent;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
if (!finished && typeof choice.finish_reason === "string" && choice.finish_reason.length > 0) {
|
|
587
|
+
const stop = mapFinishReason(choice.finish_reason);
|
|
588
|
+
if (stop !== undefined) {
|
|
589
|
+
// The terminal verdict follows the tool-call deltas, which were
|
|
590
|
+
// already yielded as they arrived — the client must see the
|
|
591
|
+
// calls before the finish reason, or it records
|
|
592
|
+
// `finish_reason=tool_calls` with zero tool calls.
|
|
593
|
+
finished = true;
|
|
594
|
+
// `tool_use` is a *successful* terminal verdict — the model
|
|
595
|
+
// decided to call a tool — so the terminal state is `complete`
|
|
596
|
+
// (same as `stop`), never `failed`. A `failed` state here made
|
|
597
|
+
// attempt-finalize treat a healthy tool turn as an upstream
|
|
598
|
+
// failure ("empty-handed") and replay it.
|
|
599
|
+
yield {
|
|
600
|
+
type: "terminal",
|
|
601
|
+
sequence_number: nextSeq(),
|
|
602
|
+
state: stop === "stop" || stop === "tool_use" ? "complete" : "failed",
|
|
603
|
+
stop_reason: stop,
|
|
604
|
+
provider_stop_reason: choice.finish_reason,
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
if (chunk.usage) {
|
|
610
|
+
const usage = usageFromProvider(chunk.usage);
|
|
611
|
+
if (usage !== undefined) {
|
|
612
|
+
yield { type: "usage", sequence_number: nextSeq(), usage };
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
// Tool-call deltas are emitted as they arrive — there is no end-of-stream
|
|
617
|
+
// buffer to flush; the terminal event is yielded by the finish-reason
|
|
618
|
+
// branch above (or synthesized by the caller if the stream ends without
|
|
619
|
+
// one).
|
|
620
|
+
} catch (err: any) {
|
|
621
|
+
const status = err?.code;
|
|
622
|
+
const details = err?.details ?? err?.message ?? "";
|
|
623
|
+
if (typeof status === "number" && status !== 0) {
|
|
624
|
+
if (status === 16) {
|
|
625
|
+
throw new GatewayError(
|
|
626
|
+
"authentication_failed",
|
|
627
|
+
401,
|
|
628
|
+
"Qoder gRPC rejected the credential (request {requestId}). Wrong key, right door — check the credential 🔑",
|
|
629
|
+
{},
|
|
630
|
+
"upstream",
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
if (details.includes("413")) {
|
|
634
|
+
throw new GatewayError(
|
|
635
|
+
"context_length_exceeded",
|
|
636
|
+
413,
|
|
637
|
+
"The request exceeds the upstream context cap (request {requestId}). Trim the conversation and retry ✂️",
|
|
638
|
+
{},
|
|
639
|
+
"upstream",
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
if (status === 13 && details.includes("quota")) {
|
|
643
|
+
throw new GatewayError(
|
|
644
|
+
"quota_exceeded",
|
|
645
|
+
429,
|
|
646
|
+
"Qoder quota exhausted on the gRPC surface (request {requestId})",
|
|
647
|
+
{},
|
|
648
|
+
"upstream",
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
throw new GatewayError(
|
|
652
|
+
"platform_unavailable",
|
|
653
|
+
502,
|
|
654
|
+
`Qoder gRPC stream failed (${details}) (request {requestId})`,
|
|
655
|
+
{},
|
|
656
|
+
"upstream",
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
throw err;
|
|
660
|
+
} finally {
|
|
661
|
+
signal.removeEventListener("abort", abortHandler);
|
|
662
|
+
stream.destroy();
|
|
663
|
+
}
|
|
664
|
+
} finally {
|
|
665
|
+
client.close();
|
|
666
|
+
}
|
|
667
|
+
}
|