cartethyia 2.7.6 → 2.7.8

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.
Files changed (193) hide show
  1. package/.env.example +4 -2
  2. package/README.md +26 -2
  3. package/dist/dashboard/assets/{BannedPage-B1N603cM.js → BannedPage-DeIbIiBU.js} +1 -1
  4. package/dist/dashboard/assets/BannedPage-DeIbIiBU.js.gz +0 -0
  5. package/dist/dashboard/assets/{CliToolDetailPage-9q4_4NoK.js → CliToolDetailPage-BRWKPQPf.js} +2 -2
  6. package/dist/dashboard/assets/CliToolDetailPage-BRWKPQPf.js.gz +0 -0
  7. package/dist/dashboard/assets/{CliToolsPage-DwshGtK-.js → CliToolsPage-BP-Y5iHN.js} +1 -1
  8. package/dist/dashboard/assets/CliToolsPage-BP-Y5iHN.js.gz +0 -0
  9. package/dist/dashboard/assets/{CombosPage-D762MIkp.js → CombosPage-B7JHozVa.js} +1 -1
  10. package/dist/dashboard/assets/CombosPage-B7JHozVa.js.gz +0 -0
  11. package/dist/dashboard/assets/{ConsoleLogPage-KRYjHZsX.js → ConsoleLogPage-GX7iGGcu.js} +1 -1
  12. package/dist/dashboard/assets/ConsoleLogPage-GX7iGGcu.js.gz +0 -0
  13. package/dist/dashboard/assets/CustomizationPage-HVonbGEa.js +1 -0
  14. package/dist/dashboard/assets/CustomizationPage-HVonbGEa.js.gz +0 -0
  15. package/dist/dashboard/assets/HealthEventsModal-xaut4OJ9.js +1 -0
  16. package/dist/dashboard/assets/HealthEventsModal-xaut4OJ9.js.gz +0 -0
  17. package/dist/dashboard/assets/{InjectSystemPromptPanel-d5v3K_Yc.js → InjectSystemPromptPanel-Cv-2Ol_K.js} +2 -2
  18. package/dist/dashboard/assets/InjectSystemPromptPanel-Cv-2Ol_K.js.gz +0 -0
  19. package/dist/dashboard/assets/{LoginPage-Ccv4tjNS.js → LoginPage-bKfz_b1K.js} +1 -1
  20. package/dist/dashboard/assets/LoginPage-bKfz_b1K.js.gz +0 -0
  21. package/dist/dashboard/assets/{ModelPicker-C6WlpU5E.js → ModelPicker-B-WQxx_H.js} +1 -1
  22. package/dist/dashboard/assets/ModelPicker-B-WQxx_H.js.gz +0 -0
  23. package/dist/dashboard/assets/{OverviewPage-DN9fYNWA.js → OverviewPage-Z9pJZFo8.js} +1 -1
  24. package/dist/dashboard/assets/OverviewPage-Z9pJZFo8.js.gz +0 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-CMeDBxR1.js +7 -0
  26. package/dist/dashboard/assets/ProviderDetailPage-CMeDBxR1.js.gz +0 -0
  27. package/dist/dashboard/assets/ProvidersPage-BXPdOpG4.js +1 -0
  28. package/dist/dashboard/assets/ProvidersPage-BXPdOpG4.js.gz +0 -0
  29. package/dist/dashboard/assets/{ProxyPage-DxjvUzSD.js → ProxyPage-emlTuxz6.js} +1 -1
  30. package/dist/dashboard/assets/ProxyPage-emlTuxz6.js.gz +0 -0
  31. package/dist/dashboard/assets/QuotaPage-CMJKNYWb.js +1 -0
  32. package/dist/dashboard/assets/QuotaPage-CMJKNYWb.js.gz +0 -0
  33. package/dist/dashboard/assets/{SettingsPage-BW4AKj2H.js → SettingsPage-BSy7EwGo.js} +1 -1
  34. package/dist/dashboard/assets/SettingsPage-BSy7EwGo.js.gz +0 -0
  35. package/dist/dashboard/assets/{SetupPage-44BZrxFC.js → SetupPage-CUZrKs9r.js} +1 -1
  36. package/dist/dashboard/assets/SetupPage-CUZrKs9r.js.gz +0 -0
  37. package/dist/dashboard/assets/{StudioPage-DRPavCdU.js → StudioPage-jWmUdDpy.js} +2 -2
  38. package/dist/dashboard/assets/StudioPage-jWmUdDpy.js.gz +0 -0
  39. package/dist/dashboard/assets/{UsagePage-B7M7LvMM.js → UsagePage-5F9tGVGG.js} +2 -2
  40. package/dist/dashboard/assets/UsagePage-5F9tGVGG.js.gz +0 -0
  41. package/dist/dashboard/assets/app-BEsU8hKs.css +2 -0
  42. package/dist/dashboard/assets/app-BEsU8hKs.css.gz +0 -0
  43. package/dist/dashboard/assets/{badge-BaX2GTrn.js → badge-DyI_mJfi.js} +1 -1
  44. package/dist/dashboard/assets/badge-DyI_mJfi.js.gz +0 -0
  45. package/dist/dashboard/assets/{banner-DFH4Bkte.js → banner-B-yXtCqb.js} +1 -1
  46. package/dist/dashboard/assets/banner-B-yXtCqb.js.gz +0 -0
  47. package/dist/dashboard/assets/{card-CxDw0lK6.js → card-_HwAEqFF.js} +1 -1
  48. package/dist/dashboard/assets/card-_HwAEqFF.js.gz +0 -0
  49. package/dist/dashboard/assets/{clipboard-button-DfS1ntiq.js → clipboard-button-ByjCk0o9.js} +1 -1
  50. package/dist/dashboard/assets/clipboard-button-ByjCk0o9.js.gz +0 -0
  51. package/dist/dashboard/assets/{dialog-BK-NRr5u.js → dialog-BZhOlfz-.js} +1 -1
  52. package/dist/dashboard/assets/dialog-BZhOlfz-.js.gz +0 -0
  53. package/dist/dashboard/assets/{drawer-QqPul6sC.js → drawer-BADqIRYP.js} +1 -1
  54. package/dist/dashboard/assets/drawer-BADqIRYP.js.gz +0 -0
  55. package/dist/dashboard/assets/{entry-BMlYJVFj.js → entry-COaC-irN.js} +1 -1
  56. package/dist/dashboard/assets/entry-COaC-irN.js.gz +0 -0
  57. package/dist/dashboard/assets/{entry-D6WvojeD.js → entry-Ca7LSCet.js} +3 -3
  58. package/dist/dashboard/assets/entry-Ca7LSCet.js.gz +0 -0
  59. package/dist/dashboard/assets/{entry-Dmx7gBiN.js → entry-PDd27uCd.js} +1 -1
  60. package/dist/dashboard/assets/entry-PDd27uCd.js.gz +0 -0
  61. package/dist/dashboard/assets/formatters-DcHJyRiu.js +1 -0
  62. package/dist/dashboard/assets/formatters-DcHJyRiu.js.gz +0 -0
  63. package/dist/dashboard/assets/{index-Ko4dLRnV.js → index-CgKmFngv.js} +2 -2
  64. package/dist/dashboard/assets/index-CgKmFngv.js.gz +0 -0
  65. package/dist/dashboard/assets/{inline-CHJQwxLa.js → inline-B2BirKry.js} +1 -1
  66. package/dist/dashboard/assets/inline-B2BirKry.js.gz +0 -0
  67. package/dist/dashboard/assets/{input-BlmlizVV.js → input-D-qV9dKY.js} +1 -1
  68. package/dist/dashboard/assets/input-D-qV9dKY.js.gz +0 -0
  69. package/dist/dashboard/assets/{layout-Bz93I-yE.js → layout-B7tVWhtV.js} +1 -1
  70. package/dist/dashboard/assets/layout-B7tVWhtV.js.gz +0 -0
  71. package/dist/dashboard/assets/{list-row-Ml16KCfu.js → list-row-CtkGE3bF.js} +1 -1
  72. package/dist/dashboard/assets/list-row-CtkGE3bF.js.gz +0 -0
  73. package/dist/dashboard/assets/{live-7_QpnYGu.js → live-DGizF06-.js} +1 -1
  74. package/dist/dashboard/assets/live-DGizF06-.js.gz +0 -0
  75. package/dist/dashboard/assets/{network-CuMDaTzT.js → network-C9UE-bOr.js} +1 -1
  76. package/dist/dashboard/assets/network-C9UE-bOr.js.gz +0 -0
  77. package/dist/dashboard/assets/{numbers-CoqpaDMx.js → numbers-BnAd22a_.js} +1 -1
  78. package/dist/dashboard/assets/numbers-BnAd22a_.js.gz +0 -0
  79. package/dist/dashboard/assets/{pagination-B5aOoweZ.js → pagination-BAywyMzS.js} +1 -1
  80. package/dist/dashboard/assets/pagination-BAywyMzS.js.gz +0 -0
  81. package/dist/dashboard/assets/{select-BHqpd-bg.js → select-BCOcv8WK.js} +1 -1
  82. package/dist/dashboard/assets/select-BCOcv8WK.js.gz +0 -0
  83. package/dist/dashboard/assets/{stack-j7cXjgYX.js → stack-ByKt3Q1g.js} +1 -1
  84. package/dist/dashboard/assets/stack-ByKt3Q1g.js.gz +0 -0
  85. package/dist/dashboard/assets/{state-DHmEnLa2.js → state-DXbGTx7N.js} +1 -1
  86. package/dist/dashboard/assets/state-DXbGTx7N.js.gz +0 -0
  87. package/dist/dashboard/assets/{switch-DmZcyTIC.js → switch-ZeTsW623.js} +1 -1
  88. package/dist/dashboard/assets/switch-ZeTsW623.js.gz +0 -0
  89. package/dist/dashboard/index.html +2 -2
  90. package/dist/dashboard/index.html.gz +0 -0
  91. package/dist/dashboard/precompressed-assets.json +44 -44
  92. package/migrations/full-postgres/0043_add_provider_custom_user_agent.sql +14 -0
  93. package/migrations/lite-sqlite3/0043_add_provider_custom_user_agent.sql +10 -0
  94. package/package.json +3 -1
  95. package/src/config/security.ts +1 -1
  96. package/src/config/spec.ts +13 -6
  97. package/src/console/cli-tools/cartethyia-websearch-plugin.ts +186 -0
  98. package/src/console/cli-tools/injectors/opencode.ts +42 -14
  99. package/src/console/providers/catalog/contracts.ts +4 -0
  100. package/src/console/providers/catalog/routes.ts +1 -0
  101. package/src/console/providers/detail/routes.ts +8 -0
  102. package/src/console/providers/detail/store.ts +4 -0
  103. package/src/console/quota/account-quota-tenant.ts +74 -0
  104. package/src/observability/telemetry-buffer.ts +2 -1
  105. package/src/persistence/boot.ts +13 -4
  106. package/src/persistence/schema.ts +7 -1
  107. package/src/protocol/canonical-model.ts +7 -0
  108. package/src/protocol/request/gemini.ts +20 -1
  109. package/src/protocol/tool-identity.ts +15 -1
  110. package/src/providers/default-registry.ts +8 -2
  111. package/src/providers/discovery/discovery-types.ts +18 -0
  112. package/src/providers/discovery/probe-phases.ts +45 -0
  113. package/src/providers/discovery/probing-service.ts +41 -6
  114. package/src/providers/integrations/antigravity/antigravity-protocol.ts +20 -2
  115. package/src/providers/integrations/antigravity/antigravity.ts +24 -0
  116. package/src/providers/integrations/codex/codex.ts +28 -1
  117. package/src/providers/integrations/qoder/chat-proto.ts +328 -0
  118. package/src/providers/integrations/qoder/claim-qoder-credits.ts +258 -0
  119. package/src/providers/integrations/qoder/qoder-adapter.ts +275 -0
  120. package/src/providers/integrations/qoder/qoder-auth.ts +201 -0
  121. package/src/providers/integrations/qoder/qoder-body.ts +78 -0
  122. package/src/providers/integrations/qoder/qoder-catalog.ts +5 -1
  123. package/src/providers/integrations/qoder/qoder-chat.ts +514 -0
  124. package/src/providers/integrations/qoder/qoder-cosy.ts +162 -0
  125. package/src/providers/integrations/qoder/qoder-grpc.ts +667 -0
  126. package/src/providers/integrations/qoder/qoder-ide.ts +584 -0
  127. package/src/providers/integrations/qoder/qoder-oauth.ts +4 -4
  128. package/src/providers/integrations/qoder/qoder-quota.ts +1 -1
  129. package/src/providers/integrations/qoder/qoder-realm.ts +40 -4
  130. package/src/providers/integrations/qoder/qoder-types.ts +51 -0
  131. package/src/providers/integrations/qoder/qoder-umid.ts +123 -8
  132. package/src/providers/integrations/qoder/qoder.ts +147 -1889
  133. package/src/providers/operations/byok-wire-profile.ts +9 -12
  134. package/src/providers/provider-contracts.ts +7 -0
  135. package/src/providers/provider-usage.ts +9 -6
  136. package/src/security/abuse.ts +5 -5
  137. package/src/transport/dispatch/upstream.ts +4 -2
  138. package/src/transport/routing/route-catalog.ts +8 -0
  139. package/src/transport/routing/route-model.ts +4 -0
  140. package/src/transport/routing/router.ts +8 -117
  141. package/dist/dashboard/assets/BannedPage-B1N603cM.js.gz +0 -0
  142. package/dist/dashboard/assets/CliToolDetailPage-9q4_4NoK.js.gz +0 -0
  143. package/dist/dashboard/assets/CliToolsPage-DwshGtK-.js.gz +0 -0
  144. package/dist/dashboard/assets/CombosPage-D762MIkp.js.gz +0 -0
  145. package/dist/dashboard/assets/ConsoleLogPage-KRYjHZsX.js.gz +0 -0
  146. package/dist/dashboard/assets/CustomizationPage-DXsxk5uM.js +0 -1
  147. package/dist/dashboard/assets/CustomizationPage-DXsxk5uM.js.gz +0 -0
  148. package/dist/dashboard/assets/HealthEventsModal-BEjAjD4F.js +0 -1
  149. package/dist/dashboard/assets/HealthEventsModal-BEjAjD4F.js.gz +0 -0
  150. package/dist/dashboard/assets/InjectSystemPromptPanel-d5v3K_Yc.js.gz +0 -0
  151. package/dist/dashboard/assets/LoginPage-Ccv4tjNS.js.gz +0 -0
  152. package/dist/dashboard/assets/ModelPicker-C6WlpU5E.js.gz +0 -0
  153. package/dist/dashboard/assets/OverviewPage-DN9fYNWA.js.gz +0 -0
  154. package/dist/dashboard/assets/ProviderDetailPage-BY0ieUgz.js +0 -5
  155. package/dist/dashboard/assets/ProviderDetailPage-BY0ieUgz.js.gz +0 -0
  156. package/dist/dashboard/assets/ProvidersPage-DNsbfea2.js +0 -1
  157. package/dist/dashboard/assets/ProvidersPage-DNsbfea2.js.gz +0 -0
  158. package/dist/dashboard/assets/ProxyPage-DxjvUzSD.js.gz +0 -0
  159. package/dist/dashboard/assets/QuotaPage-OaHScjpX.js +0 -1
  160. package/dist/dashboard/assets/QuotaPage-OaHScjpX.js.gz +0 -0
  161. package/dist/dashboard/assets/SettingsPage-BW4AKj2H.js.gz +0 -0
  162. package/dist/dashboard/assets/SetupPage-44BZrxFC.js.gz +0 -0
  163. package/dist/dashboard/assets/StudioPage-DRPavCdU.js.gz +0 -0
  164. package/dist/dashboard/assets/UsagePage-B7M7LvMM.js.gz +0 -0
  165. package/dist/dashboard/assets/app-mweRcsKp.css +0 -2
  166. package/dist/dashboard/assets/app-mweRcsKp.css.gz +0 -0
  167. package/dist/dashboard/assets/badge-BaX2GTrn.js.gz +0 -0
  168. package/dist/dashboard/assets/banner-DFH4Bkte.js.gz +0 -0
  169. package/dist/dashboard/assets/card-CxDw0lK6.js.gz +0 -0
  170. package/dist/dashboard/assets/clipboard-button-DfS1ntiq.js.gz +0 -0
  171. package/dist/dashboard/assets/dialog-BK-NRr5u.js.gz +0 -0
  172. package/dist/dashboard/assets/drawer-QqPul6sC.js.gz +0 -0
  173. package/dist/dashboard/assets/entry-BMlYJVFj.js.gz +0 -0
  174. package/dist/dashboard/assets/entry-D6WvojeD.js.gz +0 -0
  175. package/dist/dashboard/assets/entry-Dmx7gBiN.js.gz +0 -0
  176. package/dist/dashboard/assets/formatters-u7JPB5BX.js +0 -1
  177. package/dist/dashboard/assets/formatters-u7JPB5BX.js.gz +0 -0
  178. package/dist/dashboard/assets/index-Ko4dLRnV.js.gz +0 -0
  179. package/dist/dashboard/assets/inline-CHJQwxLa.js.gz +0 -0
  180. package/dist/dashboard/assets/input-BlmlizVV.js.gz +0 -0
  181. package/dist/dashboard/assets/layout-Bz93I-yE.js.gz +0 -0
  182. package/dist/dashboard/assets/list-row-Ml16KCfu.js.gz +0 -0
  183. package/dist/dashboard/assets/live-7_QpnYGu.js.gz +0 -0
  184. package/dist/dashboard/assets/network-CuMDaTzT.js.gz +0 -0
  185. package/dist/dashboard/assets/numbers-CoqpaDMx.js.gz +0 -0
  186. package/dist/dashboard/assets/pagination-B5aOoweZ.js.gz +0 -0
  187. package/dist/dashboard/assets/select-BHqpd-bg.js.gz +0 -0
  188. package/dist/dashboard/assets/stack-j7cXjgYX.js.gz +0 -0
  189. package/dist/dashboard/assets/state-DHmEnLa2.js.gz +0 -0
  190. package/dist/dashboard/assets/switch-DmZcyTIC.js.gz +0 -0
  191. package/src/providers/integrations/qoder/qoder-text-tools.ts +0 -277
  192. /package/dist/dashboard/assets/{app-DRnQLhsc.js → app-CTxoqyYB.js} +0 -0
  193. /package/dist/dashboard/assets/{app-DRnQLhsc.js.gz → app-CTxoqyYB.js.gz} +0 -0
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Qoder credential resolution & PAT→jobToken exchange.
3
+ *
4
+ * Device tokens (`dt-…`) from OAuth login are used directly; a PAT
5
+ * (`pt-…`) must be exchanged for a short-lived job token (`jt-…`) on the
6
+ * realm's job-token endpoint before it can authenticate any surface.
7
+ *
8
+ * The exchange is the modern v1 endpoint (`/api/v1/jobToken/exchange`) with
9
+ * a plain JSON body — verified live (2026-11-05): `POST
10
+ * center.qoder.sh/api/v1/jobToken/exchange` with `{personal_token, machine_id}`
11
+ * returns `{token: jt-…, refresh_token: jrt-…, expires_at}`. The old v3
12
+ * scrambled-COSY endpoint is retired. `X-Request-ID` is sent uppercase and
13
+ * the User-Agent is `qoder/<version>` — both mirror the real client.
14
+ *
15
+ * The v1 response does not include the account's user id, so `userId`
16
+ * falls back to the machine id (decision 2026-11-05). That is sufficient
17
+ * for Bearer surfaces (gRPC / IDE); COSY signing (free/lite PAT) uses it
18
+ * as the identity placeholder until the user id is sourced another way.
19
+ */
20
+ import { createHash, randomUUID } from "node:crypto";
21
+ import { GatewayError } from "../../../protocol/gateway-error";
22
+ import { mapUpstreamHttpError } from "../../../transport/failure-policy";
23
+ import { getQoderVersion } from "../../operations/client-versions";
24
+ import { qoderMachineOs } from "../../operations/cli-platform";
25
+ import { parseQoderOAuthState } from "./qoder-oauth";
26
+ import { QODER_REALMS, type QoderRealm } from "./qoder-realm";
27
+ import type { QoderAuth } from "./qoder-types";
28
+
29
+ /** A PAT (`pt-…`) must be exchanged for a job token before use. */
30
+ export function isQoderPat(secret: string): boolean {
31
+ return secret.startsWith("pt-");
32
+ }
33
+
34
+ /**
35
+ * Stable per-account machine id for OAuth dispatches. Derived from the
36
+ * account id (not minted per request) so pooled accounts keep one device
37
+ * fingerprint across restarts instead of looking like a new device every
38
+ * time, while staying distinct per account.
39
+ */
40
+ export function qoderMachineIdForAccount(accountId: string): string {
41
+ const hex = createHash("sha256").update(`qoder-machine-v1:${accountId}`).digest("hex");
42
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
43
+ }
44
+
45
+ /**
46
+ * Builds dispatch auth from an OAuth device token. The token is used
47
+ * directly as the COSY identity's oauth token; the user id and machine id
48
+ * come from the `auth_state` the login persisted. Fails closed with a
49
+ * reconnect prompt when the identity is absent (e.g. an account created
50
+ * before `auth_state` existed, or an import that dropped the field) instead
51
+ * of signing with a guessed id. The message names the missing field so an
52
+ * operator can fix the export instead of re-logging blind.
53
+ */
54
+ export function qoderOAuthAuth(
55
+ secret: string,
56
+ authState: unknown,
57
+ accountId: string | undefined,
58
+ ): QoderAuth {
59
+ const state = parseQoderOAuthState(authState);
60
+ if (state === undefined) {
61
+ throw new GatewayError(
62
+ "authentication_failed",
63
+ 401,
64
+ "Qoder OAuth account is missing its user identity (request {requestId}) (auth_state.userId/machineId absent or empty — re-login, or re-export with authState included); sign in again. Wrong key, right door — check the credential 🔑",
65
+ );
66
+ }
67
+ return {
68
+ userId: state.userId,
69
+ userName: "",
70
+ userType: "personal_standard",
71
+ securityOauthToken: secret,
72
+ refreshToken: "",
73
+ machineId: state.machineId || (accountId ? qoderMachineIdForAccount(accountId) : randomUUID()),
74
+ };
75
+ }
76
+
77
+ const QODER_MACHINE_ID_TTL_MS = 60 * 60 * 1000;
78
+ const QODER_MAX_MACHINE_IDS = 1_024;
79
+ const qoderMachineIds = new Map<string, { id: string; expiresAt: number }>();
80
+
81
+ /**
82
+ * Stable pseudo-device id per PAT for the whole hour (pool-friendly): a PAT
83
+ * keeps one machine id across requests within the TTL instead of minting a
84
+ * fresh device each time.
85
+ */
86
+ function qoderMachineIdFromPat(pat: string): string {
87
+ const key = createHash("sha256").update(pat).digest("hex");
88
+ const now = Date.now();
89
+ const existing = qoderMachineIds.get(key);
90
+ if (existing && existing.expiresAt > now) return existing.id;
91
+ const id = randomUUID();
92
+ qoderMachineIds.delete(key);
93
+ qoderMachineIds.set(key, { id, expiresAt: now + QODER_MACHINE_ID_TTL_MS });
94
+ for (const [candidate, value] of qoderMachineIds) {
95
+ if (value.expiresAt <= now) qoderMachineIds.delete(candidate);
96
+ }
97
+ while (qoderMachineIds.size > QODER_MAX_MACHINE_IDS) {
98
+ const oldest = qoderMachineIds.keys().next();
99
+ if (oldest.done) break;
100
+ qoderMachineIds.delete(oldest.value);
101
+ }
102
+ return id;
103
+ }
104
+
105
+ /**
106
+ * Stable 32-hex session id for one user+model conversation. Mirrors the
107
+ * live IDE surface (`X-Session-ID` = sha256(userId:model), 32 hex) and is
108
+ * shared by the HTTP and gRPC transports so body `session_id` matches the
109
+ * transport-level session header.
110
+ */
111
+ export function qoderSessionId(userId: string, modelId: string): string {
112
+ return createHash("sha256").update(`qoder-session:${userId}:${modelId}`).digest("hex").slice(0, 32);
113
+ }
114
+
115
+ type QoderFetcher = (url: string, init: RequestInit) => Promise<Response>;
116
+
117
+ /**
118
+ * Exchanges a PAT (`pt-…`) for a short-lived job token (`jt-…`) using the
119
+ * modern v1 endpoint. Verified live (2026-11-05): `POST
120
+ * center.qoder.sh/api/v1/jobToken/exchange` with a plain JSON body returns
121
+ * `{token: jt-…, refresh_token: jrt-…, expires_at}`. `X-Request-ID` is
122
+ * sent uppercase and the User-Agent is `qoder/<version>` — both mirror the
123
+ * real client.
124
+ *
125
+ * The v1 response does not include the account's user id, so `userId`
126
+ * falls back to the machine id (decision 2026-11-05). That is sufficient
127
+ * for Bearer surfaces (gRPC / IDE); COSY signing (free/lite PAT) uses it
128
+ * as the identity placeholder until the user id is sourced another way.
129
+ */
130
+ async function exchangeQoderPat(
131
+ pat: string,
132
+ signal: AbortSignal,
133
+ fetcher: QoderFetcher,
134
+ version = getQoderVersion(),
135
+ realm: QoderRealm = "intl",
136
+ ): Promise<QoderAuth> {
137
+ const machineId = qoderMachineIdFromPat(pat);
138
+ const response = await fetcher(QODER_REALMS[realm].jobTokenUrl, {
139
+ method: "POST",
140
+ headers: {
141
+ "Content-Type": "application/json",
142
+ Accept: "application/json",
143
+ "User-Agent": `qoder/${version}`,
144
+ "X-Request-ID": randomUUID().toUpperCase(),
145
+ "Cosy-ClientType": "5",
146
+ "Cosy-Version": version,
147
+ "Cosy-MachineOS": qoderMachineOs(),
148
+ },
149
+ body: JSON.stringify({ personal_token: pat, machine_id: machineId }),
150
+ signal,
151
+ });
152
+ if (!response.ok) throw await mapUpstreamHttpError(response, "qoder");
153
+ let responseBody: unknown;
154
+ try {
155
+ responseBody = await response.json();
156
+ } catch {
157
+ throw new GatewayError("platform_unavailable", 502, "Qoder PAT exchange returned invalid JSON (request {requestId}). The upstream came back empty-handed — retry 🌙", {}, "upstream");
158
+ }
159
+ if (typeof responseBody !== "object" || responseBody === null || Array.isArray(responseBody)) {
160
+ throw new GatewayError("platform_unavailable", 502, "Qoder PAT exchange returned unexpected response (request {requestId}). The upstream came back empty-handed — retry 🌙", {}, "upstream");
161
+ }
162
+ const rec = responseBody as Record<string, unknown>;
163
+ const token = typeof rec["token"] === "string" ? (rec["token"] as string) : "";
164
+ if (!token) {
165
+ throw new GatewayError("platform_unavailable", 502, "Qoder PAT exchange returned incomplete credentials (request {requestId}). The upstream came back empty-handed — retry 🌙", {}, "upstream");
166
+ }
167
+ return {
168
+ userId: machineId,
169
+ userName: typeof rec["name"] === "string" ? (rec["name"] as string) : "",
170
+ userType: typeof rec["userType"] === "string" ? (rec["userType"] as string) : "personal_standard",
171
+ securityOauthToken: token,
172
+ refreshToken: typeof rec["refresh_token"] === "string" ? (rec["refresh_token"] as string) : "",
173
+ machineId,
174
+ };
175
+ }
176
+
177
+ /**
178
+ * Resolves the dispatch identity for one Qoder credential.
179
+ *
180
+ * Shared by the adapter and by discovery: both need the same `QoderAuth` the
181
+ * COSY signature is built from, and duplicating the PAT-vs-OAuth branch would
182
+ * let the two drift.
183
+ */
184
+ export async function resolveQoderAuth(input: {
185
+ readonly secret: string;
186
+ readonly authState: unknown;
187
+ readonly signal: AbortSignal;
188
+ readonly fetcher: (url: string, init: RequestInit) => Promise<Response>;
189
+ /** Which deployment mints the job token; a PAT is realm-bound. */
190
+ readonly realm?: QoderRealm;
191
+ }): Promise<QoderAuth> {
192
+ const realm = input.realm ?? "intl";
193
+ if (isQoderPat(input.secret)) {
194
+ return await exchangeQoderPat(input.secret, input.signal, input.fetcher, getQoderVersion(), realm);
195
+ }
196
+ // Discovery is handed only the secret and the persisted state, so the
197
+ // account id is read from that state — the same place `qoderOAuthAuth` reads
198
+ // it — rather than being a separate field callers can pass wrongly.
199
+ const state = parseQoderOAuthState(input.authState);
200
+ return qoderOAuthAuth(input.secret, input.authState, state?.userId);
201
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Shared canonical→Qoder message helpers.
3
+ *
4
+ * Kept in a separate module so both transports (`qoder.ts` HTTP and
5
+ * `qoder-grpc.ts`) build and flatten message content the same way.
6
+ */
7
+ import type { ContentPart, ToolChoice, ToolDefinition } from "../../../protocol/canonical-model";
8
+
9
+ /** Joins text/tool-result content of one canonical message into plain text. */
10
+ export function flattenCanonicalContent(content: readonly ContentPart[]): string {
11
+ return content
12
+ .filter((b) => b.kind === "text" || b.kind === "toolResult")
13
+ .map((b) => {
14
+ if (b.kind === "text") return b.text;
15
+ if (b.kind === "toolResult") return typeof b.content === "string" ? b.content : JSON.stringify(b.content);
16
+ return "";
17
+ })
18
+ .join("\n");
19
+ }
20
+
21
+ /**
22
+ * Projects the canonical tools onto Qoder's chat-completions `tools` array
23
+ * (the same OpenAI-shaped declaration both the HTTP and the gRPC surface
24
+ * expect).
25
+ *
26
+ * Without this the upstream is never told a tool exists, so a model that wants
27
+ * one improvises markup in its prose (`<Read file_path="..." />`) instead of
28
+ * emitting a structured call — the call is then neither runnable nor
29
+ * recoverable, because it never was a call on either side.
30
+ *
31
+ * Only `function` tools are representable upstream. Anything else is dropped
32
+ * rather than forwarded: Qoder rejects an unknown variant during
33
+ * deserialization and fails the whole turn, which is worse than omitting a
34
+ * tool the model could not have used anyway.
35
+ */
36
+ export function qoderTools(
37
+ tools: readonly ToolDefinition[] | undefined,
38
+ ): Array<Record<string, unknown>> {
39
+ const out: Array<Record<string, unknown>> = [];
40
+ for (const tool of tools ?? []) {
41
+ if (tool.tool_type !== undefined && tool.tool_type !== "function") continue;
42
+ // The upstream requires `tools[].function` to be an object. A flat
43
+ // declaration naming the tool at the top level is rejected outright with
44
+ // "'function' is a required property, expected an object", which fails the
45
+ // whole request rather than just this tool.
46
+ out.push({
47
+ type: "function",
48
+ function: {
49
+ name: tool.name,
50
+ ...(tool.description === undefined ? { description: "" } : { description: tool.description }),
51
+ parameters: tool.jsonSchema ?? { type: "object", properties: {} },
52
+ ...(tool.strict === undefined ? {} : { strict: tool.strict }),
53
+ },
54
+ });
55
+ }
56
+ return out;
57
+ }
58
+
59
+ /**
60
+ * Projects the canonical tool choice onto the upstream's field.
61
+ *
62
+ * The upstream only accepts a string here (both surfaces). Given the object
63
+ * form `{type:"function",function:{name}}` it ignores the field, and the model
64
+ * then calls the tool with no arguments filled in.
65
+ *
66
+ * `allowed_tools` has no chat-completions equivalent, so it collapses to the
67
+ * direction it already names rather than being dropped: a caller that asked for
68
+ * `required` must not silently get `auto`.
69
+ */
70
+ export function qoderToolChoice(choice: ToolChoice | undefined): unknown {
71
+ if (choice === undefined) return undefined;
72
+ if (typeof choice === "string") return choice;
73
+ if (choice.type === "tool" || choice.type === "custom") {
74
+ const name = choice.name.trim();
75
+ return name.length > 0 ? name : "auto";
76
+ }
77
+ return choice.mode;
78
+ }
@@ -147,7 +147,11 @@ export const QODER_INTL_MODEL_CONFIGS: Readonly<Record<string, QoderModelConfig>
147
147
  smodel: { id: "smodel", display_name: "Sonus", max_input_tokens: 1000000, max_output_tokens: 128_000, is_vl: true, is_free: false, is_reasoning: true, default_reasoning_effort: "high" },
148
148
  cmodel: { id: "cmodel", display_name: "Cantus", max_input_tokens: 1000000, max_output_tokens: 128_000, is_vl: true, is_free: false, is_reasoning: true, default_reasoning_effort: "high" },
149
149
  qmodel_38max: { id: "qmodel_38max", display_name: "Qwen3.8-Max", max_input_tokens: 1000000, max_output_tokens: 131_072, is_vl: true, is_reasoning: true },
150
- qfmodel: { id: "qfmodel", display_name: "Qwen3.8-Flash", max_input_tokens: 1000000, max_output_tokens: 131_072, is_vl: true, is_free: true, is_reasoning: true },
150
+ // `qfmodel` is billed, not free: upstream's live `is_free: true` for this
151
+ // slot is a plan-tier flag, not a zero-cost tier — a free account gets
152
+ // `quota exceeded` (116) when dispatching it. The only truly free slot is
153
+ // `lite`. Do not re-add `is_free` here.
154
+ qfmodel: { id: "qfmodel", display_name: "Qwen3.8-Flash", max_input_tokens: 1000000, max_output_tokens: 131_072, is_vl: true, is_reasoning: true },
151
155
  qmodel_latest: { id: "qmodel_latest", display_name: "Qwen3.7-Max", max_input_tokens: 1000000, max_output_tokens: 131_072, is_vl: true, is_reasoning: false },
152
156
  qmodel: { id: "qmodel", display_name: "Qwen3.7-Plus", max_input_tokens: 1000000, max_output_tokens: 131_072, is_vl: true, is_reasoning: false },
153
157
  kmodel_latest: { id: "kmodel_latest", display_name: "Kimi-K3.1-Pro", max_input_tokens: 1000000, max_output_tokens: 131_072, is_vl: true, is_free: false, is_reasoning: false },