cartethyia 2.7.8 → 2.7.9

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 (371) hide show
  1. package/.env.example +0 -1
  2. package/README.md +29 -59
  3. package/dist/dashboard/assets/BannedPage-BxoG1MrL.js +1 -0
  4. package/dist/dashboard/assets/BannedPage-BxoG1MrL.js.gz +0 -0
  5. package/dist/dashboard/assets/{CliToolDetailPage-BRWKPQPf.js → CliToolDetailPage-ixwgKk53.js} +1 -1
  6. package/dist/dashboard/assets/CliToolDetailPage-ixwgKk53.js.gz +0 -0
  7. package/dist/dashboard/assets/{CliToolsPage-BP-Y5iHN.js → CliToolsPage-BmttNV3U.js} +1 -1
  8. package/dist/dashboard/assets/CliToolsPage-BmttNV3U.js.gz +0 -0
  9. package/dist/dashboard/assets/CombosPage-BE3qwwfC.js +1 -0
  10. package/dist/dashboard/assets/CombosPage-BE3qwwfC.js.gz +0 -0
  11. package/dist/dashboard/assets/ConsoleLogPage-CRZdmMIX.js +1 -0
  12. package/dist/dashboard/assets/ConsoleLogPage-CRZdmMIX.js.gz +0 -0
  13. package/dist/dashboard/assets/CustomizationPage-Dv_FJ6Er.js +1 -0
  14. package/dist/dashboard/assets/CustomizationPage-Dv_FJ6Er.js.gz +0 -0
  15. package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js +1 -0
  16. package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js.gz +0 -0
  17. package/dist/dashboard/assets/{InjectSystemPromptPanel-Cv-2Ol_K.js → InjectSystemPromptPanel-BOoO8rpz.js} +2 -2
  18. package/dist/dashboard/assets/InjectSystemPromptPanel-BOoO8rpz.js.gz +0 -0
  19. package/dist/dashboard/assets/{LoginPage-bKfz_b1K.js → LoginPage-ChJuNNVN.js} +1 -1
  20. package/dist/dashboard/assets/LoginPage-ChJuNNVN.js.gz +0 -0
  21. package/dist/dashboard/assets/{ModelPicker-B-WQxx_H.js → ModelPicker-BZAJtPg1.js} +1 -1
  22. package/dist/dashboard/assets/ModelPicker-BZAJtPg1.js.gz +0 -0
  23. package/dist/dashboard/assets/OverviewPage-BE40s3u5.js +1 -0
  24. package/dist/dashboard/assets/OverviewPage-BE40s3u5.js.gz +0 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js +8 -0
  26. package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js.gz +0 -0
  27. package/dist/dashboard/assets/ProviderIcon-CQ6bPUf_.js +1 -0
  28. package/dist/dashboard/assets/ProviderIcon-CQ6bPUf_.js.gz +0 -0
  29. package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js +1 -0
  30. package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js.gz +0 -0
  31. package/dist/dashboard/assets/{ProxyPage-emlTuxz6.js → ProxyPage-BJ87Pp0U.js} +1 -1
  32. package/dist/dashboard/assets/ProxyPage-BJ87Pp0U.js.gz +0 -0
  33. package/dist/dashboard/assets/{QuotaPage-CMJKNYWb.js → QuotaPage-gkSyRAKa.js} +1 -1
  34. package/dist/dashboard/assets/QuotaPage-gkSyRAKa.js.gz +0 -0
  35. package/dist/dashboard/assets/SettingsPage-C5HPWqEv.js +1 -0
  36. package/dist/dashboard/assets/SettingsPage-C5HPWqEv.js.gz +0 -0
  37. package/dist/dashboard/assets/{SetupPage-CUZrKs9r.js → SetupPage-CFNcATJJ.js} +1 -1
  38. package/dist/dashboard/assets/SetupPage-CFNcATJJ.js.gz +0 -0
  39. package/dist/dashboard/assets/{StudioPage-jWmUdDpy.js → StudioPage-YoD4tY5l.js} +1 -1
  40. package/dist/dashboard/assets/StudioPage-YoD4tY5l.js.gz +0 -0
  41. package/dist/dashboard/assets/UsagePage-DxjaAhb_.js +56 -0
  42. package/dist/dashboard/assets/UsagePage-DxjaAhb_.js.gz +0 -0
  43. package/dist/dashboard/assets/api-keys-BTlkq_Uk.js +1 -0
  44. package/dist/dashboard/assets/api-keys-BTlkq_Uk.js.gz +0 -0
  45. package/dist/dashboard/assets/app-BX2nnKqs.css +2 -0
  46. package/dist/dashboard/assets/app-BX2nnKqs.css.gz +0 -0
  47. package/dist/dashboard/assets/assert-CndthXFQ.js +1 -0
  48. package/dist/dashboard/assets/assert-CndthXFQ.js.gz +0 -0
  49. package/dist/dashboard/assets/{badge-DyI_mJfi.js → badge-DdbT_7Am.js} +1 -1
  50. package/dist/dashboard/assets/badge-DdbT_7Am.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-CsGN-nbm.js} +1 -1
  54. package/dist/dashboard/assets/banner-CsGN-nbm.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-mgZ_TLLZ.js} +1 -1
  64. package/dist/dashboard/assets/card-mgZ_TLLZ.js.gz +0 -0
  65. package/dist/dashboard/assets/cli-tools-A1-v8-03.js +1 -0
  66. package/dist/dashboard/assets/cli-tools-A1-v8-03.js.gz +0 -0
  67. package/dist/dashboard/assets/{clipboard-button-ByjCk0o9.js → clipboard-button-CJ4Zty89.js} +1 -1
  68. package/dist/dashboard/assets/clipboard-button-CJ4Zty89.js.gz +0 -0
  69. package/dist/dashboard/assets/{dialog-BZhOlfz-.js → dialog-B5L2jPQ_.js} +1 -1
  70. package/dist/dashboard/assets/dialog-B5L2jPQ_.js.gz +0 -0
  71. package/dist/dashboard/assets/{drawer-BADqIRYP.js → drawer-DkjUouAj.js} +1 -1
  72. package/dist/dashboard/assets/drawer-DkjUouAj.js.gz +0 -0
  73. package/dist/dashboard/assets/{entry-COaC-irN.js → entry-B-LVLV6o.js} +1 -1
  74. package/dist/dashboard/assets/entry-B-LVLV6o.js.gz +0 -0
  75. package/dist/dashboard/assets/entry-CAhdYl3B.js +1 -0
  76. package/dist/dashboard/assets/entry-CAhdYl3B.js.gz +0 -0
  77. package/dist/dashboard/assets/entry-D2xD0Tmd.js +4 -0
  78. package/dist/dashboard/assets/entry-D2xD0Tmd.js.gz +0 -0
  79. package/dist/dashboard/assets/{formatters-DcHJyRiu.js → formatters-CrsdLVsn.js} +1 -1
  80. package/dist/dashboard/assets/formatters-CrsdLVsn.js.gz +0 -0
  81. package/dist/dashboard/assets/{index-CgKmFngv.js → index-BU64Lqvp.js} +2 -2
  82. package/dist/dashboard/assets/index-BU64Lqvp.js.gz +0 -0
  83. package/dist/dashboard/assets/{inline-B2BirKry.js → inline-B2aDLyyg.js} +1 -1
  84. package/dist/dashboard/assets/inline-B2aDLyyg.js.gz +0 -0
  85. package/dist/dashboard/assets/{input-D-qV9dKY.js → input-BCVkdR9d.js} +1 -1
  86. package/dist/dashboard/assets/input-BCVkdR9d.js.gz +0 -0
  87. package/dist/dashboard/assets/{layout-B7tVWhtV.js → layout-DNcAdMK-.js} +1 -1
  88. package/dist/dashboard/assets/layout-DNcAdMK-.js.gz +0 -0
  89. package/dist/dashboard/assets/{list-row-CtkGE3bF.js → list-row-DgrIH0hw.js} +1 -1
  90. package/dist/dashboard/assets/list-row-DgrIH0hw.js.gz +0 -0
  91. package/dist/dashboard/assets/{live-DGizF06-.js → live-Cae01iu8.js} +1 -1
  92. package/dist/dashboard/assets/live-Cae01iu8.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-CRib7H_2.js} +1 -1
  96. package/dist/dashboard/assets/network-CRib7H_2.js.gz +0 -0
  97. package/dist/dashboard/assets/{numbers-BnAd22a_.js → numbers-BKQE8Xpz.js} +1 -1
  98. package/dist/dashboard/assets/numbers-BKQE8Xpz.js.gz +0 -0
  99. package/dist/dashboard/assets/{pagination-BAywyMzS.js → pagination-ecPvG_8g.js} +1 -1
  100. package/dist/dashboard/assets/pagination-ecPvG_8g.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/{query-keys-YSoQOUh6.js → query-keys-IKLh0YFd.js} +1 -1
  106. package/dist/dashboard/assets/query-keys-IKLh0YFd.js.gz +0 -0
  107. package/dist/dashboard/assets/routing-Ckm1UXuC.js +1 -0
  108. package/dist/dashboard/assets/routing-Ckm1UXuC.js.gz +0 -0
  109. package/dist/dashboard/assets/{select-BCOcv8WK.js → select-ChoFWyKz.js} +1 -1
  110. package/dist/dashboard/assets/select-ChoFWyKz.js.gz +0 -0
  111. package/dist/dashboard/assets/settings-CKjFPlKW.js +1 -0
  112. package/dist/dashboard/assets/settings-CKjFPlKW.js.gz +0 -0
  113. package/dist/dashboard/assets/{stack-ByKt3Q1g.js → stack-DWCwU3fh.js} +1 -1
  114. package/dist/dashboard/assets/stack-DWCwU3fh.js.gz +0 -0
  115. package/dist/dashboard/assets/{state-DXbGTx7N.js → state-Cg3VL5J_.js} +1 -1
  116. package/dist/dashboard/assets/state-Cg3VL5J_.js.gz +0 -0
  117. package/dist/dashboard/assets/story/25817331.webp +0 -0
  118. package/dist/dashboard/assets/story/Shorekeeper.webp +0 -0
  119. package/dist/dashboard/assets/story/cartethyia-god.webp +0 -0
  120. package/dist/dashboard/assets/story/fleurdelys_plus.webp +0 -0
  121. package/dist/dashboard/assets/story/jinhsi-blossom.webp +0 -0
  122. package/dist/dashboard/assets/story/phrolova-a.webp +0 -0
  123. package/dist/dashboard/assets/story/requestdeniawokkjpg.webp +0 -0
  124. package/dist/dashboard/assets/story/wuthering-waves-hiyuki-aemeath-hiyuki-rover.webp +0 -0
  125. package/dist/dashboard/assets/{switch-ZeTsW623.js → switch-Db70BMix.js} +1 -1
  126. package/dist/dashboard/assets/switch-Db70BMix.js.gz +0 -0
  127. package/dist/dashboard/assets/{useMutation-CbCP4w1o.js → useMutation-BqdE0LuM.js} +1 -1
  128. package/dist/dashboard/assets/useMutation-BqdE0LuM.js.gz +0 -0
  129. package/dist/dashboard/console-backgrounds.webp +0 -0
  130. package/dist/dashboard/console-backgrounds_initial.webp +0 -0
  131. package/dist/dashboard/donation/qris-risun-marke.webp +0 -0
  132. package/dist/dashboard/index.html +3 -3
  133. package/dist/dashboard/index.html.gz +0 -0
  134. package/dist/dashboard/precompressed-assets.json +57 -55
  135. package/migrations/full-postgres/0044_add_models_warning_metadata.sql +10 -0
  136. package/migrations/full-postgres/0045_migrate_workbuddy_to_codebuddy.sql +156 -0
  137. package/migrations/lite-sqlite3/0044_add_models_warning_metadata.sql +11 -0
  138. package/migrations/lite-sqlite3/0045_migrate_workbuddy_to_codebuddy.sql +156 -0
  139. package/package.json +1 -1
  140. package/src/boot/dependencies.ts +11 -3
  141. package/src/config/cooldowns.ts +1 -1
  142. package/src/config/spec.ts +0 -6
  143. package/src/console/api-keys/routes.ts +11 -4
  144. package/src/console/console-router.ts +7 -0
  145. package/src/console/dashboard-assets.ts +5 -5
  146. package/src/console/domain-registration.ts +27 -0
  147. package/src/console/ip-bans/routes.ts +99 -0
  148. package/src/console/providers/catalog/catalog-projections.ts +5 -2
  149. package/src/console/providers/catalog/contracts.ts +123 -0
  150. package/src/console/providers/catalog/model-operations.ts +122 -10
  151. package/src/console/providers/catalog/provider-operations.ts +116 -8
  152. package/src/console/providers/catalog/public-model-store.ts +0 -0
  153. package/src/console/providers/catalog/routes.ts +64 -4
  154. package/src/console/providers/catalog/store-helpers.ts +30 -1
  155. package/src/console/providers/catalog/store.ts +243 -3
  156. package/src/console/quota/account-quota-global.ts +89 -1
  157. package/src/console/quota/native-identity.ts +2 -0
  158. package/src/console/settings/contracts.ts +18 -0
  159. package/src/console/settings/store.ts +32 -0
  160. package/src/console/system-prompt/contracts.ts +0 -1
  161. package/src/main.ts +1 -0
  162. package/src/observability/event-query.ts +18 -4
  163. package/src/persistence/dialect.ts +12 -0
  164. package/src/persistence/schema.ts +20 -2
  165. package/src/protocol/dsml.ts +1 -1
  166. package/src/protocol/gateway-error.ts +1 -0
  167. package/src/protocol/reasoning.ts +1 -1
  168. package/src/protocol/request/chat.ts +4 -4
  169. package/src/protocol/request/messages.ts +1 -1
  170. package/src/protocol/response/gemini.ts +6 -1
  171. package/src/protocol/sse-decoder.ts +25 -8
  172. package/src/protocol/stream-error-frames.ts +13 -3
  173. package/src/protocol/translation/quirks.ts +1 -1
  174. package/src/protocol/translation/tool-repair.ts +2 -2
  175. package/src/protocol/upstream/send-claude-messages.ts +15 -4
  176. package/src/protocol/upstream-parsing.ts +1 -1
  177. package/src/providers/adapters/compatible-adapter.ts +7 -5
  178. package/src/providers/authentication/oauth-refresh-service.ts +149 -36
  179. package/src/providers/default-registry.ts +68 -3
  180. package/src/providers/discovery/discovery-types.ts +12 -0
  181. package/src/providers/discovery/probe-phases.ts +61 -30
  182. package/src/providers/discovery/probing-service.ts +279 -164
  183. package/src/providers/integrations/antigravity/antigravity-protocol.ts +117 -2
  184. package/src/providers/integrations/antigravity/antigravity.ts +13 -2
  185. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +72 -34
  186. package/src/providers/integrations/buddy/buddy-chat-shared.ts +18 -18
  187. package/src/providers/integrations/buddy/buddy-checkin.ts +17 -39
  188. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +2 -2
  189. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +5 -5
  190. package/src/providers/integrations/buddy/buddy-identity.ts +70 -0
  191. package/src/providers/integrations/buddy/buddy-media.ts +4 -5
  192. package/src/providers/integrations/buddy/buddy-oauth-shared.ts +2 -2
  193. package/src/providers/integrations/buddy/buddy-quota-shared.ts +1 -1
  194. package/src/providers/integrations/buddy/buddy-usage-shared.ts +1 -3
  195. package/src/providers/integrations/buddy/capabilities.ts +4 -41
  196. package/src/providers/integrations/buddy/codebuddy/cn-catalog.ts +2 -1
  197. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +20 -1
  198. package/src/providers/integrations/buddy/codebuddy/provider.ts +2 -2
  199. package/src/providers/integrations/buddy/codebuddy/shared.ts +6 -10
  200. package/src/providers/integrations/cursor/cursor-catalog.ts +2 -0
  201. package/src/providers/integrations/cursor/cursor-exec.ts +738 -0
  202. package/src/providers/integrations/cursor/cursor-http2.ts +2 -0
  203. package/src/providers/integrations/cursor/cursor-request.ts +9 -8
  204. package/src/providers/integrations/cursor/cursor-shared.ts +19 -1
  205. package/src/providers/integrations/cursor/cursor.ts +50 -30
  206. package/src/providers/integrations/cursor/protocol/tool-canonical.ts +26 -100
  207. package/src/providers/integrations/devin/devin-auth.ts +353 -0
  208. package/src/providers/integrations/devin/devin-catalog.ts +207 -0
  209. package/src/providers/integrations/devin/devin-http.ts +91 -0
  210. package/src/providers/integrations/devin/devin-quota.ts +214 -0
  211. package/src/providers/integrations/devin/devin-shared.ts +111 -0
  212. package/src/providers/integrations/devin/devin.ts +539 -0
  213. package/src/providers/integrations/devin/protocol/devin-proto.ts +584 -0
  214. package/src/providers/integrations/devin/protocol/framing.ts +124 -0
  215. package/src/providers/integrations/factory/factory.ts +2 -2
  216. package/src/providers/integrations/freepi/freepi-catalog.ts +60 -0
  217. package/src/providers/integrations/freepi/freepi-discovery.ts +82 -0
  218. package/src/providers/integrations/freepi/freepi-headers.ts +21 -0
  219. package/src/providers/integrations/freepi/freepi-oauth.ts +142 -0
  220. package/src/providers/integrations/freepi/freepi-protocol.ts +242 -0
  221. package/src/providers/integrations/freepi/freepi-quota.ts +126 -0
  222. package/src/providers/integrations/freepi/freepi-realm.ts +35 -0
  223. package/src/providers/integrations/freepi/freepi.ts +292 -0
  224. package/src/providers/integrations/gemini.ts +2 -2
  225. package/src/providers/integrations/hermes/hermes-oauth.ts +94 -0
  226. package/src/providers/integrations/hermes.ts +4 -12
  227. package/src/providers/integrations/opencode/fingerprint.ts +1 -1
  228. package/src/providers/integrations/perch/perch-catalog.ts +105 -0
  229. package/src/providers/integrations/perch/perch-oauth.ts +170 -0
  230. package/src/providers/integrations/perch/perch-protocol.ts +211 -0
  231. package/src/providers/integrations/perch/perch-quota.ts +137 -0
  232. package/src/providers/integrations/perch/perch-realm.ts +82 -0
  233. package/src/providers/integrations/perch/perch-system-prompt.ts +13 -0
  234. package/src/providers/integrations/perch/perch.ts +315 -0
  235. package/src/providers/integrations/perch/saffron-system-prompt.txt +8 -0
  236. package/src/providers/integrations/qoder/chat-proto.ts +328 -328
  237. package/src/providers/integrations/qoder/claim-qoder-credits.ts +1 -1
  238. package/src/providers/integrations/qoder/qoder-adapter.ts +89 -28
  239. package/src/providers/integrations/qoder/qoder-cache.ts +32 -0
  240. package/src/providers/integrations/qoder/qoder-catalog.ts +6 -1
  241. package/src/providers/integrations/qoder/qoder-checkin.ts +2 -2
  242. package/src/providers/integrations/qoder/qoder-grpc.ts +210 -6
  243. package/src/providers/integrations/qoder/qoder-ide.ts +5 -1
  244. package/src/providers/integrations/qoder/qoder-identity-cache.ts +184 -0
  245. package/src/providers/integrations/qoder/qoder-native-identity-store.ts +83 -18
  246. package/src/providers/integrations/qoder/qoder-realm.ts +9 -6
  247. package/src/providers/integrations/qoder/qoder-umid.ts +109 -342
  248. package/src/providers/integrations/qoder/qoder-virtual.ts +33 -1
  249. package/src/providers/integrations/qoder/qoder.ts +3 -6
  250. package/src/providers/integrations/trae/trae-catalog.ts +9 -14
  251. package/src/providers/integrations/trae/trae-headers.ts +36 -11
  252. package/src/providers/integrations/trae/trae-oauth.ts +14 -17
  253. package/src/providers/integrations/trae/trae-protocol.ts +3 -6
  254. package/src/providers/integrations/trae/trae-quota.ts +103 -21
  255. package/src/providers/integrations/trae/trae-realm.ts +36 -15
  256. package/src/providers/integrations/trae/trae.ts +97 -7
  257. package/src/providers/operations/account-error-classification.ts +66 -1
  258. package/src/providers/operations/account-health-service.ts +192 -26
  259. package/src/providers/operations/account-refresh-status.ts +284 -0
  260. package/src/providers/operations/account-tier.ts +67 -0
  261. package/src/providers/operations/client-versions.ts +37 -99
  262. package/src/providers/operations/cooldown-rules.ts +61 -0
  263. package/src/providers/operations/provider-catalog-seeder.ts +4 -0
  264. package/src/providers/provider-contracts.ts +29 -0
  265. package/src/providers/provider-metadata.ts +59 -4
  266. package/src/providers/provider-model-definition.ts +36 -0
  267. package/src/providers/quota/refresh.ts +21 -0
  268. package/src/security/abuse.ts +71 -0
  269. package/src/transport/dispatch/proxy-request.ts +1 -0
  270. package/src/transport/dispatch/streaming/stream.ts +1 -0
  271. package/src/transport/dispatch/upstream.ts +4 -1
  272. package/src/transport/failure-policy.ts +113 -7
  273. package/src/transport/routing/router.ts +25 -1
  274. package/src/transport/surface/messages/adapter.ts +2 -1
  275. package/src/transport/surface/responses/encode.ts +12 -1
  276. package/src/workers/daily-checkin.ts +1 -1
  277. package/src/workers/quota-refresh-sweep.ts +1 -1
  278. package/src/workers/scheduler.ts +22 -4
  279. package/dist/dashboard/assets/BannedPage-DeIbIiBU.js +0 -1
  280. package/dist/dashboard/assets/BannedPage-DeIbIiBU.js.gz +0 -0
  281. package/dist/dashboard/assets/CliToolDetailPage-BRWKPQPf.js.gz +0 -0
  282. package/dist/dashboard/assets/CliToolsPage-BP-Y5iHN.js.gz +0 -0
  283. package/dist/dashboard/assets/CombosPage-B7JHozVa.js +0 -1
  284. package/dist/dashboard/assets/CombosPage-B7JHozVa.js.gz +0 -0
  285. package/dist/dashboard/assets/ConsoleLogPage-GX7iGGcu.js +0 -1
  286. package/dist/dashboard/assets/ConsoleLogPage-GX7iGGcu.js.gz +0 -0
  287. package/dist/dashboard/assets/CustomizationPage-HVonbGEa.js +0 -1
  288. package/dist/dashboard/assets/CustomizationPage-HVonbGEa.js.gz +0 -0
  289. package/dist/dashboard/assets/HealthEventsModal-xaut4OJ9.js +0 -1
  290. package/dist/dashboard/assets/HealthEventsModal-xaut4OJ9.js.gz +0 -0
  291. package/dist/dashboard/assets/InjectSystemPromptPanel-Cv-2Ol_K.js.gz +0 -0
  292. package/dist/dashboard/assets/LoginPage-bKfz_b1K.js.gz +0 -0
  293. package/dist/dashboard/assets/ModelPicker-B-WQxx_H.js.gz +0 -0
  294. package/dist/dashboard/assets/OverviewPage-Z9pJZFo8.js +0 -1
  295. package/dist/dashboard/assets/OverviewPage-Z9pJZFo8.js.gz +0 -0
  296. package/dist/dashboard/assets/ProviderDetailPage-CMeDBxR1.js +0 -7
  297. package/dist/dashboard/assets/ProviderDetailPage-CMeDBxR1.js.gz +0 -0
  298. package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js +0 -1
  299. package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js.gz +0 -0
  300. package/dist/dashboard/assets/ProvidersPage-BXPdOpG4.js +0 -1
  301. package/dist/dashboard/assets/ProvidersPage-BXPdOpG4.js.gz +0 -0
  302. package/dist/dashboard/assets/ProxyPage-emlTuxz6.js.gz +0 -0
  303. package/dist/dashboard/assets/QuotaPage-CMJKNYWb.js.gz +0 -0
  304. package/dist/dashboard/assets/SettingsPage-BSy7EwGo.js +0 -1
  305. package/dist/dashboard/assets/SettingsPage-BSy7EwGo.js.gz +0 -0
  306. package/dist/dashboard/assets/SetupPage-CUZrKs9r.js.gz +0 -0
  307. package/dist/dashboard/assets/StudioPage-jWmUdDpy.js.gz +0 -0
  308. package/dist/dashboard/assets/UsagePage-5F9tGVGG.js +0 -56
  309. package/dist/dashboard/assets/UsagePage-5F9tGVGG.js.gz +0 -0
  310. package/dist/dashboard/assets/api-keys-KvX83mRe.js +0 -1
  311. package/dist/dashboard/assets/api-keys-KvX83mRe.js.gz +0 -0
  312. package/dist/dashboard/assets/app-BEsU8hKs.css +0 -2
  313. package/dist/dashboard/assets/app-BEsU8hKs.css.gz +0 -0
  314. package/dist/dashboard/assets/assert-CidQVoNo.js +0 -1
  315. package/dist/dashboard/assets/assert-CidQVoNo.js.gz +0 -0
  316. package/dist/dashboard/assets/badge-DyI_mJfi.js.gz +0 -0
  317. package/dist/dashboard/assets/banner-B-yXtCqb.js.gz +0 -0
  318. package/dist/dashboard/assets/brand/bansos_page.png +0 -0
  319. package/dist/dashboard/assets/brand/denia_bansos.jpg +0 -0
  320. package/dist/dashboard/assets/brand/favicon.jpg +0 -0
  321. package/dist/dashboard/assets/brand/login_register.jpg +0 -0
  322. package/dist/dashboard/assets/brand/login_register_initial.jpg +0 -0
  323. package/dist/dashboard/assets/brand/og_bansos.jpg +0 -0
  324. package/dist/dashboard/assets/brand/og_image.jpg +0 -0
  325. package/dist/dashboard/assets/brand/sidebar_ico.jpg +0 -0
  326. package/dist/dashboard/assets/card-_HwAEqFF.js.gz +0 -0
  327. package/dist/dashboard/assets/cli-tools-Bwv5-zGZ.js +0 -1
  328. package/dist/dashboard/assets/cli-tools-Bwv5-zGZ.js.gz +0 -0
  329. package/dist/dashboard/assets/clipboard-button-ByjCk0o9.js.gz +0 -0
  330. package/dist/dashboard/assets/dialog-BZhOlfz-.js.gz +0 -0
  331. package/dist/dashboard/assets/drawer-BADqIRYP.js.gz +0 -0
  332. package/dist/dashboard/assets/entry-COaC-irN.js.gz +0 -0
  333. package/dist/dashboard/assets/entry-Ca7LSCet.js +0 -4
  334. package/dist/dashboard/assets/entry-Ca7LSCet.js.gz +0 -0
  335. package/dist/dashboard/assets/entry-PDd27uCd.js +0 -1
  336. package/dist/dashboard/assets/entry-PDd27uCd.js.gz +0 -0
  337. package/dist/dashboard/assets/formatters-DcHJyRiu.js.gz +0 -0
  338. package/dist/dashboard/assets/index-CgKmFngv.js.gz +0 -0
  339. package/dist/dashboard/assets/inline-B2BirKry.js.gz +0 -0
  340. package/dist/dashboard/assets/input-D-qV9dKY.js.gz +0 -0
  341. package/dist/dashboard/assets/layout-B7tVWhtV.js.gz +0 -0
  342. package/dist/dashboard/assets/list-row-CtkGE3bF.js.gz +0 -0
  343. package/dist/dashboard/assets/live-DGizF06-.js.gz +0 -0
  344. package/dist/dashboard/assets/minimize-2-BTmMTK4o.js +0 -1
  345. package/dist/dashboard/assets/minimize-2-BTmMTK4o.js.gz +0 -0
  346. package/dist/dashboard/assets/mutation-GU7XdEjz.js.gz +0 -0
  347. package/dist/dashboard/assets/network-C9UE-bOr.js.gz +0 -0
  348. package/dist/dashboard/assets/numbers-BnAd22a_.js.gz +0 -0
  349. package/dist/dashboard/assets/pagination-BAywyMzS.js.gz +0 -0
  350. package/dist/dashboard/assets/query-keys-YSoQOUh6.js.gz +0 -0
  351. package/dist/dashboard/assets/routing-fnCsxgNO.js +0 -1
  352. package/dist/dashboard/assets/routing-fnCsxgNO.js.gz +0 -0
  353. package/dist/dashboard/assets/select-BCOcv8WK.js.gz +0 -0
  354. package/dist/dashboard/assets/settings-BxslsErn.js +0 -1
  355. package/dist/dashboard/assets/settings-BxslsErn.js.gz +0 -0
  356. package/dist/dashboard/assets/stack-ByKt3Q1g.js.gz +0 -0
  357. package/dist/dashboard/assets/state-DXbGTx7N.js.gz +0 -0
  358. package/dist/dashboard/assets/switch-ZeTsW623.js.gz +0 -0
  359. package/dist/dashboard/assets/useMutation-CbCP4w1o.js.gz +0 -0
  360. package/dist/dashboard/console-backgrounds.jpg +0 -0
  361. package/dist/dashboard/console-backgrounds_initial.jpg +0 -0
  362. package/dist/dashboard/donation/qris-risun-marke.png +0 -0
  363. package/src/providers/integrations/buddy/turing-device-token.ts +0 -262
  364. package/src/providers/integrations/buddy/workbuddy/cn-oauth.ts +0 -30
  365. package/src/providers/integrations/buddy/workbuddy/cn-provider.ts +0 -58
  366. package/src/providers/integrations/buddy/workbuddy/oauth.ts +0 -31
  367. package/src/providers/integrations/buddy/workbuddy/provider.ts +0 -209
  368. package/src/providers/integrations/buddy/workbuddy/quota.ts +0 -161
  369. package/src/providers/integrations/buddy/workbuddy/shared.ts +0 -285
  370. /package/dist/dashboard/assets/{app-CTxoqyYB.js → app-B7kIWFm5.js} +0 -0
  371. /package/dist/dashboard/assets/{app-CTxoqyYB.js.gz → app-B7kIWFm5.js.gz} +0 -0
@@ -10,6 +10,7 @@ import { applyRouteUserAgent, type ProviderDispatchContext } from "../../provide
10
10
  import { abortGatewayError, createUpstreamDeadlineLifecycle } from '../../providers/operations/upstream-deadline';
11
11
  import { mapClaudeHttpError } from '../messages-errors';
12
12
  import { isRecord } from '../codecs';
13
+ import { upstreamMessageAsIs, MAX_UPSTREAM_ERROR_BYTES } from '../../transport/failure-policy';
13
14
  import { parseClaudeSseStream, claudeResponseToEvents } from '../response/messages';
14
15
 
15
16
  export async function* sendClaudeMessagesRequest(
@@ -67,20 +68,30 @@ export async function* sendClaudeMessagesRequest(
67
68
  try {
68
69
  json = JSON.parse(body) as unknown;
69
70
  } catch {
71
+ const fallback = "Claude response is not valid JSON (request {requestId}). The upstream came back empty-handed — retry 🌙";
72
+ const upstreamText = upstreamMessageAsIs(body, "");
70
73
  throw new GatewayError(
71
74
  "platform_unavailable",
72
75
  502,
73
- "Claude response is not valid JSON (request {requestId}). The upstream came back empty-handed — retry 🌙",
74
- {},
76
+ upstreamText.message || fallback,
77
+ {
78
+ raw: body.slice(0, MAX_UPSTREAM_ERROR_BYTES),
79
+ ...(upstreamText.hadUpstreamText ? { upstreamMessageAsIs: true } : {}),
80
+ },
75
81
  "upstream",
76
82
  );
77
83
  }
78
84
  if (!isRecord(json)) {
85
+ const fallback = "Claude response must be an object (request {requestId}). The upstream came back empty-handed — retry 🌙";
86
+ const upstreamText = upstreamMessageAsIs(body, "");
79
87
  throw new GatewayError(
80
88
  "platform_unavailable",
81
89
  502,
82
- "Claude response must be an object (request {requestId}). The upstream came back empty-handed — retry 🌙",
83
- {},
90
+ upstreamText.message || fallback,
91
+ {
92
+ raw: body.slice(0, MAX_UPSTREAM_ERROR_BYTES),
93
+ ...(upstreamText.hadUpstreamText ? { upstreamMessageAsIs: true } : {}),
94
+ },
84
95
  "upstream",
85
96
  );
86
97
  }
@@ -71,7 +71,7 @@ export function parseUpstreamBackoff(headers: {
71
71
  *
72
72
  * - **Relative** — "quota will reset in 3 hours", "try again in 30s".
73
73
  * - **Absolute** — "your usage will reset at 2026-09-24 02:12:51 UTC+8"
74
- * (WorkBuddy/CodeBuddy `6004`). An absolute stamp is *not* a duration, and
74
+ * (CodeBuddy `6004`). An absolute stamp is *not* a duration, and
75
75
  * the relative-only pattern silently missed it, so the account fell back to
76
76
  * the generic 15-minute default while the provider had actually parked it
77
77
  * for the rest of the window — the account kept re-entering rotation and
@@ -6,9 +6,9 @@ import {
6
6
  isContextLengthFailure,
7
7
  throwIfHtmlResponse,
8
8
  mapUpstreamHttpError,
9
- extractUpstreamMessage,
10
9
  extractHtmlTitle,
11
10
  upstreamErrorIdentifier,
11
+ upstreamMessageAsIs,
12
12
  } from "../../transport/failure-policy";
13
13
  import { applyRouteUserAgent, type ProviderDispatchTarget, type ProviderId, type ProviderDispatchContext } from "../provider-contracts";
14
14
  import { BaseProviderAdapter } from "./base-adapter";
@@ -439,11 +439,12 @@ export class OpenAICompatibleAdapter extends BaseProviderAdapter {
439
439
  }
440
440
  json = parsed as Record<string, unknown>;
441
441
  } catch {
442
- const summary = extractHtmlTitle(raw) || raw.trim() || `HTTP ${res.status}`;
442
+ const fallback = `Upstream returned a non-JSON response: ${extractHtmlTitle(raw) || raw.trim() || `HTTP ${res.status}`} (request {requestId}). The connection ghosted us — come back in a moment 👻`;
443
+ const upstreamText = upstreamMessageAsIs(raw, "");
443
444
  throw new GatewayError(
444
445
  "transport_unavailable",
445
446
  502,
446
- `Upstream returned a non-JSON response: ${summary} (request {requestId}). The connection ghosted us — come back in a moment 👻`.slice(0, 320),
447
+ `${upstreamText.message || fallback}`.slice(0, 500),
447
448
  { upstreamStatus: res.status, contentType },
448
449
  "upstream",
449
450
  );
@@ -485,15 +486,16 @@ export class OpenAICompatibleAdapter extends BaseProviderAdapter {
485
486
  const classification = isContextLengthFailure(json)
486
487
  ? { code: "context_length_exceeded" as const, status: 413, origin: "upstream" as const, credentialEvidence: false, rateLimitScope: undefined }
487
488
  : classifyUpstreamError(providerStatus, identifier);
488
- const message = extractUpstreamMessage(json) || "Upstream reported an error";
489
+ const upstreamText = upstreamMessageAsIs(json, "");
489
490
  throw new GatewayError(
490
491
  classification.code,
491
492
  classification.status,
492
- message,
493
+ upstreamText.message || "Upstream reported an error",
493
494
  {
494
495
  providerId: this.provider_id,
495
496
  ...(providerStatus === undefined ? {} : { providerStatus }),
496
497
  ...(identifier === undefined ? {} : { providerCode: identifier }),
498
+ ...(upstreamText.hadUpstreamText ? { upstreamMessageAsIs: true } : {}),
497
499
  ...(classification.credentialEvidence ? { credentialEvidence: true } : {}),
498
500
  ...(classification.rateLimitScope
499
501
  ? { rateLimitScope: classification.rateLimitScope }
@@ -169,6 +169,22 @@ async function releaseLease(
169
169
  );
170
170
  }
171
171
 
172
+ /**
173
+ * D21 invariant guard: a refresh is not a health verdict, so mutating the
174
+ * account's `status` here would silently re-enable an account a refresh was
175
+ * never meant to resurrect. The `.set()` calls in `persistRefreshed` accept
176
+ * `status` at the type level (it is a column on `provider_accounts`), so the
177
+ * guard is a runtime assertion that fails loudly in development instead of
178
+ * letting a future edit change health state from the refresh path.
179
+ */
180
+ function assertRefreshNeverMutatesStatus(set: Record<string, unknown>): void {
181
+ if ("status" in set) {
182
+ throw new Error(
183
+ "persistRefreshed must never write `status`; a refresh is not a health verdict",
184
+ );
185
+ }
186
+ }
187
+
172
188
  /** Persists a successful refresh and releases the lease, fenced on still holding it. */
173
189
  async function persistRefreshed(
174
190
  db: CartethyiaDatabase,
@@ -177,16 +193,21 @@ async function persistRefreshed(
177
193
  result: OAuthTokenRefreshResult,
178
194
  ): Promise<boolean> {
179
195
  return db.transaction(async (tx) => {
196
+ // This function must never write `status`; a refresh is not a health
197
+ // verdict. The guard below makes an accidental status write a loud dev
198
+ // failure rather than a silent re-enable.
199
+ const oauthSet = {
200
+ ...(result.refresh === undefined
201
+ ? {}
202
+ : { refreshCiphertext: encryptCredential(result.refresh) }),
203
+ expiresAt: result.expiresAt,
204
+ leaseOwner: null,
205
+ leaseExpiresAt: null,
206
+ };
207
+ assertRefreshNeverMutatesStatus(oauthSet);
180
208
  const leaseRows = await tx
181
209
  .update(providerOauthStates)
182
- .set({
183
- ...(result.refresh === undefined
184
- ? {}
185
- : { refreshCiphertext: encryptCredential(result.refresh) }),
186
- expiresAt: result.expiresAt,
187
- leaseOwner: null,
188
- leaseExpiresAt: null,
189
- })
210
+ .set(oauthSet)
190
211
  .where(
191
212
  and(
192
213
  eq(providerOauthStates.providerAccountId, accountId),
@@ -195,23 +216,27 @@ async function persistRefreshed(
195
216
  )
196
217
  .returning({ id: providerOauthStates.providerAccountId });
197
218
  if (leaseRows.length === 0) return false;
219
+ const accountSet = {
220
+ credentialCiphertext: encryptCredential(result.access),
221
+ // Only what the refresh reported: a refresher that learned nothing new
222
+ // leaves the stored configuration alone rather than blanking it.
223
+ // A learned identity only replaces a default label — an operator's
224
+ // custom rename is never overwritten. The CASE keeps the read and the
225
+ // write in one statement, so a concurrent rename cannot lose to the
226
+ // refresh's learned identity.
227
+ ...(result.accountLabel === undefined
228
+ ? {}
229
+ : {
230
+ label: sql`CASE WHEN lower(trim(${providerAccounts.label})) = lower(trim(${providerAccounts.providerId})) THEN ${result.accountLabel} ELSE ${providerAccounts.label} END`,
231
+ }),
232
+ ...(result.auth_state === undefined ? {} : { authState: result.auth_state }),
233
+ };
234
+ // The second half of the D21 invariant: the credential write must never
235
+ // touch the account's health state either.
236
+ assertRefreshNeverMutatesStatus(accountSet);
198
237
  await tx
199
238
  .update(providerAccounts)
200
- .set({
201
- credentialCiphertext: encryptCredential(result.access),
202
- // Only what the refresh reported: a refresher that learned nothing new
203
- // leaves the stored configuration alone rather than blanking it.
204
- // A learned identity only replaces a default label — an operator's
205
- // custom rename is never overwritten. The CASE keeps the read and the
206
- // write in one statement, so a concurrent rename cannot lose to the
207
- // refresh's learned identity.
208
- ...(result.accountLabel === undefined
209
- ? {}
210
- : {
211
- label: sql`CASE WHEN lower(trim(${providerAccounts.label})) = lower(trim(${providerAccounts.providerId})) THEN ${result.accountLabel} ELSE ${providerAccounts.label} END`,
212
- }),
213
- ...(result.auth_state === undefined ? {} : { authState: result.auth_state }),
214
- })
239
+ .set(accountSet)
215
240
  .where(eq(providerAccounts.id, accountId));
216
241
  invalidateCredentialCache(accountId);
217
242
  return true;
@@ -240,12 +265,21 @@ function buildRefreshContext(row: AccountRow, includeAccessToken: boolean): OAut
240
265
  /**
241
266
  * Marks an account permanently unusable after a definitive refresh failure,
242
267
  * fenced on the lease.
268
+ *
269
+ * @param preserveOperatorDisable When true, an account that is already
270
+ * `disabled` with no `cooldownUntil` — the operator's own disable — keeps its
271
+ * lack of a recovery deadline (D23). A machine disable (this function being
272
+ * the machine) always writes a deadline for itself, but it must not convert
273
+ * an operator's terminal decision into a recoverable one: adding a deadline
274
+ * to an operator-disabled row is what made the Recover button appear on an
275
+ * account the operator disabled on purpose.
243
276
  */
244
277
  async function disableAccount(
245
278
  db: CartethyiaDatabase,
246
279
  accountId: string,
247
280
  owner: string,
248
281
  message: string,
282
+ options: { readonly preserveOperatorDisable?: boolean } = {},
249
283
  ): Promise<void> {
250
284
  await db.transaction(async (tx) => {
251
285
  const leaseRows = await tx
@@ -260,6 +294,22 @@ async function disableAccount(
260
294
  .returning({ id: providerOauthStates.providerAccountId });
261
295
  if (leaseRows.length === 0) return;
262
296
  const disabledRecoveryMs = resolveAccountDisabledRecoveryMs();
297
+ // D23: read the row before writing so an operator disable (status `disabled`
298
+ // with no deadline) is not upgraded with a machine recovery deadline. The
299
+ // read is cheap and the lease already serializes refresh writers.
300
+ let isOperatorDisable = false;
301
+ if (options.preserveOperatorDisable === true) {
302
+ const rows = await tx
303
+ .select({
304
+ status: providerAccounts.status,
305
+ cooldownUntil: providerAccounts.cooldownUntil,
306
+ })
307
+ .from(providerAccounts)
308
+ .where(eq(providerAccounts.id, accountId))
309
+ .limit(1);
310
+ const row = rows[0];
311
+ isOperatorDisable = row?.status === "disabled" && row.cooldownUntil == null;
312
+ }
263
313
  await tx
264
314
  .update(providerAccounts)
265
315
  .set({
@@ -277,8 +327,10 @@ async function disableAccount(
277
327
  // parks the credential for good. The deadline is also what distinguishes
278
328
  // this (machine-disabled) from an operator's own disable, which leaves
279
329
  // `cooldownUntil` null and is therefore never swept. See
280
- // `resolveAccountDisabledRecoveryMs`.
281
- ...(disabledRecoveryMs > 0
330
+ // `resolveAccountDisabledRecoveryMs`. An operator-disabled row keeps its
331
+ // `null` deadline (D23): the machine must not make the operator's
332
+ // terminal disable look recoverable.
333
+ ...(!isOperatorDisable && disabledRecoveryMs > 0
282
334
  ? { cooldownUntil: new Date(Date.now() + disabledRecoveryMs) }
283
335
  : {}),
284
336
  })
@@ -299,20 +351,33 @@ async function disableAccount(
299
351
  * operator or an explicit recovery may re-enable it), while an `active` one
300
352
  * stays `active`. The static marker and the error clearing are orthogonal to
301
353
  * the account's health state.
354
+ *
355
+ * D24: for a `disabled` row the error fields are deliberately kept. Once the
356
+ * account is parked, `lastErrorCategory` is the only record of *why* (e.g.
357
+ * `auth_invalidated` → "Re-login required"); wiping it on the static
358
+ * annotation would leave a disabled account with no diagnosable reason. An
359
+ * `active` row still clears them — there the error is stale by definition.
302
360
  */
303
361
  async function markStaticToken(db: CartethyiaDatabase, accountId: string): Promise<void> {
362
+ const rows = await db
363
+ .select({ status: providerAccounts.status })
364
+ .from(providerAccounts)
365
+ .where(eq(providerAccounts.id, accountId))
366
+ .limit(1);
367
+ const isDisabled = rows[0]?.status === "disabled";
304
368
  await db
305
369
  .update(providerAccounts)
306
370
  .set({
307
371
  staticToken: true,
372
+ cooldownUntil: null,
308
373
  // A static token is a normal, usable credential, not an error state: any
309
374
  // error left by the failed refresh would otherwise keep the row reading
310
375
  // "Re-login required" while it is in fact serving. Mirrors the console's
311
- // static-token toggle.
312
- cooldownUntil: null,
313
- lastError: null,
314
- lastErrorCategory: null,
315
- lastErrorAt: null,
376
+ // static-token toggle. For a `disabled` row the category is preserved
377
+ // (D24), because there it is the diagnosis, not a stale artifact.
378
+ ...(isDisabled
379
+ ? {}
380
+ : { lastError: null, lastErrorCategory: null, lastErrorAt: null }),
316
381
  })
317
382
  .where(eq(providerAccounts.id, accountId));
318
383
  }
@@ -362,7 +427,8 @@ function failureStatus(error: unknown): number | undefined {
362
427
  * still perfectly usable. This pass re-reads those rows once at startup and
363
428
  * moves each to where it now belongs:
364
429
  *
365
- * - access token still usable ⇒ static token, back to `active`;
430
+ * - access token still usable ⇒ static token; the account stays `disabled`;
431
+ * only an operator can re-enable it;
366
432
  * - otherwise ⇒ the `auth_invalidated` category the console renders as
367
433
  * "Re-login required" and the quota sweep excludes.
368
434
  *
@@ -495,15 +561,20 @@ export class OAuthRefreshService {
495
561
  * that just refreshed cannot cause the caller to reuse a dead cached
496
562
  * secret. Returns the current decrypted access token, or `null` when the
497
563
  * account isn't OAuth, has no stored refresh token, or refresh failed.
564
+ *
565
+ * `opts.origin` labels who asked for the refresh (`ui`/`sweep`/`dispatch`)
566
+ * and is carried on the `token_refresh` log lines (D26). The origin is
567
+ * observational only — it never changes what a refresh does.
498
568
  */
499
569
  async ensureFreshAccessToken(
500
570
  accountId: string,
501
571
  refresher: OAuthTokenRefresher,
502
- opts: { force?: boolean; skewMs?: number } = {},
572
+ opts: { force?: boolean; skewMs?: number; origin?: "ui" | "sweep" | "dispatch" } = {},
503
573
  ): Promise<string | null> {
504
574
  const force = opts.force === true;
505
575
  // No explicit skew: the per-provider lead is applied at load time.
506
576
  const skewMs = opts.skewMs;
577
+ const origin = opts.origin ?? "sweep";
507
578
  // Forced refreshes get their own in-flight slot so they don't merge with
508
579
  // a raced non-force call that would otherwise short-circuit.
509
580
  const key = force ? `${accountId}:force` : accountId;
@@ -518,7 +589,7 @@ export class OAuthRefreshService {
518
589
  const oldestKey = this.#inFlight.keys().next().value;
519
590
  if (oldestKey !== undefined) this.#inFlight.delete(oldestKey);
520
591
  }
521
- const task = this.#refreshIfDue(accountId, refresher, skewMs, force).finally(() => {
592
+ const task = this.#refreshIfDue(accountId, refresher, skewMs, force, origin).finally(() => {
522
593
  this.#inFlight.delete(key);
523
594
  });
524
595
  this.#inFlight.set(key, task);
@@ -530,6 +601,7 @@ export class OAuthRefreshService {
530
601
  refresher: OAuthTokenRefresher,
531
602
  skewMs: number | undefined,
532
603
  force: boolean,
604
+ origin: "ui" | "sweep" | "dispatch",
533
605
  ): Promise<string | null> {
534
606
  const account = await loadAccountWithFreshness(this.#db, accountId, skewMs);
535
607
  if (!account || account.row.credentialKind !== "oauth") return null;
@@ -547,18 +619,24 @@ export class OAuthRefreshService {
547
619
  ? decryptCredentialToString(row.credentialCiphertext)
548
620
  : null;
549
621
  }
550
- return this.#refreshNow(row, refresher, skewMs);
622
+ return this.#refreshNow(row, refresher, skewMs, origin);
551
623
  }
552
624
 
553
625
  async #refreshNow(
554
626
  row: AccountRow,
555
627
  refresher: OAuthTokenRefresher,
556
628
  skewMs: number | undefined,
629
+ origin: "ui" | "sweep" | "dispatch",
557
630
  ): Promise<string | null> {
631
+ // D26: statusBefore is read once up front (the row projection used by the
632
+ // refresh path does not carry status) and statusAfter is re-read after any
633
+ // mutation, so the log proves a refresh never flipped the account.
634
+ const statusBefore = await readAccountStatus(this.#db, row.id);
558
635
  pushStructuredConsoleLog("info", "OAuth token refresh started", {
559
636
  event: "token_refresh",
560
637
  accountId: row.id,
561
638
  providerId: row.providerId,
639
+ details: { origin, statusBefore },
562
640
  });
563
641
  const owner = randomUUID();
564
642
  const leased = await acquireLease(this.#db, row.id, owner);
@@ -583,6 +661,7 @@ export class OAuthRefreshService {
583
661
  accountId: row.id,
584
662
  providerId: row.providerId,
585
663
  errorCode: "static_token",
664
+ details: { origin },
586
665
  });
587
666
  return null;
588
667
  }
@@ -594,12 +673,20 @@ export class OAuthRefreshService {
594
673
  buildRefreshContext(row, refresher.requiresAccessToken === true),
595
674
  );
596
675
  const persisted = await persistRefreshed(this.#db, row.id, owner, result);
676
+ // D26: `statusAfter` is re-read from the DB rather than assumed — a
677
+ // successful refresh never changes `status`, and the log proves it.
678
+ const statusAfter = await readAccountStatus(this.#db, row.id);
597
679
  pushStructuredConsoleLog(persisted ? "info" : "warn", persisted ? "OAuth token refresh completed" : "OAuth token refresh lost lease", {
598
680
  event: "token_refresh",
599
681
  accountId: row.id,
600
682
  providerId: row.providerId,
601
683
  ...(persisted ? {} : { errorCode: "refresh_lease_lost" }),
602
- details: { expiresAt: result.expiresAt.toISOString() },
684
+ details: {
685
+ expiresAt: result.expiresAt.toISOString(),
686
+ origin,
687
+ statusBefore,
688
+ statusAfter,
689
+ },
603
690
  });
604
691
  return persisted ? result.access : null;
605
692
  } catch (error) {
@@ -612,6 +699,7 @@ export class OAuthRefreshService {
612
699
  accountId: row.id,
613
700
  providerId: row.providerId,
614
701
  errorCode: classification,
702
+ details: { origin, statusBefore },
615
703
  });
616
704
  if (classification === "definitive") {
617
705
  // A definitive refresh failure means the refresh grant is dead — but
@@ -627,14 +715,29 @@ export class OAuthRefreshService {
627
715
  if (accessUsability === "usable") {
628
716
  await releaseLease(this.#db, row.id, owner);
629
717
  await markStaticToken(this.#db, row.id);
718
+ const statusAfter = await readAccountStatus(this.#db, row.id);
630
719
  pushStructuredConsoleLog("info", "OAuth refresh failed; access token still valid, marking static", {
631
720
  event: "token_refresh",
632
721
  accountId: row.id,
633
722
  providerId: row.providerId,
634
723
  errorCode: "static_token",
724
+ details: { origin, statusBefore, statusAfter },
635
725
  });
636
726
  } else {
637
- await disableAccount(this.#db, row.id, owner, failureMessage(error));
727
+ await disableAccount(this.#db, row.id, owner, failureMessage(error), {
728
+ // D23: a definitive refresh failure must not turn an operator's
729
+ // disable into a machine-recoverable one. If the row is already
730
+ // `disabled` without a deadline, leave it that way.
731
+ preserveOperatorDisable: true,
732
+ });
733
+ const statusAfter = await readAccountStatus(this.#db, row.id);
734
+ pushStructuredConsoleLog("warn", "OAuth refresh failed definitively; account disabled for re-auth", {
735
+ event: "token_refresh",
736
+ accountId: row.id,
737
+ providerId: row.providerId,
738
+ errorCode: classification,
739
+ details: { origin, statusBefore, statusAfter },
740
+ });
638
741
  }
639
742
  } else {
640
743
  await releaseLease(this.#db, row.id, owner);
@@ -643,3 +746,13 @@ export class OAuthRefreshService {
643
746
  }
644
747
  }
645
748
  }
749
+
750
+ /** Reads one account's status (used only for logging/observability, D26). */
751
+ async function readAccountStatus(db: CartethyiaDatabase, accountId: string): Promise<string | undefined> {
752
+ const rows = await db
753
+ .select({ status: providerAccounts.status })
754
+ .from(providerAccounts)
755
+ .where(eq(providerAccounts.id, accountId))
756
+ .limit(1);
757
+ return rows[0]?.status;
758
+ }
@@ -84,12 +84,15 @@ function traeDiscovery(
84
84
  if (rows.length === 0) return realm === "cn" ? TRAE_CN_MODELS : TRAE_INTL_MODELS;
85
85
  const fallback = realm === "cn" ? TRAE_CN_MODELS : TRAE_INTL_MODELS;
86
86
  const byId = new Map(fallback.map((model) => [model.modelId, model]));
87
+ // Return everything the upstream picker reports — the live listing is the
88
+ // truth for the account, including configs absent from the static roster.
89
+ // The static roster is only the fallback when discovery fails. A config
90
+ // discovered upstream gets the realm's first row as a template so it is
91
+ // routable with the shape this provider serves, carrying the window
92
+ // upstream reported.
87
93
  return rows.map((row) => {
88
94
  const existing = byId.get(row.id);
89
95
  if (existing === undefined) {
90
- // Discovered upstream but absent from the static roster: clone the
91
- // realm's first row so a new model is routable with the shape this
92
- // provider serves, carrying the window upstream reported.
93
96
  const template = fallback[0];
94
97
  if (template === undefined) return undefined;
95
98
  return { ...template, modelId: row.id, contextLimit: row.ctx ?? null };
@@ -315,6 +318,20 @@ export const PROVIDER_CAPABILITIES = {
315
318
  ...(fetcher === undefined ? {} : { fetcher }),
316
319
  }),
317
320
  },
321
+ devin: {
322
+ // Devin's chat wire is a Connect-RPC protobuf stream; the path is pinned
323
+ // here so a probe verifies dispatch actually calls it.
324
+ endpointPathsByWireFamily: { chat: "/exa.api_server_pb.ApiServerService/GetChatMessage" },
325
+ loadAdapter: async () => (await import("./integrations/devin/devin")).devinAdapter,
326
+ loadModels: async () => (await import("./integrations/devin/devin-catalog")).DEVIN_MODELS,
327
+ loadAuthentication: oauthCapability(() => import("./integrations/devin/devin-auth"), "devinOAuthClient"),
328
+ loadQuotaCollector: quotaCapability(() => import("./integrations/devin/devin-quota"), "fetchDevinQuota"),
329
+ loadModelDiscovery: async () => async ({ credential, fetcher }) =>
330
+ (await import("./integrations/devin/devin-catalog")).discoverDevinModels({
331
+ credential,
332
+ ...(fetcher === undefined ? {} : { fetcher }),
333
+ }),
334
+ },
318
335
  cerebras: {
319
336
  endpointPathsByWireFamily: { chat: "/chat/completions" },
320
337
  loadAdapter: async () => createApiKeyAdapter((await import("./integrations/cerebras")).CEREBRAS_SPEC),
@@ -514,6 +531,51 @@ export const PROVIDER_CAPABILITIES = {
514
531
  loadQuotaCollector: quotaCapability(() => import("./integrations/trae/trae-quota"), "fetchTraeCnQuota"),
515
532
  loadModelDiscovery: traeDiscovery("cn", "traecn"),
516
533
  },
534
+ freepi: {
535
+ loadAdapter: async () => (await import("./integrations/freepi/freepi")).createFreepiAdapter(),
536
+ loadModels: async () => (await import("./integrations/freepi/freepi-catalog")).FREEPI_MODELS,
537
+ // Free-pi login is a GitHub device flow; the issued JWT is the credential.
538
+ loadAuthentication: oauthCapability(() => import("./integrations/freepi/freepi-oauth"), "freepiOAuthClient"),
539
+ loadQuotaCollector: quotaCapability(() => import("./integrations/freepi/freepi-quota"), "fetchFreepiQuota"),
540
+ // Live `/client-version` (Bearer) is the catalog source; the static roster
541
+ // is the offline fallback.
542
+ loadModelDiscovery: async () => async (request: Parameters<ProviderModelDiscovery>[0]) => {
543
+ const { discoverFreepiModels } = await import("./integrations/freepi/freepi-discovery");
544
+ const { FREEPI_MODELS } = await import("./integrations/freepi/freepi-catalog");
545
+ const secret = request.credential.trim();
546
+ const fetcher = request.fetcher as unknown as typeof fetch | undefined;
547
+ if (secret.length === 0 || fetcher === undefined) return FREEPI_MODELS;
548
+ let token = secret;
549
+ if (secret.startsWith("{")) {
550
+ try {
551
+ const parsed = JSON.parse(secret) as Record<string, unknown>;
552
+ const found = ["accessToken", "access_token", "token"].find(
553
+ (key) => typeof parsed[key] === "string" && (parsed[key] as string).length > 0,
554
+ );
555
+ if (found !== undefined) token = parsed[found] as string;
556
+ } catch {
557
+ // Use the raw secret as the token.
558
+ }
559
+ }
560
+ const rows = await discoverFreepiModels({ token, fetcher });
561
+ if (rows.length === 0) return FREEPI_MODELS;
562
+ return rows;
563
+ },
564
+ },
565
+ perch: {
566
+ loadAdapter: async () => (await import("./integrations/perch/perch")).createPerchAdapter(),
567
+ loadModels: async () => (await import("./integrations/perch/perch-catalog")).PERCH_MODELS,
568
+ // Perch login is Supabase PKCE (Google/GitHub); the session blob
569
+ // (access/refresh token) is the credential, and the CLI refreshes it.
570
+ loadAuthentication: oauthCapability(() => import("./integrations/perch/perch-oauth"), "perchOAuthClient", { withRefresher: true }),
571
+ loadQuotaCollector: quotaCapability(() => import("./integrations/perch/perch-quota"), "fetchPerchQuota"),
572
+ // The hosted API exposes no public model list — Roost tiers are the only
573
+ // surface the CLI accepts, so discovery returns the static roster.
574
+ loadModelDiscovery: async () => async () => {
575
+ const { PERCH_MODELS } = await import("./integrations/perch/perch-catalog");
576
+ return PERCH_MODELS;
577
+ },
578
+ },
517
579
  inferhub: {
518
580
  loadAdapter: async () => (await import("./integrations/inferhub")).createInferhubAdapter(),
519
581
  // Live `/v1/models` is the primary source: the marketplace republishes its
@@ -527,6 +589,9 @@ export const PROVIDER_CAPABILITIES = {
527
589
  hermes: {
528
590
  loadAdapter: async () => createApiKeyAdapter((await import("./integrations/hermes")).HERMES_SPEC),
529
591
  loadModels: async () => (await import("./integrations/hermes")).HERMES_MODELS,
592
+ // Hermes accepts both API keys and the Nous Portal device-code OAuth flow
593
+ // (subscription route). The OAuth client is refreshable.
594
+ loadAuthentication: oauthCapability(() => import("./integrations/hermes/hermes-oauth"), "hermesOAuthClient", { withRefresher: true }),
530
595
  // `/v1/models` is open and states each row's real limits, so the free
531
596
  // allowance tracks upstream instead of a hand-kept list. The static list
532
597
  // above stays as the offline fallback for a fetch that fails.
@@ -58,6 +58,11 @@ export const PROBE_REASONING_EFFORTS = ["auto", "minimal", "low", "medium", "hig
58
58
  /** One accepted probe reasoning effort. `auto` means "send no reasoning intent". */
59
59
  export type ProbeReasoningEffort = (typeof PROBE_REASONING_EFFORTS)[number];
60
60
 
61
+ /** Modelprobe modes as a runtime tuple, so the route schema projects one list. */
62
+ export const PROBE_MODES = ["fast", "detail"] as const;
63
+ /** `detail` is the default when `mode` is absent (D39). */
64
+ export type ProbeMode = (typeof PROBE_MODES)[number];
65
+
61
66
  /** Request accepted for a one-shot model connectivity test. `route`
62
67
  * disambiguates when a model id is registered under multiple endpoint
63
68
  * paths; omit it to probe against the model's already-registered wire
@@ -85,6 +90,13 @@ export interface ProbeModelRequest {
85
90
  * result the richest available source of truth.
86
91
  */
87
92
  surface?: "default" | "cosy" | "grpc" | "ide";
93
+ /**
94
+ * `fast` is a cheap liveness check: 32 output tokens, a 10s timeout, no
95
+ * retry, no payload-body capture, compact telemetry, and an 80-char sample.
96
+ * `detail` (the default when absent) is the full diagnostic probe.
97
+ * Health recording is identical in both modes.
98
+ */
99
+ mode?: ProbeMode;
88
100
  }
89
101
  export interface ProbeModelResult {
90
102
  accountId?: string;