cartethyia 2.7.1 → 2.7.3
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 +2 -0
- package/dist/dashboard/assets/{BannedPage-D-J75hBM.js → BannedPage-2TJD37Hb.js} +1 -1
- package/dist/dashboard/assets/BannedPage-2TJD37Hb.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-xefJNRmJ.js +1 -0
- package/dist/dashboard/assets/CliToolDetailPage-xefJNRmJ.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-Rj9iHQ3F.js +1 -0
- package/dist/dashboard/assets/CliToolsPage-Rj9iHQ3F.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-QoJ6foxo.js → CombosPage-JEbYJFxw.js} +1 -1
- package/dist/dashboard/assets/CombosPage-JEbYJFxw.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-DpNRay4C.js → ConsoleLogPage-C3xx_inM.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-C3xx_inM.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-Dw306gJU.js +1 -0
- package/dist/dashboard/assets/CustomizationPage-Dw306gJU.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-jaBo1TWV.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-jaBo1TWV.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-DubdQcvX.js → LoginPage-C4w7vea-.js} +1 -1
- package/dist/dashboard/assets/LoginPage-C4w7vea-.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-BH8Tehdi.js → ModelPicker-BtPcAfot.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-BtPcAfot.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-CGC5gNs1.js +1 -0
- package/dist/dashboard/assets/OverviewPage-CGC5gNs1.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-DQcthNGt.js +4 -0
- package/dist/dashboard/assets/ProviderDetailPage-DQcthNGt.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-0OsXRGXe.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-0OsXRGXe.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-DdPc8slb.js +9 -0
- package/dist/dashboard/assets/ProxyPage-DdPc8slb.js.gz +0 -0
- package/dist/dashboard/assets/{QuotaPage-ATpaxrbl.js → QuotaPage-Ci5O4BnH.js} +1 -1
- package/dist/dashboard/assets/QuotaPage-Ci5O4BnH.js.gz +0 -0
- package/dist/dashboard/assets/{SettingsPage-CETophpx.js → SettingsPage-CDZjTt87.js} +1 -1
- package/dist/dashboard/assets/SettingsPage-CDZjTt87.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-BQS0Rx_u.js +1 -0
- package/dist/dashboard/assets/SetupPage-BQS0Rx_u.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-CAK-EN9z.js → SortableList-BAL5npBC.js} +1 -1
- package/dist/dashboard/assets/SortableList-BAL5npBC.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-B7W2_RhT.js → StudioPage-TP7voAQ0.js} +3 -3
- package/dist/dashboard/assets/StudioPage-TP7voAQ0.js.gz +0 -0
- package/dist/dashboard/assets/{UsagePage-6T4EFDjW.js → UsagePage-Cj_fEzhg.js} +13 -13
- package/dist/dashboard/assets/UsagePage-Cj_fEzhg.js.gz +0 -0
- package/dist/dashboard/assets/{api-keys-CUi6UXsR.js → api-keys-MbI7e7rt.js} +1 -1
- package/dist/dashboard/assets/api-keys-MbI7e7rt.js.gz +0 -0
- package/dist/dashboard/assets/app-B2vyPoII.css +2 -0
- package/dist/dashboard/assets/app-B2vyPoII.css.gz +0 -0
- package/dist/dashboard/assets/arrow-up-59ms1iiT.js +1 -0
- package/dist/dashboard/assets/arrow-up-59ms1iiT.js.gz +0 -0
- package/dist/dashboard/assets/{assert-67xXH5rf.js → assert-C0YYuZJ4.js} +1 -1
- package/dist/dashboard/assets/assert-C0YYuZJ4.js.gz +0 -0
- package/dist/dashboard/assets/{badge-DC2L43Mj.js → badge-B_DMpMJN.js} +1 -1
- package/dist/dashboard/assets/badge-B_DMpMJN.js.gz +0 -0
- package/dist/dashboard/assets/{banner-BBVcb0J3.js → banner-DMrMPmth.js} +1 -1
- package/dist/dashboard/assets/banner-DMrMPmth.js.gz +0 -0
- package/dist/dashboard/assets/{card-Cca1XC6G.js → card-Be5Uzamx.js} +1 -1
- package/dist/dashboard/assets/card-Be5Uzamx.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-KvsWa1Jl.js → cli-tools-muFRphDu.js} +1 -1
- package/dist/dashboard/assets/cli-tools-muFRphDu.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-DUzEbynq.js → clipboard-button-DOfrVmsi.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-DOfrVmsi.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-DFvWhwTa.js → dialog-C9omdjQt.js} +1 -1
- package/dist/dashboard/assets/dialog-C9omdjQt.js.gz +0 -0
- package/dist/dashboard/assets/drawer-Bm8WKaGY.js +1 -0
- package/dist/dashboard/assets/drawer-Bm8WKaGY.js.gz +0 -0
- package/dist/dashboard/assets/{entry-_xHo5x6r.js → entry-DS90gUks.js} +1 -1
- package/dist/dashboard/assets/entry-DS90gUks.js.gz +0 -0
- package/dist/dashboard/assets/{entry-BLBLxX0h.js → entry-j40LJotO.js} +3 -3
- package/dist/dashboard/assets/entry-j40LJotO.js.gz +0 -0
- package/dist/dashboard/assets/{entry-Bqy8RFvM.js → entry-q5HwZ8nN.js} +1 -1
- package/dist/dashboard/assets/entry-q5HwZ8nN.js.gz +0 -0
- package/dist/dashboard/assets/{concurrency-BDF0ok7y.js → fingerprint-pattern-BWuHtbW4.js} +1 -1
- package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
- package/dist/dashboard/assets/{formatters-mpBPqAQ8.js → formatters-BTScrQpe.js} +1 -1
- package/dist/dashboard/assets/formatters-BTScrQpe.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-C9e8Ts7e.js → github-badge-Orjuzemr.js} +1 -1
- package/dist/dashboard/assets/github-badge-Orjuzemr.js.gz +0 -0
- package/dist/dashboard/assets/{index-D6QzEC_n.js → index-cPrX5vn6.js} +2 -2
- package/dist/dashboard/assets/index-cPrX5vn6.js.gz +0 -0
- package/dist/dashboard/assets/{inline-BFUcvllj.js → inline-Dvux8w-0.js} +1 -1
- package/dist/dashboard/assets/inline-Dvux8w-0.js.gz +0 -0
- package/dist/dashboard/assets/{input-C_ILAvBb.js → input-CU1Rgf3p.js} +1 -1
- package/dist/dashboard/assets/input-CU1Rgf3p.js.gz +0 -0
- package/dist/dashboard/assets/{layout-CBIyT6Rt.js → layout-C_7A6H55.js} +1 -1
- package/dist/dashboard/assets/layout-C_7A6H55.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-ebJ_-Nsb.js → list-row-CzFvogbj.js} +1 -1
- package/dist/dashboard/assets/list-row-CzFvogbj.js.gz +0 -0
- package/dist/dashboard/assets/{live-D0mm--ME.js → live-KaG15HLQ.js} +1 -1
- package/dist/dashboard/assets/live-KaG15HLQ.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-sMD965oO.js → mutation-JJGQm8TD.js} +1 -1
- package/dist/dashboard/assets/mutation-JJGQm8TD.js.gz +0 -0
- package/dist/dashboard/assets/{numbers-BO9dKaFr.js → numbers-BNDZnsUE.js} +1 -1
- package/dist/dashboard/assets/numbers-BNDZnsUE.js.gz +0 -0
- package/dist/dashboard/assets/{query-keys-CfL_Zdoh.js → query-keys-CLaNz8Xz.js} +1 -1
- package/dist/dashboard/assets/query-keys-CLaNz8Xz.js.gz +0 -0
- package/dist/dashboard/assets/{routing-BnIpi1IJ.js → routing-BEH32QOC.js} +1 -1
- package/dist/dashboard/assets/routing-BEH32QOC.js.gz +0 -0
- package/dist/dashboard/assets/{select-Cj5Nw43g.js → select-B3iTetHN.js} +1 -1
- package/dist/dashboard/assets/select-B3iTetHN.js.gz +0 -0
- package/dist/dashboard/assets/settings-2-Ds9DQp0t.js +1 -0
- package/dist/dashboard/assets/settings-2-Ds9DQp0t.js.gz +0 -0
- package/dist/dashboard/assets/{settings-BAFhDgX8.js → settings-Dt3Eqbnq.js} +1 -1
- package/dist/dashboard/assets/settings-Dt3Eqbnq.js.gz +0 -0
- package/dist/dashboard/assets/{stack-EMrOMt4I.js → stack-KrOrm7JZ.js} +1 -1
- package/dist/dashboard/assets/stack-KrOrm7JZ.js.gz +0 -0
- package/dist/dashboard/assets/{state-CWoo7ty5.js → state-DLS7IDMy.js} +1 -1
- package/dist/dashboard/assets/state-DLS7IDMy.js.gz +0 -0
- package/dist/dashboard/assets/{switch-CCjT6IkF.js → switch-42ItvbAs.js} +1 -1
- package/dist/dashboard/assets/switch-42ItvbAs.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-DjkLHUcE.js → useMutation-Dmnfh1Ue.js} +1 -1
- package/dist/dashboard/assets/useMutation-Dmnfh1Ue.js.gz +0 -0
- package/dist/dashboard/index.html +3 -2
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +117 -115
- package/migrations/full-postgres/0026_add_qoder_native_identity.sql +20 -0
- package/migrations/full-postgres/0027_qoder_identity_genuine_seed.sql +22 -0
- package/migrations/full-postgres/0028_add_family_template_key_telemetry.sql +40 -0
- package/migrations/full-postgres/0029_null_share_child_policy_columns.sql +38 -0
- package/migrations/full-postgres/0030_add_api_key_max_children.sql +10 -0
- package/migrations/full-postgres/0031_tenant_system_prompts.sql +16 -0
- package/migrations/lite-sqlite3/0026_add_qoder_native_identity.sql +15 -0
- package/migrations/lite-sqlite3/0027_qoder_identity_genuine_seed.sql +22 -0
- package/migrations/lite-sqlite3/0028_add_family_template_key_telemetry.sql +40 -0
- package/migrations/lite-sqlite3/0029_null_share_child_policy_columns.sql +38 -0
- package/migrations/lite-sqlite3/0030_add_api_key_max_children.sql +10 -0
- package/migrations/lite-sqlite3/0031_tenant_system_prompts.sql +10 -0
- package/package.json +1 -1
- package/src/config/network.ts +1 -1
- package/src/console/api-keys/bucket-spend.ts +7 -7
- package/src/console/api-keys/contracts.ts +423 -410
- package/src/console/api-keys/routes.ts +118 -19
- package/src/console/backup/export-endpoint.ts +8 -11
- package/src/console/backup/routes.ts +40 -8
- package/src/console/backup/service.ts +11 -6
- package/src/console/backup/store.ts +384 -88
- package/src/console/cli-tools/contracts.ts +10 -4
- package/src/console/dashboard-assets.ts +16 -1
- package/src/console/domain-registration.ts +2 -0
- package/src/console/quota/account-quota-tenant.ts +8 -1
- package/src/console/quota/account-quota.ts +4 -1
- package/src/console/quota/native-identity.ts +189 -0
- package/src/console/remote-support/contracts.ts +12 -6
- package/src/console/remote-support/service.ts +2 -2
- package/src/console/share/share-router.ts +39 -6
- package/src/console/share/share-stats.ts +64 -25
- package/src/console/share/share-usage.ts +24 -7
- package/src/console/system-prompt/contracts.ts +102 -0
- package/src/console/system-prompt/routes.ts +165 -0
- package/src/main.ts +11 -0
- package/src/network/outbound-fetch.ts +281 -226
- package/src/network/pool/agent.ts +7 -0
- package/src/observability/event-query.ts +118 -7
- package/src/persistence/db-pg.ts +42 -0
- package/src/persistence/schema.ts +102 -0
- package/src/persistence/stores/api-key-store.ts +753 -581
- package/src/persistence/stores/share-store.ts +65 -13
- package/src/persistence/stores/telemetry-store.ts +57 -1
- package/src/persistence/system-prompt-store.ts +104 -0
- package/src/protocol/reasoning.ts +65 -1
- package/src/protocol/request/chat.ts +1 -0
- package/src/providers/default-registry.ts +1 -0
- package/src/providers/integrations/antigravity/antigravity.ts +20 -23
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +30 -2
- package/src/providers/integrations/buddy/buddy-media.ts +5 -1
- package/src/providers/integrations/buddy/codebuddy/provider.ts +5 -1
- package/src/providers/integrations/buddy/workbuddy/provider.ts +5 -1
- package/src/providers/integrations/cline/cline.ts +7 -0
- package/src/providers/integrations/github/github.ts +20 -1
- package/src/providers/integrations/hermes.ts +5 -1
- package/src/providers/integrations/kilo/kilo.ts +4 -0
- package/src/providers/integrations/ollamacloud-models.ts +50 -0
- package/src/providers/integrations/openrouter/provider.ts +4 -0
- package/src/providers/integrations/qoder/qoder-checkin.ts +224 -51
- package/src/providers/integrations/qoder/qoder-native-identity-store.ts +156 -0
- package/src/providers/integrations/qoder/qoder-oauth.ts +24 -4
- package/src/providers/integrations/qoder/qoder-realm.ts +20 -0
- package/src/providers/integrations/qoder/qoder-text-tools.ts +277 -0
- package/src/providers/integrations/qoder/qoder-umid.ts +288 -0
- package/src/providers/integrations/qoder/qoder.ts +1375 -1641
- package/src/providers/operations/account-error-classification.ts +1682 -1660
- package/src/providers/operations/account-health-service.ts +619 -621
- package/src/providers/operations/client-versions.ts +37 -22
- package/src/providers/operations/provider-catalog-service.ts +11 -1
- package/src/providers/provider-contracts.ts +15 -2
- package/src/security/access-control.ts +19 -0
- package/src/security/api-key-auth.ts +14 -1
- package/src/security/cli-client-fingerprint.ts +9 -2
- package/src/transport/dispatch/empty-stream.ts +224 -224
- package/src/transport/dispatch/proxy-request.ts +31 -2
- package/src/transport/dispatch/streaming/stream.ts +16 -1
- package/src/transport/dispatch/upstream.ts +10 -0
- package/src/transport/failure-policy.ts +33 -0
- package/src/transport/request/preparer.ts +17 -1
- package/src/transport/request/system-prompt-injection.ts +89 -0
- package/src/transport/request/tenant-preferences.ts +43 -0
- package/src/transport/routing/admission.ts +13 -3
- package/src/transport/routing/route-catalog.ts +41 -12
- package/src/transport/routing/route-model.ts +53 -0
- package/src/transport/routing/router.ts +306 -228
- package/src/transport/surface/responses/encode.ts +2 -3
- package/src/transport/surface/responses/parse.ts +49 -7
- package/src/types/undici.d.ts +33 -0
- package/src/workers/daily-checkin.ts +2 -0
- package/dist/dashboard/assets/BannedPage-D-J75hBM.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-CVe6RaBH.js +0 -1
- package/dist/dashboard/assets/CliToolDetailPage-CVe6RaBH.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-Dd8ZSamp.js +0 -1
- package/dist/dashboard/assets/CliToolsPage-Dd8ZSamp.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-QoJ6foxo.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-DpNRay4C.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-CKUFTcW4.js +0 -1
- package/dist/dashboard/assets/CustomizationPage-CKUFTcW4.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-CzSLiDzr.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-CzSLiDzr.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-DubdQcvX.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-BH8Tehdi.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-DuWzAQFL.js +0 -1
- package/dist/dashboard/assets/OverviewPage-DuWzAQFL.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-BKEKe2Fm.js +0 -4
- package/dist/dashboard/assets/ProviderDetailPage-BKEKe2Fm.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-Bwotu8UJ.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-Bwotu8UJ.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-D7hpWbg9.js +0 -9
- package/dist/dashboard/assets/ProxyPage-D7hpWbg9.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-ATpaxrbl.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-CETophpx.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-BdgECjqq.js +0 -1
- package/dist/dashboard/assets/SetupPage-BdgECjqq.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-CAK-EN9z.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-B7W2_RhT.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-6T4EFDjW.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-CUi6UXsR.js.gz +0 -0
- package/dist/dashboard/assets/app-KvsUJUMF.css +0 -2
- package/dist/dashboard/assets/app-KvsUJUMF.css.gz +0 -0
- package/dist/dashboard/assets/assert-67xXH5rf.js.gz +0 -0
- package/dist/dashboard/assets/badge-DC2L43Mj.js.gz +0 -0
- package/dist/dashboard/assets/banner-BBVcb0J3.js.gz +0 -0
- package/dist/dashboard/assets/card-Cca1XC6G.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-KvsWa1Jl.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-DUzEbynq.js.gz +0 -0
- package/dist/dashboard/assets/concurrency-BDF0ok7y.js.gz +0 -0
- package/dist/dashboard/assets/dialog-DFvWhwTa.js.gz +0 -0
- package/dist/dashboard/assets/drawer-CFM2sMX1.js +0 -1
- package/dist/dashboard/assets/drawer-CFM2sMX1.js.gz +0 -0
- package/dist/dashboard/assets/entry-BLBLxX0h.js.gz +0 -0
- package/dist/dashboard/assets/entry-Bqy8RFvM.js.gz +0 -0
- package/dist/dashboard/assets/entry-_xHo5x6r.js.gz +0 -0
- package/dist/dashboard/assets/formatters-mpBPqAQ8.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-C9e8Ts7e.js.gz +0 -0
- package/dist/dashboard/assets/index-D6QzEC_n.js.gz +0 -0
- package/dist/dashboard/assets/inline-BFUcvllj.js.gz +0 -0
- package/dist/dashboard/assets/input-C_ILAvBb.js.gz +0 -0
- package/dist/dashboard/assets/layout-CBIyT6Rt.js.gz +0 -0
- package/dist/dashboard/assets/list-row-ebJ_-Nsb.js.gz +0 -0
- package/dist/dashboard/assets/live-D0mm--ME.js.gz +0 -0
- package/dist/dashboard/assets/mutation-sMD965oO.js.gz +0 -0
- package/dist/dashboard/assets/numbers-BO9dKaFr.js.gz +0 -0
- package/dist/dashboard/assets/query-keys-CfL_Zdoh.js.gz +0 -0
- package/dist/dashboard/assets/routing-BnIpi1IJ.js.gz +0 -0
- package/dist/dashboard/assets/select-Cj5Nw43g.js.gz +0 -0
- package/dist/dashboard/assets/settings-BAFhDgX8.js.gz +0 -0
- package/dist/dashboard/assets/stack-EMrOMt4I.js.gz +0 -0
- package/dist/dashboard/assets/state-CWoo7ty5.js.gz +0 -0
- package/dist/dashboard/assets/switch-CCjT6IkF.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-DjkLHUcE.js.gz +0 -0
- /package/dist/dashboard/assets/{app-BaWd0puM.js → app-wvwCi4Eh.js} +0 -0
- /package/dist/dashboard/assets/{app-BaWd0puM.js.gz → app-wvwCi4Eh.js.gz} +0 -0
|
@@ -1,1641 +1,1375 @@
|
|
|
1
|
-
// Factory-blocked (Phase C4): PAT-to-jobToken session handshake, COSY
|
|
2
|
-
// AES/RSA request signing, alphabet-scrambled binary body, enveloped SSE
|
|
3
|
-
// frames — session/crypto protocol outside factory reach; stays bespoke.
|
|
4
|
-
import { constants, createCipheriv, createHash, publicEncrypt, randomUUID } from "node:crypto";
|
|
5
|
-
import {
|
|
6
|
-
classifyUpstreamError,
|
|
7
|
-
extractUpstreamMessage,
|
|
8
|
-
isUpstreamQueued,
|
|
9
|
-
mapUpstreamHttpError,
|
|
10
|
-
queueRetryAfterMs,
|
|
11
|
-
upstreamErrorIdentifier,
|
|
12
|
-
} from "../../../transport/failure-policy";
|
|
13
|
-
import { GatewayError } from "../../../protocol/gateway-error";
|
|
14
|
-
import { resolveImageSource } from "../../../protocol/codecs";
|
|
15
|
-
import type {
|
|
16
|
-
CanonicalEvent,
|
|
17
|
-
CanonicalRequest,
|
|
18
|
-
CanonicalStopReason,
|
|
19
|
-
ContentPart,
|
|
20
|
-
ToolChoice,
|
|
21
|
-
ToolDefinition,
|
|
22
|
-
} from "../../../protocol/canonical-model";
|
|
23
|
-
import { toolResultParts } from "../../../protocol/canonical-model";
|
|
24
|
-
import { decodeSseEvents } from "../../../protocol/sse-decoder";
|
|
25
|
-
import { usageFromProvider, readReasoningText } from "../../provider-usage";
|
|
26
|
-
import { readCredentialSecret, type ProviderDispatchTarget, type ProviderAdapter, type ProviderDispatchContext, type ProviderId } from "../../provider-contracts";
|
|
27
|
-
import type { ModelDefinition } from "../../provider-model-definition";
|
|
28
|
-
import { parseQoderOAuthState } from "./qoder-oauth";
|
|
29
|
-
import { getQoderVersion } from "../../operations/client-versions";
|
|
30
|
-
import { abortGatewayError, createUpstreamDeadlineLifecycle } from "../../operations/upstream-deadline";
|
|
31
|
-
import { qoderMachineOs } from "../../operations/cli-platform";
|
|
32
|
-
import {
|
|
33
|
-
import { QODER_CN_MODEL_CONFIGS, QODER_INTL_MODEL_CONFIGS, QODER_INTL_MODELS } from "./qoder-catalog";
|
|
34
|
-
import { isQoderVirtualModel, qoderDispatchModelId } from "./qoder-virtual";
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
id:
|
|
122
|
-
display_name:
|
|
123
|
-
max_input_tokens:
|
|
124
|
-
|
|
125
|
-
is_vl:
|
|
126
|
-
is_reasoning:
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
"
|
|
292
|
-
"
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
"
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
if (
|
|
386
|
-
let
|
|
387
|
-
try {
|
|
388
|
-
|
|
389
|
-
} catch {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
:
|
|
485
|
-
|
|
486
|
-
:
|
|
487
|
-
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
function
|
|
506
|
-
const
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
)
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
*
|
|
562
|
-
*
|
|
563
|
-
*
|
|
564
|
-
*
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
)
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
*
|
|
592
|
-
*
|
|
593
|
-
*
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
if (
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
return
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
/**
|
|
612
|
-
*
|
|
613
|
-
*
|
|
614
|
-
* The
|
|
615
|
-
*
|
|
616
|
-
*
|
|
617
|
-
*
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
*
|
|
625
|
-
*
|
|
626
|
-
*
|
|
627
|
-
*
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
const
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
//
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
if (
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
for (const
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
)
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
if (
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
}
|
|
1300
|
-
//
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
arguments_delta: call.arguments,
|
|
1377
|
-
} as CanonicalEvent;
|
|
1378
|
-
}
|
|
1379
|
-
// The INTL leak marker is plain text, not DSML, so it flowed through to
|
|
1380
|
-
// `plainBuf` instead of the buffered `buf`. Recover it the same way: a
|
|
1381
|
-
// model that answers with `[assistant 请求调用工具] [{"name":…,"arguments":…}]`
|
|
1382
|
-
// intended the tool to run, and left alone the client sees the JSON as
|
|
1383
|
-
// content while nothing executes. Only when no other channel already
|
|
1384
|
-
// claimed the turn.
|
|
1385
|
-
if (!hasEmittedToolCall) {
|
|
1386
|
-
// Held-back marker block first: it was withheld from the client, so a
|
|
1387
|
-
// recovery here never duplicates text already shown. A valid echo becomes
|
|
1388
|
-
// tool calls; anything else is released as ordinary prose.
|
|
1389
|
-
if (leakBuf.length > 0) {
|
|
1390
|
-
const leaked = parseLeakedToolCalls(leakBuf);
|
|
1391
|
-
if (leaked.calls.length > 0) {
|
|
1392
|
-
for (const call of leaked.calls) {
|
|
1393
|
-
hasEmittedToolCall = true;
|
|
1394
|
-
yield {
|
|
1395
|
-
type: "tool_call_delta",
|
|
1396
|
-
sequence_number: seq++,
|
|
1397
|
-
call_id: call.id,
|
|
1398
|
-
name: call.name,
|
|
1399
|
-
arguments_delta: call.arguments,
|
|
1400
|
-
} as CanonicalEvent;
|
|
1401
|
-
}
|
|
1402
|
-
finishReason = "tool_calls";
|
|
1403
|
-
} else if (leaked.clean.length > 0) {
|
|
1404
|
-
yield {
|
|
1405
|
-
type: "content_delta",
|
|
1406
|
-
sequence_number: seq++,
|
|
1407
|
-
content: { kind: "text", text: leaked.clean },
|
|
1408
|
-
} as CanonicalEvent;
|
|
1409
|
-
}
|
|
1410
|
-
}
|
|
1411
|
-
if (plainBuf.length > 0 && !hasEmittedToolCall) {
|
|
1412
|
-
const leaked = parseLeakedToolCalls(plainBuf);
|
|
1413
|
-
if (leaked.calls.length > 0) {
|
|
1414
|
-
for (const call of leaked.calls) {
|
|
1415
|
-
hasEmittedToolCall = true;
|
|
1416
|
-
yield {
|
|
1417
|
-
type: "tool_call_delta",
|
|
1418
|
-
sequence_number: seq++,
|
|
1419
|
-
call_id: call.id,
|
|
1420
|
-
name: call.name,
|
|
1421
|
-
arguments_delta: call.arguments,
|
|
1422
|
-
} as CanonicalEvent;
|
|
1423
|
-
}
|
|
1424
|
-
finishReason = "tool_calls";
|
|
1425
|
-
}
|
|
1426
|
-
}
|
|
1427
|
-
}
|
|
1428
|
-
// A recovered call outranks what the upstream reported. It says `stop`
|
|
1429
|
-
// because the markup never became a real tool call on its side, so leaving it
|
|
1430
|
-
// alone ends the turn "finished" with a call the caller never sees run.
|
|
1431
|
-
if (reasonCalls.length > 0) finishReason = "tool_calls";
|
|
1432
|
-
if (tail.length > 0) {
|
|
1433
|
-
yield {
|
|
1434
|
-
type: "content_delta",
|
|
1435
|
-
sequence_number: seq++,
|
|
1436
|
-
content: { kind: "text", text: tail },
|
|
1437
|
-
} as CanonicalEvent;
|
|
1438
|
-
}
|
|
1439
|
-
// If no terminal arrived, synthesize one (prevents truncation error)
|
|
1440
|
-
const stopReason = mapQoderFinishReason(finishReason);
|
|
1441
|
-
const _providerStop = typeof finishReason === "string" ? finishReason : undefined;
|
|
1442
|
-
void hasTerminal;
|
|
1443
|
-
yield {
|
|
1444
|
-
type: "terminal",
|
|
1445
|
-
sequence_number: seq++,
|
|
1446
|
-
state: "complete",
|
|
1447
|
-
...(stopReason === undefined ? {} : { stop_reason: stopReason }),
|
|
1448
|
-
...(_providerStop === undefined ? {} : { provider_stop_reason: _providerStop }),
|
|
1449
|
-
usage: usageFromProvider(rawUsage),
|
|
1450
|
-
} as CanonicalEvent;
|
|
1451
|
-
}
|
|
1452
|
-
|
|
1453
|
-
// Model catalog — maps QODER_MODEL_CONFIGS (provider) to Cartethyia ModelDefinition
|
|
1454
|
-
|
|
1455
|
-
/**
|
|
1456
|
-
* The intl catalog this provider publishes.
|
|
1457
|
-
*
|
|
1458
|
-
* `QODER_MODEL_CONFIGS` is not the source here: it freezes most rows at 180k
|
|
1459
|
-
* while the upstream reports 1M for the same keys. That number is not
|
|
1460
|
-
* cosmetic: it is sent upstream as `model_config.max_input_tokens`, so a stale
|
|
1461
|
-
* low value tells Qoder the conversation window is small and makes it behave
|
|
1462
|
-
* that way. Both the published catalog and the dispatched request come from the
|
|
1463
|
-
* realm's roster, which carries the upstream's own numbers.
|
|
1464
|
-
*/
|
|
1465
|
-
export const QODER_MODELS: readonly ModelDefinition[] = QODER_INTL_MODELS;
|
|
1466
|
-
|
|
1467
|
-
// ProviderAdapter — manual dispatch preserving COSY encryption + envelope logic
|
|
1468
|
-
|
|
1469
|
-
interface QoderAdapterOptions {
|
|
1470
|
-
readonly fetch?: typeof fetch;
|
|
1471
|
-
/** Which deployment this adapter dispatches to. Defaults to intl. */
|
|
1472
|
-
readonly realm?: QoderRealm;
|
|
1473
|
-
/** Bundled provider id, so the adapter's own id matches the route's. */
|
|
1474
|
-
readonly providerId?: string;
|
|
1475
|
-
}
|
|
1476
|
-
|
|
1477
|
-
class QoderAdapter implements ProviderAdapter {
|
|
1478
|
-
readonly provider_id: ProviderId;
|
|
1479
|
-
private readonly realm: QoderRealm;
|
|
1480
|
-
private readonly fetchFn: typeof fetch;
|
|
1481
|
-
|
|
1482
|
-
constructor(options: QoderAdapterOptions = {}) {
|
|
1483
|
-
this.provider_id = (options.providerId ?? QODER_PROVIDER_ID) as ProviderId;
|
|
1484
|
-
this.realm = options.realm ?? "intl";
|
|
1485
|
-
this.fetchFn = options.fetch ?? globalThis.fetch;
|
|
1486
|
-
}
|
|
1487
|
-
|
|
1488
|
-
/**
|
|
1489
|
-
* The static roster for this adapter's realm.
|
|
1490
|
-
*
|
|
1491
|
-
* A single merged table advertised CN-only keys to global accounts and vice
|
|
1492
|
-
* versa, so a request was routed to a deployment that refuses it.
|
|
1493
|
-
*/
|
|
1494
|
-
#modelConfigs(): Record<string, QoderModelConfig> {
|
|
1495
|
-
return (this.realm === "cn" ? QODER_CN_MODEL_CONFIGS : QODER_INTL_MODEL_CONFIGS) as Record<
|
|
1496
|
-
string,
|
|
1497
|
-
QoderModelConfig
|
|
1498
|
-
>;
|
|
1499
|
-
}
|
|
1500
|
-
|
|
1501
|
-
async *dispatch(
|
|
1502
|
-
request: CanonicalRequest,
|
|
1503
|
-
candidate: ProviderDispatchTarget,
|
|
1504
|
-
context: ProviderDispatchContext,
|
|
1505
|
-
): AsyncIterable<CanonicalEvent> {
|
|
1506
|
-
if (candidate.wire_family !== "chat") {
|
|
1507
|
-
throw new GatewayError("capability_unsupported", 400, `qoder supports chat only, got ${candidate.wire_family}`);
|
|
1508
|
-
}
|
|
1509
|
-
const secret = readCredentialSecret(context.credential, "Qoder credential is required");
|
|
1510
|
-
|
|
1511
|
-
/**
|
|
1512
|
-
* A caller-facing virtual name is translated to its roster key here.
|
|
1513
|
-
*
|
|
1514
|
-
* The translation has to happen before the config lookup and before the
|
|
1515
|
-
* request body: the body carries the roster key, and looking up the
|
|
1516
|
-
* virtual id in the config table would fall through to the synthetic
|
|
1517
|
-
* default and silently dispatch under-declared capabilities.
|
|
1518
|
-
*/
|
|
1519
|
-
const requestedModelId = candidate.model_id || request.model;
|
|
1520
|
-
if (isQoderVirtualModel(requestedModelId) && !this.#modelConfigs()[qoderDispatchModelId(requestedModelId)]) {
|
|
1521
|
-
throw new GatewayError(
|
|
1522
|
-
"model_not_found",
|
|
1523
|
-
404,
|
|
1524
|
-
`Qoder virtual model ${requestedModelId} resolves to a roster key this realm does not serve`,
|
|
1525
|
-
{ virtual_model: requestedModelId, target: qoderDispatchModelId(requestedModelId) },
|
|
1526
|
-
"upstream",
|
|
1527
|
-
);
|
|
1528
|
-
}
|
|
1529
|
-
const modelId = qoderDispatchModelId(requestedModelId);
|
|
1530
|
-
const modelConfig = this.#modelConfigs()[modelId] ?? {
|
|
1531
|
-
id: modelId,
|
|
1532
|
-
display_name: modelId,
|
|
1533
|
-
max_input_tokens: 180_000,
|
|
1534
|
-
is_vl: true,
|
|
1535
|
-
is_reasoning: false,
|
|
1536
|
-
};
|
|
1537
|
-
|
|
1538
|
-
const lifecycle = createUpstreamDeadlineLifecycle(context);
|
|
1539
|
-
|
|
1540
|
-
// Outbound fetch respects SSRF binding when provided
|
|
1541
|
-
const outboundFetch: typeof fetch = (context.outbound_fetch as unknown as typeof fetch) ?? this.fetchFn;
|
|
1542
|
-
|
|
1543
|
-
const fetcher: QoderFetcher = (url, init) =>
|
|
1544
|
-
outboundFetch(url, { ...init, signal: lifecycle.signal } as RequestInit) as Promise<Response>;
|
|
1545
|
-
|
|
1546
|
-
try {
|
|
1547
|
-
const version = getQoderVersion();
|
|
1548
|
-
// Dual mode like Cline: a pasted PAT is exchanged for a job token per
|
|
1549
|
-
// request, while an OAuth device token is used directly with the
|
|
1550
|
-
// identity the login persisted.
|
|
1551
|
-
const auth = isQoderPat(secret)
|
|
1552
|
-
? await exchangeQoderPat(secret, lifecycle.signal, fetcher, version, this.realm)
|
|
1553
|
-
: qoderOAuthAuth(secret, context.credential.auth_state, context.credential.account_id);
|
|
1554
|
-
const qoderBody = buildQoderRequest(modelId, request, modelConfig, auth, this.realm);
|
|
1555
|
-
// A full URL from the route wins, but the fallback is this realm's
|
|
1556
|
-
// gateway — not the intl host hardcoded into `MODERN_PROFILE`. Dispatching
|
|
1557
|
-
// a CN credential to `*.qoder.sh` is refused outright.
|
|
1558
|
-
const url =
|
|
1559
|
-
candidate.endpoint_path && candidate.endpoint_path.startsWith("http")
|
|
1560
|
-
? candidate.endpoint_path
|
|
1561
|
-
: `${qoderGatewayCandidates(this.realm)[0] ?? ""}${QODER_CHAT_PATH}`;
|
|
1562
|
-
|
|
1563
|
-
const response = await callQoder(url, qoderBody, modelId, auth, lifecycle.signal, fetcher, version);
|
|
1564
|
-
if (!response.ok) throw await mapUpstreamHttpError(response, "qoder");
|
|
1565
|
-
if (!response.body) throw new GatewayError("platform_unavailable", 502, "Qoder returned empty body", {}, "upstream");
|
|
1566
|
-
// Headers arrived: the pre-stream deadline has served its purpose.
|
|
1567
|
-
// From here the gateway stall/first-chunk watchdog (propagated via
|
|
1568
|
-
// context.abort_signal) governs the body. Keeping this timer armed
|
|
1569
|
-
// would kill healthy long streams at the stale pre-stream deadline.
|
|
1570
|
-
lifecycle.release();
|
|
1571
|
-
yield* qoderBodyToCanonicalEvents(response.body as ReadableStream<Uint8Array>, lifecycle.signal, request.model);
|
|
1572
|
-
} catch (err: unknown) {
|
|
1573
|
-
const abortError = abortGatewayError(lifecycle, err, context.abort_signal);
|
|
1574
|
-
if (abortError) throw abortError;
|
|
1575
|
-
throw err;
|
|
1576
|
-
} finally {
|
|
1577
|
-
lifecycle.release();
|
|
1578
|
-
}
|
|
1579
|
-
}
|
|
1580
|
-
}
|
|
1581
|
-
|
|
1582
|
-
export function createQoderAdapter(options: QoderAdapterOptions = {}): ProviderAdapter {
|
|
1583
|
-
return new QoderAdapter(options);
|
|
1584
|
-
}
|
|
1585
|
-
|
|
1586
|
-
/**
|
|
1587
|
-
* Adapter for the China deployment.
|
|
1588
|
-
*
|
|
1589
|
-
* Same wire, different gateway host and roster — see `qoder-realm.ts`.
|
|
1590
|
-
*/
|
|
1591
|
-
export function createQoderCnAdapter(options: QoderAdapterOptions = {}): ProviderAdapter {
|
|
1592
|
-
return new QoderAdapter({ ...options, realm: "cn", providerId: "qodercn" });
|
|
1593
|
-
}
|
|
1594
|
-
|
|
1595
|
-
/**
|
|
1596
|
-
* A COSY signer for one already-resolved account identity.
|
|
1597
|
-
*
|
|
1598
|
-
* Discovery needs signed headers but lives in `qoder-catalog.ts`, which cannot
|
|
1599
|
-
* import this module (the adapter imports its rosters — that would be a cycle).
|
|
1600
|
-
* Exporting the signer keeps one signing implementation and lets discovery
|
|
1601
|
-
* stay pure and injectable.
|
|
1602
|
-
*/
|
|
1603
|
-
export function qoderSignerFor(
|
|
1604
|
-
auth: QoderAuth,
|
|
1605
|
-
version = getQoderVersion(),
|
|
1606
|
-
): (url: string, body: string) => Record<string, string> {
|
|
1607
|
-
return (url: string, body: string): Record<string, string> =>
|
|
1608
|
-
qoderBuildCosyHeaders(Buffer.from(body, "latin1"), url, auth, version);
|
|
1609
|
-
}
|
|
1610
|
-
|
|
1611
|
-
/** Qoder's own base64 alphabet transform, for a request body. */
|
|
1612
|
-
export function qoderEncodeBody(plaintext: Uint8Array | string): string {
|
|
1613
|
-
const text = typeof plaintext === "string" ? plaintext : new TextDecoder().decode(plaintext);
|
|
1614
|
-
return Buffer.from(transformQoderBody(text)).toString("latin1");
|
|
1615
|
-
}
|
|
1616
|
-
|
|
1617
|
-
/**
|
|
1618
|
-
* Resolves the dispatch identity for one Qoder credential.
|
|
1619
|
-
*
|
|
1620
|
-
* Shared by the adapter and by discovery: both need the same `QoderAuth` the
|
|
1621
|
-
* COSY signature is built from, and duplicating the PAT-vs-OAuth branch would
|
|
1622
|
-
* let the two drift.
|
|
1623
|
-
*/
|
|
1624
|
-
export async function resolveQoderAuth(input: {
|
|
1625
|
-
readonly secret: string;
|
|
1626
|
-
readonly authState: unknown;
|
|
1627
|
-
readonly signal: AbortSignal;
|
|
1628
|
-
readonly fetcher: (url: string, init: RequestInit) => Promise<Response>;
|
|
1629
|
-
/** Which deployment mints the job token; a PAT is realm-bound. */
|
|
1630
|
-
readonly realm?: QoderRealm;
|
|
1631
|
-
}): Promise<QoderAuth> {
|
|
1632
|
-
const realm = input.realm ?? "intl";
|
|
1633
|
-
if (isQoderPat(input.secret)) {
|
|
1634
|
-
return await exchangeQoderPat(input.secret, input.signal, input.fetcher, getQoderVersion(), realm);
|
|
1635
|
-
}
|
|
1636
|
-
// Discovery is handed only the secret and the persisted state, so the
|
|
1637
|
-
// account id is read from that state — the same place `qoderOAuthAuth` reads
|
|
1638
|
-
// it — rather than being a separate field callers can pass wrongly.
|
|
1639
|
-
const state = parseQoderOAuthState(input.authState);
|
|
1640
|
-
return qoderOAuthAuth(input.secret, input.authState, state?.userId);
|
|
1641
|
-
}
|
|
1
|
+
// Factory-blocked (Phase C4): PAT-to-jobToken session handshake, COSY
|
|
2
|
+
// AES/RSA request signing, alphabet-scrambled binary body, enveloped SSE
|
|
3
|
+
// frames — session/crypto protocol outside factory reach; stays bespoke.
|
|
4
|
+
import { constants, createCipheriv, createHash, publicEncrypt, randomUUID } from "node:crypto";
|
|
5
|
+
import {
|
|
6
|
+
classifyUpstreamError,
|
|
7
|
+
extractUpstreamMessage,
|
|
8
|
+
isUpstreamQueued,
|
|
9
|
+
mapUpstreamHttpError,
|
|
10
|
+
queueRetryAfterMs,
|
|
11
|
+
upstreamErrorIdentifier,
|
|
12
|
+
} from "../../../transport/failure-policy";
|
|
13
|
+
import { GatewayError } from "../../../protocol/gateway-error";
|
|
14
|
+
import { resolveImageSource } from "../../../protocol/codecs";
|
|
15
|
+
import type {
|
|
16
|
+
CanonicalEvent,
|
|
17
|
+
CanonicalRequest,
|
|
18
|
+
CanonicalStopReason,
|
|
19
|
+
ContentPart,
|
|
20
|
+
ToolChoice,
|
|
21
|
+
ToolDefinition,
|
|
22
|
+
} from "../../../protocol/canonical-model";
|
|
23
|
+
import { toolResultParts, toolCallParts } from "../../../protocol/canonical-model";
|
|
24
|
+
import { decodeSseEvents } from "../../../protocol/sse-decoder";
|
|
25
|
+
import { usageFromProvider, readReasoningText } from "../../provider-usage";
|
|
26
|
+
import { readCredentialSecret, type ProviderDispatchTarget, type ProviderAdapter, type ProviderDispatchContext, type ProviderId } from "../../provider-contracts";
|
|
27
|
+
import type { ModelDefinition } from "../../provider-model-definition";
|
|
28
|
+
import { parseQoderOAuthState } from "./qoder-oauth";
|
|
29
|
+
import { getQoderVersion } from "../../operations/client-versions";
|
|
30
|
+
import { abortGatewayError, createUpstreamDeadlineLifecycle } from "../../operations/upstream-deadline";
|
|
31
|
+
import { qoderMachineOs } from "../../operations/cli-platform";
|
|
32
|
+
import { qoderInferenceCandidates, QODER_CHAT_PATH, QODER_REALMS, type QoderRealm } from "./qoder-realm";
|
|
33
|
+
import { QODER_CN_MODEL_CONFIGS, QODER_INTL_MODEL_CONFIGS, QODER_INTL_MODELS } from "./qoder-catalog";
|
|
34
|
+
import { isQoderVirtualModel, qoderDispatchModelId } from "./qoder-virtual";
|
|
35
|
+
import {
|
|
36
|
+
LEAK_MARKER_ANY_RE,
|
|
37
|
+
LEAK_MARKER_LITERALS,
|
|
38
|
+
parseQoderTextToolCalls,
|
|
39
|
+
isQoderEchoAttempt,
|
|
40
|
+
} from "./qoder-text-tools";
|
|
41
|
+
|
|
42
|
+
/** How long a trailing partial marker may be before it is held for the next frame. */
|
|
43
|
+
const LEAK_HOLD_WINDOW = 24;
|
|
44
|
+
/** Fail-open caps so an endless lookalike cannot hold text forever. */
|
|
45
|
+
const LEAK_HOLD_MAX_CHARS = 32_768;
|
|
46
|
+
const LEAK_HOLD_MAX_FRAMES = 200;
|
|
47
|
+
|
|
48
|
+
const BAD_ECHO_MESSAGE =
|
|
49
|
+
"Qoder returned an unparseable tool-call echo; no tools were emitted";
|
|
50
|
+
|
|
51
|
+
function badEchoError(): GatewayError {
|
|
52
|
+
return new GatewayError(
|
|
53
|
+
"platform_unavailable",
|
|
54
|
+
502,
|
|
55
|
+
BAD_ECHO_MESSAGE,
|
|
56
|
+
{ providerId: QODER_PROVIDER_ID },
|
|
57
|
+
"upstream",
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** True when `suffix` is a strict prefix of some marker literal. */
|
|
62
|
+
function isLeakMarkerPrefix(suffix: string): boolean {
|
|
63
|
+
if (suffix.length === 0 || suffix.length >= LEAK_HOLD_WINDOW) return false;
|
|
64
|
+
return LEAK_MARKER_LITERALS.some((literal) => literal.startsWith(suffix));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* True when `text` is still plausibly the INTL marker echo block (marker +
|
|
69
|
+
* JSON array/object, or a tool-style tag). Used to keep holding across frames;
|
|
70
|
+
* false means it is ordinary prose and can be released.
|
|
71
|
+
*/
|
|
72
|
+
function qoderStillLeakCandidate(text: string): boolean {
|
|
73
|
+
if (text.length === 0) return true;
|
|
74
|
+
const marker = LEAK_MARKER_ANY_RE.exec(text);
|
|
75
|
+
if (!marker) {
|
|
76
|
+
return isLeakMarkerPrefix(text) || LEAK_MARKER_ANY_RE.test(text);
|
|
77
|
+
}
|
|
78
|
+
const rest = text.slice(marker.index + marker[0].length).trim();
|
|
79
|
+
if (rest.length === 0) return true;
|
|
80
|
+
if (rest.startsWith("[") || rest.startsWith("{")) return true;
|
|
81
|
+
if (/^<[a-zA-Z0-9_.:-]+\b/.test(rest)) return true;
|
|
82
|
+
// Angle-wrapped JSON: `<{` / `<[` — candidate until a closing `>` appears.
|
|
83
|
+
if (rest.startsWith("<{") || rest.startsWith("<[")) {
|
|
84
|
+
return !rest.includes(">") || rest.endsWith(">");
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface QoderModeProfile {
|
|
90
|
+
readonly chatUrl: string;
|
|
91
|
+
readonly businessProduct: string;
|
|
92
|
+
readonly businessType: string;
|
|
93
|
+
readonly businessVersion: string;
|
|
94
|
+
readonly cosyScene: string;
|
|
95
|
+
readonly mirrorTopLevelSystem: boolean;
|
|
96
|
+
readonly sendBusinessHeaders: boolean;
|
|
97
|
+
readonly sendModelSourceHeaders: boolean;
|
|
98
|
+
readonly emptyAliyunUserType: boolean;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export const MODERN_PROFILE: QoderModeProfile = {
|
|
102
|
+
chatUrl: "https://api2.qoder.sh/algo/api/v2/service/pro/sse/agent_chat_generation?FetchKeys=llm_model_result&AgentId=agent_common&Encode=1",
|
|
103
|
+
businessProduct: "cli",
|
|
104
|
+
businessType: "agent",
|
|
105
|
+
businessVersion: "1.0.22",
|
|
106
|
+
cosyScene: "assistant",
|
|
107
|
+
mirrorTopLevelSystem: true,
|
|
108
|
+
sendBusinessHeaders: true,
|
|
109
|
+
sendModelSourceHeaders: true,
|
|
110
|
+
emptyAliyunUserType: true,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const QODER_PROVIDER_ID = "qoder" as const;
|
|
114
|
+
|
|
115
|
+
export const QODER_MODEL_CONFIGS: Record<
|
|
116
|
+
string,
|
|
117
|
+
{ id: string; display_name: string; max_input_tokens: number; max_output_tokens?: number; is_vl: boolean; is_reasoning: boolean; source?: string }
|
|
118
|
+
> = {
|
|
119
|
+
auto: { id: "auto", display_name: "Auto", max_input_tokens: 180000, is_vl: true, is_reasoning: false },
|
|
120
|
+
ultimate: { id: "ultimate", display_name: "Ultimate", max_input_tokens: 180000, is_vl: true, is_reasoning: true },
|
|
121
|
+
performance: { id: "performance", display_name: "Performance", max_input_tokens: 272000, is_vl: true, is_reasoning: false },
|
|
122
|
+
efficient: { id: "efficient", display_name: "Efficient", max_input_tokens: 180000, is_vl: true, is_reasoning: false },
|
|
123
|
+
lite: { id: "lite", display_name: "Lite", max_input_tokens: 180000, is_vl: false, is_reasoning: true },
|
|
124
|
+
qmodel: { id: "qmodel", display_name: "Qwen 3.6 Plus", max_input_tokens: 180000, is_vl: true, is_reasoning: false },
|
|
125
|
+
qmodel_latest: { id: "qmodel_latest", display_name: "Qwen 3.7 Max", max_input_tokens: 1000000, is_vl: true, is_reasoning: false },
|
|
126
|
+
qmodel_preview: { id: "qmodel_preview", display_name: "Qwen 3.8 Max", max_input_tokens: 1000000, is_vl: true, is_reasoning: false },
|
|
127
|
+
dmodel: { id: "dmodel", display_name: "DeepSeek V4 Pro", max_input_tokens: 180000, is_vl: true, is_reasoning: true },
|
|
128
|
+
dfmodel: { id: "dfmodel", display_name: "DeepSeek V4 Flash", max_input_tokens: 180000, is_vl: true, is_reasoning: true },
|
|
129
|
+
gm51model: { id: "gm51model", display_name: "GLM 5.1", max_input_tokens: 180000, is_vl: true, is_reasoning: true },
|
|
130
|
+
kmodel: { id: "kmodel", display_name: "Kimi K2.6", max_input_tokens: 256000, is_vl: true, is_reasoning: false },
|
|
131
|
+
mmodel: { id: "mmodel", display_name: "MiniMax M2.7", max_input_tokens: 180000, is_vl: true, is_reasoning: false },
|
|
132
|
+
kmodel_latest: { id: "kmodel_latest", display_name: "Kimi K2.7 Latest", max_input_tokens: 256000, is_vl: true, is_reasoning: false },
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const QODER_APPCODE = "cosy";
|
|
136
|
+
const QODER_SIG_SECRET = "d2FyLCB3YXIgbmV2ZXIgY2hhbmdlcw==";
|
|
137
|
+
/**
|
|
138
|
+
* Where a PAT is exchanged for a short-lived job token, per realm.
|
|
139
|
+
*
|
|
140
|
+
* The job token is realm-bound: one minted on the global host answers "Login
|
|
141
|
+
* expired" on the China gateway. Exchanging against the wrong host therefore
|
|
142
|
+
* looks like a dead credential rather than a routing bug, which is why the
|
|
143
|
+
* URL is derived from the realm table instead of being one constant.
|
|
144
|
+
*
|
|
145
|
+
* `intl` keeps its historical `center.qoder.sh` v3 endpoint. `cn` uses the
|
|
146
|
+
* openapi host the donor's `PATH_JOB_EXCHANGE` describes; if that path is
|
|
147
|
+
* ever wrong for CN, the failure surfaces as a 401/403 on exchange, not as a
|
|
148
|
+
* silently misrouted request.
|
|
149
|
+
*/
|
|
150
|
+
const QODER_JOB_TOKEN_URL = "https://center.qoder.sh/algo/api/v3/user/jobToken?Encode=1";
|
|
151
|
+
function qoderJobTokenUrl(realm: QoderRealm): string {
|
|
152
|
+
return realm === "cn"
|
|
153
|
+
? `${QODER_REALMS.cn.openapi}/api/v1/jobToken/exchange`
|
|
154
|
+
: QODER_JOB_TOKEN_URL;
|
|
155
|
+
}
|
|
156
|
+
const QODER_STANDARD_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
157
|
+
const QODER_ALPHABET = "_doRTgHZBKcGVjlvpC,@aFSx#DPuNJme&i*MzLOEn)sUrthbf%Y^w.(kIQyXqWA!";
|
|
158
|
+
const QODER_RSA_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
|
|
159
|
+
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDA8iMH5c02LilrsERw9t6Pv5Nc
|
|
160
|
+
4k6Pz1EaDicBMpdpxKduSZu5OANqUq8er4GM95omAGIOPOh+Nx0spthYA2BqGz+l
|
|
161
|
+
6HRkPJ7S236FZz73In/KVuLnwI8JJ2CbuJap8kvheCCZpmAWpb/cPx/3Vr/J6I17
|
|
162
|
+
XcW+ML9FoCI6AOvOzwIDAQAB
|
|
163
|
+
-----END PUBLIC KEY-----`;
|
|
164
|
+
|
|
165
|
+
export interface QoderAuth {
|
|
166
|
+
userId: string;
|
|
167
|
+
userName: string;
|
|
168
|
+
userType: string;
|
|
169
|
+
securityOauthToken: string;
|
|
170
|
+
refreshToken: string;
|
|
171
|
+
machineId: string;
|
|
172
|
+
}
|
|
173
|
+
interface QoderModelConfig {
|
|
174
|
+
id: string;
|
|
175
|
+
display_name: string;
|
|
176
|
+
max_input_tokens: number;
|
|
177
|
+
max_output_tokens?: number;
|
|
178
|
+
is_vl: boolean;
|
|
179
|
+
is_reasoning: boolean;
|
|
180
|
+
source?: string;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function qoderMd5(value: string | Uint8Array): string {
|
|
184
|
+
return createHash("md5").update(value).digest("hex");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Canonical compact JSON used by Qoder's COSY signing: keys sorted
|
|
189
|
+
* lexicographically, no whitespace, null/undefined rendered as empty string.
|
|
190
|
+
* The upstream verifies the signature against this exact serialization, so a
|
|
191
|
+
* payload built with plain `JSON.stringify` (insertion order) fails with
|
|
192
|
+
* `code 101 / Signature invalid`.
|
|
193
|
+
*/
|
|
194
|
+
function qoderJsonSortedCompact(mapping: Record<string, unknown>): string {
|
|
195
|
+
const parts: string[] = [];
|
|
196
|
+
for (const key of Object.keys(mapping).sort()) {
|
|
197
|
+
const value = mapping[key] === null || mapping[key] === undefined ? "" : mapping[key];
|
|
198
|
+
parts.push(`${JSON.stringify(key)}:${JSON.stringify(value)}`);
|
|
199
|
+
}
|
|
200
|
+
return `{${parts.join(",")}}`;
|
|
201
|
+
}
|
|
202
|
+
function qoderPathSignature(url: string): string {
|
|
203
|
+
const path = new URL(url).pathname;
|
|
204
|
+
return path.startsWith("/algo") ? path.slice(5) : path;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const QODER_MACHINE_ID_TTL_MS = 60 * 60 * 1000;
|
|
208
|
+
const QODER_MAX_MACHINE_IDS = 1_024;
|
|
209
|
+
const qoderMachineIds = new Map<string, { id: string; expiresAt: number }>();
|
|
210
|
+
/**
|
|
211
|
+
* A PAT (`pt-...`) must be exchanged for a short-lived job token before it
|
|
212
|
+
* can sign COSY requests. Device tokens (`dt-...`) from OAuth login and job
|
|
213
|
+
* tokens (`jt-...`) are used directly.
|
|
214
|
+
*/
|
|
215
|
+
export function isQoderPat(secret: string): boolean {
|
|
216
|
+
return secret.startsWith("pt-");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Stable per-account machine id for OAuth dispatches. Derived from the
|
|
221
|
+
* account id (not minted per request) so pooled accounts keep one device
|
|
222
|
+
* fingerprint across restarts instead of looking like a new device every
|
|
223
|
+
* time, while staying distinct per account.
|
|
224
|
+
*/
|
|
225
|
+
export function qoderMachineIdForAccount(accountId: string): string {
|
|
226
|
+
const hex = createHash("sha256").update(`qoder-machine-v1:${accountId}`).digest("hex");
|
|
227
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Builds dispatch auth from an OAuth device token. The token is used
|
|
232
|
+
* directly as the COSY identity's oauth token; the user id and machine id
|
|
233
|
+
* come from the `auth_state` the login persisted. Fails closed with a
|
|
234
|
+
* reconnect prompt when the identity is absent (e.g. an account created
|
|
235
|
+
* before `auth_state` existed, or an import that dropped the field) instead
|
|
236
|
+
* of signing with a guessed id. The message names the missing field so an
|
|
237
|
+
* operator can fix the export instead of re-logging blind.
|
|
238
|
+
*/
|
|
239
|
+
export function qoderOAuthAuth(
|
|
240
|
+
secret: string,
|
|
241
|
+
authState: unknown,
|
|
242
|
+
accountId: string | undefined,
|
|
243
|
+
): QoderAuth {
|
|
244
|
+
const state = parseQoderOAuthState(authState);
|
|
245
|
+
if (state === undefined) {
|
|
246
|
+
throw new GatewayError(
|
|
247
|
+
"authentication_failed",
|
|
248
|
+
401,
|
|
249
|
+
"Qoder OAuth account is missing its user identity (auth_state.userId/machineId absent or empty — re-login, or re-export with authState included); sign in again",
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
userId: state.userId,
|
|
254
|
+
userName: "",
|
|
255
|
+
userType: "personal_standard",
|
|
256
|
+
securityOauthToken: secret,
|
|
257
|
+
refreshToken: "",
|
|
258
|
+
machineId: state.machineId || (accountId ? qoderMachineIdForAccount(accountId) : randomUUID()),
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function qoderMachineIdFromPat(pat: string): string {
|
|
263
|
+
const key = createHash("sha256").update(pat).digest("hex");
|
|
264
|
+
const now = Date.now();
|
|
265
|
+
const existing = qoderMachineIds.get(key);
|
|
266
|
+
if (existing && existing.expiresAt > now) return existing.id;
|
|
267
|
+
const id = randomUUID();
|
|
268
|
+
qoderMachineIds.delete(key);
|
|
269
|
+
qoderMachineIds.set(key, { id, expiresAt: now + QODER_MACHINE_ID_TTL_MS });
|
|
270
|
+
for (const [candidate, value] of qoderMachineIds) {
|
|
271
|
+
if (value.expiresAt <= now) qoderMachineIds.delete(candidate);
|
|
272
|
+
}
|
|
273
|
+
while (qoderMachineIds.size > QODER_MAX_MACHINE_IDS) {
|
|
274
|
+
const oldest = qoderMachineIds.keys().next();
|
|
275
|
+
if (oldest.done) break;
|
|
276
|
+
qoderMachineIds.delete(oldest.value);
|
|
277
|
+
}
|
|
278
|
+
return id;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function qoderStaticHeaders(machineId: string, version = getQoderVersion()): Record<string, string> {
|
|
282
|
+
const date = new Date().toUTCString();
|
|
283
|
+
return {
|
|
284
|
+
"cosy-machinetoken": machineId,
|
|
285
|
+
"cosy-machinetype": "5",
|
|
286
|
+
"cosy-machineid": machineId,
|
|
287
|
+
"cosy-clienttype": "5",
|
|
288
|
+
"cosy-version": version,
|
|
289
|
+
"login-version": "v2",
|
|
290
|
+
appcode: QODER_APPCODE,
|
|
291
|
+
accept: "application/json",
|
|
292
|
+
"accept-encoding": "identity",
|
|
293
|
+
date,
|
|
294
|
+
signature: qoderMd5(`${QODER_APPCODE}&${QODER_SIG_SECRET}&${date}`),
|
|
295
|
+
"content-type": "application/json",
|
|
296
|
+
"user-agent": "Go-http-client/2.0",
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function transformQoderBody(plaintext: string): Uint8Array {
|
|
301
|
+
const base64 = Buffer.from(plaintext).toString("base64");
|
|
302
|
+
const third = Math.floor(base64.length / 3);
|
|
303
|
+
const reordered = base64.slice(base64.length - third) + base64.slice(third, base64.length - third) + base64.slice(0, third);
|
|
304
|
+
let encoded = "";
|
|
305
|
+
for (const char of reordered) {
|
|
306
|
+
if (char === "=") encoded += "$";
|
|
307
|
+
else encoded += QODER_ALPHABET[QODER_STANDARD_ALPHABET.indexOf(char)] ?? char;
|
|
308
|
+
}
|
|
309
|
+
return Buffer.from(encoded, "latin1");
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function qoderBuildCosyHeaders(
|
|
313
|
+
body: Uint8Array,
|
|
314
|
+
url: string,
|
|
315
|
+
auth: QoderAuth,
|
|
316
|
+
version = getQoderVersion(),
|
|
317
|
+
): Record<string, string> {
|
|
318
|
+
const key = randomUUID().replaceAll("-", "").slice(0, 16);
|
|
319
|
+
const identity = qoderJsonSortedCompact({
|
|
320
|
+
name: auth.userName,
|
|
321
|
+
aid: auth.userId,
|
|
322
|
+
uid: auth.userId,
|
|
323
|
+
yx_uid: "",
|
|
324
|
+
organization_id: "",
|
|
325
|
+
organization_name: "",
|
|
326
|
+
user_type: auth.userType,
|
|
327
|
+
security_oauth_token: auth.securityOauthToken,
|
|
328
|
+
refresh_token: auth.refreshToken,
|
|
329
|
+
});
|
|
330
|
+
const cipher = createCipheriv("aes-128-cbc", Buffer.from(key), Buffer.from(key));
|
|
331
|
+
const info = Buffer.concat([cipher.update(identity), cipher.final()]).toString("base64");
|
|
332
|
+
const cosyKey = publicEncrypt({ key: QODER_RSA_PUBLIC_KEY, padding: constants.RSA_PKCS1_PADDING }, Buffer.from(key)).toString("base64");
|
|
333
|
+
const payload = Buffer.from(
|
|
334
|
+
qoderJsonSortedCompact({ version: "v1", requestId: randomUUID(), info, cosyVersion: version, ideVersion: "" }),
|
|
335
|
+
).toString("base64");
|
|
336
|
+
const date = String(Math.floor(Date.now() / 1000));
|
|
337
|
+
const signature = qoderMd5(`${payload}\n${cosyKey}\n${date}\n${Buffer.from(body).toString("latin1")}\n${qoderPathSignature(url)}`);
|
|
338
|
+
return {
|
|
339
|
+
authorization: `Bearer COSY.${payload}.${signature}`,
|
|
340
|
+
"cosy-data-policy": "agree",
|
|
341
|
+
"cosy-machinetype": "5",
|
|
342
|
+
"cosy-machineos": qoderMachineOs(),
|
|
343
|
+
"cosy-clienttype": "5",
|
|
344
|
+
"cosy-date": date,
|
|
345
|
+
"cosy-user": auth.userId,
|
|
346
|
+
"cosy-key": cosyKey,
|
|
347
|
+
"cosy-bodyhash": qoderMd5(body),
|
|
348
|
+
"cosy-bodylength": String(body.byteLength),
|
|
349
|
+
"cosy-sigpath": qoderPathSignature(url),
|
|
350
|
+
"cosy-organization-id": "",
|
|
351
|
+
"cosy-organization-tags": "",
|
|
352
|
+
"x-request-id": randomUUID(),
|
|
353
|
+
...(MODERN_PROFILE.sendBusinessHeaders
|
|
354
|
+
? {
|
|
355
|
+
"cosy-business-product": MODERN_PROFILE.businessProduct,
|
|
356
|
+
"cosy-business-type": MODERN_PROFILE.businessType,
|
|
357
|
+
"cosy-business-version": version,
|
|
358
|
+
"cosy-scene": MODERN_PROFILE.cosyScene,
|
|
359
|
+
}
|
|
360
|
+
: {}),
|
|
361
|
+
"cosy-version": version,
|
|
362
|
+
"login-version": "v2",
|
|
363
|
+
appcode: QODER_APPCODE,
|
|
364
|
+
accept: "application/json",
|
|
365
|
+
"accept-encoding": "identity",
|
|
366
|
+
date: new Date().toUTCString(),
|
|
367
|
+
signature: qoderMd5(`${QODER_APPCODE}&${QODER_SIG_SECRET}&${new Date().toUTCString()}`),
|
|
368
|
+
"content-type": "application/json",
|
|
369
|
+
"user-agent": "Go-http-client/2.0",
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
type QoderFetcher = (url: string, init: RequestInit) => Promise<Response>;
|
|
374
|
+
|
|
375
|
+
async function exchangeQoderPat(
|
|
376
|
+
pat: string,
|
|
377
|
+
signal: AbortSignal,
|
|
378
|
+
fetcher: QoderFetcher,
|
|
379
|
+
version = getQoderVersion(),
|
|
380
|
+
realm: QoderRealm = "intl",
|
|
381
|
+
): Promise<QoderAuth> {
|
|
382
|
+
const machineId = qoderMachineIdFromPat(pat);
|
|
383
|
+
const encodedBody = transformQoderBody(JSON.stringify({ payload: JSON.stringify({ personalToken: pat, securityOauthToken: "", refreshToken: "", needRefresh: false, authInfo: {} }), encodeVersion: "1" }));
|
|
384
|
+
const response = await fetcher(qoderJobTokenUrl(realm), { method: "POST", headers: qoderStaticHeaders(machineId, version), body: encodedBody as unknown as BodyInit, signal });
|
|
385
|
+
if (!response.ok) throw await mapUpstreamHttpError(response, "qoder");
|
|
386
|
+
let responseBody: unknown;
|
|
387
|
+
try {
|
|
388
|
+
responseBody = await response.json();
|
|
389
|
+
} catch {
|
|
390
|
+
throw new GatewayError("platform_unavailable", 502, "Qoder PAT exchange returned invalid JSON", {}, "upstream");
|
|
391
|
+
}
|
|
392
|
+
if (typeof responseBody !== "object" || responseBody === null || Array.isArray(responseBody)) {
|
|
393
|
+
throw new GatewayError("platform_unavailable", 502, "Qoder PAT exchange returned unexpected response", {}, "upstream");
|
|
394
|
+
}
|
|
395
|
+
const rec = responseBody as Record<string, unknown>;
|
|
396
|
+
if (typeof rec["id"] !== "string" || typeof rec["securityOauthToken"] !== "string") {
|
|
397
|
+
throw new GatewayError("platform_unavailable", 502, "Qoder PAT exchange returned incomplete credentials", {}, "upstream");
|
|
398
|
+
}
|
|
399
|
+
return {
|
|
400
|
+
userId: rec["id"] as string,
|
|
401
|
+
userName: typeof rec["name"] === "string" ? (rec["name"] as string) : "",
|
|
402
|
+
userType: typeof rec["userType"] === "string" ? (rec["userType"] as string) : "personal_standard",
|
|
403
|
+
securityOauthToken: rec["securityOauthToken"] as string,
|
|
404
|
+
refreshToken: typeof rec["refreshToken"] === "string" ? (rec["refreshToken"] as string) : "",
|
|
405
|
+
machineId,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async function callQoder(
|
|
410
|
+
url: string,
|
|
411
|
+
body: Record<string, unknown>,
|
|
412
|
+
modelId: string,
|
|
413
|
+
auth: QoderAuth,
|
|
414
|
+
signal: AbortSignal,
|
|
415
|
+
fetcher: QoderFetcher,
|
|
416
|
+
version = getQoderVersion(),
|
|
417
|
+
): Promise<Response> {
|
|
418
|
+
const encoded = transformQoderBody(JSON.stringify(body));
|
|
419
|
+
const modelConfig = (body["model_config"] as Record<string, unknown> | null) ?? null;
|
|
420
|
+
return fetcher(url, {
|
|
421
|
+
method: "POST",
|
|
422
|
+
headers: {
|
|
423
|
+
...qoderBuildCosyHeaders(encoded, url, auth, version),
|
|
424
|
+
...(MODERN_PROFILE.sendModelSourceHeaders
|
|
425
|
+
? { "x-model-key": modelId, "x-model-source": typeof modelConfig?.["source"] === "string" ? (modelConfig["source"] as string) : "system" }
|
|
426
|
+
: {}),
|
|
427
|
+
Accept: "text/event-stream",
|
|
428
|
+
"Cache-Control": "no-cache",
|
|
429
|
+
"Accept-Encoding": "identity",
|
|
430
|
+
},
|
|
431
|
+
body: encoded as unknown as BodyInit,
|
|
432
|
+
signal,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* The human-readable text out of a Qoder error envelope.
|
|
438
|
+
*
|
|
439
|
+
* The envelope is Qoder's own wrapper around the stream, and it does not use
|
|
440
|
+
* one field for the message: the text has been seen as a nested `error`
|
|
441
|
+
* object, a bare string `body`, and a top-level `message`/`msg`. The canonical
|
|
442
|
+
* extractor covers the structured shapes; a string body and the top-level
|
|
443
|
+
* fields cover the rest. Whatever survives is bounded the same way an upstream
|
|
444
|
+
* message is, so a verbose envelope cannot blow up the public error line.
|
|
445
|
+
*/
|
|
446
|
+
function qoderEnvelopeMessage(rec: Record<string, unknown>): string {
|
|
447
|
+
const structured = extractUpstreamMessage(rec["body"]);
|
|
448
|
+
if (structured.length > 0) return structured;
|
|
449
|
+
if (typeof rec["body"] === "string" && rec["body"].trim().length > 0 && rec["body"] !== "[DONE]")
|
|
450
|
+
return rec["body"].replace(/[\r\n]+/g, " ").trim().slice(0, 500);
|
|
451
|
+
return extractUpstreamMessage(rec);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function qoderEnvelopeToFrames(data: string): Array<{ event: string | null; data: string }> {
|
|
455
|
+
if (data === "[DONE]") return [{ event: null, data: "[DONE]" }];
|
|
456
|
+
let envelope: unknown;
|
|
457
|
+
try {
|
|
458
|
+
envelope = JSON.parse(data);
|
|
459
|
+
} catch {
|
|
460
|
+
}
|
|
461
|
+
if (typeof envelope !== "object" || envelope === null || Array.isArray(envelope)) return [];
|
|
462
|
+
const rec = envelope as Record<string, unknown>;
|
|
463
|
+
if (rec["statusCodeValue"] !== undefined && rec["statusCodeValue"] !== 200) {
|
|
464
|
+
const status = typeof rec["statusCodeValue"] === "number" ? (rec["statusCodeValue"] as number) : 502;
|
|
465
|
+
// Qoder reports failures inside the stream envelope instead of as HTTP
|
|
466
|
+
// responses, so classify its structured provider code and status through
|
|
467
|
+
// the same gateway error policy as the other dispatch paths.
|
|
468
|
+
const detail = qoderEnvelopeMessage(rec);
|
|
469
|
+
const identifier = upstreamErrorIdentifier(rec["body"]) ?? upstreamErrorIdentifier(rec);
|
|
470
|
+
// Qoder signals a queued request the same way inside the stream as it does
|
|
471
|
+
// on HTTP: `{"code":"10605", "isQueued":true, "retryAfterSeconds":N}` on a
|
|
472
|
+
// non-200 envelope. The status table would read the 403 as a refusal, and
|
|
473
|
+
// the caller would end its turn on a request that was only waiting.
|
|
474
|
+
const queuedWaitMs = isUpstreamQueued(rec["body"]) ?? false;
|
|
475
|
+
const waitMs = queuedWaitMs
|
|
476
|
+
? (queueRetryAfterMs(rec["body"]) ?? undefined)
|
|
477
|
+
: undefined;
|
|
478
|
+
const classification = queuedWaitMs
|
|
479
|
+
? { code: "upstream_queued" as const, status: 503, origin: "upstream" as const }
|
|
480
|
+
: classifyUpstreamError(status, identifier);
|
|
481
|
+
throw new GatewayError(
|
|
482
|
+
classification.code,
|
|
483
|
+
classification.status,
|
|
484
|
+
detail.length > 0 ? detail : `Qoder stream envelope returned HTTP ${status}`,
|
|
485
|
+
{
|
|
486
|
+
providerId: QODER_PROVIDER_ID,
|
|
487
|
+
providerStatus: status,
|
|
488
|
+
...(identifier ? { providerCode: identifier } : {}),
|
|
489
|
+
...(classification.credentialEvidence ? { credentialEvidence: true } : {}),
|
|
490
|
+
...(classification.rateLimitScope
|
|
491
|
+
? { rateLimitScope: classification.rateLimitScope }
|
|
492
|
+
: {}),
|
|
493
|
+
...(waitMs === undefined ? {} : { retryAfterMs: waitMs, cooldownMs: waitMs }),
|
|
494
|
+
...(queuedWaitMs ? { reason: "upstream-queued" } : {}),
|
|
495
|
+
},
|
|
496
|
+
classification.origin,
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
const body = rec["body"];
|
|
500
|
+
if (body === "[DONE]") return [{ event: null, data: "[DONE]" }];
|
|
501
|
+
if (typeof body !== "string") return [];
|
|
502
|
+
return [{ event: null, data: body.replace(/\r?\n/g, "") }];
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function qoderStableHash(prefix: string, ...parts: string[]): string {
|
|
506
|
+
const h = createHash("sha256");
|
|
507
|
+
h.update(prefix);
|
|
508
|
+
for (const p of parts) {
|
|
509
|
+
h.update("\0");
|
|
510
|
+
h.update(p);
|
|
511
|
+
}
|
|
512
|
+
return h.digest("hex").slice(0, 16);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function flattenCanonicalContent(content: readonly ContentPart[]): string {
|
|
516
|
+
return content
|
|
517
|
+
.filter((b) => b.kind === "text" || b.kind === "toolResult")
|
|
518
|
+
.map((b) => {
|
|
519
|
+
if (b.kind === "text") return b.text;
|
|
520
|
+
if (b.kind === "toolResult") return typeof b.content === "string" ? b.content : JSON.stringify(b.content);
|
|
521
|
+
return "";
|
|
522
|
+
})
|
|
523
|
+
.join("\n");
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function qoderStableRecordId(modelId: string, messages: CanonicalRequest["messages"], maxTokens: number): string {
|
|
527
|
+
const h = createHash("sha256");
|
|
528
|
+
h.update("qoder-record\0");
|
|
529
|
+
h.update(modelId);
|
|
530
|
+
for (const m of messages) {
|
|
531
|
+
h.update("\0");
|
|
532
|
+
h.update(m.role);
|
|
533
|
+
const text = flattenCanonicalContent(m.content);
|
|
534
|
+
if (text) {
|
|
535
|
+
h.update("\0");
|
|
536
|
+
h.update(text);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
h.update(`\0mt=${maxTokens}`);
|
|
540
|
+
return h.digest("hex").slice(0, 16);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function qoderRequestedMaxTokens(request: CanonicalRequest, modelConfig: QoderModelConfig): number {
|
|
544
|
+
/**
|
|
545
|
+
* The ceiling is the roster's stated one. A fixed 32k fallback would clamp
|
|
546
|
+
* every model alike, so a request asking for more would be silently lowered
|
|
547
|
+
* no matter which model it named.
|
|
548
|
+
*/
|
|
549
|
+
const configured = typeof modelConfig.max_output_tokens === "number" ? modelConfig.max_output_tokens : 65_536;
|
|
550
|
+
const controls = request.generation_controls as Record<string, unknown>;
|
|
551
|
+
const requested =
|
|
552
|
+
typeof controls["max_tokens"] === "number"
|
|
553
|
+
? (controls["max_tokens"] as number)
|
|
554
|
+
: typeof controls["max_output_tokens"] === "number"
|
|
555
|
+
? (controls["max_output_tokens"] as number)
|
|
556
|
+
: null;
|
|
557
|
+
return typeof requested === "number" && requested > 0 ? Math.min(requested, configured) : configured;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Whether this model needs the prior turn's reasoning echoed back.
|
|
562
|
+
*
|
|
563
|
+
* The DeepSeek family ties multi-turn consistency to `reasoning_content`: an
|
|
564
|
+
* assistant turn that thought must be replayed with that field or the upstream
|
|
565
|
+
* refuses the whole history. The upstream reference bridge keeps it for exactly
|
|
566
|
+
* these models and drops it as noise for every other one
|
|
567
|
+
* (`keep_reasoning=is_deepseek_model(...)`, and `reasoning_content` in
|
|
568
|
+
* `NOISE_KEYS` otherwise), and notes the symptom when it is missed: occasional
|
|
569
|
+
* failures that sometimes pass on retry.
|
|
570
|
+
*
|
|
571
|
+
* Matched by the upstream key and by the display name, because a caller may
|
|
572
|
+
* name either — the reference warns that matching only the "deepseek" prefix
|
|
573
|
+
* missed every request that used the key.
|
|
574
|
+
*/
|
|
575
|
+
function qoderKeepsReasoning(modelId: string): boolean {
|
|
576
|
+
const key = modelId.trim().toLowerCase();
|
|
577
|
+
if (key === "dmodel" || key === "dfmodel") return true;
|
|
578
|
+
return key.startsWith("deepseek");
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* The `reasoning_content` field for an assistant turn, when one is needed.
|
|
583
|
+
*
|
|
584
|
+
* Presence, not length, decides: a client that already knows the rule sends
|
|
585
|
+
* `""` on a thinking turn whose text is not readable back, and dropping that
|
|
586
|
+
* empty string re-triggers the complaint it was sent to avoid.
|
|
587
|
+
*
|
|
588
|
+
* DeepSeek multi-turn consistency also backfills the field on every assistant
|
|
589
|
+
* turn once the conversation carries a reasoning trace at all — the reference
|
|
590
|
+
* bridge does the same (`backfill_reasoning_content`), and notes the symptom
|
|
591
|
+
* when it is missed: occasional failures that sometimes pass on retry. An
|
|
592
|
+
* assistant turn with no reasoning and no trace gains nothing, so nothing is
|
|
593
|
+
* fabricated.
|
|
594
|
+
*/
|
|
595
|
+
function qoderReasoningEcho(
|
|
596
|
+
message: { content: readonly ContentPart[] },
|
|
597
|
+
modelId: string,
|
|
598
|
+
hasReasoningTrace = false,
|
|
599
|
+
): { reasoning_content: string } | Record<string, never> {
|
|
600
|
+
if (!qoderKeepsReasoning(modelId)) return {};
|
|
601
|
+
const reasoning = message.content.filter((part) => part.kind === "reasoning");
|
|
602
|
+
if (reasoning.length === 0) {
|
|
603
|
+
return hasReasoningTrace ? { reasoning_content: "" } : {};
|
|
604
|
+
}
|
|
605
|
+
const text = reasoning
|
|
606
|
+
.map((part) => (part.kind === "reasoning" ? (part.summary ?? part.payload ?? "") : ""))
|
|
607
|
+
.join("");
|
|
608
|
+
return { reasoning_content: text };
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* The tool arguments as the object the `tool_use` block expects.
|
|
613
|
+
*
|
|
614
|
+
* The canonical form keeps them as the raw JSON text the caller sent, which is
|
|
615
|
+
* what a chat-completions `tool_calls` entry holds. A `tool_use` block wants
|
|
616
|
+
* structured input, so the text is parsed back.
|
|
617
|
+
*
|
|
618
|
+
* The fallbacks mirror the upstream reference bridge rather than inventing
|
|
619
|
+
* shapes: unparseable text becomes `{}` and a non-object becomes `{value}`.
|
|
620
|
+
* Smuggling the raw text through instead would emit an argument shape the
|
|
621
|
+
* upstream has never seen for this field.
|
|
622
|
+
*/
|
|
623
|
+
/**
|
|
624
|
+
* Projects the canonical tools onto Qoder's chat-completions `tools` array.
|
|
625
|
+
*
|
|
626
|
+
* Without this the upstream is never told a tool exists, so a model that wants
|
|
627
|
+
* one improvises markup in its prose (`<Read file_path="..." />`) instead of
|
|
628
|
+
* emitting a structured call — the call is then neither runnable nor
|
|
629
|
+
* recoverable, because it never was a call on either side.
|
|
630
|
+
*
|
|
631
|
+
* Only `function` tools are representable upstream. Anything else is dropped
|
|
632
|
+
* rather than forwarded: Qoder rejects an unknown variant during
|
|
633
|
+
* deserialization and fails the whole turn, which is worse than omitting a
|
|
634
|
+
* tool the model could not have used anyway.
|
|
635
|
+
*/
|
|
636
|
+
function qoderTools(
|
|
637
|
+
tools: readonly ToolDefinition[] | undefined,
|
|
638
|
+
): Array<Record<string, unknown>> {
|
|
639
|
+
const out: Array<Record<string, unknown>> = [];
|
|
640
|
+
for (const tool of tools ?? []) {
|
|
641
|
+
if (tool.tool_type !== undefined && tool.tool_type !== "function") continue;
|
|
642
|
+
// The upstream requires `tools[].function` to be an object. A flat
|
|
643
|
+
// declaration naming the tool at the top level is rejected outright with
|
|
644
|
+
// "'function' is a required property, expected an object", which fails the
|
|
645
|
+
// whole request rather than just this tool.
|
|
646
|
+
out.push({
|
|
647
|
+
type: "function",
|
|
648
|
+
function: {
|
|
649
|
+
name: tool.name,
|
|
650
|
+
...(tool.description === undefined ? { description: "" } : { description: tool.description }),
|
|
651
|
+
parameters: tool.jsonSchema ?? { type: "object", properties: {} },
|
|
652
|
+
...(tool.strict === undefined ? {} : { strict: tool.strict }),
|
|
653
|
+
},
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
return out;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Projects the canonical tool choice onto the upstream's field.
|
|
661
|
+
*
|
|
662
|
+
* The upstream only accepts a string here. Given the object form
|
|
663
|
+
* `{type:"function",function:{name}}` it ignores the field, and the model then
|
|
664
|
+
* calls the tool with no arguments filled in.
|
|
665
|
+
*
|
|
666
|
+
* `allowed_tools` has no chat-completions equivalent, so it collapses to the
|
|
667
|
+
* direction it already names rather than being dropped: a caller that asked for
|
|
668
|
+
* `required` must not silently get `auto`.
|
|
669
|
+
*/
|
|
670
|
+
function qoderToolChoice(choice: ToolChoice | undefined): unknown {
|
|
671
|
+
if (choice === undefined) return undefined;
|
|
672
|
+
if (typeof choice === "string") return choice;
|
|
673
|
+
if (choice.type === "tool" || choice.type === "custom") {
|
|
674
|
+
const name = choice.name.trim();
|
|
675
|
+
return name.length > 0 ? name : "auto";
|
|
676
|
+
}
|
|
677
|
+
return choice.mode;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Whether this request sends its tool history as structured turns.
|
|
682
|
+
*
|
|
683
|
+
* Qoder's wire is OpenAI-compatible `tool_calls` deltas on every realm; the
|
|
684
|
+
* model is fed the same structured turns in both INTL and CN. Writing a past
|
|
685
|
+
* call as prose (`[assistant 请求调用工具] …`) contaminates the conversation
|
|
686
|
+
* history with a text echo the model starts imitating — the origin of the
|
|
687
|
+
* text-leak feedback loop. Structured history keeps the model seeing only real
|
|
688
|
+
* tool calls, so it never learns the prose marker.
|
|
689
|
+
*/
|
|
690
|
+
function qoderStructuredTools(_realm: QoderRealm): boolean {
|
|
691
|
+
return true;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
export function buildQoderRequest(
|
|
695
|
+
modelId: string,
|
|
696
|
+
request: CanonicalRequest,
|
|
697
|
+
modelConfig: QoderModelConfig,
|
|
698
|
+
auth: QoderAuth,
|
|
699
|
+
realm: QoderRealm = "intl",
|
|
700
|
+
): Record<string, unknown> {
|
|
701
|
+
const allMessages = request.messages;
|
|
702
|
+
const systemParts: string[] = [];
|
|
703
|
+
if (request.system) {
|
|
704
|
+
for (const p of request.system) if (p.kind === "text") systemParts.push(p.text);
|
|
705
|
+
}
|
|
706
|
+
if (request.instructions) {
|
|
707
|
+
for (const p of request.instructions) if (p.kind === "text") systemParts.push(p.text);
|
|
708
|
+
}
|
|
709
|
+
for (const m of allMessages) {
|
|
710
|
+
if (m.role === "system" || m.role === "developer") {
|
|
711
|
+
const t = flattenCanonicalContent(m.content);
|
|
712
|
+
if (t) systemParts.push(t);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
const systemPrompt = systemParts.join("\n\n").trim();
|
|
716
|
+
const qoderMessages: Array<Record<string, unknown>> = [];
|
|
717
|
+
const structuredTools = qoderStructuredTools(realm);
|
|
718
|
+
// DeepSeek multi-turn: once any turn in the conversation carries a reasoning
|
|
719
|
+
// trace, every assistant message must replay `reasoning_content` (see
|
|
720
|
+
// `qoderReasoningEcho`).
|
|
721
|
+
const hasReasoningTrace = allMessages.some((message) =>
|
|
722
|
+
message.content.some((part) => part.kind === "reasoning"),
|
|
723
|
+
);
|
|
724
|
+
if (MODERN_PROFILE.mirrorTopLevelSystem && systemPrompt.length > 0) {
|
|
725
|
+
qoderMessages.push({
|
|
726
|
+
role: "system",
|
|
727
|
+
content: systemPrompt,
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
// Every tool result across the whole conversation, keyed by call id. An
|
|
731
|
+
// assistant `tool_calls` turn is only sent structured when every call has a
|
|
732
|
+
// matching result somewhere in the history — otherwise the upstream rejects
|
|
733
|
+
// the transcript ("An assistant message with 'tool_calls' must be followed by
|
|
734
|
+
// tool messages responding to each 'tool_call_id'"). Calls without a result
|
|
735
|
+
// are left out of the structured turn instead of poisoning it.
|
|
736
|
+
const toolResultsById = new Map<string, string>();
|
|
737
|
+
for (const msg of allMessages) {
|
|
738
|
+
for (const part of toolResultParts(msg)) {
|
|
739
|
+
if (typeof part.call_id === "string" && part.call_id.length > 0) {
|
|
740
|
+
toolResultsById.set(part.call_id, part.call_id);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
// The tool calls that actually survive onto the wire. A call only makes it
|
|
745
|
+
// when every call on its assistant turn carries an id AND a matching result
|
|
746
|
+
// exists in the history. This is the single source of truth for both sides
|
|
747
|
+
// of the transcript: the assistant `tool_calls` emit and the `tool` result
|
|
748
|
+
// turns. If a result's call is not here, its `tool` turn is dropped too —
|
|
749
|
+
// leaving it would emit an orphan `tool` turn with no preceding call, which
|
|
750
|
+
// the upstream rejects ("Messages with role 'tool' must be a response to a
|
|
751
|
+
// preceding message with 'tool_calls'"). This is what a cancelled or
|
|
752
|
+
// compacted parallel call looks like, and it is the "insufficient tool
|
|
753
|
+
// messages" failure the client sees on the turn after.
|
|
754
|
+
const emittableCallIds = new Set<string>();
|
|
755
|
+
for (const msg of allMessages) {
|
|
756
|
+
if (msg.role !== "assistant") continue;
|
|
757
|
+
const calls = toolCallParts(msg);
|
|
758
|
+
if (calls.length === 0) continue;
|
|
759
|
+
const idsOk = calls.every(
|
|
760
|
+
(part) => typeof part.call_id === "string" && part.call_id.length > 0,
|
|
761
|
+
);
|
|
762
|
+
const everyCallHasResult = calls.every((part) =>
|
|
763
|
+
typeof part.call_id === "string" ? toolResultsById.has(part.call_id) : false,
|
|
764
|
+
);
|
|
765
|
+
if (idsOk && everyCallHasResult) {
|
|
766
|
+
for (const part of calls) emittableCallIds.add(part.call_id as string);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
for (const m of allMessages) {
|
|
770
|
+
if (m.role === "system" || m.role === "developer") continue;
|
|
771
|
+
// Answers may live in a `user` turn (the Messages ledger re-homes them),
|
|
772
|
+
// so the result scan is role-agnostic. Each result becomes its own `tool`
|
|
773
|
+
// turn carrying its `tool_call_id`; any non-result content on the same turn
|
|
774
|
+
// is emitted after, so nothing is duplicated or lost.
|
|
775
|
+
const results = structuredTools ? toolResultParts(m) : [];
|
|
776
|
+
if (results.length > 0) {
|
|
777
|
+
let emittedAnyTool = false;
|
|
778
|
+
for (const block of results) {
|
|
779
|
+
// Drop orphan results: their assistant `tool_calls` turn was filtered
|
|
780
|
+
// out (a sibling call has no result), and emitting the `tool` turn
|
|
781
|
+
// anyway would make the upstream reject the whole transcript.
|
|
782
|
+
if (!emittableCallIds.has(block.call_id)) continue;
|
|
783
|
+
emittedAnyTool = true;
|
|
784
|
+
const text = flattenCanonicalContent(
|
|
785
|
+
typeof block.content === "string"
|
|
786
|
+
? [{ kind: "text", text: block.content }]
|
|
787
|
+
: block.content,
|
|
788
|
+
);
|
|
789
|
+
qoderMessages.push({
|
|
790
|
+
role: "tool",
|
|
791
|
+
content: text,
|
|
792
|
+
tool_call_id: block.call_id,
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
// Text that shared the turn with a result is emitted after it, minus the
|
|
796
|
+
// result itself. Re-emitting the result's own text here was doubling
|
|
797
|
+
// every tool answer: the `tool` turn above and a `user` turn with the
|
|
798
|
+
// same content, which doubled the payload for no reason.
|
|
799
|
+
const remaining = m.content.filter((part) => part.kind !== "toolResult");
|
|
800
|
+
const flat = flattenCanonicalContent(remaining);
|
|
801
|
+
if (flat.length === 0 && !emittedAnyTool) continue;
|
|
802
|
+
if (flat.length > 0) {
|
|
803
|
+
qoderMessages.push({
|
|
804
|
+
role: "user",
|
|
805
|
+
content: flat,
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
// An assistant turn that called tools must carry the calls, not just its
|
|
811
|
+
// text. Falling through to the text-only emit dropped them, so the payload
|
|
812
|
+
// arrived as a `tool` result with no preceding call and the upstream
|
|
813
|
+
// rejected the whole transcript: "Messages with role 'tool' must be a
|
|
814
|
+
// response to a preceding message with 'tool_calls'".
|
|
815
|
+
const allCalls = m.content.filter((part) => part.kind === "toolCall");
|
|
816
|
+
const reasoningEcho = qoderReasoningEcho(m, modelId, hasReasoningTrace);
|
|
817
|
+
// Structured tool turns are only safe when every call carries an id AND a
|
|
818
|
+
// matching result exists in the history — the upstream rejects a transcript
|
|
819
|
+
// whose `tool_calls` is not fully answered by `tool` turns. The same set
|
|
820
|
+
// that gates the `tool` emit above decides this, so the two halves of the
|
|
821
|
+
// transcript can never disagree.
|
|
822
|
+
const calls =
|
|
823
|
+
structuredTools && allCalls.every((part) => emittableCallIds.has(part.call_id))
|
|
824
|
+
? allCalls
|
|
825
|
+
: [];
|
|
826
|
+
if (calls.length > 0) {
|
|
827
|
+
const textParts = m.content.filter((part) => part.kind !== "toolCall");
|
|
828
|
+
const flat = flattenCanonicalContent(textParts);
|
|
829
|
+
qoderMessages.push({
|
|
830
|
+
role: "assistant",
|
|
831
|
+
content: flat,
|
|
832
|
+
...reasoningEcho,
|
|
833
|
+
tool_calls: calls.map((part) => ({
|
|
834
|
+
id: part.call_id,
|
|
835
|
+
type: "function",
|
|
836
|
+
function: {
|
|
837
|
+
name: part.name,
|
|
838
|
+
arguments:
|
|
839
|
+
typeof part.arguments === "string"
|
|
840
|
+
? part.arguments
|
|
841
|
+
: part.arguments !== undefined && part.arguments !== null
|
|
842
|
+
? JSON.stringify(part.arguments)
|
|
843
|
+
: "",
|
|
844
|
+
},
|
|
845
|
+
})),
|
|
846
|
+
});
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
const role = m.role === "assistant" ? "assistant" : "user";
|
|
850
|
+
// Images must travel as structured OpenAI parts inside the message content —
|
|
851
|
+
// the upstream reads `image_url` parts there and not the top-level
|
|
852
|
+
// `image_urls` (mirroring `flatten_messages` in the reference bridge). A
|
|
853
|
+
// bare file_id reference degrades to a text note instead of being dropped.
|
|
854
|
+
const hasImages = m.content.some((part) => part.kind === "image");
|
|
855
|
+
if (hasImages) {
|
|
856
|
+
const parts: Array<Record<string, unknown>> = [];
|
|
857
|
+
for (const part of m.content) {
|
|
858
|
+
if (part.kind === "text") {
|
|
859
|
+
parts.push({ type: "text", text: part.text });
|
|
860
|
+
} else if (part.kind === "image") {
|
|
861
|
+
const source = resolveImageSource(part.payload);
|
|
862
|
+
if (source?.url !== undefined) {
|
|
863
|
+
parts.push({ type: "image_url", image_url: { url: source.url } });
|
|
864
|
+
} else {
|
|
865
|
+
parts.push({
|
|
866
|
+
type: "text",
|
|
867
|
+
text: source?.fileId !== undefined ? `[image: ${source.fileId}]` : "[image: unsupported source]",
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
qoderMessages.push({
|
|
873
|
+
role,
|
|
874
|
+
content: parts,
|
|
875
|
+
...(role === "assistant" ? qoderReasoningEcho(m, modelId, hasReasoningTrace) : {}),
|
|
876
|
+
});
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
const flat = flattenCanonicalContent(m.content);
|
|
880
|
+
qoderMessages.push({
|
|
881
|
+
role,
|
|
882
|
+
content: flat,
|
|
883
|
+
...(role === "assistant" ? qoderReasoningEcho(m, modelId, hasReasoningTrace) : {}),
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
const latestUserMessage = [...qoderMessages].reverse().find((m) => m["role"] === "user");
|
|
887
|
+
const latestUserText = typeof latestUserMessage?.["content"] === "string" ? (latestUserMessage["content"] as string) : "";
|
|
888
|
+
const maxTokens = qoderRequestedMaxTokens(request, modelConfig);
|
|
889
|
+
const reasoning = modelConfig.is_reasoning === true;
|
|
890
|
+
const sessionId = qoderStableHash("qoder-session", auth.userId, modelId);
|
|
891
|
+
const recordId = qoderStableRecordId(modelId, allMessages, maxTokens);
|
|
892
|
+
return {
|
|
893
|
+
request_id: randomUUID(),
|
|
894
|
+
request_set_id: recordId,
|
|
895
|
+
chat_record_id: recordId,
|
|
896
|
+
session_id: sessionId,
|
|
897
|
+
stream: true,
|
|
898
|
+
...(MODERN_PROFILE.emptyAliyunUserType ? { aliyun_user_type: "" } : {}),
|
|
899
|
+
chat_task: "FREE_INPUT",
|
|
900
|
+
is_reply: true,
|
|
901
|
+
is_retry: false,
|
|
902
|
+
source: 1,
|
|
903
|
+
version: "3",
|
|
904
|
+
session_type: "qodercli",
|
|
905
|
+
agent_id: "agent_common",
|
|
906
|
+
task_id: "common",
|
|
907
|
+
code_language: "",
|
|
908
|
+
chat_prompt: MODERN_PROFILE.mirrorTopLevelSystem ? systemPrompt : "",
|
|
909
|
+
image_urls: null,
|
|
910
|
+
chat_context: {
|
|
911
|
+
chatPrompt: MODERN_PROFILE.mirrorTopLevelSystem ? systemPrompt : "",
|
|
912
|
+
imageUrls: null,
|
|
913
|
+
extra: { context: [], modelConfig: { key: modelId, is_reasoning: reasoning }, originalContent: { text: latestUserText } },
|
|
914
|
+
features: [],
|
|
915
|
+
// An object, as the reference sends it: `cc["text"]["text"] = prompt`.
|
|
916
|
+
text: { text: latestUserText },
|
|
917
|
+
},
|
|
918
|
+
model_config: {
|
|
919
|
+
key: modelId,
|
|
920
|
+
display_name: modelConfig.display_name,
|
|
921
|
+
// Declares the wire dialect for the tool calls. Without it the upstream
|
|
922
|
+
// does not read the OpenAI-shaped `tools` array the same way, and the
|
|
923
|
+
// model then emits a call with no arguments filled in.
|
|
924
|
+
format: "openai",
|
|
925
|
+
model: "",
|
|
926
|
+
api_key: "",
|
|
927
|
+
url: "",
|
|
928
|
+
is_vl: modelConfig.is_vl,
|
|
929
|
+
is_reasoning: modelConfig.is_reasoning,
|
|
930
|
+
max_input_tokens: modelConfig.max_input_tokens,
|
|
931
|
+
// Mirrors the roster's stated ceiling. A fixed 32k fallback would be sent
|
|
932
|
+
// for every model alike, so the request would declare a cap the catalog
|
|
933
|
+
// does not publish.
|
|
934
|
+
max_output_tokens: modelConfig.max_output_tokens ?? 65_536,
|
|
935
|
+
source: modelConfig.source ?? "system",
|
|
936
|
+
},
|
|
937
|
+
messages: qoderMessages,
|
|
938
|
+
max_tokens: maxTokens,
|
|
939
|
+
// Always present, even when empty. Omitting the field makes Qoder fall back
|
|
940
|
+
// to the desktop agent's own toolset (Bash/Edit/...), which then leaks to a
|
|
941
|
+
// caller that declared none — the reference bridge sends `[]` for exactly
|
|
942
|
+
// that reason. Parallel calls are stated explicitly rather than left to the
|
|
943
|
+
// upstream default, so a caller expecting them is not served serial
|
|
944
|
+
// behaviour.
|
|
945
|
+
tools: qoderTools(request.tools),
|
|
946
|
+
parallel_tool_calls: true,
|
|
947
|
+
...(() => {
|
|
948
|
+
const choice = qoderToolChoice(request.tool_choice);
|
|
949
|
+
return choice === undefined ? {} : { tool_choice: choice };
|
|
950
|
+
})(),
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// Stream transformation — decode SSE, unwrap COSY envelope, map OpenAI chat chunks to CanonicalEvent
|
|
955
|
+
|
|
956
|
+
function mapQoderFinishReason(raw: unknown): CanonicalStopReason | undefined {
|
|
957
|
+
if (raw === "length") return "length";
|
|
958
|
+
if (raw === "tool_calls") return "tool_use";
|
|
959
|
+
if (raw === "content_filter") return "content_filter";
|
|
960
|
+
return undefined;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
export async function* qoderBodyToCanonicalEvents(
|
|
964
|
+
body: ReadableStream<Uint8Array>,
|
|
965
|
+
signal: AbortSignal,
|
|
966
|
+
model: string,
|
|
967
|
+
): AsyncIterable<CanonicalEvent> {
|
|
968
|
+
let seq = 1;
|
|
969
|
+
yield { type: "response_start", sequence_number: seq++, model } as CanonicalEvent;
|
|
970
|
+
let finishReason: unknown = undefined;
|
|
971
|
+
let rawUsage: Record<string, unknown> | undefined = undefined;
|
|
972
|
+
let hasTerminal = false;
|
|
973
|
+
/** First-sighting id per wire tool-call index, for continuation chunks. */
|
|
974
|
+
const indexIds = new Map<number, string>();
|
|
975
|
+
/** Last seen call id/name, for index-less continuation fragments. */
|
|
976
|
+
let lastSeenToolId: string | undefined = undefined;
|
|
977
|
+
let lastSeenToolName: string | undefined = undefined;
|
|
978
|
+
// INTL leak-marker hold-back: text that may grow into the marker+JSON echo
|
|
979
|
+
// is withheld from the client until end of stream proves it is a tool call
|
|
980
|
+
// (recovered) or ordinary prose (released).
|
|
981
|
+
let leakBuf = "";
|
|
982
|
+
let leakFrames = 0;
|
|
983
|
+
|
|
984
|
+
for await (const sse of decodeSseEvents(body, { signal })) {
|
|
985
|
+
const sseData = sse.data;
|
|
986
|
+
let frames: Array<{ event: string | null; data: string }>;
|
|
987
|
+
try {
|
|
988
|
+
frames = qoderEnvelopeToFrames(sseData);
|
|
989
|
+
} catch (e) {
|
|
990
|
+
throw e;
|
|
991
|
+
}
|
|
992
|
+
for (const frame of frames) {
|
|
993
|
+
const data = frame.data.trim();
|
|
994
|
+
if (!data) continue;
|
|
995
|
+
if (data === "[DONE]") {
|
|
996
|
+
hasTerminal = true;
|
|
997
|
+
continue;
|
|
998
|
+
}
|
|
999
|
+
let json: Record<string, unknown>;
|
|
1000
|
+
try {
|
|
1001
|
+
json = JSON.parse(data) as Record<string, unknown>;
|
|
1002
|
+
} catch {
|
|
1003
|
+
continue;
|
|
1004
|
+
}
|
|
1005
|
+
if (json["usage"] && typeof json["usage"] === "object") rawUsage = json["usage"] as Record<string, unknown>;
|
|
1006
|
+
const choices = (json["choices"] as Array<Record<string, unknown>>) ?? [];
|
|
1007
|
+
for (const choice of choices) {
|
|
1008
|
+
const delta = choice["delta"] as Record<string, unknown> | undefined;
|
|
1009
|
+
if (delta) {
|
|
1010
|
+
if (typeof delta["content"] === "string" && (delta["content"] as string).length > 0) {
|
|
1011
|
+
const piece = delta["content"] as string;
|
|
1012
|
+
// Marker+JSON hold-back. A complete marker and everything after it is
|
|
1013
|
+
// held until end of stream; prose before it is emitted now.
|
|
1014
|
+
if (leakBuf.length > 0) {
|
|
1015
|
+
leakBuf += piece;
|
|
1016
|
+
leakFrames = piece.length === 0 ? leakFrames + 1 : 0;
|
|
1017
|
+
if (
|
|
1018
|
+
leakBuf.length > LEAK_HOLD_MAX_CHARS ||
|
|
1019
|
+
leakFrames > LEAK_HOLD_MAX_FRAMES ||
|
|
1020
|
+
!qoderStillLeakCandidate(leakBuf)
|
|
1021
|
+
) {
|
|
1022
|
+
if (leakBuf.length > 0) {
|
|
1023
|
+
yield {
|
|
1024
|
+
type: "content_delta",
|
|
1025
|
+
sequence_number: seq++,
|
|
1026
|
+
content: { kind: "text", text: leakBuf },
|
|
1027
|
+
} as CanonicalEvent;
|
|
1028
|
+
}
|
|
1029
|
+
leakBuf = "";
|
|
1030
|
+
leakFrames = 0;
|
|
1031
|
+
}
|
|
1032
|
+
continue;
|
|
1033
|
+
}
|
|
1034
|
+
const marker = LEAK_MARKER_ANY_RE.exec(piece);
|
|
1035
|
+
if (marker) {
|
|
1036
|
+
const emit = piece.slice(0, marker.index);
|
|
1037
|
+
if (emit.length > 0) {
|
|
1038
|
+
yield {
|
|
1039
|
+
type: "content_delta",
|
|
1040
|
+
sequence_number: seq++,
|
|
1041
|
+
content: { kind: "text", text: emit },
|
|
1042
|
+
} as CanonicalEvent;
|
|
1043
|
+
}
|
|
1044
|
+
leakBuf = piece.slice(marker.index);
|
|
1045
|
+
leakFrames = 1;
|
|
1046
|
+
continue;
|
|
1047
|
+
}
|
|
1048
|
+
// No marker: hold only a possible partial-marker suffix.
|
|
1049
|
+
const tail = piece.slice(-LEAK_HOLD_WINDOW);
|
|
1050
|
+
let heldFrom = -1;
|
|
1051
|
+
for (let n = tail.length; n >= 1; n -= 1) {
|
|
1052
|
+
if (isLeakMarkerPrefix(tail.slice(-n))) {
|
|
1053
|
+
heldFrom = piece.length - n;
|
|
1054
|
+
break;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
if (heldFrom >= 0) {
|
|
1058
|
+
const emit = piece.slice(0, heldFrom);
|
|
1059
|
+
if (emit.length > 0) {
|
|
1060
|
+
yield {
|
|
1061
|
+
type: "content_delta",
|
|
1062
|
+
sequence_number: seq++,
|
|
1063
|
+
content: { kind: "text", text: emit },
|
|
1064
|
+
} as CanonicalEvent;
|
|
1065
|
+
}
|
|
1066
|
+
leakBuf = piece.slice(heldFrom);
|
|
1067
|
+
leakFrames = 1;
|
|
1068
|
+
continue;
|
|
1069
|
+
}
|
|
1070
|
+
yield {
|
|
1071
|
+
type: "content_delta",
|
|
1072
|
+
sequence_number: seq++,
|
|
1073
|
+
content: { kind: "text", text: piece },
|
|
1074
|
+
} as CanonicalEvent;
|
|
1075
|
+
continue;
|
|
1076
|
+
}
|
|
1077
|
+
const reasoningText = readReasoningText(delta);
|
|
1078
|
+
if (typeof reasoningText === "string" && reasoningText.length > 0) {
|
|
1079
|
+
yield {
|
|
1080
|
+
type: "content_delta",
|
|
1081
|
+
sequence_number: seq++,
|
|
1082
|
+
content: { kind: "reasoning", payload: null, summary: reasoningText },
|
|
1083
|
+
} as CanonicalEvent;
|
|
1084
|
+
continue;
|
|
1085
|
+
}
|
|
1086
|
+
const toolCalls = delta["tool_calls"] as Array<Record<string, unknown>> | undefined;
|
|
1087
|
+
if (toolCalls) {
|
|
1088
|
+
for (const tc of toolCalls) {
|
|
1089
|
+
const fn = tc["function"] as Record<string, unknown> | undefined;
|
|
1090
|
+
const index = typeof tc["index"] === "number" ? (tc["index"] as number) : undefined;
|
|
1091
|
+
const declaredId = typeof tc["id"] === "string" ? (tc["id"] as string) : undefined;
|
|
1092
|
+
const fragName = typeof fn?.["name"] === "string" ? (fn["name"] as string) : undefined;
|
|
1093
|
+
// OpenAI continuation chunks re-identify their call by wire
|
|
1094
|
+
// `index` alone — the `id` appears only once, in the first chunk.
|
|
1095
|
+
// Some bridges omit `index` too, so a fragment then falls back to
|
|
1096
|
+
// the most recently seen call id (mirroring the generic chat
|
|
1097
|
+
// parser's `lastSeenToolId`). Pinning the first id per index and
|
|
1098
|
+
// reusing it for continuations is what keeps fragments attached
|
|
1099
|
+
// to the right call; synthesizing a fresh `call_<index>` id per
|
|
1100
|
+
// chunk instead made every continuation an unknown nameless call
|
|
1101
|
+
// that encoders dropped, so the client saw a call whose arguments
|
|
1102
|
+
// were always empty.
|
|
1103
|
+
let callId: string;
|
|
1104
|
+
if (declaredId !== undefined) {
|
|
1105
|
+
callId = declaredId;
|
|
1106
|
+
if (index !== undefined) indexIds.set(index, callId);
|
|
1107
|
+
lastSeenToolId = callId;
|
|
1108
|
+
if (fragName !== undefined) lastSeenToolName = fragName;
|
|
1109
|
+
} else if (index !== undefined && indexIds.has(index)) {
|
|
1110
|
+
callId = indexIds.get(index)!;
|
|
1111
|
+
} else if (
|
|
1112
|
+
lastSeenToolId !== undefined &&
|
|
1113
|
+
(fragName === undefined || lastSeenToolName === fragName)
|
|
1114
|
+
) {
|
|
1115
|
+
callId = lastSeenToolId;
|
|
1116
|
+
} else {
|
|
1117
|
+
callId = `call_${String(index ?? 0)}`;
|
|
1118
|
+
}
|
|
1119
|
+
yield {
|
|
1120
|
+
type: "tool_call_delta",
|
|
1121
|
+
sequence_number: seq++,
|
|
1122
|
+
call_id: callId,
|
|
1123
|
+
...(index !== undefined ? { index } : {}),
|
|
1124
|
+
...(fragName === undefined ? {} : { name: fragName }),
|
|
1125
|
+
arguments_delta: (fn?.["arguments"] as string) ?? "",
|
|
1126
|
+
} as CanonicalEvent;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
const fr = choice["finish_reason"];
|
|
1131
|
+
if (fr !== null && fr !== undefined) finishReason = fr;
|
|
1132
|
+
// Some providers send usage in choices[0].usage or top-level
|
|
1133
|
+
if (choice["usage"] && typeof choice["usage"] === "object") rawUsage = choice["usage"] as Record<string, unknown>;
|
|
1134
|
+
}
|
|
1135
|
+
// Some Qoder chunks carry finish_reason at top level
|
|
1136
|
+
if (json["finish_reason"] !== undefined) finishReason = json["finish_reason"];
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
// End of stream: recover a held-back marker+JSON echo. A valid echo becomes
|
|
1140
|
+
// tool calls; anything else is released as ordinary prose (or fails closed
|
|
1141
|
+
// when it is unmistakably an echo that cannot be parsed).
|
|
1142
|
+
if (leakBuf.length > 0) {
|
|
1143
|
+
const leaked = parseQoderTextToolCalls(leakBuf);
|
|
1144
|
+
if (leaked.calls.length > 0) {
|
|
1145
|
+
for (const call of leaked.calls) {
|
|
1146
|
+
yield {
|
|
1147
|
+
type: "tool_call_delta",
|
|
1148
|
+
sequence_number: seq++,
|
|
1149
|
+
call_id: call.id,
|
|
1150
|
+
name: call.name,
|
|
1151
|
+
arguments_delta: call.arguments,
|
|
1152
|
+
} as CanonicalEvent;
|
|
1153
|
+
}
|
|
1154
|
+
finishReason = "tool_calls";
|
|
1155
|
+
} else {
|
|
1156
|
+
// An echo that could not be recovered must fail the turn (502) so the
|
|
1157
|
+
// harness retries — never surface protocol text as visible content.
|
|
1158
|
+
if (isQoderEchoAttempt(leakBuf)) {
|
|
1159
|
+
throw badEchoError();
|
|
1160
|
+
}
|
|
1161
|
+
const visible = leaked.clean.trim();
|
|
1162
|
+
if (visible.length > 0) {
|
|
1163
|
+
yield {
|
|
1164
|
+
type: "content_delta",
|
|
1165
|
+
sequence_number: seq++,
|
|
1166
|
+
content: { kind: "text", text: visible },
|
|
1167
|
+
} as CanonicalEvent;
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
// If no terminal arrived, synthesize one (prevents truncation error)
|
|
1172
|
+
const stopReason = mapQoderFinishReason(finishReason);
|
|
1173
|
+
const _providerStop = typeof finishReason === "string" ? finishReason : undefined;
|
|
1174
|
+
void hasTerminal;
|
|
1175
|
+
yield {
|
|
1176
|
+
type: "terminal",
|
|
1177
|
+
sequence_number: seq++,
|
|
1178
|
+
state: "complete",
|
|
1179
|
+
...(stopReason === undefined ? {} : { stop_reason: stopReason }),
|
|
1180
|
+
...(_providerStop === undefined ? {} : { provider_stop_reason: _providerStop }),
|
|
1181
|
+
usage: usageFromProvider(rawUsage),
|
|
1182
|
+
} as CanonicalEvent;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// Model catalog — maps QODER_MODEL_CONFIGS (provider) to Cartethyia ModelDefinition
|
|
1186
|
+
|
|
1187
|
+
/**
|
|
1188
|
+
* The intl catalog this provider publishes.
|
|
1189
|
+
*
|
|
1190
|
+
* `QODER_MODEL_CONFIGS` is not the source here: it freezes most rows at 180k
|
|
1191
|
+
* while the upstream reports 1M for the same keys. That number is not
|
|
1192
|
+
* cosmetic: it is sent upstream as `model_config.max_input_tokens`, so a stale
|
|
1193
|
+
* low value tells Qoder the conversation window is small and makes it behave
|
|
1194
|
+
* that way. Both the published catalog and the dispatched request come from the
|
|
1195
|
+
* realm's roster, which carries the upstream's own numbers.
|
|
1196
|
+
*/
|
|
1197
|
+
export const QODER_MODELS: readonly ModelDefinition[] = QODER_INTL_MODELS;
|
|
1198
|
+
|
|
1199
|
+
// ProviderAdapter — manual dispatch preserving COSY encryption + envelope logic
|
|
1200
|
+
|
|
1201
|
+
interface QoderAdapterOptions {
|
|
1202
|
+
readonly fetch?: typeof fetch;
|
|
1203
|
+
/** Which deployment this adapter dispatches to. Defaults to intl. */
|
|
1204
|
+
readonly realm?: QoderRealm;
|
|
1205
|
+
/** Bundled provider id, so the adapter's own id matches the route's. */
|
|
1206
|
+
readonly providerId?: string;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
class QoderAdapter implements ProviderAdapter {
|
|
1210
|
+
readonly provider_id: ProviderId;
|
|
1211
|
+
private readonly realm: QoderRealm;
|
|
1212
|
+
private readonly fetchFn: typeof fetch;
|
|
1213
|
+
|
|
1214
|
+
constructor(options: QoderAdapterOptions = {}) {
|
|
1215
|
+
this.provider_id = (options.providerId ?? QODER_PROVIDER_ID) as ProviderId;
|
|
1216
|
+
this.realm = options.realm ?? "intl";
|
|
1217
|
+
this.fetchFn = options.fetch ?? globalThis.fetch;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
/**
|
|
1221
|
+
* The static roster for this adapter's realm.
|
|
1222
|
+
*
|
|
1223
|
+
* A single merged table advertised CN-only keys to global accounts and vice
|
|
1224
|
+
* versa, so a request was routed to a deployment that refuses it.
|
|
1225
|
+
*/
|
|
1226
|
+
#modelConfigs(): Record<string, QoderModelConfig> {
|
|
1227
|
+
return (this.realm === "cn" ? QODER_CN_MODEL_CONFIGS : QODER_INTL_MODEL_CONFIGS) as Record<
|
|
1228
|
+
string,
|
|
1229
|
+
QoderModelConfig
|
|
1230
|
+
>;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
async *dispatch(
|
|
1234
|
+
request: CanonicalRequest,
|
|
1235
|
+
candidate: ProviderDispatchTarget,
|
|
1236
|
+
context: ProviderDispatchContext,
|
|
1237
|
+
): AsyncIterable<CanonicalEvent> {
|
|
1238
|
+
if (candidate.wire_family !== "chat") {
|
|
1239
|
+
throw new GatewayError("capability_unsupported", 400, `qoder supports chat only, got ${candidate.wire_family}`);
|
|
1240
|
+
}
|
|
1241
|
+
const secret = readCredentialSecret(context.credential, "Qoder credential is required");
|
|
1242
|
+
|
|
1243
|
+
/**
|
|
1244
|
+
* A caller-facing virtual name is translated to its roster key here.
|
|
1245
|
+
*
|
|
1246
|
+
* The translation has to happen before the config lookup and before the
|
|
1247
|
+
* request body: the body carries the roster key, and looking up the
|
|
1248
|
+
* virtual id in the config table would fall through to the synthetic
|
|
1249
|
+
* default and silently dispatch under-declared capabilities.
|
|
1250
|
+
*/
|
|
1251
|
+
const requestedModelId = candidate.model_id || request.model;
|
|
1252
|
+
if (isQoderVirtualModel(requestedModelId) && !this.#modelConfigs()[qoderDispatchModelId(requestedModelId)]) {
|
|
1253
|
+
throw new GatewayError(
|
|
1254
|
+
"model_not_found",
|
|
1255
|
+
404,
|
|
1256
|
+
`Qoder virtual model ${requestedModelId} resolves to a roster key this realm does not serve`,
|
|
1257
|
+
{ virtual_model: requestedModelId, target: qoderDispatchModelId(requestedModelId) },
|
|
1258
|
+
"upstream",
|
|
1259
|
+
);
|
|
1260
|
+
}
|
|
1261
|
+
const modelId = qoderDispatchModelId(requestedModelId);
|
|
1262
|
+
const modelConfig = this.#modelConfigs()[modelId] ?? {
|
|
1263
|
+
id: modelId,
|
|
1264
|
+
display_name: modelId,
|
|
1265
|
+
max_input_tokens: 180_000,
|
|
1266
|
+
is_vl: true,
|
|
1267
|
+
is_reasoning: false,
|
|
1268
|
+
};
|
|
1269
|
+
|
|
1270
|
+
const lifecycle = createUpstreamDeadlineLifecycle(context);
|
|
1271
|
+
|
|
1272
|
+
// Outbound fetch respects SSRF binding when provided
|
|
1273
|
+
const outboundFetch: typeof fetch = (context.outbound_fetch as unknown as typeof fetch) ?? this.fetchFn;
|
|
1274
|
+
|
|
1275
|
+
const fetcher: QoderFetcher = (url, init) =>
|
|
1276
|
+
outboundFetch(url, { ...init, signal: lifecycle.signal } as RequestInit) as Promise<Response>;
|
|
1277
|
+
|
|
1278
|
+
try {
|
|
1279
|
+
const version = getQoderVersion();
|
|
1280
|
+
// Dual mode like Cline: a pasted PAT is exchanged for a job token per
|
|
1281
|
+
// request, while an OAuth device token is used directly with the
|
|
1282
|
+
// identity the login persisted.
|
|
1283
|
+
const auth = isQoderPat(secret)
|
|
1284
|
+
? await exchangeQoderPat(secret, lifecycle.signal, fetcher, version, this.realm)
|
|
1285
|
+
: qoderOAuthAuth(secret, context.credential.auth_state, context.credential.account_id);
|
|
1286
|
+
const qoderBody = buildQoderRequest(modelId, request, modelConfig, auth, this.realm);
|
|
1287
|
+
// A full URL from the route wins, but the fallback is this realm's
|
|
1288
|
+
// gateway — not the intl host hardcoded into `MODERN_PROFILE`. Dispatching
|
|
1289
|
+
// a CN credential to `*.qoder.sh` is refused outright. The host order
|
|
1290
|
+
// follows the token kind: job tokens (`jt-...`) belong on `api2`, which
|
|
1291
|
+
// is the only intl host that serves them (`api3` answers "Login expired").
|
|
1292
|
+
const url =
|
|
1293
|
+
candidate.endpoint_path && candidate.endpoint_path.startsWith("http")
|
|
1294
|
+
? candidate.endpoint_path
|
|
1295
|
+
: `${qoderInferenceCandidates(this.realm, auth.securityOauthToken)[0] ?? ""}${QODER_CHAT_PATH}`;
|
|
1296
|
+
|
|
1297
|
+
const response = await callQoder(url, qoderBody, modelId, auth, lifecycle.signal, fetcher, version);
|
|
1298
|
+
if (!response.ok) throw await mapUpstreamHttpError(response, "qoder");
|
|
1299
|
+
if (!response.body) throw new GatewayError("platform_unavailable", 502, "Qoder returned empty body", {}, "upstream");
|
|
1300
|
+
// Headers arrived: the pre-stream deadline has served its purpose.
|
|
1301
|
+
// From here the gateway stall/first-chunk watchdog (propagated via
|
|
1302
|
+
// context.abort_signal) governs the body. Keeping this timer armed
|
|
1303
|
+
// would kill healthy long streams at the stale pre-stream deadline.
|
|
1304
|
+
lifecycle.release();
|
|
1305
|
+
yield* qoderBodyToCanonicalEvents(response.body as ReadableStream<Uint8Array>, lifecycle.signal, request.model);
|
|
1306
|
+
} catch (err: unknown) {
|
|
1307
|
+
const abortError = abortGatewayError(lifecycle, err, context.abort_signal);
|
|
1308
|
+
if (abortError) throw abortError;
|
|
1309
|
+
throw err;
|
|
1310
|
+
} finally {
|
|
1311
|
+
lifecycle.release();
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
export function createQoderAdapter(options: QoderAdapterOptions = {}): ProviderAdapter {
|
|
1317
|
+
return new QoderAdapter(options);
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
/**
|
|
1321
|
+
* Adapter for the China deployment.
|
|
1322
|
+
*
|
|
1323
|
+
* Same wire, different gateway host and roster — see `qoder-realm.ts`.
|
|
1324
|
+
*/
|
|
1325
|
+
export function createQoderCnAdapter(options: QoderAdapterOptions = {}): ProviderAdapter {
|
|
1326
|
+
return new QoderAdapter({ ...options, realm: "cn", providerId: "qodercn" });
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
/**
|
|
1330
|
+
* A COSY signer for one already-resolved account identity.
|
|
1331
|
+
*
|
|
1332
|
+
* Discovery needs signed headers but lives in `qoder-catalog.ts`, which cannot
|
|
1333
|
+
* import this module (the adapter imports its rosters — that would be a cycle).
|
|
1334
|
+
* Exporting the signer keeps one signing implementation and lets discovery
|
|
1335
|
+
* stay pure and injectable.
|
|
1336
|
+
*/
|
|
1337
|
+
export function qoderSignerFor(
|
|
1338
|
+
auth: QoderAuth,
|
|
1339
|
+
version = getQoderVersion(),
|
|
1340
|
+
): (url: string, body: string) => Record<string, string> {
|
|
1341
|
+
return (url: string, body: string): Record<string, string> =>
|
|
1342
|
+
qoderBuildCosyHeaders(Buffer.from(body, "latin1"), url, auth, version);
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
/** Qoder's own base64 alphabet transform, for a request body. */
|
|
1346
|
+
export function qoderEncodeBody(plaintext: Uint8Array | string): string {
|
|
1347
|
+
const text = typeof plaintext === "string" ? plaintext : new TextDecoder().decode(plaintext);
|
|
1348
|
+
return Buffer.from(transformQoderBody(text)).toString("latin1");
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
/**
|
|
1352
|
+
* Resolves the dispatch identity for one Qoder credential.
|
|
1353
|
+
*
|
|
1354
|
+
* Shared by the adapter and by discovery: both need the same `QoderAuth` the
|
|
1355
|
+
* COSY signature is built from, and duplicating the PAT-vs-OAuth branch would
|
|
1356
|
+
* let the two drift.
|
|
1357
|
+
*/
|
|
1358
|
+
export async function resolveQoderAuth(input: {
|
|
1359
|
+
readonly secret: string;
|
|
1360
|
+
readonly authState: unknown;
|
|
1361
|
+
readonly signal: AbortSignal;
|
|
1362
|
+
readonly fetcher: (url: string, init: RequestInit) => Promise<Response>;
|
|
1363
|
+
/** Which deployment mints the job token; a PAT is realm-bound. */
|
|
1364
|
+
readonly realm?: QoderRealm;
|
|
1365
|
+
}): Promise<QoderAuth> {
|
|
1366
|
+
const realm = input.realm ?? "intl";
|
|
1367
|
+
if (isQoderPat(input.secret)) {
|
|
1368
|
+
return await exchangeQoderPat(input.secret, input.signal, input.fetcher, getQoderVersion(), realm);
|
|
1369
|
+
}
|
|
1370
|
+
// Discovery is handed only the secret and the persisted state, so the
|
|
1371
|
+
// account id is read from that state — the same place `qoderOAuthAuth` reads
|
|
1372
|
+
// it — rather than being a separate field callers can pass wrongly.
|
|
1373
|
+
const state = parseQoderOAuthState(input.authState);
|
|
1374
|
+
return qoderOAuthAuth(input.secret, input.authState, state?.userId);
|
|
1375
|
+
}
|