cartethyia 2.7.9 → 2.8.1

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 (496) hide show
  1. package/.env.example +6 -5
  2. package/dist/dashboard/assets/{BannedPage-BxoG1MrL.js → BannedPage-Dq530awD.js} +1 -1
  3. package/dist/dashboard/assets/BannedPage-Dq530awD.js.gz +0 -0
  4. package/dist/dashboard/assets/{CliToolDetailPage-ixwgKk53.js → CliToolDetailPage-DZzaS-7S.js} +1 -1
  5. package/dist/dashboard/assets/CliToolDetailPage-DZzaS-7S.js.gz +0 -0
  6. package/dist/dashboard/assets/{CliToolsPage-BmttNV3U.js → CliToolsPage-DaZZ3cqP.js} +1 -1
  7. package/dist/dashboard/assets/CliToolsPage-DaZZ3cqP.js.gz +0 -0
  8. package/dist/dashboard/assets/{CombosPage-BE3qwwfC.js → CombosPage-BlC_xdkX.js} +1 -1
  9. package/dist/dashboard/assets/CombosPage-BlC_xdkX.js.gz +0 -0
  10. package/dist/dashboard/assets/{ConsoleLogPage-CRZdmMIX.js → ConsoleLogPage-D-rqeVtG.js} +1 -1
  11. package/dist/dashboard/assets/ConsoleLogPage-D-rqeVtG.js.gz +0 -0
  12. package/dist/dashboard/assets/{CustomizationPage-Dv_FJ6Er.js → CustomizationPage-7xT5yg2E.js} +1 -1
  13. package/dist/dashboard/assets/CustomizationPage-7xT5yg2E.js.gz +0 -0
  14. package/dist/dashboard/assets/HealthEventsModal-Clq8E9cW.js +1 -0
  15. package/dist/dashboard/assets/HealthEventsModal-Clq8E9cW.js.gz +0 -0
  16. package/dist/dashboard/assets/{InjectSystemPromptPanel-BOoO8rpz.js → InjectSystemPromptPanel-6Teoq9Se.js} +2 -2
  17. package/dist/dashboard/assets/InjectSystemPromptPanel-6Teoq9Se.js.gz +0 -0
  18. package/dist/dashboard/assets/{LoginPage-ChJuNNVN.js → LoginPage-CZuI0MAR.js} +1 -1
  19. package/dist/dashboard/assets/LoginPage-CZuI0MAR.js.gz +0 -0
  20. package/dist/dashboard/assets/{ModelPicker-BZAJtPg1.js → ModelPicker-C1hNTlkr.js} +1 -1
  21. package/dist/dashboard/assets/ModelPicker-C1hNTlkr.js.gz +0 -0
  22. package/dist/dashboard/assets/{OverviewPage-BE40s3u5.js → OverviewPage-0yfhSI1C.js} +1 -1
  23. package/dist/dashboard/assets/OverviewPage-0yfhSI1C.js.gz +0 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-xqTxdQ4c.js +8 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-xqTxdQ4c.js.gz +0 -0
  26. package/dist/dashboard/assets/{ProviderIcon-CQ6bPUf_.js → ProviderIcon-Ddhm1InL.js} +1 -1
  27. package/dist/dashboard/assets/ProviderIcon-Ddhm1InL.js.gz +0 -0
  28. package/dist/dashboard/assets/ProvidersPage-C3fYuu6i.js +1 -0
  29. package/dist/dashboard/assets/ProvidersPage-C3fYuu6i.js.gz +0 -0
  30. package/dist/dashboard/assets/ProxyPage-CHtJtZ3u.js +9 -0
  31. package/dist/dashboard/assets/ProxyPage-CHtJtZ3u.js.gz +0 -0
  32. package/dist/dashboard/assets/QuotaPage-D1RV_2BX.js +1 -0
  33. package/dist/dashboard/assets/QuotaPage-D1RV_2BX.js.gz +0 -0
  34. package/dist/dashboard/assets/{SettingsPage-C5HPWqEv.js → SettingsPage-BzZAvYSX.js} +1 -1
  35. package/dist/dashboard/assets/SettingsPage-BzZAvYSX.js.gz +0 -0
  36. package/dist/dashboard/assets/{SetupPage-CFNcATJJ.js → SetupPage--s-EfEIL.js} +1 -1
  37. package/dist/dashboard/assets/SetupPage--s-EfEIL.js.gz +0 -0
  38. package/dist/dashboard/assets/{StudioPage-YoD4tY5l.js → StudioPage-BjXFGSK4.js} +1 -1
  39. package/dist/dashboard/assets/StudioPage-BjXFGSK4.js.gz +0 -0
  40. package/dist/dashboard/assets/{UsagePage-DxjaAhb_.js → UsagePage-CP8F8BN7.js} +1 -1
  41. package/dist/dashboard/assets/UsagePage-CP8F8BN7.js.gz +0 -0
  42. package/dist/dashboard/assets/{api-keys-BTlkq_Uk.js → api-keys-2hc12Uz2.js} +1 -1
  43. package/dist/dashboard/assets/api-keys-2hc12Uz2.js.gz +0 -0
  44. package/dist/dashboard/assets/app-DGXbNfAR.css +2 -0
  45. package/dist/dashboard/assets/app-DGXbNfAR.css.gz +0 -0
  46. package/dist/dashboard/assets/assert-BMcj1xZE.js +1 -0
  47. package/dist/dashboard/assets/assert-BMcj1xZE.js.gz +0 -0
  48. package/dist/dashboard/assets/{badge-DdbT_7Am.js → badge-DkRUuVfz.js} +1 -1
  49. package/dist/dashboard/assets/badge-DkRUuVfz.js.gz +0 -0
  50. package/dist/dashboard/assets/{banner-CsGN-nbm.js → banner-CDjW841Q.js} +1 -1
  51. package/dist/dashboard/assets/banner-CDjW841Q.js.gz +0 -0
  52. package/dist/dashboard/assets/{card-mgZ_TLLZ.js → card-Bto5UlL9.js} +1 -1
  53. package/dist/dashboard/assets/card-Bto5UlL9.js.gz +0 -0
  54. package/dist/dashboard/assets/{cli-tools-A1-v8-03.js → cli-tools-DaH6g9hL.js} +1 -1
  55. package/dist/dashboard/assets/cli-tools-DaH6g9hL.js.gz +0 -0
  56. package/dist/dashboard/assets/{clipboard-button-CJ4Zty89.js → clipboard-button-BYKBCzjk.js} +1 -1
  57. package/dist/dashboard/assets/clipboard-button-BYKBCzjk.js.gz +0 -0
  58. package/dist/dashboard/assets/{dialog-B5L2jPQ_.js → dialog-DL0Zftwv.js} +1 -1
  59. package/dist/dashboard/assets/dialog-DL0Zftwv.js.gz +0 -0
  60. package/dist/dashboard/assets/{drawer-DkjUouAj.js → drawer-Dpx_UXaS.js} +1 -1
  61. package/dist/dashboard/assets/drawer-Dpx_UXaS.js.gz +0 -0
  62. package/dist/dashboard/assets/{entry-B-LVLV6o.js → entry-BV7-1NoM.js} +1 -1
  63. package/dist/dashboard/assets/entry-BV7-1NoM.js.gz +0 -0
  64. package/dist/dashboard/assets/{entry-CAhdYl3B.js → entry-CGvdNU6r.js} +1 -1
  65. package/dist/dashboard/assets/entry-CGvdNU6r.js.gz +0 -0
  66. package/dist/dashboard/assets/entry-DScKYvZj.js +4 -0
  67. package/dist/dashboard/assets/entry-DScKYvZj.js.gz +0 -0
  68. package/dist/dashboard/assets/{formatters-CrsdLVsn.js → formatters-BS3DZlZj.js} +1 -1
  69. package/dist/dashboard/assets/formatters-BS3DZlZj.js.gz +0 -0
  70. package/dist/dashboard/assets/{index-BU64Lqvp.js → index-SQnvvTWX.js} +2 -2
  71. package/dist/dashboard/assets/index-SQnvvTWX.js.gz +0 -0
  72. package/dist/dashboard/assets/{inline-B2aDLyyg.js → inline-Dl-cuLK0.js} +1 -1
  73. package/dist/dashboard/assets/inline-Dl-cuLK0.js.gz +0 -0
  74. package/dist/dashboard/assets/{input-BCVkdR9d.js → input-Bfx0EzRc.js} +1 -1
  75. package/dist/dashboard/assets/input-Bfx0EzRc.js.gz +0 -0
  76. package/dist/dashboard/assets/{layout-DNcAdMK-.js → layout-BxhbQ_JS.js} +1 -1
  77. package/dist/dashboard/assets/layout-BxhbQ_JS.js.gz +0 -0
  78. package/dist/dashboard/assets/{list-row-DgrIH0hw.js → list-row-D49Y4ptn.js} +1 -1
  79. package/dist/dashboard/assets/list-row-D49Y4ptn.js.gz +0 -0
  80. package/dist/dashboard/assets/{live-Cae01iu8.js → live-CbVTdX9V.js} +1 -1
  81. package/dist/dashboard/assets/live-CbVTdX9V.js.gz +0 -0
  82. package/dist/dashboard/assets/{network-CRib7H_2.js → network-JGvDveof.js} +1 -1
  83. package/dist/dashboard/assets/network-JGvDveof.js.gz +0 -0
  84. package/dist/dashboard/assets/{numbers-BKQE8Xpz.js → numbers-Dw6kiew-.js} +1 -1
  85. package/dist/dashboard/assets/numbers-Dw6kiew-.js.gz +0 -0
  86. package/dist/dashboard/assets/{pagination-ecPvG_8g.js → pagination-iUoI8GeE.js} +1 -1
  87. package/dist/dashboard/assets/pagination-iUoI8GeE.js.gz +0 -0
  88. package/dist/dashboard/assets/providers/sensenova-intl.png +0 -0
  89. package/dist/dashboard/assets/providers/sensenova.png +0 -0
  90. package/dist/dashboard/assets/providers/zro.svg +3 -0
  91. package/dist/dashboard/assets/providers/zro.svg.gz +0 -0
  92. package/dist/dashboard/assets/quota-DiPD6UKH.js +1 -0
  93. package/dist/dashboard/assets/quota-DiPD6UKH.js.gz +0 -0
  94. package/dist/dashboard/assets/{routing-Ckm1UXuC.js → routing-8jZr-3WE.js} +1 -1
  95. package/dist/dashboard/assets/routing-8jZr-3WE.js.gz +0 -0
  96. package/dist/dashboard/assets/{select-ChoFWyKz.js → select-oO1FxMVb.js} +2 -2
  97. package/dist/dashboard/assets/select-oO1FxMVb.js.gz +0 -0
  98. package/dist/dashboard/assets/{settings-CKjFPlKW.js → settings-CLr3kdIx.js} +1 -1
  99. package/dist/dashboard/assets/settings-CLr3kdIx.js.gz +0 -0
  100. package/dist/dashboard/assets/{stack-DWCwU3fh.js → stack-Y3pDteZr.js} +1 -1
  101. package/dist/dashboard/assets/stack-Y3pDteZr.js.gz +0 -0
  102. package/dist/dashboard/assets/{state-Cg3VL5J_.js → state-CArX_Iak.js} +1 -1
  103. package/dist/dashboard/assets/state-CArX_Iak.js.gz +0 -0
  104. package/dist/dashboard/assets/{switch-Db70BMix.js → switch-DwlwmDO-.js} +1 -1
  105. package/dist/dashboard/assets/switch-DwlwmDO-.js.gz +0 -0
  106. package/dist/dashboard/assets/{useMutation-BqdE0LuM.js → useMutation-DJ9GYEgf.js} +1 -1
  107. package/dist/dashboard/assets/useMutation-DJ9GYEgf.js.gz +0 -0
  108. package/dist/dashboard/index.html +2 -2
  109. package/dist/dashboard/index.html.gz +0 -0
  110. package/dist/dashboard/precompressed-assets.json +54 -53
  111. package/migrations/full-postgres/0046_add_accounts_quota_plan_fetched_at.sql +7 -0
  112. package/migrations/full-postgres/0047_add_provider_device_flow_proxy.sql +17 -0
  113. package/migrations/lite-sqlite3/0046_add_accounts_quota_plan_fetched_at.sql +4 -0
  114. package/migrations/lite-sqlite3/0047_add_provider_device_flow_proxy.sql +19 -0
  115. package/package.json +1 -1
  116. package/src/boot/app.ts +11 -14
  117. package/src/boot/dependencies.ts +5 -7
  118. package/src/boot/flush.ts +20 -0
  119. package/src/boot/lifecycle.ts +7 -5
  120. package/src/config/providers.ts +6 -5
  121. package/src/config/routing.ts +26 -0
  122. package/src/config/spec-metadata.ts +72 -0
  123. package/src/config/spec.ts +28 -8
  124. package/src/console/api-keys/contracts.ts +12 -1
  125. package/src/console/api-keys/routes.ts +40 -28
  126. package/src/console/auth/account-reset.ts +7 -9
  127. package/src/console/backup/contracts.ts +4 -0
  128. package/src/console/backup/export-endpoint.ts +7 -5
  129. package/src/console/backup/nine-router.ts +71 -10
  130. package/src/console/backup/service.ts +8 -1
  131. package/src/console/cli-tools/service.ts +6 -12
  132. package/src/console/console-router-guards.ts +23 -16
  133. package/src/console/console-router.ts +1 -1
  134. package/src/console/domain-registration.ts +7 -4
  135. package/src/console/inferhub/client.ts +1 -1
  136. package/src/console/inferhub/normalize.ts +24 -28
  137. package/src/console/inferhub/types.ts +3 -0
  138. package/src/console/ip-bans/routes.ts +7 -13
  139. package/src/console/observability/live.ts +2 -2
  140. package/src/console/observability/store.ts +0 -53
  141. package/src/console/providers/catalog/contracts.ts +9 -3
  142. package/src/console/providers/catalog/model-operations.ts +2 -3
  143. package/src/console/providers/catalog/provider-operations.ts +1 -1
  144. package/src/console/providers/catalog/routes.ts +3 -2
  145. package/src/console/providers/catalog/store.ts +1914 -1902
  146. package/src/console/providers/detail/routes.ts +1 -0
  147. package/src/console/providers/detail/store.ts +4 -0
  148. package/src/console/providers/oauth/callback-listener.ts +30 -11
  149. package/src/console/providers/oauth/device-egress.ts +123 -0
  150. package/src/console/providers/oauth/routes.ts +123 -16
  151. package/src/console/quota/account-quota-global.ts +14 -24
  152. package/src/console/quota/account-quota-shared.ts +49 -0
  153. package/src/console/quota/account-quota-tenant.ts +11 -23
  154. package/src/console/quota/account-test-egress.ts +2 -2
  155. package/src/console/quota/daily-growth-job.ts +1 -1
  156. package/src/console/quota/quota-view.ts +19 -3
  157. package/src/console/routing/pools/contracts.ts +1 -1
  158. package/src/console/routing/pools/operations.ts +32 -47
  159. package/src/console/routing/pools/probe-verdict.ts +2 -2
  160. package/src/console/routing/pools/relay-deploy.ts +1 -1
  161. package/src/console/routing/pools/store.ts +4 -4
  162. package/src/console/shared/errors.ts +33 -6
  163. package/src/main.ts +2 -3
  164. package/src/network/client.ts +67 -0
  165. package/src/network/egress/bidi-session.ts +322 -0
  166. package/src/network/egress/dial.ts +201 -0
  167. package/src/network/egress/factory.ts +191 -0
  168. package/src/network/{outbound-fetch.ts → egress/fetch.ts} +195 -29
  169. package/src/network/egress/tls-profile.ts +123 -0
  170. package/src/network/egress/ws.ts +175 -0
  171. package/src/network/pool/{byte-accounting.ts → accounting.ts} +3 -2
  172. package/src/network/{types.ts → pool/agent-config.ts} +1 -1
  173. package/src/network/pool/agent.ts +2 -2
  174. package/src/network/{pool-cooldown-store.ts → pool/cooldown-store.ts} +95 -20
  175. package/src/network/{pool-health-machine.ts → pool/health.ts} +205 -6
  176. package/src/network/pool/resolver.ts +10 -849
  177. package/src/network/pool/selector.ts +184 -41
  178. package/src/network/ssrf.ts +7 -7
  179. package/src/network/{retry.ts → util/retry.ts} +1 -1
  180. package/src/network/{rate-limit-verdict.ts → verdict.ts} +1 -1
  181. package/src/observability/event-log.ts +7 -361
  182. package/src/observability/performance-metrics.ts +9 -0
  183. package/src/observability/runtime-metrics.ts +14 -9
  184. package/src/observability/telemetry-buffer.ts +16 -26
  185. package/src/observability/telemetry-reconciliation.ts +9 -37
  186. package/src/observability/telemetry-row.ts +125 -0
  187. package/src/persistence/db-mode.ts +4 -1
  188. package/src/persistence/dialect.ts +18 -0
  189. package/src/persistence/migrate.ts +6 -3
  190. package/src/persistence/schema.ts +17 -0
  191. package/src/persistence/ttl-cache.ts +21 -3
  192. package/src/protocol/codecs.ts +59 -2
  193. package/src/protocol/dsml.ts +34 -86
  194. package/src/protocol/markup-stream-parser.ts +195 -0
  195. package/src/protocol/reasoning-text.ts +19 -0
  196. package/src/protocol/request/chat.ts +10 -10
  197. package/src/protocol/request/gemini.ts +8 -2
  198. package/src/protocol/request/messages.ts +34 -22
  199. package/src/protocol/response/chat.ts +12 -20
  200. package/src/protocol/response/messages.ts +34 -7
  201. package/src/protocol/response/omp-tool-call-markup.ts +43 -86
  202. package/src/protocol/response/responses.ts +18 -2
  203. package/src/protocol/stop-reason.ts +23 -1
  204. package/src/protocol/think-split.ts +112 -0
  205. package/src/protocol/translation/dialect/qwen.ts +26 -58
  206. package/src/protocol/translation/quirks.ts +15 -0
  207. package/src/protocol/upstream/send-claude-messages.ts +2 -2
  208. package/src/providers/adapters/compatible-adapter.ts +76 -12
  209. package/src/providers/authentication/oauth-client.ts +40 -6
  210. package/src/providers/authentication/oauth-device-flow.ts +105 -11
  211. package/src/providers/authentication/oauth-flow-store.ts +82 -1
  212. package/src/providers/authentication/oauth-refresh-service.ts +766 -758
  213. package/src/providers/configured-provider.ts +148 -0
  214. package/src/providers/default-registry.ts +137 -36
  215. package/src/providers/discovery/discovery-types.ts +7 -0
  216. package/src/providers/discovery/probe-phases.ts +20 -6
  217. package/src/providers/discovery/probing-service.ts +22 -2
  218. package/src/providers/integrations/alibabacp/alibabacp-models.ts +1 -2
  219. package/src/providers/integrations/{dashscope-models.ts → alibabacp/dashscope-models.ts} +2 -2
  220. package/src/providers/integrations/antigravity/antigravity-protocol.ts +16 -1
  221. package/src/providers/integrations/buddy/account.ts +969 -0
  222. package/src/providers/integrations/buddy/capabilities.ts +12 -12
  223. package/src/providers/integrations/buddy/{buddy-catalog-shared.ts → catalog.ts} +198 -136
  224. package/src/providers/integrations/buddy/{buddy-chat-shared.ts → chat.ts} +176 -20
  225. package/src/providers/integrations/buddy/codebuddy/index.ts +429 -0
  226. package/src/providers/integrations/buddy/codebuddy/shared.ts +41 -26
  227. package/src/providers/integrations/buddy/headers.ts +311 -0
  228. package/src/providers/integrations/buddy/lifecycle.ts +1018 -0
  229. package/src/providers/integrations/buddy/{buddy-media.ts → media.ts} +1 -1
  230. package/src/providers/integrations/claude/claude-quota.ts +14 -17
  231. package/src/providers/integrations/cline/cline-oauth.ts +10 -5
  232. package/src/providers/integrations/cline/cline.ts +5 -6
  233. package/src/providers/integrations/clixad/clixad-oauth.ts +98 -0
  234. package/src/providers/integrations/clixad/clixad-quota.ts +132 -0
  235. package/src/providers/integrations/clixad/provider.ts +145 -0
  236. package/src/providers/integrations/codex/codex-oauth.ts +8 -5
  237. package/src/providers/integrations/commandcode/quota.ts +22 -28
  238. package/src/providers/integrations/cursor/cursor-auth.ts +1 -9
  239. package/src/providers/integrations/cursor/cursor-exec.ts +9 -2
  240. package/src/providers/integrations/cursor/cursor-hardening.ts +279 -0
  241. package/src/providers/integrations/cursor/cursor-http2.ts +605 -6
  242. package/src/providers/integrations/cursor/cursor-request.ts +26 -12
  243. package/src/providers/integrations/cursor/cursor-shared.ts +8 -5
  244. package/src/providers/integrations/cursor/cursor.ts +574 -110
  245. package/src/providers/integrations/cursor/protocol/messages.ts +22 -29
  246. package/src/providers/integrations/cursor/protocol/protobuf-schema.ts +37 -0
  247. package/src/providers/integrations/deepai/deepai-catalog.ts +149 -0
  248. package/src/providers/integrations/deepai/deepai-common.ts +145 -0
  249. package/src/providers/integrations/deepai/deepai-oauth.ts +129 -0
  250. package/src/providers/integrations/deepai/deepai-quota.ts +97 -0
  251. package/src/providers/integrations/deepai/provider.ts +42 -0
  252. package/src/providers/integrations/devin/devin-auth.ts +7 -0
  253. package/src/providers/integrations/devin/protocol/devin-proto.ts +1 -27
  254. package/src/providers/integrations/factory/factory-oauth.ts +33 -51
  255. package/src/providers/integrations/freebuff/assets/official-system-manager.txt +88 -0
  256. package/src/providers/integrations/freebuff/assets/official-system-worker.txt +57 -0
  257. package/src/providers/integrations/freebuff/assets/official-tools.json +1070 -0
  258. package/src/providers/integrations/freebuff/freebuff-catalog.ts +120 -0
  259. package/src/providers/integrations/freebuff/freebuff-constants.ts +1233 -0
  260. package/src/providers/integrations/freebuff/freebuff-discovery.ts +108 -0
  261. package/src/providers/integrations/freebuff/freebuff-oauth.ts +240 -0
  262. package/src/providers/integrations/freebuff/freebuff-quota.ts +260 -0
  263. package/src/providers/integrations/freebuff/freebuff-virtual.ts +89 -0
  264. package/src/providers/integrations/freebuff/freebuff.ts +682 -0
  265. package/src/providers/integrations/freepi/freepi-catalog.ts +5 -0
  266. package/src/providers/integrations/freepi/freepi-discovery.ts +2 -2
  267. package/src/providers/integrations/freepi/freepi-headers.ts +2 -2
  268. package/src/providers/integrations/freepi/freepi-protocol.ts +11 -2
  269. package/src/providers/integrations/freepi/freepi-realm.ts +0 -3
  270. package/src/providers/integrations/freepi/freepi-session.ts +158 -0
  271. package/src/providers/integrations/freepi/freepi.ts +80 -54
  272. package/src/providers/integrations/github/github-discovery.ts +30 -13
  273. package/src/providers/integrations/github/github-oauth.ts +217 -44
  274. package/src/providers/integrations/github/github-quota.ts +22 -5
  275. package/src/providers/integrations/github/github.ts +40 -32
  276. package/src/providers/integrations/grok/grok-oauth.ts +52 -49
  277. package/src/providers/integrations/grok/grok-ratelimit-store.ts +10 -17
  278. package/src/providers/integrations/grok/grok-turn-index.ts +11 -16
  279. package/src/providers/integrations/hermes/hermes-oauth.ts +3 -3
  280. package/src/providers/integrations/{hermes.ts → hermes/hermes.ts} +42 -13
  281. package/src/providers/integrations/hermes/index.ts +2 -0
  282. package/src/providers/integrations/kilo/kilo-oauth.ts +4 -4
  283. package/src/providers/integrations/kilo/kilo.ts +1 -1
  284. package/src/providers/integrations/kimi/kimi-oauth.ts +3 -3
  285. package/src/providers/integrations/kiro/kiro-catalog.ts +17 -1
  286. package/src/providers/integrations/kiro/kiro-discovery.ts +10 -12
  287. package/src/providers/integrations/kiro/kiro-oauth.ts +2 -2
  288. package/src/providers/integrations/kiro/kiro-quota.ts +8 -4
  289. package/src/providers/integrations/kiro/kiro.ts +15 -19
  290. package/src/providers/integrations/muse/muse-oauth.ts +5 -5
  291. package/src/providers/integrations/ollamacloud/index.ts +2 -0
  292. package/src/providers/integrations/{ollamacloud-models.ts → ollamacloud/models.ts} +2 -2
  293. package/src/providers/integrations/{ollamacloud-quota.ts → ollamacloud/quota.ts} +3 -3
  294. package/src/providers/integrations/opencode/fingerprint.ts +1 -1
  295. package/src/providers/integrations/opencode/opencode-go-quota.ts +2 -8
  296. package/src/providers/integrations/openrouter/provider.ts +1 -1
  297. package/src/providers/integrations/perch/perch-credential.ts +25 -0
  298. package/src/providers/integrations/perch/perch-protocol.ts +0 -6
  299. package/src/providers/integrations/perch/perch-quota.ts +2 -16
  300. package/src/providers/integrations/perch/perch.ts +6 -16
  301. package/src/providers/integrations/qoder/claim-qoder-credits.ts +19 -60
  302. package/src/providers/integrations/qoder/qoder-adapter.ts +20 -125
  303. package/src/providers/integrations/qoder/qoder-auth.ts +109 -30
  304. package/src/providers/integrations/qoder/qoder-catalog.ts +21 -6
  305. package/src/providers/integrations/qoder/qoder-chat.ts +32 -29
  306. package/src/providers/integrations/qoder/qoder-checkin.ts +5 -5
  307. package/src/providers/integrations/qoder/qoder-cosy.ts +37 -9
  308. package/src/providers/integrations/qoder/qoder-oauth.ts +9 -2
  309. package/src/providers/integrations/qoder/qoder-realm.ts +13 -36
  310. package/src/providers/integrations/qoder/qoder-sse.ts +447 -68
  311. package/src/providers/integrations/qoder/qoder-types.ts +9 -0
  312. package/src/providers/integrations/qoder/qoder.ts +3 -26
  313. package/src/providers/integrations/sensenova/provider.ts +144 -0
  314. package/src/providers/integrations/trae/trae-protocol.ts +9 -5
  315. package/src/providers/integrations/trae/trae-quota.ts +21 -29
  316. package/src/providers/integrations/trae/trae-realm.ts +38 -0
  317. package/src/providers/integrations/trae/trae.ts +7 -24
  318. package/src/providers/integrations/vercel/provider.ts +117 -0
  319. package/src/providers/integrations/vercel/vercel-quota.ts +96 -0
  320. package/src/providers/integrations/xai/xai-oauth.ts +5 -5
  321. package/src/providers/integrations/zcode-start-plan/gifts.ts +1 -1
  322. package/src/providers/integrations/zcode-start-plan/oauth.ts +3 -3
  323. package/src/providers/integrations/zcode-start-plan/provider.ts +226 -23
  324. package/src/providers/integrations/zcode-start-plan/quota.ts +19 -2
  325. package/src/providers/integrations/zro/zro.ts +205 -0
  326. package/src/providers/operations/account-error-classification.ts +199 -307
  327. package/src/providers/operations/account-health-service.ts +82 -1
  328. package/src/providers/operations/account-refresh-status.ts +36 -37
  329. package/src/providers/operations/account-tier.ts +15 -16
  330. package/src/providers/operations/client-version-resolver.ts +1 -1
  331. package/src/providers/operations/client-versions.ts +100 -81
  332. package/src/providers/operations/cooldown-rules.ts +11 -16
  333. package/src/providers/operations/oauth-refresh-lead.ts +46 -22
  334. package/src/providers/operations/{model-catalog-cache.ts → provider-caches.ts} +68 -16
  335. package/src/providers/operations/provider-catalog-service.ts +1 -1
  336. package/src/providers/operations/provider-credential-service.ts +118 -51
  337. package/src/providers/operations/provider-rules/github.ts +48 -0
  338. package/src/providers/operations/provider-rules/grok.ts +33 -0
  339. package/src/providers/operations/provider-rules/inception.ts +49 -0
  340. package/src/providers/operations/provider-rules/index.ts +32 -0
  341. package/src/providers/operations/provider-rules/perch.ts +37 -0
  342. package/src/providers/operations/provider-rules/qoder.ts +70 -0
  343. package/src/providers/operations/provider-rules/trae.ts +81 -0
  344. package/src/providers/operations/session-resolution.ts +17 -1
  345. package/src/providers/provider-contracts.ts +6 -4
  346. package/src/providers/{provider-credential-resolver.ts → provider-credentials.ts} +40 -3
  347. package/src/providers/provider-metadata.ts +70 -10
  348. package/src/providers/provider-probe.ts +15 -1
  349. package/src/providers/provider-registry.ts +42 -33
  350. package/src/providers/quota/quota-contracts.ts +2 -0
  351. package/src/providers/quota/quota-limit-windows.ts +16 -5
  352. package/src/providers/quota/quota-support.ts +1 -1
  353. package/src/providers/quota/quota-window-parser.ts +90 -18
  354. package/src/providers/quota/refresh.ts +100 -21
  355. package/src/security/api-key-auth-cache.ts +1 -1
  356. package/src/security/api-key-auth.ts +6 -2
  357. package/src/security/ip-boundary.ts +1 -1
  358. package/src/transport/dispatch/attempt-finalize.ts +15 -14
  359. package/src/transport/dispatch/attempt-loop.ts +73 -19
  360. package/src/transport/dispatch/generation.ts +2 -8
  361. package/src/transport/dispatch/handler-deps.ts +1 -1
  362. package/src/transport/dispatch/leases.ts +1 -1
  363. package/src/transport/dispatch/native-shared.ts +25 -0
  364. package/src/transport/dispatch/proxy-request.ts +8 -46
  365. package/src/transport/dispatch/responses-compact.ts +2 -8
  366. package/src/transport/dispatch/retry-policy.ts +1 -1
  367. package/src/transport/dispatch/streaming/finalize.ts +10 -7
  368. package/src/transport/dispatch/streaming/stream.ts +9 -37
  369. package/src/transport/dispatch/systemone.ts +2 -8
  370. package/src/transport/dispatch/token-count.ts +2 -6
  371. package/src/transport/dispatch/upstream.ts +63 -0
  372. package/src/transport/drain-endpoint.ts +31 -4
  373. package/src/transport/failure-policy.ts +16 -30
  374. package/src/transport/guards/body-policy.ts +7 -1
  375. package/src/transport/guards/error-lifecycle.ts +13 -1
  376. package/src/transport/guards/request-context.ts +22 -14
  377. package/src/transport/request/preparer.ts +81 -71
  378. package/src/transport/routing/bounded-lru.ts +14 -0
  379. package/src/transport/routing/drain-reserve.ts +129 -0
  380. package/src/transport/routing/plan-gate.ts +7 -1
  381. package/src/transport/routing/round-robin.ts +60 -0
  382. package/src/transport/routing/route-catalog.ts +123 -111
  383. package/src/transport/routing/route-model.ts +51 -9
  384. package/src/transport/routing/router.ts +393 -32
  385. package/src/transport/shutdown-notice.ts +1 -1
  386. package/src/transport/surface/adapters.ts +11 -2
  387. package/src/transport/surface/chat/adapter.ts +2 -2
  388. package/src/transport/surface/chat/encode.ts +47 -4
  389. package/src/transport/surface/chat/parse.ts +2 -12
  390. package/src/transport/surface/completion.ts +44 -33
  391. package/src/transport/surface/content-parts.ts +21 -0
  392. package/src/transport/surface/messages/adapter.ts +7 -1
  393. package/src/transport/surface/messages/encode.ts +11 -3
  394. package/src/transport/surface/messages/parse.ts +2 -1
  395. package/src/transport/surface/messages/stream.ts +33 -17
  396. package/src/transport/surface/responses/encode.ts +22 -5
  397. package/src/transport/surface/responses/parse.ts +3 -11
  398. package/src/workers/buddy-growth-tasks.ts +8 -8
  399. package/src/workers/daily-checkin.ts +4 -4
  400. package/src/workers/quota-refresh-sweep.ts +1 -1
  401. package/src/workers/zcode-gift-claim.ts +1 -1
  402. package/dist/dashboard/assets/BannedPage-BxoG1MrL.js.gz +0 -0
  403. package/dist/dashboard/assets/CliToolDetailPage-ixwgKk53.js.gz +0 -0
  404. package/dist/dashboard/assets/CliToolsPage-BmttNV3U.js.gz +0 -0
  405. package/dist/dashboard/assets/CombosPage-BE3qwwfC.js.gz +0 -0
  406. package/dist/dashboard/assets/ConsoleLogPage-CRZdmMIX.js.gz +0 -0
  407. package/dist/dashboard/assets/CustomizationPage-Dv_FJ6Er.js.gz +0 -0
  408. package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js +0 -1
  409. package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js.gz +0 -0
  410. package/dist/dashboard/assets/InjectSystemPromptPanel-BOoO8rpz.js.gz +0 -0
  411. package/dist/dashboard/assets/LoginPage-ChJuNNVN.js.gz +0 -0
  412. package/dist/dashboard/assets/ModelPicker-BZAJtPg1.js.gz +0 -0
  413. package/dist/dashboard/assets/OverviewPage-BE40s3u5.js.gz +0 -0
  414. package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js +0 -8
  415. package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js.gz +0 -0
  416. package/dist/dashboard/assets/ProviderIcon-CQ6bPUf_.js.gz +0 -0
  417. package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js +0 -1
  418. package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js.gz +0 -0
  419. package/dist/dashboard/assets/ProxyPage-BJ87Pp0U.js +0 -9
  420. package/dist/dashboard/assets/ProxyPage-BJ87Pp0U.js.gz +0 -0
  421. package/dist/dashboard/assets/QuotaPage-gkSyRAKa.js +0 -1
  422. package/dist/dashboard/assets/QuotaPage-gkSyRAKa.js.gz +0 -0
  423. package/dist/dashboard/assets/SettingsPage-C5HPWqEv.js.gz +0 -0
  424. package/dist/dashboard/assets/SetupPage-CFNcATJJ.js.gz +0 -0
  425. package/dist/dashboard/assets/StudioPage-YoD4tY5l.js.gz +0 -0
  426. package/dist/dashboard/assets/UsagePage-DxjaAhb_.js.gz +0 -0
  427. package/dist/dashboard/assets/api-keys-BTlkq_Uk.js.gz +0 -0
  428. package/dist/dashboard/assets/app-BX2nnKqs.css +0 -2
  429. package/dist/dashboard/assets/app-BX2nnKqs.css.gz +0 -0
  430. package/dist/dashboard/assets/assert-CndthXFQ.js +0 -1
  431. package/dist/dashboard/assets/assert-CndthXFQ.js.gz +0 -0
  432. package/dist/dashboard/assets/badge-DdbT_7Am.js.gz +0 -0
  433. package/dist/dashboard/assets/banner-CsGN-nbm.js.gz +0 -0
  434. package/dist/dashboard/assets/card-mgZ_TLLZ.js.gz +0 -0
  435. package/dist/dashboard/assets/cli-tools-A1-v8-03.js.gz +0 -0
  436. package/dist/dashboard/assets/clipboard-button-CJ4Zty89.js.gz +0 -0
  437. package/dist/dashboard/assets/dialog-B5L2jPQ_.js.gz +0 -0
  438. package/dist/dashboard/assets/drawer-DkjUouAj.js.gz +0 -0
  439. package/dist/dashboard/assets/entry-B-LVLV6o.js.gz +0 -0
  440. package/dist/dashboard/assets/entry-CAhdYl3B.js.gz +0 -0
  441. package/dist/dashboard/assets/entry-D2xD0Tmd.js +0 -4
  442. package/dist/dashboard/assets/entry-D2xD0Tmd.js.gz +0 -0
  443. package/dist/dashboard/assets/formatters-CrsdLVsn.js.gz +0 -0
  444. package/dist/dashboard/assets/index-BU64Lqvp.js.gz +0 -0
  445. package/dist/dashboard/assets/inline-B2aDLyyg.js.gz +0 -0
  446. package/dist/dashboard/assets/input-BCVkdR9d.js.gz +0 -0
  447. package/dist/dashboard/assets/layout-DNcAdMK-.js.gz +0 -0
  448. package/dist/dashboard/assets/list-row-DgrIH0hw.js.gz +0 -0
  449. package/dist/dashboard/assets/live-Cae01iu8.js.gz +0 -0
  450. package/dist/dashboard/assets/network-CRib7H_2.js.gz +0 -0
  451. package/dist/dashboard/assets/numbers-BKQE8Xpz.js.gz +0 -0
  452. package/dist/dashboard/assets/pagination-ecPvG_8g.js.gz +0 -0
  453. package/dist/dashboard/assets/quota-DJ1QlAOq.js +0 -1
  454. package/dist/dashboard/assets/quota-DJ1QlAOq.js.gz +0 -0
  455. package/dist/dashboard/assets/routing-Ckm1UXuC.js.gz +0 -0
  456. package/dist/dashboard/assets/select-ChoFWyKz.js.gz +0 -0
  457. package/dist/dashboard/assets/settings-CKjFPlKW.js.gz +0 -0
  458. package/dist/dashboard/assets/stack-DWCwU3fh.js.gz +0 -0
  459. package/dist/dashboard/assets/state-Cg3VL5J_.js.gz +0 -0
  460. package/dist/dashboard/assets/switch-Db70BMix.js.gz +0 -0
  461. package/dist/dashboard/assets/useMutation-BqdE0LuM.js.gz +0 -0
  462. package/src/network/pool-health.ts +0 -210
  463. package/src/providers/bundled-discovery-providers.ts +0 -98
  464. package/src/providers/configured-openai-providers.ts +0 -120
  465. package/src/providers/integrations/buddy/buddy-checkin.ts +0 -521
  466. package/src/providers/integrations/buddy/buddy-cli-headers.ts +0 -76
  467. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +0 -130
  468. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +0 -234
  469. package/src/providers/integrations/buddy/buddy-growth-shared.ts +0 -496
  470. package/src/providers/integrations/buddy/buddy-identity.ts +0 -300
  471. package/src/providers/integrations/buddy/buddy-oauth-shared.ts +0 -341
  472. package/src/providers/integrations/buddy/buddy-quota-shared.ts +0 -417
  473. package/src/providers/integrations/buddy/buddy-server-tools.ts +0 -240
  474. package/src/providers/integrations/buddy/buddy-usage-shared.ts +0 -221
  475. package/src/providers/integrations/buddy/buddy-usage-store.ts +0 -181
  476. package/src/providers/integrations/buddy/codebuddy/cn-catalog.ts +0 -37
  477. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +0 -185
  478. package/src/providers/integrations/buddy/codebuddy/oauth.ts +0 -47
  479. package/src/providers/integrations/buddy/codebuddy/provider.ts +0 -175
  480. package/src/providers/integrations/buddy/codebuddy/quota.ts +0 -154
  481. package/src/providers/integrations/qoder/chat-proto.ts +0 -328
  482. package/src/providers/integrations/qoder/qoder-grpc.ts +0 -871
  483. package/src/providers/integrations/qoder/qoder-ide.ts +0 -588
  484. package/src/providers/operations/model-discovery-cache.ts +0 -34
  485. package/src/providers/operations/probe-fetch.ts +0 -15
  486. package/src/providers/operations/provider-version-cache.ts +0 -30
  487. package/src/providers/provider-capabilities.ts +0 -33
  488. package/src/providers/provider-credential-envelope.ts +0 -43
  489. /package/dist/dashboard/assets/{app-B7kIWFm5.js → app-C0gTXEoN.js} +0 -0
  490. /package/dist/dashboard/assets/{app-B7kIWFm5.js.gz → app-C0gTXEoN.js.gz} +0 -0
  491. /package/src/network/{http2-bidi.ts → bidi.ts} +0 -0
  492. /package/src/network/pool/{pool-scripts.ts → scripts.ts} +0 -0
  493. /package/src/network/pool/{pool-state.ts → state.ts} +0 -0
  494. /package/src/network/{backoff.ts → util/backoff.ts} +0 -0
  495. /package/src/network/{deduplication.ts → util/dedupe.ts} +0 -0
  496. /package/src/network/{ip-utils.ts → util/ip.ts} +0 -0
@@ -19,12 +19,12 @@
19
19
  import { defineModel, type ModelDefinition } from "../../provider-model-definition";
20
20
  import {
21
21
  FREEPI_CHAT_PATH,
22
- FREEPI_CLI_VERSION,
23
22
  FREEPI_CONTEXT_WINDOW,
24
23
  FREEPI_DEFAULT_MODEL,
25
24
  FREEPI_MAX_OUTPUT_TOKENS,
26
25
  FREEPI_PROVIDER_ID,
27
26
  } from "./freepi-realm";
27
+ import { getFreepiVersion } from "../../operations/client-versions";
28
28
 
29
29
  /** Catalog id → canonical roster id for one model. */
30
30
  const CATALOG_ID_REMAP: Readonly<Record<string, string>> = {
@@ -44,7 +44,7 @@ export async function discoverFreepiModels(input: {
44
44
  headers: {
45
45
  authorization: `Bearer ${input.token}`,
46
46
  accept: "application/json",
47
- "x-client-version": FREEPI_CLI_VERSION,
47
+ "x-client-version": getFreepiVersion(),
48
48
  },
49
49
  ...(input.signal ? { signal: input.signal } : {}),
50
50
  });
@@ -4,7 +4,7 @@
4
4
  // CLI version) on the chat wire, and uses `application/json`. There is no
5
5
  // device fingerprint — the JWT is the whole identity.
6
6
 
7
- import { FREEPI_CLI_VERSION } from "./freepi-realm";
7
+ import { getFreepiVersion } from "../../operations/client-versions";
8
8
 
9
9
  /** Builds the header set for one free-pi request. */
10
10
  export function buildFreepiHeaders(jwt: string, stream = false, sessionId?: string): Record<string, string> {
@@ -12,7 +12,7 @@ export function buildFreepiHeaders(jwt: string, stream = false, sessionId?: stri
12
12
  authorization: `Bearer ${jwt}`,
13
13
  "content-type": "application/json",
14
14
  accept: stream ? "text/event-stream" : "application/json",
15
- "x-client-version": FREEPI_CLI_VERSION,
15
+ "x-client-version": getFreepiVersion(),
16
16
  };
17
17
  if (sessionId !== undefined && sessionId.length > 0) {
18
18
  headers["x-session-id"] = sessionId;
@@ -194,8 +194,8 @@ export async function* parseFreepiSse(
194
194
  }
195
195
  } else if (line.startsWith(":")) {
196
196
  continue;
197
- } else if (line.startsWith("data")) {
198
- let value = line.slice(4);
197
+ } else if (line === "data" || line.startsWith("data:")) {
198
+ let value = line === "data" ? "" : line.slice(5);
199
199
  if (value.startsWith(" ")) value = value.slice(1);
200
200
  data.push(value);
201
201
  }
@@ -238,5 +238,14 @@ export function throwFreepiUpstreamError(code: unknown, message: unknown): never
238
238
  if (codeStr === "upstream_error") {
239
239
  throw new GatewayError("platform_unavailable", 502, `${msg} (request {requestId}). The upstream model host errored. 🌙`, { upstream_code: codeStr }, "upstream");
240
240
  }
241
+ // A connection-bound session refusal: the caller's transcript carries native
242
+ // items (tool_call_id) from a session this x-session-id does not own. Named
243
+ // distinctly so the adapter can rotate the session and rebase the history.
244
+ // The upstream also reports it as a free-form message with only a generic
245
+ // code, so match the wording too (donor: "does not belong to this connection").
246
+ const ownershipRefusal = /(?:does not|doesn't|not) belong to|belongs to (?:another|a different)/i.test(msg);
247
+ if (codeStr === "session_mismatch" || codeStr === "invalid_session" || codeStr === "stale_session" || ownershipRefusal) {
248
+ throw new GatewayError("upstream_conflict", 409, `Free-pi session ownership refused: ${msg} (request {requestId}). Rebasing onto a fresh session 🔄`, { upstream_code: codeStr, freepi_session_mismatch: true }, "upstream");
249
+ }
241
250
  throw new GatewayError("platform_unavailable", 502, `${msg} (request {requestId}). The free-pi upstream refused the request. 🌙`, { upstream_code: codeStr }, "upstream");
242
251
  }
@@ -16,9 +16,6 @@ export const FREEPI_CHAT_PATH = "/v1/chat/completions";
16
16
  export const FREEPI_AD_NEXT_PATH = "/ads/next";
17
17
  export const FREEPI_AD_IMPRESSION_PATH = "/ads/impression";
18
18
 
19
- /** Client version the gateway reports via `x-client-version`. */
20
- export const FREEPI_CLI_VERSION = "0.6.14";
21
-
22
19
  /** Consent version shipped in 0.6.14. */
23
20
  export const FREEPI_CONSENT_VERSION = "4";
24
21
 
@@ -0,0 +1,158 @@
1
+ // Free-pi session identity and connection-bound history repair.
2
+ //
3
+ // Free-pi binds a conversation's native items (tool_call_id / assistant tool
4
+ // calls) to the `x-session-id` that opened it. Reusing one session id across
5
+ // unrelated conversations, or carrying a stale affinity onto a new account,
6
+ // makes the upstream answer "does not belong to this session". The donor's
7
+ // rule for connection-bound history (oh-my-pi's Copilot compat) is to rebuild
8
+ // prior turns from message content when the connection changes.
9
+ //
10
+ // So: one session per (credential, affinity) — a changed affinity *is* a fresh
11
+ // session — and on a session-ownership refusal, rotate the session and rebase
12
+ // the transcript into plain messages before one bounded retry.
13
+ import { createHash, randomUUID } from "node:crypto";
14
+ import type { CanonicalRequest, CanonicalMessage, ContentPart } from "../../../protocol/canonical-model";
15
+ import { resolvePromptCacheKey } from "../../operations/session-resolution";
16
+ import type { ProviderDispatchContext } from "../../provider-contracts";
17
+
18
+ /** Stable, non-reversible identity of one free-pi session owner. */
19
+ export interface FreepiSessionOwner {
20
+ readonly credential: string;
21
+ readonly affinity: string | undefined;
22
+ /** Account id is part of the scope: a new account must never inherit a session. */
23
+ readonly accountId: string;
24
+ readonly modelId: string;
25
+ /** Digest of the owner, used as the bounded map key (no live JWT in keys). */
26
+ readonly key: string;
27
+ }
28
+
29
+ /** Builds one session owner from the dispatch inputs. */
30
+ export function freepiSessionOwner(input: {
31
+ readonly secret: string;
32
+ readonly context: ProviderDispatchContext;
33
+ readonly request: CanonicalRequest;
34
+ readonly modelId: string;
35
+ }): FreepiSessionOwner {
36
+ const affinity = resolvePromptCacheKey(input.request, input.context) ?? input.context.conversation_affinity;
37
+ const accountId = input.context.credential.account_id ?? "";
38
+ const key = createHash("sha256")
39
+ .update(JSON.stringify([input.secret, accountId, input.modelId, affinity ?? ""]))
40
+ .digest("hex")
41
+ .slice(0, 32);
42
+ return { credential: input.secret, affinity, accountId, modelId: input.modelId, key };
43
+ }
44
+
45
+ /** Returns a new owner whose affinity (and thus session key) cannot collide. */
46
+ export function withFreshFreepiAffinity(owner: FreepiSessionOwner): FreepiSessionOwner {
47
+ const affinity = `fresh-${randomUUID()}`;
48
+ const key = createHash("sha256")
49
+ .update(JSON.stringify([owner.credential, owner.accountId, owner.modelId, affinity]))
50
+ .digest("hex")
51
+ .slice(0, 32);
52
+ return { ...owner, affinity, key };
53
+ }
54
+
55
+ /** Bounded, per-owner sessions. */
56
+ export interface FreepiSessionEntry {
57
+ readonly sessionId: string;
58
+ readonly owner: FreepiSessionOwner;
59
+ /** True until the session's ad registration has been attempted once. */
60
+ fresh: boolean;
61
+ }
62
+
63
+ const FREEPI_SESSION_CACHE_MAX = 256;
64
+
65
+ export class FreepiSessionRegistry {
66
+ readonly #sessions = new Map<string, FreepiSessionEntry>();
67
+
68
+ /** Existing session for this owner, or a freshly minted one. */
69
+ get(owner: FreepiSessionOwner): FreepiSessionEntry {
70
+ const existing = this.#sessions.get(owner.key);
71
+ if (existing !== undefined) {
72
+ // Touch for LRU: the map is small, but a long-lived process must not
73
+ // pin a stale affinity forever.
74
+ this.#sessions.delete(owner.key);
75
+ const touched: FreepiSessionEntry = { ...existing, fresh: false };
76
+ this.#sessions.set(owner.key, touched);
77
+ return touched;
78
+ }
79
+ const entry: FreepiSessionEntry = { sessionId: randomUUID(), owner, fresh: true };
80
+ this.#sessions.set(owner.key, entry);
81
+ if (this.#sessions.size > FREEPI_SESSION_CACHE_MAX) {
82
+ const oldest = this.#sessions.keys().next();
83
+ if (!oldest.done) this.#sessions.delete(oldest.value);
84
+ }
85
+ return entry;
86
+ }
87
+
88
+ /** Drops one owner's session; the next call mints a fresh one. */
89
+ rotate(owner: FreepiSessionOwner): FreepiSessionEntry {
90
+ this.#sessions.delete(owner.key);
91
+ return this.get(owner);
92
+ }
93
+
94
+ /** Test seam. */
95
+ get size(): number {
96
+ return this.#sessions.size;
97
+ }
98
+ }
99
+
100
+ /** Upstream refusal shapes that say a native item/session belongs elsewhere. */
101
+ const SESSION_OWNERSHIP_REFUSAL = /(?:does not|doesn't|not) belong to|belongs to (?:another|a different)|session (?:mismatch|expired|invalid)|invalid session|unknown session|stale session|tool_call_id.*(?:unknown|invalid|mismatch)|not (?:found|valid) for (?:this )?session/i;
102
+
103
+ /** True when an upstream error is a session/connection ownership refusal. */
104
+ export function isFreepiSessionOwnershipError(error: unknown): boolean {
105
+ if (!(error instanceof Error)) return false;
106
+ const details = (error as Error & { details?: Readonly<Record<string, unknown>> }).details;
107
+ if (details?.["freepi_session_mismatch"] === true) return true;
108
+ const raw = typeof details?.["raw"] === "string" ? details["raw"] : "";
109
+ const message = `${error.message} ${raw}`;
110
+ return SESSION_OWNERSHIP_REFUSAL.test(message);
111
+ }
112
+
113
+ /**
114
+ * Rebases a connection-bound transcript into plain messages: a fresh session
115
+ * has never seen the old `tool_call_id`s, so replaying them verbatim is exactly
116
+ * what the upstream refuses. Prior tool calls and their results become text in
117
+ * the same conversation order (the donor's cold-session rebuild rule).
118
+ */
119
+ export function rebaseFreepiRequest(request: CanonicalRequest): CanonicalRequest {
120
+ const messages: CanonicalMessage[] = [];
121
+ for (const message of request.messages) {
122
+ const parts: ContentPart[] = [];
123
+ let native = false;
124
+ for (const part of message.content) {
125
+ if (part.kind === "toolCall") {
126
+ native = true;
127
+ const args = typeof part.arguments === "string" ? part.arguments : JSON.stringify(part.arguments ?? {});
128
+ parts.push({ kind: "text", text: `[Earlier tool call ${part.name}: ${args}]` });
129
+ continue;
130
+ }
131
+ if (part.kind === "toolResult") {
132
+ native = true;
133
+ const text = typeof part.content === "string"
134
+ ? part.content
135
+ : part.content.map((child) => (child.kind === "text" ? child.text : "")).join("\n");
136
+ parts.push({ kind: "text", text: `[Earlier tool result: ${text}]` });
137
+ continue;
138
+ }
139
+ parts.push(part);
140
+ }
141
+ // Keep the role, but collapse a native tool message onto a user/assistant
142
+ // text turn so no orphan `tool_call_id` reaches the fresh session.
143
+ const role = native && message.role === "tool" ? "user" : message.role;
144
+ messages.push({ ...message, role, content: parts });
145
+ }
146
+ return {
147
+ ...request,
148
+ messages,
149
+ generation_controls: {
150
+ ...request.generation_controls,
151
+ // The old cache affinity is bound to the old session: drop it so the
152
+ // fresh session starts with a clean prefix.
153
+ "extension:prompt_cache_key": undefined,
154
+ "extension:responses.prompt_cache_key": undefined,
155
+ "extension:metadata_user_id": undefined,
156
+ },
157
+ };
158
+ }
@@ -5,7 +5,6 @@
5
5
  // `/v1/chat/completions` and translates the OpenAI-style SSE stream into
6
6
  // canonical events.
7
7
 
8
- import { randomUUID } from "node:crypto";
9
8
  import type {
10
9
  CanonicalEvent,
11
10
  CanonicalRequest,
@@ -23,10 +22,17 @@ import {
23
22
  } from "../../provider-contracts";
24
23
  import { mapUpstreamHttpError } from "../../../transport/failure-policy";
25
24
  import { abortGatewayError, createUpstreamDeadlineLifecycle } from "../../operations/upstream-deadline";
26
- import { resolvePromptCacheKey } from "../../operations/session-resolution";
27
25
  import { usageFromProvider } from "../../provider-usage";
28
26
  import { buildFreepiChatBody, parseFreepiSse, throwFreepiUpstreamError } from "./freepi-protocol";
29
27
  import { buildFreepiHeaders } from "./freepi-headers";
28
+ import {
29
+ FreepiSessionRegistry,
30
+ freepiSessionOwner,
31
+ isFreepiSessionOwnershipError,
32
+ rebaseFreepiRequest,
33
+ withFreshFreepiAffinity,
34
+ type FreepiSessionOwner,
35
+ } from "./freepi-session";
30
36
  import {
31
37
  FREEPI_AD_IMPRESSION_PATH,
32
38
  FREEPI_AD_NEXT_PATH,
@@ -51,12 +57,13 @@ export class FreepiAdapter implements ProviderAdapter {
51
57
  readonly provider_id: ProviderId;
52
58
  private readonly fetchFn: typeof fetch;
53
59
  /**
54
- * One stable ad-registered session id per account credential, so repeated
55
- * chats reuse a session the upstream knows has shown ads. The session must
56
- * exist for the chat wire: without it the server answers `ads_required`
57
- * ("Ads aren't showing for this account, so free completions are paused").
60
+ * One ad-registered session per (credential, account, model, affinity).
61
+ * A changed affinity is a fresh session by construction: free-pi binds
62
+ * native tool_call ids to the session that opened them, and reusing a
63
+ * session across conversations/accounts is what answers "does not belong
64
+ * to this session".
58
65
  */
59
- #sessionByCredential = new Map<string, string>();
66
+ readonly #sessions = new FreepiSessionRegistry();
60
67
 
61
68
  constructor(options: FreepiAdapterOptions = {}) {
62
69
  this.provider_id = (options.providerId ?? FREEPI_PROVIDER_ID) as ProviderId;
@@ -64,24 +71,19 @@ export class FreepiAdapter implements ProviderAdapter {
64
71
  }
65
72
 
66
73
  /**
67
- * Ensures a session id exists for this credential and that the session has
74
+ * Ensures a session id exists for this owner and that the session has
68
75
  * registered at least one ad impression (banner + inline). The upstream
69
76
  * refuses the chat wire with `ads_required` until a session has shown ads,
70
- * so the first call per credential pays the two ad-registration requests.
71
- *
72
- * Returns the session id to send as `x-session-id`. Falls back to a fresh
73
- * random id when registration fails — a broken ad surface must not block
74
- * chat entirely, and the error is caught upstream anyway.
77
+ * so a freshly minted session pays the two ad-registration requests.
75
78
  */
76
- async #ensureSession(secret: string, outboundFetch: typeof fetch): Promise<string> {
77
- const existing = this.#sessionByCredential.get(secret);
78
- if (existing !== undefined) return existing;
79
- const sessionId = randomUUID();
79
+ async #ensureSession(secret: string, owner: FreepiSessionOwner, outboundFetch: typeof fetch): Promise<string> {
80
+ const entry = this.#sessions.get(owner);
81
+ if (!entry.fresh) return entry.sessionId;
80
82
  try {
81
83
  for (const slot of ["banner", "inline"] as const) {
82
84
  const next = await outboundFetch(new URL(`${FREEPI_AD_NEXT_PATH}?slot=${slot}`, FREEPI_BASE_URL), {
83
85
  method: "GET",
84
- headers: buildFreepiHeaders(secret, false, sessionId),
86
+ headers: buildFreepiHeaders(secret, false, entry.sessionId),
85
87
  signal: AbortSignal.timeout(15_000),
86
88
  });
87
89
  if (!next.ok || next.status === 204) continue;
@@ -91,7 +93,7 @@ export class FreepiAdapter implements ProviderAdapter {
91
93
  if (typeof adId !== "string" || typeof clickToken !== "string") continue;
92
94
  await outboundFetch(new URL(FREEPI_AD_IMPRESSION_PATH, FREEPI_BASE_URL), {
93
95
  method: "POST",
94
- headers: buildFreepiHeaders(secret, false, sessionId),
96
+ headers: buildFreepiHeaders(secret, false, entry.sessionId),
95
97
  body: JSON.stringify({ ad_id: adId, click_token: clickToken, imp_fired: true }),
96
98
  signal: AbortSignal.timeout(15_000),
97
99
  });
@@ -99,8 +101,7 @@ export class FreepiAdapter implements ProviderAdapter {
99
101
  } catch {
100
102
  // The ad surface is best-effort; chat may still work or fail upstream.
101
103
  }
102
- this.#sessionByCredential.set(secret, sessionId);
103
- return sessionId;
104
+ return entry.sessionId;
104
105
  }
105
106
 
106
107
  async *dispatch(
@@ -113,45 +114,70 @@ export class FreepiAdapter implements ProviderAdapter {
113
114
  }
114
115
  const secret = readCredentialSecret(context.credential, "Free-pi credential is required");
115
116
  const modelId = candidate.model_id || request.model;
116
- const lifecycle = createUpstreamDeadlineLifecycle(context);
117
117
  const outboundFetch =
118
118
  (context.outbound_fetch as unknown as typeof fetch | undefined) ?? this.fetchFn;
119
-
120
- const body = buildFreepiChatBody(modelId, request, candidate.output_limit ?? undefined);
121
-
122
- const sessionKey = resolvePromptCacheKey(request, context);
123
- const cacheSessionKey = sessionKey === undefined ? undefined : `${modelId}:${sessionKey}`;
124
-
125
- const sessionId = await this.#ensureSession(secret, outboundFetch);
126
- const headers = buildFreepiHeaders(secret, true, sessionId);
119
+ const owner = freepiSessionOwner({ secret, context, request, modelId });
127
120
  const url = `${FREEPI_BASE_URL}${FREEPI_CHAT_PATH}`;
128
121
 
129
- try {
130
- const response = await outboundFetch(url, {
131
- method: "POST",
132
- headers,
133
- body: JSON.stringify(body),
134
- signal: lifecycle.signal,
135
- });
136
- if (!response.ok) throw await mapUpstreamHttpError(response, this.provider_id);
137
- if (!response.body) {
138
- throw new GatewayError("platform_unavailable", 502, "Free-pi returned empty body (request {requestId}). The upstream came back empty-handed — retry 🌙", {}, "upstream");
122
+ // Two attempts at most: a connection-bound refusal rotates the session and
123
+ // rebases the transcript (dropping native tool_call ids the fresh session
124
+ // never saw) before one retry. Anything after that is terminal.
125
+ let activeRequest = request;
126
+ let activeOwner = owner;
127
+ for (let attempt = 0; attempt < 2; attempt += 1) {
128
+ const sessionId = await this.#ensureSession(secret, activeOwner, outboundFetch);
129
+ const body = buildFreepiChatBody(modelId, activeRequest, candidate.output_limit ?? undefined);
130
+ const lifecycle = createUpstreamDeadlineLifecycle(context);
131
+ let events: CanonicalEvent[] = [];
132
+ try {
133
+ const response = await outboundFetch(url, {
134
+ method: "POST",
135
+ headers: buildFreepiHeaders(secret, true, sessionId),
136
+ body: JSON.stringify(body),
137
+ signal: lifecycle.signal,
138
+ });
139
+ if (!response.ok) throw await mapUpstreamHttpError(response, this.provider_id);
140
+ if (!response.body) {
141
+ throw new GatewayError("platform_unavailable", 502, "Free-pi returned empty body (request {requestId}). The upstream came back empty-handed — retry 🌙", {}, "upstream");
142
+ }
143
+ lifecycle.release();
144
+ // Buffered until the stream proves it carries no session-ownership
145
+ // refusal: an SSE 200 `{"error":{...}}` can arrive after the first
146
+ // delta, and yielding events before that would make a retry replay
147
+ // half an answer to the client. Free-pi turns are short; correctness
148
+ // of the tool loop wins over one RTT of time-to-first-token.
149
+ events = [];
150
+ for await (const event of this.#events(
151
+ response.body as ReadableStream<Uint8Array>,
152
+ lifecycle.signal,
153
+ request.model,
154
+ `${modelId}:${activeOwner.affinity ?? "fresh"}`,
155
+ activeRequest.messages,
156
+ )) {
157
+ events.push(event);
158
+ }
159
+ } catch (err: unknown) {
160
+ const abortError = abortGatewayError(lifecycle, err, context.abort_signal);
161
+ if (abortError) throw abortError;
162
+ if (attempt === 0 && isFreepiSessionOwnershipError(err)) {
163
+ const rebased = rebaseFreepiRequest(request);
164
+ // A fresh affinity = a fresh session by construction. The old
165
+ // owner's session is dropped so it can never be reused by a later
166
+ // turn that still carries the stale tool_call ids.
167
+ this.#sessions.rotate(owner);
168
+ activeRequest = rebased;
169
+ activeOwner = withFreshFreepiAffinity(owner);
170
+ lifecycle.release();
171
+ continue;
172
+ }
173
+ throw err;
174
+ } finally {
175
+ lifecycle.release();
139
176
  }
140
- lifecycle.release();
141
- yield* this.#events(
142
- response.body as ReadableStream<Uint8Array>,
143
- lifecycle.signal,
144
- request.model,
145
- cacheSessionKey,
146
- request.messages,
147
- );
148
- } catch (err: unknown) {
149
- const abortError = abortGatewayError(lifecycle, err, context.abort_signal);
150
- if (abortError) throw abortError;
151
- throw err;
152
- } finally {
153
- lifecycle.release();
177
+ yield* events;
178
+ return;
154
179
  }
180
+ throw new GatewayError("upstream_conflict", 409, "Free-pi still refused the session after a rebase (request {requestId}). Resend the complete conversation 🔌", { providerId: this.provider_id }, "upstream");
155
181
  }
156
182
 
157
183
  async *#events(
@@ -24,6 +24,7 @@ import { isRecord } from "../../../protocol/codecs";
24
24
  import { boundedUpstreamArray, boundedUpstreamNumber, sanitizeUpstreamLabel } from "../../provider-sanitize";
25
25
  import { modelsDevCatalog } from "../../discovery/models-dev-catalog";
26
26
  import type { DiscoveryInput } from "../../discovery/discovery-types";
27
+ import { resolveGithubCopilotMode } from "../../../config/providers";
27
28
  import {
28
29
  GITHUB_EXCLUDED_MODEL_IDS,
29
30
  GITHUB_PROVIDER_ID,
@@ -33,7 +34,13 @@ import {
33
34
  GITHUB_AUTO_MODELS,
34
35
  recordGithubModelRoutes,
35
36
  } from "./github";
36
- import { GITHUB_REQUEST_HEADERS, githubCopilotClientIds, parseGithubCredential } from "./github-oauth";
37
+ import {
38
+ GITHUB_REQUEST_HEADERS,
39
+ copilotGitHubDomain,
40
+ githubCopilotApiHostForProfile,
41
+ githubCopilotClientIds,
42
+ parseGithubCredential,
43
+ } from "./github-oauth";
37
44
  import { createHash } from "node:crypto";
38
45
 
39
46
  /** A context window above this is unsatisfiable headroom, not a real limit. */
@@ -165,7 +172,8 @@ function toModelDefinition(entry: Record<string, unknown>): ModelDefinition | un
165
172
  export async function discoverGithubModels(
166
173
  input: DiscoveryInput,
167
174
  ): Promise<readonly ModelDefinition[] | null> {
168
- const { access, apiHost } = parseGithubCredential(input.credential);
175
+ const credential = parseGithubCredential(input.credential);
176
+ const { access, apiHost, profile } = credential;
169
177
  const fetcher = input.fetcher ?? fetch;
170
178
  const timeoutSignal = AbortSignal.timeout(15_000);
171
179
  const signal =
@@ -176,17 +184,26 @@ export async function discoverGithubModels(
176
184
  // Machine id is per-account — see `githubCopilotClientIds`.
177
185
  const accountKey = createHash("sha256").update(`${apiHost}:${access}`).digest("hex");
178
186
  const { sessionId, machineId } = await githubCopilotClientIds(accountKey);
179
- const response = await fetcher(`https://${apiHost}/models`, {
180
- headers: {
181
- accept: "application/json",
182
- authorization: `Bearer ${access}`,
183
- ...GITHUB_REQUEST_HEADERS,
184
- "VScode-SessionId": sessionId,
185
- "VScode-MachineId": machineId,
186
- "editor-device-id": machineId,
187
- },
188
- signal,
189
- });
187
+ if (profile !== resolveGithubCopilotMode()) return null;
188
+ const headers: Record<string, string> = profile === "opencode-copilot"
189
+ ? {
190
+ accept: "application/json",
191
+ authorization: `Bearer ${credential.github ?? access}`,
192
+ "user-agent": "opencode/0.1.0",
193
+ "x-github-api-version": "2026-08-01",
194
+ }
195
+ : {
196
+ accept: "application/json",
197
+ authorization: `Bearer ${access}`,
198
+ ...GITHUB_REQUEST_HEADERS,
199
+ "VScode-SessionId": sessionId,
200
+ "VScode-MachineId": machineId,
201
+ "editor-device-id": machineId,
202
+ };
203
+ const discoveryHost = profile === "opencode-copilot"
204
+ ? githubCopilotApiHostForProfile(profile, copilotGitHubDomain())
205
+ : apiHost;
206
+ const response = await fetcher(`https://${discoveryHost}/models`, { headers, signal });
190
207
  if (!response.ok) return null;
191
208
  const payload: unknown = await response.json();
192
209
  if (!isRecord(payload)) return null;