cartethyia 2.7.8 → 2.8.0

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 (519) hide show
  1. package/.env.example +3 -3
  2. package/README.md +29 -59
  3. package/dist/dashboard/assets/BannedPage-Cc0vKpMG.js +1 -0
  4. package/dist/dashboard/assets/BannedPage-Cc0vKpMG.js.gz +0 -0
  5. package/dist/dashboard/assets/{CliToolDetailPage-BRWKPQPf.js → CliToolDetailPage-Bqj2aa1T.js} +1 -1
  6. package/dist/dashboard/assets/CliToolDetailPage-Bqj2aa1T.js.gz +0 -0
  7. package/dist/dashboard/assets/{CliToolsPage-BP-Y5iHN.js → CliToolsPage-CXsaOAO2.js} +1 -1
  8. package/dist/dashboard/assets/CliToolsPage-CXsaOAO2.js.gz +0 -0
  9. package/dist/dashboard/assets/CombosPage-BeLpJ4ma.js +1 -0
  10. package/dist/dashboard/assets/CombosPage-BeLpJ4ma.js.gz +0 -0
  11. package/dist/dashboard/assets/ConsoleLogPage-DnV0uZ3m.js +1 -0
  12. package/dist/dashboard/assets/ConsoleLogPage-DnV0uZ3m.js.gz +0 -0
  13. package/dist/dashboard/assets/CustomizationPage-CBxAjo63.js +1 -0
  14. package/dist/dashboard/assets/CustomizationPage-CBxAjo63.js.gz +0 -0
  15. package/dist/dashboard/assets/HealthEventsModal-B5yEE0k6.js +1 -0
  16. package/dist/dashboard/assets/HealthEventsModal-B5yEE0k6.js.gz +0 -0
  17. package/dist/dashboard/assets/{InjectSystemPromptPanel-Cv-2Ol_K.js → InjectSystemPromptPanel-BWFLECmz.js} +2 -2
  18. package/dist/dashboard/assets/InjectSystemPromptPanel-BWFLECmz.js.gz +0 -0
  19. package/dist/dashboard/assets/{LoginPage-bKfz_b1K.js → LoginPage-CgoPc5B9.js} +1 -1
  20. package/dist/dashboard/assets/LoginPage-CgoPc5B9.js.gz +0 -0
  21. package/dist/dashboard/assets/{ModelPicker-B-WQxx_H.js → ModelPicker-D9bFQern.js} +1 -1
  22. package/dist/dashboard/assets/ModelPicker-D9bFQern.js.gz +0 -0
  23. package/dist/dashboard/assets/OverviewPage-B_p4tL8k.js +1 -0
  24. package/dist/dashboard/assets/OverviewPage-B_p4tL8k.js.gz +0 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-rs5E2rxv.js +8 -0
  26. package/dist/dashboard/assets/ProviderDetailPage-rs5E2rxv.js.gz +0 -0
  27. package/dist/dashboard/assets/ProviderIcon-D_0PiZtg.js +1 -0
  28. package/dist/dashboard/assets/ProviderIcon-D_0PiZtg.js.gz +0 -0
  29. package/dist/dashboard/assets/ProvidersPage-CLI2bJTK.js +1 -0
  30. package/dist/dashboard/assets/ProvidersPage-CLI2bJTK.js.gz +0 -0
  31. package/dist/dashboard/assets/{ProxyPage-emlTuxz6.js → ProxyPage-BwNC_-Cx.js} +1 -1
  32. package/dist/dashboard/assets/ProxyPage-BwNC_-Cx.js.gz +0 -0
  33. package/dist/dashboard/assets/QuotaPage-DA-a1iSN.js +1 -0
  34. package/dist/dashboard/assets/QuotaPage-DA-a1iSN.js.gz +0 -0
  35. package/dist/dashboard/assets/SettingsPage-CctTga2C.js +1 -0
  36. package/dist/dashboard/assets/SettingsPage-CctTga2C.js.gz +0 -0
  37. package/dist/dashboard/assets/{SetupPage-CUZrKs9r.js → SetupPage-febLU4r1.js} +1 -1
  38. package/dist/dashboard/assets/SetupPage-febLU4r1.js.gz +0 -0
  39. package/dist/dashboard/assets/{StudioPage-jWmUdDpy.js → StudioPage-M6fvR5Wz.js} +1 -1
  40. package/dist/dashboard/assets/StudioPage-M6fvR5Wz.js.gz +0 -0
  41. package/dist/dashboard/assets/UsagePage-nyeuRqz9.js +56 -0
  42. package/dist/dashboard/assets/UsagePage-nyeuRqz9.js.gz +0 -0
  43. package/dist/dashboard/assets/api-keys-BFOlvrJC.js +1 -0
  44. package/dist/dashboard/assets/api-keys-BFOlvrJC.js.gz +0 -0
  45. package/dist/dashboard/assets/app-Dx2QgsNj.css +2 -0
  46. package/dist/dashboard/assets/app-Dx2QgsNj.css.gz +0 -0
  47. package/dist/dashboard/assets/{assert-CidQVoNo.js → assert-CqTLw5e7.js} +1 -1
  48. package/dist/dashboard/assets/assert-CqTLw5e7.js.gz +0 -0
  49. package/dist/dashboard/assets/{badge-DyI_mJfi.js → badge-C83EqLB0.js} +1 -1
  50. package/dist/dashboard/assets/badge-C83EqLB0.js.gz +0 -0
  51. package/dist/dashboard/assets/ban-durations-B58FyRxC.js +1 -0
  52. package/dist/dashboard/assets/ban-durations-B58FyRxC.js.gz +0 -0
  53. package/dist/dashboard/assets/{banner-B-yXtCqb.js → banner-D1xn7pIe.js} +1 -1
  54. package/dist/dashboard/assets/banner-D1xn7pIe.js.gz +0 -0
  55. package/dist/dashboard/assets/brand/bansos_page.webp +0 -0
  56. package/dist/dashboard/assets/brand/denia_bansos.webp +0 -0
  57. package/dist/dashboard/assets/brand/favicon.webp +0 -0
  58. package/dist/dashboard/assets/brand/login_register.webp +0 -0
  59. package/dist/dashboard/assets/brand/login_register_initial.webp +0 -0
  60. package/dist/dashboard/assets/brand/og_bansos.webp +0 -0
  61. package/dist/dashboard/assets/brand/og_image.webp +0 -0
  62. package/dist/dashboard/assets/brand/sidebar_ico.webp +0 -0
  63. package/dist/dashboard/assets/{card-_HwAEqFF.js → card-DU3Lq5Lm.js} +1 -1
  64. package/dist/dashboard/assets/card-DU3Lq5Lm.js.gz +0 -0
  65. package/dist/dashboard/assets/cli-tools-Dw1U_drK.js +1 -0
  66. package/dist/dashboard/assets/cli-tools-Dw1U_drK.js.gz +0 -0
  67. package/dist/dashboard/assets/{clipboard-button-ByjCk0o9.js → clipboard-button-CEL5Sg_J.js} +1 -1
  68. package/dist/dashboard/assets/clipboard-button-CEL5Sg_J.js.gz +0 -0
  69. package/dist/dashboard/assets/{dialog-BZhOlfz-.js → dialog-BjFR5UHy.js} +1 -1
  70. package/dist/dashboard/assets/dialog-BjFR5UHy.js.gz +0 -0
  71. package/dist/dashboard/assets/{drawer-BADqIRYP.js → drawer-HvBW0AjI.js} +1 -1
  72. package/dist/dashboard/assets/drawer-HvBW0AjI.js.gz +0 -0
  73. package/dist/dashboard/assets/entry-BSu3gFlt.js +4 -0
  74. package/dist/dashboard/assets/entry-BSu3gFlt.js.gz +0 -0
  75. package/dist/dashboard/assets/{entry-COaC-irN.js → entry-CECVELyy.js} +1 -1
  76. package/dist/dashboard/assets/entry-CECVELyy.js.gz +0 -0
  77. package/dist/dashboard/assets/entry-DYYofQ7W.js +1 -0
  78. package/dist/dashboard/assets/entry-DYYofQ7W.js.gz +0 -0
  79. package/dist/dashboard/assets/{formatters-DcHJyRiu.js → formatters-DLW4i8zK.js} +1 -1
  80. package/dist/dashboard/assets/formatters-DLW4i8zK.js.gz +0 -0
  81. package/dist/dashboard/assets/{index-CgKmFngv.js → index-1-fCqe64.js} +2 -2
  82. package/dist/dashboard/assets/index-1-fCqe64.js.gz +0 -0
  83. package/dist/dashboard/assets/{inline-B2BirKry.js → inline-DGvU2oLK.js} +1 -1
  84. package/dist/dashboard/assets/inline-DGvU2oLK.js.gz +0 -0
  85. package/dist/dashboard/assets/{input-D-qV9dKY.js → input-iNnO1X2K.js} +1 -1
  86. package/dist/dashboard/assets/input-iNnO1X2K.js.gz +0 -0
  87. package/dist/dashboard/assets/{layout-B7tVWhtV.js → layout-0q8UGypb.js} +1 -1
  88. package/dist/dashboard/assets/layout-0q8UGypb.js.gz +0 -0
  89. package/dist/dashboard/assets/{list-row-CtkGE3bF.js → list-row-C0w2bQjJ.js} +1 -1
  90. package/dist/dashboard/assets/list-row-C0w2bQjJ.js.gz +0 -0
  91. package/dist/dashboard/assets/{live-DGizF06-.js → live-M5aJ_xCa.js} +1 -1
  92. package/dist/dashboard/assets/live-M5aJ_xCa.js.gz +0 -0
  93. package/dist/dashboard/assets/{mutation-GU7XdEjz.js → mutation-BQ0ccH_A.js} +1 -1
  94. package/dist/dashboard/assets/mutation-BQ0ccH_A.js.gz +0 -0
  95. package/dist/dashboard/assets/{network-C9UE-bOr.js → network-BKeQGT3K.js} +1 -1
  96. package/dist/dashboard/assets/network-BKeQGT3K.js.gz +0 -0
  97. package/dist/dashboard/assets/{numbers-BnAd22a_.js → numbers-CivAk3da.js} +1 -1
  98. package/dist/dashboard/assets/numbers-CivAk3da.js.gz +0 -0
  99. package/dist/dashboard/assets/{pagination-BAywyMzS.js → pagination-CpzvI5ep.js} +1 -1
  100. package/dist/dashboard/assets/pagination-CpzvI5ep.js.gz +0 -0
  101. package/dist/dashboard/assets/providers/devin.svg +9 -0
  102. package/dist/dashboard/assets/providers/devin.svg.gz +0 -0
  103. package/dist/dashboard/assets/providers/windsurf.svg +10 -0
  104. package/dist/dashboard/assets/providers/windsurf.svg.gz +0 -0
  105. package/dist/dashboard/assets/providers/zro.svg +3 -0
  106. package/dist/dashboard/assets/providers/zro.svg.gz +0 -0
  107. package/dist/dashboard/assets/{query-keys-YSoQOUh6.js → query-keys-IKLh0YFd.js} +1 -1
  108. package/dist/dashboard/assets/query-keys-IKLh0YFd.js.gz +0 -0
  109. package/dist/dashboard/assets/quota-DiPD6UKH.js +1 -0
  110. package/dist/dashboard/assets/quota-DiPD6UKH.js.gz +0 -0
  111. package/dist/dashboard/assets/routing-BFQX2VBc.js +1 -0
  112. package/dist/dashboard/assets/routing-BFQX2VBc.js.gz +0 -0
  113. package/dist/dashboard/assets/{select-BCOcv8WK.js → select-CXbck0jG.js} +2 -2
  114. package/dist/dashboard/assets/select-CXbck0jG.js.gz +0 -0
  115. package/dist/dashboard/assets/settings-C_XvMHh9.js +1 -0
  116. package/dist/dashboard/assets/settings-C_XvMHh9.js.gz +0 -0
  117. package/dist/dashboard/assets/{stack-ByKt3Q1g.js → stack-DggRiCxE.js} +1 -1
  118. package/dist/dashboard/assets/stack-DggRiCxE.js.gz +0 -0
  119. package/dist/dashboard/assets/{state-DXbGTx7N.js → state-ClFzfbYp.js} +1 -1
  120. package/dist/dashboard/assets/state-ClFzfbYp.js.gz +0 -0
  121. package/dist/dashboard/assets/story/25817331.webp +0 -0
  122. package/dist/dashboard/assets/story/Shorekeeper.webp +0 -0
  123. package/dist/dashboard/assets/story/cartethyia-god.webp +0 -0
  124. package/dist/dashboard/assets/story/fleurdelys_plus.webp +0 -0
  125. package/dist/dashboard/assets/story/jinhsi-blossom.webp +0 -0
  126. package/dist/dashboard/assets/story/phrolova-a.webp +0 -0
  127. package/dist/dashboard/assets/story/requestdeniawokkjpg.webp +0 -0
  128. package/dist/dashboard/assets/story/wuthering-waves-hiyuki-aemeath-hiyuki-rover.webp +0 -0
  129. package/dist/dashboard/assets/{switch-ZeTsW623.js → switch-CTIFxKqS.js} +1 -1
  130. package/dist/dashboard/assets/switch-CTIFxKqS.js.gz +0 -0
  131. package/dist/dashboard/assets/{useMutation-CbCP4w1o.js → useMutation-BClG3yWj.js} +1 -1
  132. package/dist/dashboard/assets/useMutation-BClG3yWj.js.gz +0 -0
  133. package/dist/dashboard/console-backgrounds.webp +0 -0
  134. package/dist/dashboard/console-backgrounds_initial.webp +0 -0
  135. package/dist/dashboard/donation/qris-risun-marke.webp +0 -0
  136. package/dist/dashboard/index.html +3 -3
  137. package/dist/dashboard/index.html.gz +0 -0
  138. package/dist/dashboard/precompressed-assets.json +59 -56
  139. package/migrations/full-postgres/0044_add_models_warning_metadata.sql +10 -0
  140. package/migrations/full-postgres/0045_migrate_workbuddy_to_codebuddy.sql +156 -0
  141. package/migrations/full-postgres/0046_add_accounts_quota_plan_fetched_at.sql +7 -0
  142. package/migrations/lite-sqlite3/0044_add_models_warning_metadata.sql +11 -0
  143. package/migrations/lite-sqlite3/0045_migrate_workbuddy_to_codebuddy.sql +156 -0
  144. package/migrations/lite-sqlite3/0046_add_accounts_quota_plan_fetched_at.sql +4 -0
  145. package/package.json +1 -1
  146. package/src/boot/app.ts +10 -13
  147. package/src/boot/dependencies.ts +12 -4
  148. package/src/boot/flush.ts +20 -0
  149. package/src/boot/lifecycle.ts +7 -5
  150. package/src/config/cooldowns.ts +1 -1
  151. package/src/config/routing.ts +26 -0
  152. package/src/config/spec-metadata.ts +72 -0
  153. package/src/config/spec.ts +20 -6
  154. package/src/console/api-keys/contracts.ts +12 -1
  155. package/src/console/api-keys/routes.ts +50 -31
  156. package/src/console/auth/account-reset.ts +7 -9
  157. package/src/console/backup/contracts.ts +4 -0
  158. package/src/console/backup/export-endpoint.ts +7 -5
  159. package/src/console/cli-tools/service.ts +6 -12
  160. package/src/console/console-router-guards.ts +23 -16
  161. package/src/console/console-router.ts +7 -0
  162. package/src/console/dashboard-assets.ts +5 -5
  163. package/src/console/domain-registration.ts +27 -2
  164. package/src/console/inferhub/normalize.ts +24 -28
  165. package/src/console/inferhub/types.ts +3 -0
  166. package/src/console/ip-bans/routes.ts +93 -0
  167. package/src/console/observability/store.ts +0 -53
  168. package/src/console/providers/catalog/catalog-projections.ts +5 -2
  169. package/src/console/providers/catalog/contracts.ts +123 -0
  170. package/src/console/providers/catalog/discover-constants.ts +6 -0
  171. package/src/console/providers/catalog/model-operations.ts +122 -10
  172. package/src/console/providers/catalog/provider-operations.ts +116 -8
  173. package/src/console/providers/catalog/public-model-store.ts +0 -0
  174. package/src/console/providers/catalog/routes.ts +65 -4
  175. package/src/console/providers/catalog/store-helpers.ts +30 -1
  176. package/src/console/providers/catalog/store.ts +249 -4
  177. package/src/console/quota/account-quota-global.ts +97 -19
  178. package/src/console/quota/account-quota-shared.ts +49 -0
  179. package/src/console/quota/account-quota-tenant.ts +6 -18
  180. package/src/console/quota/native-identity.ts +2 -0
  181. package/src/console/quota/quota-view.ts +19 -3
  182. package/src/console/routing/pools/operations.ts +31 -46
  183. package/src/console/settings/contracts.ts +18 -0
  184. package/src/console/settings/store.ts +32 -0
  185. package/src/console/shared/errors.ts +33 -6
  186. package/src/console/system-prompt/contracts.ts +0 -1
  187. package/src/main.ts +3 -3
  188. package/src/network/pool/byte-accounting.ts +3 -2
  189. package/src/network/pool/selector.ts +182 -39
  190. package/src/network/pool-cooldown-store.ts +89 -14
  191. package/src/network/pool-health.ts +49 -61
  192. package/src/network/rate-limit-verdict.ts +1 -1
  193. package/src/network/ssrf.ts +6 -6
  194. package/src/observability/event-log.ts +7 -361
  195. package/src/observability/event-query.ts +18 -4
  196. package/src/observability/performance-metrics.ts +9 -0
  197. package/src/observability/runtime-metrics.ts +14 -9
  198. package/src/observability/telemetry-buffer.ts +15 -25
  199. package/src/observability/telemetry-reconciliation.ts +9 -37
  200. package/src/observability/telemetry-row.ts +125 -0
  201. package/src/persistence/db-mode.ts +4 -1
  202. package/src/persistence/dialect.ts +30 -0
  203. package/src/persistence/migrate.ts +6 -3
  204. package/src/persistence/schema.ts +28 -2
  205. package/src/persistence/ttl-cache.ts +20 -2
  206. package/src/protocol/codecs.ts +1 -1
  207. package/src/protocol/dsml.ts +35 -87
  208. package/src/protocol/gateway-error.ts +1 -0
  209. package/src/protocol/markup-stream-parser.ts +195 -0
  210. package/src/protocol/reasoning-text.ts +19 -0
  211. package/src/protocol/reasoning.ts +1 -1
  212. package/src/protocol/request/chat.ts +14 -14
  213. package/src/protocol/request/gemini.ts +8 -2
  214. package/src/protocol/request/messages.ts +4 -5
  215. package/src/protocol/response/chat.ts +12 -20
  216. package/src/protocol/response/gemini.ts +6 -1
  217. package/src/protocol/response/omp-tool-call-markup.ts +43 -86
  218. package/src/protocol/response/responses.ts +18 -2
  219. package/src/protocol/sse-decoder.ts +25 -8
  220. package/src/protocol/stop-reason.ts +23 -1
  221. package/src/protocol/stream-error-frames.ts +13 -3
  222. package/src/protocol/think-split.ts +112 -0
  223. package/src/protocol/translation/dialect/qwen.ts +26 -58
  224. package/src/protocol/translation/quirks.ts +1 -1
  225. package/src/protocol/translation/tool-repair.ts +2 -2
  226. package/src/protocol/upstream/send-claude-messages.ts +15 -4
  227. package/src/protocol/upstream-parsing.ts +1 -1
  228. package/src/providers/adapters/compatible-adapter.ts +78 -17
  229. package/src/providers/authentication/oauth-client.ts +1 -1
  230. package/src/providers/authentication/oauth-device-flow.ts +98 -8
  231. package/src/providers/authentication/oauth-refresh-service.ts +153 -59
  232. package/src/providers/configured-provider.ts +148 -0
  233. package/src/providers/default-registry.ts +117 -27
  234. package/src/providers/discovery/discovery-types.ts +12 -0
  235. package/src/providers/discovery/probe-phases.ts +74 -33
  236. package/src/providers/discovery/probing-service.ts +293 -165
  237. package/src/providers/integrations/alibabacp/alibabacp-models.ts +1 -2
  238. package/src/providers/integrations/{dashscope-models.ts → alibabacp/dashscope-models.ts} +2 -2
  239. package/src/providers/integrations/antigravity/antigravity-protocol.ts +121 -2
  240. package/src/providers/integrations/antigravity/antigravity.ts +13 -2
  241. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +72 -34
  242. package/src/providers/integrations/buddy/buddy-chat-shared.ts +191 -35
  243. package/src/providers/integrations/buddy/buddy-checkin.ts +17 -39
  244. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +2 -2
  245. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +5 -5
  246. package/src/providers/integrations/buddy/buddy-identity.ts +89 -6
  247. package/src/providers/integrations/buddy/buddy-media.ts +4 -5
  248. package/src/providers/integrations/buddy/buddy-oauth-shared.ts +2 -2
  249. package/src/providers/integrations/buddy/buddy-quota-shared.ts +1 -1
  250. package/src/providers/integrations/buddy/buddy-usage-shared.ts +1 -3
  251. package/src/providers/integrations/buddy/capabilities.ts +6 -43
  252. package/src/providers/integrations/buddy/codebuddy/cn-catalog.ts +2 -1
  253. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +23 -1
  254. package/src/providers/integrations/buddy/codebuddy/provider.ts +6 -2
  255. package/src/providers/integrations/buddy/codebuddy/shared.ts +38 -27
  256. package/src/providers/integrations/claude/claude-quota.ts +14 -17
  257. package/src/providers/integrations/cline/cline.ts +2 -2
  258. package/src/providers/integrations/commandcode/quota.ts +22 -28
  259. package/src/providers/integrations/cursor/cursor-auth.ts +0 -8
  260. package/src/providers/integrations/cursor/cursor-catalog.ts +2 -0
  261. package/src/providers/integrations/cursor/cursor-exec.ts +745 -0
  262. package/src/providers/integrations/cursor/cursor-http2.ts +2 -0
  263. package/src/providers/integrations/cursor/cursor-request.ts +35 -20
  264. package/src/providers/integrations/cursor/cursor-shared.ts +26 -5
  265. package/src/providers/integrations/cursor/cursor.ts +139 -70
  266. package/src/providers/integrations/cursor/protocol/messages.ts +22 -29
  267. package/src/providers/integrations/cursor/protocol/protobuf-schema.ts +37 -0
  268. package/src/providers/integrations/cursor/protocol/tool-canonical.ts +26 -100
  269. package/src/providers/integrations/devin/devin-auth.ts +353 -0
  270. package/src/providers/integrations/devin/devin-catalog.ts +207 -0
  271. package/src/providers/integrations/devin/devin-http.ts +91 -0
  272. package/src/providers/integrations/devin/devin-quota.ts +214 -0
  273. package/src/providers/integrations/devin/devin-shared.ts +111 -0
  274. package/src/providers/integrations/devin/devin.ts +539 -0
  275. package/src/providers/integrations/devin/protocol/devin-proto.ts +558 -0
  276. package/src/providers/integrations/devin/protocol/framing.ts +124 -0
  277. package/src/providers/integrations/factory/factory-oauth.ts +17 -44
  278. package/src/providers/integrations/factory/factory.ts +2 -2
  279. package/src/providers/integrations/freebuff/assets/official-system-manager.txt +88 -0
  280. package/src/providers/integrations/freebuff/assets/official-system-worker.txt +57 -0
  281. package/src/providers/integrations/freebuff/assets/official-tools.json +1070 -0
  282. package/src/providers/integrations/freebuff/freebuff-catalog.ts +120 -0
  283. package/src/providers/integrations/freebuff/freebuff-constants.ts +1233 -0
  284. package/src/providers/integrations/freebuff/freebuff-discovery.ts +108 -0
  285. package/src/providers/integrations/freebuff/freebuff-oauth.ts +240 -0
  286. package/src/providers/integrations/freebuff/freebuff-quota.ts +260 -0
  287. package/src/providers/integrations/freebuff/freebuff-virtual.ts +89 -0
  288. package/src/providers/integrations/freebuff/freebuff.ts +682 -0
  289. package/src/providers/integrations/freepi/freepi-catalog.ts +60 -0
  290. package/src/providers/integrations/freepi/freepi-discovery.ts +82 -0
  291. package/src/providers/integrations/freepi/freepi-headers.ts +21 -0
  292. package/src/providers/integrations/freepi/freepi-oauth.ts +142 -0
  293. package/src/providers/integrations/freepi/freepi-protocol.ts +242 -0
  294. package/src/providers/integrations/freepi/freepi-quota.ts +126 -0
  295. package/src/providers/integrations/freepi/freepi-realm.ts +35 -0
  296. package/src/providers/integrations/freepi/freepi.ts +292 -0
  297. package/src/providers/integrations/gemini.ts +2 -2
  298. package/src/providers/integrations/grok/grok-oauth.ts +48 -45
  299. package/src/providers/integrations/grok/grok-ratelimit-store.ts +10 -17
  300. package/src/providers/integrations/grok/grok-turn-index.ts +11 -16
  301. package/src/providers/integrations/hermes/hermes-oauth.ts +94 -0
  302. package/src/providers/integrations/{hermes.ts → hermes/hermes.ts} +13 -21
  303. package/src/providers/integrations/hermes/index.ts +2 -0
  304. package/src/providers/integrations/kiro/kiro-catalog.ts +17 -1
  305. package/src/providers/integrations/kiro/kiro-discovery.ts +10 -12
  306. package/src/providers/integrations/kiro/kiro-quota.ts +8 -4
  307. package/src/providers/integrations/kiro/kiro.ts +15 -19
  308. package/src/providers/integrations/ollamacloud/index.ts +2 -0
  309. package/src/providers/integrations/{ollamacloud-models.ts → ollamacloud/models.ts} +2 -2
  310. package/src/providers/integrations/{ollamacloud-quota.ts → ollamacloud/quota.ts} +3 -3
  311. package/src/providers/integrations/opencode/fingerprint.ts +1 -1
  312. package/src/providers/integrations/opencode/opencode-go-quota.ts +2 -8
  313. package/src/providers/integrations/perch/perch-catalog.ts +105 -0
  314. package/src/providers/integrations/perch/perch-credential.ts +25 -0
  315. package/src/providers/integrations/perch/perch-oauth.ts +170 -0
  316. package/src/providers/integrations/perch/perch-protocol.ts +208 -0
  317. package/src/providers/integrations/perch/perch-quota.ts +123 -0
  318. package/src/providers/integrations/perch/perch-realm.ts +82 -0
  319. package/src/providers/integrations/perch/perch-system-prompt.ts +13 -0
  320. package/src/providers/integrations/perch/perch.ts +305 -0
  321. package/src/providers/integrations/perch/saffron-system-prompt.txt +8 -0
  322. package/src/providers/integrations/qoder/chat-proto.ts +328 -328
  323. package/src/providers/integrations/qoder/claim-qoder-credits.ts +19 -60
  324. package/src/providers/integrations/qoder/qoder-adapter.ts +90 -40
  325. package/src/providers/integrations/qoder/qoder-auth.ts +101 -29
  326. package/src/providers/integrations/qoder/qoder-cache.ts +32 -0
  327. package/src/providers/integrations/qoder/qoder-catalog.ts +26 -6
  328. package/src/providers/integrations/qoder/qoder-chat.ts +7 -21
  329. package/src/providers/integrations/qoder/qoder-checkin.ts +6 -6
  330. package/src/providers/integrations/qoder/qoder-grpc.ts +210 -7
  331. package/src/providers/integrations/qoder/qoder-ide.ts +5 -108
  332. package/src/providers/integrations/qoder/qoder-identity-cache.ts +184 -0
  333. package/src/providers/integrations/qoder/qoder-native-identity-store.ts +83 -18
  334. package/src/providers/integrations/qoder/qoder-realm.ts +9 -6
  335. package/src/providers/integrations/qoder/qoder-umid.ts +109 -342
  336. package/src/providers/integrations/qoder/qoder-virtual.ts +33 -1
  337. package/src/providers/integrations/qoder/qoder.ts +3 -7
  338. package/src/providers/integrations/trae/trae-catalog.ts +9 -14
  339. package/src/providers/integrations/trae/trae-headers.ts +36 -11
  340. package/src/providers/integrations/trae/trae-oauth.ts +14 -17
  341. package/src/providers/integrations/trae/trae-protocol.ts +12 -11
  342. package/src/providers/integrations/trae/trae-quota.ts +96 -36
  343. package/src/providers/integrations/trae/trae-realm.ts +74 -15
  344. package/src/providers/integrations/trae/trae.ts +99 -26
  345. package/src/providers/integrations/zcode-start-plan/provider.ts +226 -23
  346. package/src/providers/integrations/zcode-start-plan/quota.ts +19 -2
  347. package/src/providers/integrations/zro/zro.ts +204 -0
  348. package/src/providers/operations/account-error-classification.ts +235 -278
  349. package/src/providers/operations/account-health-service.ts +273 -26
  350. package/src/providers/operations/account-refresh-status.ts +269 -0
  351. package/src/providers/operations/account-tier.ts +66 -0
  352. package/src/providers/operations/client-version-resolver.ts +1 -1
  353. package/src/providers/operations/client-versions.ts +42 -150
  354. package/src/providers/operations/cooldown-rules.ts +56 -0
  355. package/src/providers/operations/{model-catalog-cache.ts → provider-caches.ts} +68 -16
  356. package/src/providers/operations/provider-catalog-seeder.ts +4 -0
  357. package/src/providers/operations/provider-catalog-service.ts +1 -1
  358. package/src/providers/operations/provider-credential-service.ts +102 -32
  359. package/src/providers/operations/provider-rules/github.ts +48 -0
  360. package/src/providers/operations/provider-rules/grok.ts +33 -0
  361. package/src/providers/operations/provider-rules/inception.ts +49 -0
  362. package/src/providers/operations/provider-rules/index.ts +32 -0
  363. package/src/providers/operations/provider-rules/perch.ts +37 -0
  364. package/src/providers/operations/provider-rules/qoder.ts +70 -0
  365. package/src/providers/operations/provider-rules/trae.ts +81 -0
  366. package/src/providers/provider-contracts.ts +33 -2
  367. package/src/providers/{provider-credential-resolver.ts → provider-credentials.ts} +40 -3
  368. package/src/providers/provider-metadata.ts +99 -14
  369. package/src/providers/provider-model-definition.ts +36 -0
  370. package/src/providers/provider-probe.ts +15 -1
  371. package/src/providers/provider-registry.ts +42 -33
  372. package/src/providers/quota/quota-limit-windows.ts +16 -5
  373. package/src/providers/quota/quota-support.ts +1 -1
  374. package/src/providers/quota/quota-window-parser.ts +90 -18
  375. package/src/providers/quota/refresh.ts +96 -18
  376. package/src/security/abuse.ts +71 -0
  377. package/src/security/api-key-auth-cache.ts +1 -1
  378. package/src/security/api-key-auth.ts +6 -2
  379. package/src/transport/dispatch/attempt-finalize.ts +14 -13
  380. package/src/transport/dispatch/attempt-loop.ts +71 -17
  381. package/src/transport/dispatch/generation.ts +2 -8
  382. package/src/transport/dispatch/native-shared.ts +25 -0
  383. package/src/transport/dispatch/proxy-request.ts +9 -46
  384. package/src/transport/dispatch/responses-compact.ts +2 -8
  385. package/src/transport/dispatch/streaming/finalize.ts +9 -6
  386. package/src/transport/dispatch/streaming/stream.ts +9 -36
  387. package/src/transport/dispatch/systemone.ts +2 -8
  388. package/src/transport/dispatch/token-count.ts +2 -6
  389. package/src/transport/dispatch/upstream.ts +67 -1
  390. package/src/transport/drain-endpoint.ts +31 -4
  391. package/src/transport/failure-policy.ts +129 -37
  392. package/src/transport/guards/body-policy.ts +7 -1
  393. package/src/transport/guards/error-lifecycle.ts +13 -1
  394. package/src/transport/guards/request-context.ts +22 -14
  395. package/src/transport/request/preparer.ts +81 -71
  396. package/src/transport/routing/bounded-lru.ts +14 -0
  397. package/src/transport/routing/drain-reserve.ts +129 -0
  398. package/src/transport/routing/plan-gate.ts +7 -1
  399. package/src/transport/routing/round-robin.ts +60 -0
  400. package/src/transport/routing/route-catalog.ts +123 -111
  401. package/src/transport/routing/route-model.ts +51 -9
  402. package/src/transport/routing/router.ts +418 -33
  403. package/src/transport/surface/adapters.ts +11 -2
  404. package/src/transport/surface/chat/adapter.ts +2 -2
  405. package/src/transport/surface/chat/encode.ts +47 -4
  406. package/src/transport/surface/chat/parse.ts +2 -12
  407. package/src/transport/surface/completion.ts +44 -33
  408. package/src/transport/surface/content-parts.ts +21 -0
  409. package/src/transport/surface/messages/adapter.ts +9 -2
  410. package/src/transport/surface/messages/encode.ts +11 -3
  411. package/src/transport/surface/messages/parse.ts +2 -1
  412. package/src/transport/surface/messages/stream.ts +33 -17
  413. package/src/transport/surface/responses/encode.ts +34 -6
  414. package/src/transport/surface/responses/parse.ts +3 -11
  415. package/src/workers/daily-checkin.ts +1 -1
  416. package/src/workers/quota-refresh-sweep.ts +1 -1
  417. package/src/workers/scheduler.ts +22 -4
  418. package/dist/dashboard/assets/BannedPage-DeIbIiBU.js +0 -1
  419. package/dist/dashboard/assets/BannedPage-DeIbIiBU.js.gz +0 -0
  420. package/dist/dashboard/assets/CliToolDetailPage-BRWKPQPf.js.gz +0 -0
  421. package/dist/dashboard/assets/CliToolsPage-BP-Y5iHN.js.gz +0 -0
  422. package/dist/dashboard/assets/CombosPage-B7JHozVa.js +0 -1
  423. package/dist/dashboard/assets/CombosPage-B7JHozVa.js.gz +0 -0
  424. package/dist/dashboard/assets/ConsoleLogPage-GX7iGGcu.js +0 -1
  425. package/dist/dashboard/assets/ConsoleLogPage-GX7iGGcu.js.gz +0 -0
  426. package/dist/dashboard/assets/CustomizationPage-HVonbGEa.js +0 -1
  427. package/dist/dashboard/assets/CustomizationPage-HVonbGEa.js.gz +0 -0
  428. package/dist/dashboard/assets/HealthEventsModal-xaut4OJ9.js +0 -1
  429. package/dist/dashboard/assets/HealthEventsModal-xaut4OJ9.js.gz +0 -0
  430. package/dist/dashboard/assets/InjectSystemPromptPanel-Cv-2Ol_K.js.gz +0 -0
  431. package/dist/dashboard/assets/LoginPage-bKfz_b1K.js.gz +0 -0
  432. package/dist/dashboard/assets/ModelPicker-B-WQxx_H.js.gz +0 -0
  433. package/dist/dashboard/assets/OverviewPage-Z9pJZFo8.js +0 -1
  434. package/dist/dashboard/assets/OverviewPage-Z9pJZFo8.js.gz +0 -0
  435. package/dist/dashboard/assets/ProviderDetailPage-CMeDBxR1.js +0 -7
  436. package/dist/dashboard/assets/ProviderDetailPage-CMeDBxR1.js.gz +0 -0
  437. package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js +0 -1
  438. package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js.gz +0 -0
  439. package/dist/dashboard/assets/ProvidersPage-BXPdOpG4.js +0 -1
  440. package/dist/dashboard/assets/ProvidersPage-BXPdOpG4.js.gz +0 -0
  441. package/dist/dashboard/assets/ProxyPage-emlTuxz6.js.gz +0 -0
  442. package/dist/dashboard/assets/QuotaPage-CMJKNYWb.js +0 -1
  443. package/dist/dashboard/assets/QuotaPage-CMJKNYWb.js.gz +0 -0
  444. package/dist/dashboard/assets/SettingsPage-BSy7EwGo.js +0 -1
  445. package/dist/dashboard/assets/SettingsPage-BSy7EwGo.js.gz +0 -0
  446. package/dist/dashboard/assets/SetupPage-CUZrKs9r.js.gz +0 -0
  447. package/dist/dashboard/assets/StudioPage-jWmUdDpy.js.gz +0 -0
  448. package/dist/dashboard/assets/UsagePage-5F9tGVGG.js +0 -56
  449. package/dist/dashboard/assets/UsagePage-5F9tGVGG.js.gz +0 -0
  450. package/dist/dashboard/assets/api-keys-KvX83mRe.js +0 -1
  451. package/dist/dashboard/assets/api-keys-KvX83mRe.js.gz +0 -0
  452. package/dist/dashboard/assets/app-BEsU8hKs.css +0 -2
  453. package/dist/dashboard/assets/app-BEsU8hKs.css.gz +0 -0
  454. package/dist/dashboard/assets/assert-CidQVoNo.js.gz +0 -0
  455. package/dist/dashboard/assets/badge-DyI_mJfi.js.gz +0 -0
  456. package/dist/dashboard/assets/banner-B-yXtCqb.js.gz +0 -0
  457. package/dist/dashboard/assets/brand/bansos_page.png +0 -0
  458. package/dist/dashboard/assets/brand/denia_bansos.jpg +0 -0
  459. package/dist/dashboard/assets/brand/favicon.jpg +0 -0
  460. package/dist/dashboard/assets/brand/login_register.jpg +0 -0
  461. package/dist/dashboard/assets/brand/login_register_initial.jpg +0 -0
  462. package/dist/dashboard/assets/brand/og_bansos.jpg +0 -0
  463. package/dist/dashboard/assets/brand/og_image.jpg +0 -0
  464. package/dist/dashboard/assets/brand/sidebar_ico.jpg +0 -0
  465. package/dist/dashboard/assets/card-_HwAEqFF.js.gz +0 -0
  466. package/dist/dashboard/assets/cli-tools-Bwv5-zGZ.js +0 -1
  467. package/dist/dashboard/assets/cli-tools-Bwv5-zGZ.js.gz +0 -0
  468. package/dist/dashboard/assets/clipboard-button-ByjCk0o9.js.gz +0 -0
  469. package/dist/dashboard/assets/dialog-BZhOlfz-.js.gz +0 -0
  470. package/dist/dashboard/assets/drawer-BADqIRYP.js.gz +0 -0
  471. package/dist/dashboard/assets/entry-COaC-irN.js.gz +0 -0
  472. package/dist/dashboard/assets/entry-Ca7LSCet.js +0 -4
  473. package/dist/dashboard/assets/entry-Ca7LSCet.js.gz +0 -0
  474. package/dist/dashboard/assets/entry-PDd27uCd.js +0 -1
  475. package/dist/dashboard/assets/entry-PDd27uCd.js.gz +0 -0
  476. package/dist/dashboard/assets/formatters-DcHJyRiu.js.gz +0 -0
  477. package/dist/dashboard/assets/index-CgKmFngv.js.gz +0 -0
  478. package/dist/dashboard/assets/inline-B2BirKry.js.gz +0 -0
  479. package/dist/dashboard/assets/input-D-qV9dKY.js.gz +0 -0
  480. package/dist/dashboard/assets/layout-B7tVWhtV.js.gz +0 -0
  481. package/dist/dashboard/assets/list-row-CtkGE3bF.js.gz +0 -0
  482. package/dist/dashboard/assets/live-DGizF06-.js.gz +0 -0
  483. package/dist/dashboard/assets/minimize-2-BTmMTK4o.js +0 -1
  484. package/dist/dashboard/assets/minimize-2-BTmMTK4o.js.gz +0 -0
  485. package/dist/dashboard/assets/mutation-GU7XdEjz.js.gz +0 -0
  486. package/dist/dashboard/assets/network-C9UE-bOr.js.gz +0 -0
  487. package/dist/dashboard/assets/numbers-BnAd22a_.js.gz +0 -0
  488. package/dist/dashboard/assets/pagination-BAywyMzS.js.gz +0 -0
  489. package/dist/dashboard/assets/query-keys-YSoQOUh6.js.gz +0 -0
  490. package/dist/dashboard/assets/quota-DJ1QlAOq.js +0 -1
  491. package/dist/dashboard/assets/quota-DJ1QlAOq.js.gz +0 -0
  492. package/dist/dashboard/assets/routing-fnCsxgNO.js +0 -1
  493. package/dist/dashboard/assets/routing-fnCsxgNO.js.gz +0 -0
  494. package/dist/dashboard/assets/select-BCOcv8WK.js.gz +0 -0
  495. package/dist/dashboard/assets/settings-BxslsErn.js +0 -1
  496. package/dist/dashboard/assets/settings-BxslsErn.js.gz +0 -0
  497. package/dist/dashboard/assets/stack-ByKt3Q1g.js.gz +0 -0
  498. package/dist/dashboard/assets/state-DXbGTx7N.js.gz +0 -0
  499. package/dist/dashboard/assets/switch-ZeTsW623.js.gz +0 -0
  500. package/dist/dashboard/assets/useMutation-CbCP4w1o.js.gz +0 -0
  501. package/dist/dashboard/console-backgrounds.jpg +0 -0
  502. package/dist/dashboard/console-backgrounds_initial.jpg +0 -0
  503. package/dist/dashboard/donation/qris-risun-marke.png +0 -0
  504. package/src/providers/bundled-discovery-providers.ts +0 -98
  505. package/src/providers/configured-openai-providers.ts +0 -120
  506. package/src/providers/integrations/buddy/turing-device-token.ts +0 -262
  507. package/src/providers/integrations/buddy/workbuddy/cn-oauth.ts +0 -30
  508. package/src/providers/integrations/buddy/workbuddy/cn-provider.ts +0 -58
  509. package/src/providers/integrations/buddy/workbuddy/oauth.ts +0 -31
  510. package/src/providers/integrations/buddy/workbuddy/provider.ts +0 -209
  511. package/src/providers/integrations/buddy/workbuddy/quota.ts +0 -161
  512. package/src/providers/integrations/buddy/workbuddy/shared.ts +0 -285
  513. package/src/providers/operations/model-discovery-cache.ts +0 -34
  514. package/src/providers/operations/probe-fetch.ts +0 -15
  515. package/src/providers/operations/provider-version-cache.ts +0 -30
  516. package/src/providers/provider-capabilities.ts +0 -33
  517. package/src/providers/provider-credential-envelope.ts +0 -43
  518. /package/dist/dashboard/assets/{app-CTxoqyYB.js → app-DIXLjojg.js} +0 -0
  519. /package/dist/dashboard/assets/{app-CTxoqyYB.js.gz → app-DIXLjojg.js.gz} +0 -0
@@ -16,10 +16,14 @@
16
16
  */
17
17
  import type { CartethyiaDatabase } from "../../../persistence/db-handle";
18
18
  import type { ValidatedFetch } from "../../../network/outbound-fetch";
19
- import { getQoderVersion } from "../../operations/client-versions";
20
19
  import { qoderRealmForProvider, QODER_REALMS } from "./qoder-realm";
20
+ import { exchangeQoderPatForHost } from "./qoder-auth";
21
21
  import {
22
+ asNumber,
23
+ asRecord,
22
24
  buildCampaignHeaders,
25
+ normalizeCampaign,
26
+ safeParse,
23
27
  } from "./qoder-checkin";
24
28
  import { qoderMachineHeaders } from "./qoder-umid";
25
29
  import {
@@ -38,31 +42,6 @@ export const QODER_CREDIT_PROVIDER_IDS = ["qoder", "qodercn"] as const;
38
42
 
39
43
  const PAT_PREFIX = "pt-";
40
44
 
41
- function asRecord(value: unknown): Record<string, unknown> | null {
42
- if (typeof value !== "object" || value === null) return null;
43
- return value as Record<string, unknown>;
44
- }
45
-
46
- function asNumber(value: unknown): number | null {
47
- if (typeof value === "number" && Number.isFinite(value)) return value;
48
- if (typeof value === "string" && value.trim() !== "") {
49
- const parsed = Number(value);
50
- return Number.isFinite(parsed) ? parsed : null;
51
- }
52
- return null;
53
- }
54
-
55
- function normalizeCampaign(raw: Record<string, unknown>): Record<string, unknown> {
56
- const benefit = asRecord(raw["benefit"]);
57
- return {
58
- campaign_id: String(raw["campaignId"] ?? raw["campaign_id"] ?? ""),
59
- campaign_key: String(raw["campaignKey"] ?? raw["campaign_key"] ?? ""),
60
- action_type: String(raw["actionType"] ?? raw["action_type"] ?? ""),
61
- claim_status: String(raw["claimStatus"] ?? raw["claim_status"] ?? ""),
62
- ...(benefit !== null ? { benefit } : {}),
63
- };
64
- }
65
-
66
45
  /**
67
46
  * Exchanges a PAT (`pt-…`) for a job token (`jt-…`) on the realm's openapi
68
47
  * host. Returns the job token, or `null` when the exchange fails.
@@ -70,7 +49,8 @@ function normalizeCampaign(raw: Record<string, unknown>): Record<string, unknown
70
49
  * The real client binds the machine identity at exchange time
71
50
  * (`getMachineIdentityRequestFields`), so `machine_id` + `machine_token` are
72
51
  * sent when available — the job token is then bound to that machine. The
73
- * User-Agent and Cosy version mirror the real client (`qoder/<version>`).
52
+ * request is the shared `exchangeQoderPatForHost` (same v1 body and client
53
+ * identity headers as the auth path), parameterized by this site's own host.
74
54
  */
75
55
  async function exchangePatToJobToken(
76
56
  base: string,
@@ -78,30 +58,17 @@ async function exchangePatToJobToken(
78
58
  fetcher: ValidatedFetch,
79
59
  machine?: { readonly id?: string; readonly token?: string },
80
60
  ): Promise<string | null> {
81
- try {
82
- const body: Record<string, unknown> = { personal_token: pat };
83
- if (machine?.id) body.machine_id = machine.id;
84
- if (machine?.token) body.machine_token = machine.token;
85
- const version = getQoderVersion();
86
- const res = await fetcher(`${base}/api/v1/jobToken/exchange`, {
87
- method: "POST",
88
- headers: {
89
- "Content-Type": "application/json",
90
- "User-Agent": `qoder/${version}`,
91
- "Cosy-ClientType": "5",
92
- "Cosy-Version": version,
93
- },
94
- body: JSON.stringify(body),
95
- signal: AbortSignal.timeout(25_000),
96
- });
97
- if (!res.ok) return null;
98
- const payload: unknown = await res.json().catch(() => null);
99
- const rec = asRecord(payload);
100
- const token = String(rec?.["token"] ?? rec?.["device_token"] ?? rec?.["access_token"] ?? "");
101
- return token.length > 0 ? token : null;
102
- } catch {
103
- return null;
104
- }
61
+ const exchanged = await exchangeQoderPatForHost({
62
+ pat,
63
+ url: `${base}/api/v1/jobToken/exchange`,
64
+ ...(machine === undefined ? {} : { machine }),
65
+ fetcher,
66
+ signal: AbortSignal.timeout(25_000),
67
+ tokenKeys: ["token", "device_token", "access_token"],
68
+ });
69
+ if (exchanged === null) return null;
70
+ if (!exchanged.response.ok) return null;
71
+ return exchanged.result?.token ?? null;
105
72
  }
106
73
 
107
74
  /**
@@ -139,7 +106,7 @@ export async function claimQoderCredits(args: {
139
106
  const store = new DrizzleQoderNativeIdentityStore(db);
140
107
  let bound = await store.get(providerId, accountId);
141
108
  if (!bound) {
142
- const minted = await allocateQoderNativeIdentity(store, providerId, accountId);
109
+ const minted = await allocateQoderNativeIdentity(store, providerId, accountId, { realm });
143
110
  if (!minted) return { state: "error", amount: 0, detail: "Could not mint a UMID identity for this account." };
144
111
  bound = minted;
145
112
  }
@@ -248,11 +215,3 @@ export async function claimQoderCredits(args: {
248
215
  return { state: "error", amount: 0, detail: "Claim request failed." };
249
216
  }
250
217
  }
251
-
252
- function safeParse(raw: string): unknown {
253
- try {
254
- return JSON.parse(raw);
255
- } catch {
256
- return null;
257
- }
258
- }
@@ -1,14 +1,13 @@
1
1
  /**
2
2
  * Qoder adapter — transport selection, credential resolution, dispatch.
3
3
  *
4
- * Surface contract (2026-11-05):
5
- * - `grpc` (default, production): `model.chat.ChatService/ChatCompletionStream`
6
- * — the only proven carrier of real 1M-token context (HTTP IDE caps at
7
- * ~256 KiB). Serves billed rows; a free-tier row (`lite`) is refused there
8
- * (`quota exceeded` 116) and must use legacy COSY.
9
- * - `cosy` (free/lite + forced legacy): `/algo/.../agent_chat_generation` —
10
- * the only surface that accepts free-tier rows. Signing is the COSY Bearer
11
- * scheme (AES/RSA + alphabet-scrambled body).
4
+ * Surface contract (2026-11-07):
5
+ * - `cosy` (default, production): `/algo/.../agent_chat_generation` on
6
+ * `api1` → `api2` → `api3.qoder.sh` (legacy COSY) — the only surface
7
+ * verified alive. Accepts free-tier rows (`lite`).
8
+ * - `grpc` (`model.chat.ChatService/ChatCompletionStream`): **not the
9
+ * default anymore** — its host (`api2-v2.qoder.sh`) is NXDOMAIN since
10
+ * 2026-11-07. Kept as an explicit override for probes.
12
11
  * - `ide` (`/model/v1/chat/completions`): **probe/testing only — never
13
12
  * production.** It is the only surface that echoes strategy metadata
14
13
  * (`raw_usage` → provider_model/target) and it streams immediately, but its
@@ -31,12 +30,13 @@ import { abortGatewayError, createUpstreamDeadlineLifecycle } from "../../operat
31
30
  import { getQoderVersion } from "../../operations/client-versions";
32
31
  import { qoderMachineHostname, qoderMachineOs } from "../../operations/cli-platform";
33
32
  import { mapUpstreamHttpError } from "../../../transport/failure-policy";
34
- import { QODER_CN_MODEL_CONFIGS, QODER_INTL_MODEL_CONFIGS, qoderDiscoveredMaxContexts } from "./qoder-catalog";
33
+ import { QODER_CN_MODEL_CONFIGS, QODER_INTL_MODEL_CONFIGS } from "./qoder-catalog";
35
34
  import { isQoderVirtualModel, qoderDispatchModelId } from "./qoder-virtual";
36
35
  import { isQoderPat, qoderOAuthAuth, qoderSessionId, resolveQoderAuth } from "./qoder-auth";
37
36
  import { buildQoderRequest, callQoder } from "./qoder-chat";
38
37
  import { buildQoderGrpcRequest, qoderGrpcMetadata, qoderGrpcToCanonicalEvents } from "./qoder-grpc";
39
38
  import { buildQoderIdeRequest, qoderBodyToCanonicalEvents } from "./qoder-ide";
39
+ import { estimateQoderSessionCache } from "./qoder-cache";
40
40
  import {
41
41
  QODER_CHAT_PATH,
42
42
  QODER_PROVIDER_ID,
@@ -55,17 +55,16 @@ interface QoderAdapterOptions {
55
55
  /**
56
56
  * Which chat transport to dispatch on.
57
57
  *
58
- * `grpc` (default) uses the gRPC model-server surface
59
- * (`model.chat.ChatService/ChatCompletionStream`) — the only proven carrier
60
- * of real 1M-token context (the HTTP IDE surface caps at ~256 KiB). gRPC
61
- * serves billed models; a free-tier row (`lite`) is refused there
62
- * (`quota exceeded` 116) and must use the legacy COSY surface.
58
+ * `cosy` (default) = legacy COSY (`/algo/.../agent_chat_generation`) on
59
+ * `api1` → `api2` → `api3.qoder.sh` — the only surface verified alive
60
+ * (2026-11-07; the `api2-v2` model-server host is NXDOMAIN). This path also
61
+ * accepts free-tier rows (`lite`).
63
62
  *
64
- * `cosy` forces only the legacy surface (`/algo/.../agent_chat_generation`)
65
- * — the free-tier path and the pre-gRPC behavior.
63
+ * `grpc` forces the gRPC surface (`model.chat.ChatService/ChatCompletionStream`)
64
+ * — **no longer the default**; its host is not verified alive.
66
65
  *
67
- * `ide` forces the modern `/model/v1` surface — **probe/testing only, never
68
- * production** (see module doc).
66
+ * `ide` forces `/model/v1` — **probe/testing only, never production**
67
+ * (see module doc).
69
68
  */
70
69
  readonly surface?: "grpc" | "cosy" | "ide";
71
70
  }
@@ -75,7 +74,7 @@ export function resolveQoderTransport(
75
74
  surface: "grpc" | "cosy" | "ide" | undefined,
76
75
  modelConfig: QoderModelConfig,
77
76
  ): "grpc" | "cosy" | "ide" {
78
- const chosen = surface ?? "grpc";
77
+ const chosen = surface ?? "cosy";
79
78
  // `ide` is an explicit testing override — never auto-selected, and it does
80
79
  // not apply the free-tier rule (a forced IDE call is a test call).
81
80
  if (chosen === "ide") return "ide";
@@ -85,16 +84,6 @@ export function resolveQoderTransport(
85
84
  return chosen;
86
85
  }
87
86
 
88
- /**
89
- * The widest selectable context window for one model key (from the live list's
90
- * `context_config`; falls back to the static roster's stated window).
91
- */
92
- function qoderMaxContextFor(modelId: string, modelConfig: QoderModelConfig, realm: QoderRealm): number {
93
- const discovered = qoderDiscoveredMaxContexts(realm).get(modelId);
94
- if (discovered !== undefined) return discovered;
95
- return modelConfig.max_input_tokens;
96
- }
97
-
98
87
  class QoderAdapter implements ProviderAdapter {
99
88
  readonly provider_id: ProviderId;
100
89
  private readonly realm: QoderRealm;
@@ -104,7 +93,14 @@ class QoderAdapter implements ProviderAdapter {
104
93
  constructor(options: QoderAdapterOptions = {}) {
105
94
  this.provider_id = (options.providerId ?? QODER_PROVIDER_ID) as ProviderId;
106
95
  this.realm = options.realm ?? "intl";
107
- this.surface = options.surface ?? "grpc";
96
+ // The legacy COSY surface is the production default (2026-11-07): the
97
+ // gRPC host is NXDOMAIN and the IDE surface is probe-only. The constructor
98
+ // must default to it too, or every billed model dispatches to the dead
99
+ // gRPC path while free-tier rows get cosy via the is_free rule — the
100
+ // mismatch that made `deepseek-v4.1-flash` fail while `qwen3.8-flash`
101
+ // passed. This is the same default `resolveQoderTransport(undefined)`
102
+ // already returns.
103
+ this.surface = options.surface ?? "cosy";
108
104
  this.fetchFn = options.fetch ?? globalThis.fetch;
109
105
  }
110
106
 
@@ -121,6 +117,30 @@ class QoderAdapter implements ProviderAdapter {
121
117
  >;
122
118
  }
123
119
 
120
+ /**
121
+ * Wraps an upstream event stream and applies the local session-cache
122
+ * estimate to every `usage` record (and terminal-carried usage).
123
+ *
124
+ * Qoder's upstream never reports prompt-cache hits (verified live on both
125
+ * gRPC and IDE HTTP surfaces), so the gap is closed the same way Trae and
126
+ * Kiro do: estimate the cached prefix from this session's request history.
127
+ */
128
+ async *#withSessionCacheEstimate(
129
+ events: AsyncIterable<CanonicalEvent>,
130
+ request: CanonicalRequest,
131
+ sessionId: string | undefined,
132
+ ): AsyncGenerator<CanonicalEvent> {
133
+ for await (const event of events) {
134
+ if (event.type === "usage") {
135
+ yield { ...event, usage: estimateQoderSessionCache(event.usage, sessionId, request.messages) };
136
+ } else if (event.type === "terminal" && event.usage !== undefined) {
137
+ yield { ...event, usage: estimateQoderSessionCache(event.usage, sessionId, request.messages) };
138
+ } else {
139
+ yield event;
140
+ }
141
+ }
142
+ }
143
+
124
144
  async *dispatch(
125
145
  request: CanonicalRequest,
126
146
  candidate: ProviderDispatchTarget,
@@ -207,7 +227,11 @@ class QoderAdapter implements ProviderAdapter {
207
227
  if (!res.ok) throw await mapUpstreamHttpError(res, "qoder");
208
228
  if (!res.body) throw new GatewayError("platform_unavailable", 502, "Qoder returned empty body (request {requestId}). The upstream came back empty-handed — retry 🌙", {}, "upstream");
209
229
  lifecycle.release();
210
- yield* qoderBodyToCanonicalEvents(res.body as ReadableStream<Uint8Array>, lifecycle.signal, modelId);
230
+ yield* this.#withSessionCacheEstimate(
231
+ qoderBodyToCanonicalEvents(res.body as ReadableStream<Uint8Array>, lifecycle.signal, modelId),
232
+ request,
233
+ qoderSessionId(auth.userId, modelId),
234
+ );
211
235
  return;
212
236
  }
213
237
 
@@ -222,7 +246,6 @@ class QoderAdapter implements ProviderAdapter {
222
246
  modelId,
223
247
  request,
224
248
  auth.userId,
225
- qoderMaxContextFor(modelId, modelConfig, this.realm),
226
249
  {
227
250
  messages: legacy.messages as Array<Record<string, unknown>>,
228
251
  tools: legacy.tools as Array<Record<string, unknown>>,
@@ -233,16 +256,39 @@ class QoderAdapter implements ProviderAdapter {
233
256
  );
234
257
  const grpcMetadata = qoderGrpcMetadata(auth);
235
258
  lifecycle.release();
236
- yield* qoderGrpcToCanonicalEvents(grpcRequest, grpcMetadata, this.realm, lifecycle.signal);
259
+ yield* this.#withSessionCacheEstimate(
260
+ qoderGrpcToCanonicalEvents(grpcRequest, grpcMetadata, this.realm, lifecycle.signal, context.provider_capture),
261
+ request,
262
+ qoderSessionId(auth.userId, modelId),
263
+ );
237
264
  return;
238
265
  }
239
266
 
240
- const cosyUrl = candidate.endpoint_path?.startsWith("http")
241
- ? candidate.endpoint_path
242
- : `${qoderInferenceCandidates(this.realm, auth.securityOauthToken)[0] ?? ""}${QODER_CHAT_PATH}`;
243
- const cosyRequest = () =>
244
- callQoder(cosyUrl, buildQoderRequest(modelId, request, modelConfig, auth, this.realm), modelId, auth, lifecycle.signal, fetcher, version);
245
- const response = await cosyRequest();
267
+ // Legacy COSY hosts are tried in order (api1 → api2 → api3) with the
268
+ // endpoint path from discovery when present. A network-level failure on
269
+ // one host falls through to the next; an HTTP error response surfaces as-is.
270
+ const cosyCandidates = candidate.endpoint_path?.startsWith("http")
271
+ ? [candidate.endpoint_path]
272
+ : qoderInferenceCandidates(this.realm, auth.securityOauthToken).map(
273
+ (host) => `${host}${QODER_CHAT_PATH}`,
274
+ );
275
+ let response: Response | undefined;
276
+ let lastError: unknown;
277
+ for (const cosyUrl of cosyCandidates) {
278
+ try {
279
+ const attempt = () =>
280
+ callQoder(cosyUrl, buildQoderRequest(modelId, request, modelConfig, auth, this.realm), modelId, auth, lifecycle.signal, fetcher, version);
281
+ response = await attempt();
282
+ break;
283
+ } catch (err: unknown) {
284
+ lastError = err;
285
+ }
286
+ }
287
+ if (response === undefined) {
288
+ throw lastError instanceof Error
289
+ ? lastError
290
+ : new GatewayError("transport_unavailable", 502, "Qoder legacy COSY hosts are unreachable (request {requestId}). The connection ghosted us — come back in a moment 👻", {}, "upstream");
291
+ }
246
292
  if (!response.ok) throw await mapUpstreamHttpError(response, "qoder");
247
293
  if (!response.body) throw new GatewayError("platform_unavailable", 502, "Qoder returned empty body (request {requestId}). The upstream came back empty-handed — retry 🌙", {}, "upstream");
248
294
  // Headers arrived: the pre-stream deadline has served its purpose.
@@ -250,7 +296,11 @@ class QoderAdapter implements ProviderAdapter {
250
296
  // context.abort_signal) governs the body. Keeping this timer armed
251
297
  // would kill healthy long streams at the stale pre-stream deadline.
252
298
  lifecycle.release();
253
- yield* qoderBodyToCanonicalEvents(response.body as ReadableStream<Uint8Array>, lifecycle.signal, request.model);
299
+ yield* this.#withSessionCacheEstimate(
300
+ qoderBodyToCanonicalEvents(response.body as ReadableStream<Uint8Array>, lifecycle.signal, request.model),
301
+ request,
302
+ qoderSessionId(auth.userId, modelId),
303
+ );
254
304
  } catch (err: unknown) {
255
305
  const abortError = abortGatewayError(lifecycle, err, context.abort_signal);
256
306
  if (abortError) throw abortError;
@@ -114,6 +114,91 @@ export function qoderSessionId(userId: string, modelId: string): string {
114
114
 
115
115
  type QoderFetcher = (url: string, init: RequestInit) => Promise<Response>;
116
116
 
117
+ /** The fields a PAT→job-token exchange response carries. */
118
+ export interface ExchangeQoderPatResult {
119
+ readonly token: string;
120
+ readonly name?: string;
121
+ readonly userType?: string;
122
+ readonly refreshToken?: string;
123
+ readonly expiresAt?: string;
124
+ }
125
+
126
+ /**
127
+ * One PAT→job-token exchange against an explicit endpoint URL. Shared by
128
+ * `exchangeQoderPat` (auth) and the claim flow: both POST the same v1 body
129
+ * (`{personal_token, machine_id?, machine_token?}`) with the client identity
130
+ * headers and parse the same response token. Parameterized by URL so each call
131
+ * site keeps its own verified host (`center.qoder.sh` vs the openapi host);
132
+ * the header set is the one verified live against both (2026-11-05/05).
133
+ * Returns the raw `Response` plus the parsed result (or `null` when the body
134
+ * carried no usable token); `null` itself means the request failed to reach
135
+ * upstream. Callers decide how to surface each case (throw vs null).
136
+ */
137
+ export async function exchangeQoderPatForHost(args: {
138
+ readonly pat: string;
139
+ readonly url: string;
140
+ readonly machine?: { readonly id?: string; readonly token?: string };
141
+ readonly fetcher: QoderFetcher;
142
+ readonly signal: AbortSignal;
143
+ readonly version?: string;
144
+ /** Response keys to read the job token from, tried in order. Defaults to `["token"]`. */
145
+ readonly tokenKeys?: readonly string[];
146
+ }): Promise<{ readonly response: Response; readonly result: ExchangeQoderPatResult | null } | null> {
147
+ const body: Record<string, unknown> = { personal_token: args.pat };
148
+ if (args.machine?.id) body.machine_id = args.machine.id;
149
+ if (args.machine?.token) body.machine_token = args.machine.token;
150
+ const version = args.version ?? getQoderVersion();
151
+ const tokenKeys = args.tokenKeys ?? ["token"];
152
+ let response: Response;
153
+ try {
154
+ response = await args.fetcher(args.url, {
155
+ method: "POST",
156
+ headers: {
157
+ "Content-Type": "application/json",
158
+ Accept: "application/json",
159
+ "User-Agent": `qoder/${version}`,
160
+ "X-Request-ID": randomUUID().toUpperCase(),
161
+ "Cosy-ClientType": "5",
162
+ "Cosy-Version": version,
163
+ "Cosy-MachineOS": qoderMachineOs(),
164
+ },
165
+ body: JSON.stringify(body),
166
+ signal: args.signal,
167
+ });
168
+ } catch {
169
+ return null;
170
+ }
171
+ if (!response.ok) return { response, result: null };
172
+ let responseBody: unknown;
173
+ try {
174
+ responseBody = await response.json();
175
+ } catch {
176
+ return { response, result: null };
177
+ }
178
+ if (typeof responseBody !== "object" || responseBody === null || Array.isArray(responseBody)) {
179
+ return { response, result: null };
180
+ }
181
+ const rec = responseBody as Record<string, unknown>;
182
+ let token = "";
183
+ for (const key of tokenKeys) {
184
+ if (typeof rec[key] === "string" && (rec[key] as string).length > 0) {
185
+ token = rec[key] as string;
186
+ break;
187
+ }
188
+ }
189
+ if (token.length === 0) return { response, result: null };
190
+ return {
191
+ response,
192
+ result: {
193
+ token,
194
+ ...(typeof rec["name"] === "string" ? { name: rec["name"] as string } : {}),
195
+ ...(typeof rec["userType"] === "string" ? { userType: rec["userType"] as string } : {}),
196
+ ...(typeof rec["refresh_token"] === "string" ? { refreshToken: rec["refresh_token"] as string } : {}),
197
+ ...(typeof rec["expires_at"] === "string" ? { expiresAt: rec["expires_at"] as string } : {}),
198
+ },
199
+ };
200
+ }
201
+
117
202
  /**
118
203
  * Exchanges a PAT (`pt-…`) for a short-lived job token (`jt-…`) using the
119
204
  * modern v1 endpoint. Verified live (2026-11-05): `POST
@@ -135,41 +220,28 @@ async function exchangeQoderPat(
135
220
  realm: QoderRealm = "intl",
136
221
  ): Promise<QoderAuth> {
137
222
  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 }),
223
+ const exchanged = await exchangeQoderPatForHost({
224
+ pat,
225
+ url: QODER_REALMS[realm].jobTokenUrl,
226
+ machine: { id: machineId },
227
+ fetcher,
150
228
  signal,
229
+ version,
151
230
  });
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");
231
+ if (exchanged === null) {
232
+ throw new GatewayError("platform_unavailable", 502, "Qoder PAT exchange failed to reach upstream (request {requestId}). The upstream came back empty-handed — retry 🌙", {}, "upstream");
158
233
  }
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");
234
+ if (!exchanged.response.ok) throw await mapUpstreamHttpError(exchanged.response, "qoder");
235
+ const result = exchanged.result;
236
+ if (result === null) {
237
+ throw new GatewayError("platform_unavailable", 502, "Qoder PAT exchange returned no usable token (request {requestId}). The upstream came back empty-handed — retry 🌙", {}, "upstream");
166
238
  }
167
239
  return {
168
240
  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) : "",
241
+ userName: result.name ?? "",
242
+ userType: result.userType ?? "personal_standard",
243
+ securityOauthToken: result.token,
244
+ refreshToken: result.refreshToken ?? "",
173
245
  machineId,
174
246
  };
175
247
  }
@@ -0,0 +1,32 @@
1
+ // Qoder session-cache estimation.
2
+ //
3
+ // The Qoder upstream never reports prompt-cache hits: neither the gRPC surface
4
+ // (used `prompt_tokens_details` absent on every chunk — verified live
5
+ // 2026-11-05, `scripts/provider-probing/qoder/direct/direct-cache-hit.ts`) nor
6
+ // the IDE HTTP surface (`usage: null` / `raw_usage` absent — `direct-cache-hit-ide.ts`)
7
+ // carries a cached-token counter. The underlying models do cache byte-identical
8
+ // prefixes, so for the same conversation we ESTIMATE the hit locally from the
9
+ // request history — exactly how the Trae and Kiro adapters bridge the same gap.
10
+ // This only fills the gap when upstream reports nothing (0 or unavailable); a
11
+ // genuine upstream report (>0) is trusted and left untouched.
12
+ //
13
+ // The state lives in a factory-created instance, so Qoder's sessions are fully
14
+ // isolated from Trae's/Kiro's (and any other consumer of the shared factory).
15
+
16
+ import type { CanonicalMessage, UsageRecord } from "../../../protocol/canonical-model";
17
+ import { createSessionCache, estimateSessionCache, resetSessionCacheForTesting } from "../shared/session-cache";
18
+
19
+ const cache = createSessionCache();
20
+
21
+ export function estimateQoderSessionCache(
22
+ record: UsageRecord,
23
+ sessionId: string | undefined,
24
+ messages: readonly CanonicalMessage[] | undefined,
25
+ ): UsageRecord {
26
+ return estimateSessionCache(cache, record, sessionId, messages);
27
+ }
28
+
29
+ /** Drops all tracked sessions. Test-only. */
30
+ export function resetQoderSessionCacheForTesting(): void {
31
+ resetSessionCacheForTesting(cache);
32
+ }
@@ -12,7 +12,7 @@ import { GatewayError } from "../../../protocol/gateway-error";
12
12
  import { defineModel } from "../../provider-model-definition";
13
13
  import type { ModelDefinition } from "../../provider-model-definition";
14
14
  import { QODER_CHAT_PATH, QODER_MODELS_PATH, qoderGatewayCandidates, type QoderRealm } from "./qoder-realm";
15
- import { qoderVirtualIdFor, QODER_VIRTUAL_MODELS } from "./qoder-virtual";
15
+ import { qoderVirtualIdFor, QODER_PROVIDER_WARNINGS, QODER_VIRTUAL_MODELS } from "./qoder-virtual";
16
16
 
17
17
  /** One upstream catalog row, as `/algo/api/v2/model/list` reports it. */
18
18
  export interface QoderCatalogEntry {
@@ -199,6 +199,8 @@ function qoderModels(
199
199
  // Only an explicit `true` marks the row free; an unstated flag is left
200
200
  // unset so nothing claims a tier the roster did not state.
201
201
  ...(cfg.is_free === undefined ? {} : { free: cfg.is_free, freeTier: cfg.is_free }),
202
+ // Provider-level caveats, not per-model claims (D37).
203
+ warnings: QODER_PROVIDER_WARNINGS,
202
204
  }),
203
205
  );
204
206
  }
@@ -255,6 +257,7 @@ export function qoderVirtualModels(
255
257
  : target.freeTier === undefined
256
258
  ? {}
257
259
  : { free: target.freeTier, freeTier: target.freeTier }),
260
+ warnings: QODER_PROVIDER_WARNINGS,
258
261
  }),
259
262
  );
260
263
  }
@@ -319,7 +322,7 @@ export function mapQoderCatalogResponse(raw: unknown): QoderCatalogEntry[] {
319
322
  * to a base-catalog/majority vote that can sit below the roster's ceiling
320
323
  * (e.g. `qwen3.7-plus` votes 64k while the roster says 131k).
321
324
  */
322
- function qoderRosterMaxOutputFor(providerId: string, key: string): number | undefined {
325
+ export function qoderRosterMaxOutput(providerId: string, key: string): number | undefined {
323
326
  const configs = providerId === "qodercn" ? QODER_CN_MODEL_CONFIGS : QODER_INTL_MODEL_CONFIGS;
324
327
  return configs[key]?.max_output_tokens;
325
328
  }
@@ -329,7 +332,7 @@ export function qoderCatalogModel(
329
332
  entry: QoderCatalogEntry,
330
333
  providerId: string,
331
334
  ): ModelDefinition {
332
- const rosterOutput = qoderRosterMaxOutputFor(providerId, entry.key);
335
+ const rosterOutput = qoderRosterMaxOutput(providerId, entry.key);
333
336
  return defineModel({
334
337
  id: entry.key,
335
338
  providerId,
@@ -347,8 +350,9 @@ export function qoderCatalogModel(
347
350
  free: entry.isFree,
348
351
  freeTier: entry.isFree,
349
352
  // The live row carries no output ceiling, so inherit the static roster's
350
- // stated one when it exists (see `qoderRosterMaxOutputFor`).
353
+ // stated one when it exists (see `qoderRosterMaxOutput`).
351
354
  ...(rosterOutput === undefined ? {} : { out: rosterOutput }),
355
+ warnings: QODER_PROVIDER_WARNINGS,
352
356
  });
353
357
  }
354
358
 
@@ -442,6 +446,21 @@ export function qoderDiscoveredMaxContexts(realm: QoderRealm): ReadonlyMap<strin
442
446
  return discoveryMaxContextByKey.get(realm) ?? new Map();
443
447
  }
444
448
 
449
+ /**
450
+ * The widest selectable context window for one model key.
451
+ *
452
+ * The live list's `context_config` is the authority when discovery has run:
453
+ * it names every window the account can switch to, and the widest one is what
454
+ * a dispatch should send (`model_config.max_input_tokens`) so a conversation is
455
+ * never capped at the *currently active* window (often 180k) when the account
456
+ * can use 1M. Falls back to the static roster's stated window otherwise.
457
+ */
458
+ export function qoderMaxContextFor(modelId: string, modelConfig: QoderModelConfig, realm: QoderRealm): number {
459
+ const discovered = qoderDiscoveredMaxContexts(realm).get(modelId);
460
+ if (discovered !== undefined) return discovered;
461
+ return modelConfig.max_input_tokens;
462
+ }
463
+
445
464
  export async function discoverQoderModels(input: {
446
465
  readonly realm: QoderRealm;
447
466
  /** Account secret the injected signer signs with; blank fails fast. */
@@ -522,7 +541,7 @@ export async function discoverQoderModels(input: {
522
541
  // is published.
523
542
  const virtuals = QODER_VIRTUAL_MODELS.filter((virtual) => virtual.target === entry.key);
524
543
  if (virtuals.length === 0) return [qoderCatalogModel(entry, providerId)];
525
- const rosterOutput = qoderRosterMaxOutputFor(providerId, entry.key);
544
+ const rosterOutput = qoderRosterMaxOutput(providerId, entry.key);
526
545
  return virtuals.map((virtual) =>
527
546
  defineModel({
528
547
  id: virtual.id,
@@ -534,7 +553,7 @@ export async function discoverQoderModels(input: {
534
553
  reasoning: entry.isReasoning,
535
554
  // A virtual override wins; otherwise inherit the static roster's
536
555
  // ceiling for the slot instead of a base-catalog/majority estimate
537
- // (see `qoderRosterMaxOutputFor`).
556
+ // (see `qoderRosterMaxOutput`).
538
557
  ...(virtual.outputLimit !== undefined
539
558
  ? { out: virtual.outputLimit }
540
559
  : rosterOutput === undefined
@@ -542,6 +561,7 @@ export async function discoverQoderModels(input: {
542
561
  : { out: rosterOutput }),
543
562
  free: virtual.free ?? entry.isFree,
544
563
  freeTier: virtual.free ?? entry.isFree,
564
+ warnings: QODER_PROVIDER_WARNINGS,
545
565
  }),
546
566
  );
547
567
  });