cartethyia 2.7.4 → 2.7.6

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 (595) hide show
  1. package/.env.example +5 -0
  2. package/README.md +18 -6
  3. package/dist/dashboard/assets/BannedPage-B1N603cM.js +1 -0
  4. package/dist/dashboard/assets/BannedPage-B1N603cM.js.gz +0 -0
  5. package/dist/dashboard/assets/CliToolDetailPage-9q4_4NoK.js +3 -0
  6. package/dist/dashboard/assets/CliToolDetailPage-9q4_4NoK.js.gz +0 -0
  7. package/dist/dashboard/assets/CliToolsPage-DwshGtK-.js +1 -0
  8. package/dist/dashboard/assets/CliToolsPage-DwshGtK-.js.gz +0 -0
  9. package/dist/dashboard/assets/CombosPage-D762MIkp.js +1 -0
  10. package/dist/dashboard/assets/CombosPage-D762MIkp.js.gz +0 -0
  11. package/dist/dashboard/assets/ConsoleLogPage-KRYjHZsX.js +1 -0
  12. package/dist/dashboard/assets/ConsoleLogPage-KRYjHZsX.js.gz +0 -0
  13. package/dist/dashboard/assets/CustomizationPage-DXsxk5uM.js +1 -0
  14. package/dist/dashboard/assets/CustomizationPage-DXsxk5uM.js.gz +0 -0
  15. package/dist/dashboard/assets/HealthEventsModal-BEjAjD4F.js +1 -0
  16. package/dist/dashboard/assets/HealthEventsModal-BEjAjD4F.js.gz +0 -0
  17. package/dist/dashboard/assets/InjectSystemPromptPanel-d5v3K_Yc.js +899 -0
  18. package/dist/dashboard/assets/InjectSystemPromptPanel-d5v3K_Yc.js.gz +0 -0
  19. package/dist/dashboard/assets/LoginPage-Ccv4tjNS.js +1 -0
  20. package/dist/dashboard/assets/LoginPage-Ccv4tjNS.js.gz +0 -0
  21. package/dist/dashboard/assets/{ModelPicker-BMS2t8BA.js → ModelPicker-C6WlpU5E.js} +1 -1
  22. package/dist/dashboard/assets/ModelPicker-C6WlpU5E.js.gz +0 -0
  23. package/dist/dashboard/assets/OverviewPage-DN9fYNWA.js +1 -0
  24. package/dist/dashboard/assets/OverviewPage-DN9fYNWA.js.gz +0 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-BY0ieUgz.js +5 -0
  26. package/dist/dashboard/assets/ProviderDetailPage-BY0ieUgz.js.gz +0 -0
  27. package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js +1 -0
  28. package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js.gz +0 -0
  29. package/dist/dashboard/assets/ProvidersPage-DNsbfea2.js +1 -0
  30. package/dist/dashboard/assets/ProvidersPage-DNsbfea2.js.gz +0 -0
  31. package/dist/dashboard/assets/{ProxyPage-lsrx7NYg.js → ProxyPage-DxjvUzSD.js} +1 -1
  32. package/dist/dashboard/assets/ProxyPage-DxjvUzSD.js.gz +0 -0
  33. package/dist/dashboard/assets/QuotaPage-OaHScjpX.js +1 -0
  34. package/dist/dashboard/assets/QuotaPage-OaHScjpX.js.gz +0 -0
  35. package/dist/dashboard/assets/SettingsPage-BW4AKj2H.js +1 -0
  36. package/dist/dashboard/assets/SettingsPage-BW4AKj2H.js.gz +0 -0
  37. package/dist/dashboard/assets/SetupPage-44BZrxFC.js +1 -0
  38. package/dist/dashboard/assets/SetupPage-44BZrxFC.js.gz +0 -0
  39. package/dist/dashboard/assets/{StudioPage-Cq5tW7hW.js → StudioPage-DRPavCdU.js} +4 -4
  40. package/dist/dashboard/assets/StudioPage-DRPavCdU.js.gz +0 -0
  41. package/dist/dashboard/assets/ToolIcon-CfeLA-Z_.js +1 -0
  42. package/dist/dashboard/assets/ToolIcon-CfeLA-Z_.js.gz +0 -0
  43. package/dist/dashboard/assets/UsagePage-B7M7LvMM.js +56 -0
  44. package/dist/dashboard/assets/UsagePage-B7M7LvMM.js.gz +0 -0
  45. package/dist/dashboard/assets/api-keys-KvX83mRe.js +1 -0
  46. package/dist/dashboard/assets/api-keys-KvX83mRe.js.gz +0 -0
  47. package/dist/dashboard/assets/app-DRnQLhsc.js +9 -0
  48. package/dist/dashboard/assets/app-DRnQLhsc.js.gz +0 -0
  49. package/dist/dashboard/assets/app-mweRcsKp.css +2 -0
  50. package/dist/dashboard/assets/app-mweRcsKp.css.gz +0 -0
  51. package/dist/dashboard/assets/assert-CidQVoNo.js +1 -0
  52. package/dist/dashboard/assets/assert-CidQVoNo.js.gz +0 -0
  53. package/dist/dashboard/assets/{badge-D9CS6dZk.js → badge-BaX2GTrn.js} +1 -1
  54. package/dist/dashboard/assets/badge-BaX2GTrn.js.gz +0 -0
  55. package/dist/dashboard/assets/{banner-DympBNq4.js → banner-DFH4Bkte.js} +1 -1
  56. package/dist/dashboard/assets/banner-DFH4Bkte.js.gz +0 -0
  57. package/dist/dashboard/assets/brand/login_register.jpg +0 -0
  58. package/dist/dashboard/assets/brand/login_register_initial.jpg +0 -0
  59. package/dist/dashboard/assets/brand/og_bansos.jpg +0 -0
  60. package/dist/dashboard/assets/brand/og_image.jpg +0 -0
  61. package/dist/dashboard/assets/{card-ibRWU_uT.js → card-CxDw0lK6.js} +1 -1
  62. package/dist/dashboard/assets/card-CxDw0lK6.js.gz +0 -0
  63. package/dist/dashboard/assets/chevron-left-Bf6CaTSJ.js +1 -0
  64. package/dist/dashboard/assets/chevron-left-Bf6CaTSJ.js.gz +0 -0
  65. package/dist/dashboard/assets/circle-question-mark-Cz4un8sH.js +1 -0
  66. package/dist/dashboard/assets/circle-question-mark-Cz4un8sH.js.gz +0 -0
  67. package/dist/dashboard/assets/cli-tools-Bwv5-zGZ.js +1 -0
  68. package/dist/dashboard/assets/cli-tools-Bwv5-zGZ.js.gz +0 -0
  69. package/dist/dashboard/assets/{clipboard-button-DJm-9pz7.js → clipboard-button-DfS1ntiq.js} +1 -1
  70. package/dist/dashboard/assets/clipboard-button-DfS1ntiq.js.gz +0 -0
  71. package/dist/dashboard/assets/dialog-BK-NRr5u.js +1 -0
  72. package/dist/dashboard/assets/dialog-BK-NRr5u.js.gz +0 -0
  73. package/dist/dashboard/assets/{drawer-CqGYeOrd.js → drawer-QqPul6sC.js} +1 -1
  74. package/dist/dashboard/assets/drawer-QqPul6sC.js.gz +0 -0
  75. package/dist/dashboard/assets/entry-BMlYJVFj.js +1 -0
  76. package/dist/dashboard/assets/entry-BMlYJVFj.js.gz +0 -0
  77. package/dist/dashboard/assets/entry-D6WvojeD.js +4 -0
  78. package/dist/dashboard/assets/entry-D6WvojeD.js.gz +0 -0
  79. package/dist/dashboard/assets/entry-Dmx7gBiN.js +1 -0
  80. package/dist/dashboard/assets/entry-Dmx7gBiN.js.gz +0 -0
  81. package/dist/dashboard/assets/{formatters-D-MTJxMr.js → formatters-u7JPB5BX.js} +1 -1
  82. package/dist/dashboard/assets/formatters-u7JPB5BX.js.gz +0 -0
  83. package/dist/dashboard/assets/house-h9AZlJuT.js +1 -0
  84. package/dist/dashboard/assets/house-h9AZlJuT.js.gz +0 -0
  85. package/dist/dashboard/assets/{index-DBNPOfhV.js → index-Ko4dLRnV.js} +2 -2
  86. package/dist/dashboard/assets/index-Ko4dLRnV.js.gz +0 -0
  87. package/dist/dashboard/assets/{inline-BT0zntEr.js → inline-CHJQwxLa.js} +1 -1
  88. package/dist/dashboard/assets/inline-CHJQwxLa.js.gz +0 -0
  89. package/dist/dashboard/assets/{input-Da2ZViWg.js → input-BlmlizVV.js} +1 -1
  90. package/dist/dashboard/assets/input-BlmlizVV.js.gz +0 -0
  91. package/dist/dashboard/assets/{layout-BXNeLw6M.js → layout-Bz93I-yE.js} +1 -1
  92. package/dist/dashboard/assets/layout-Bz93I-yE.js.gz +0 -0
  93. package/dist/dashboard/assets/{list-row-BycOrrIV.js → list-row-Ml16KCfu.js} +1 -1
  94. package/dist/dashboard/assets/list-row-Ml16KCfu.js.gz +0 -0
  95. package/dist/dashboard/assets/live-7_QpnYGu.js +1 -0
  96. package/dist/dashboard/assets/live-7_QpnYGu.js.gz +0 -0
  97. package/dist/dashboard/assets/{mutation-Rr0TIvcj.js → mutation-GU7XdEjz.js} +1 -1
  98. package/dist/dashboard/assets/mutation-GU7XdEjz.js.gz +0 -0
  99. package/dist/dashboard/assets/network-CuMDaTzT.js +1 -0
  100. package/dist/dashboard/assets/network-CuMDaTzT.js.gz +0 -0
  101. package/dist/dashboard/assets/numbers-CoqpaDMx.js +2 -0
  102. package/dist/dashboard/assets/numbers-CoqpaDMx.js.gz +0 -0
  103. package/dist/dashboard/assets/pagination-B5aOoweZ.js +1 -0
  104. package/dist/dashboard/assets/pagination-B5aOoweZ.js.gz +0 -0
  105. package/dist/dashboard/assets/providers/zai.webp +0 -0
  106. package/dist/dashboard/assets/providers/zcode.webp +0 -0
  107. package/dist/dashboard/assets/{query-keys-BloKtaXJ.js → query-keys-YSoQOUh6.js} +1 -1
  108. package/dist/dashboard/assets/{query-keys-BloKtaXJ.js.gz → query-keys-YSoQOUh6.js.gz} +0 -0
  109. package/dist/dashboard/assets/quota-DJ1QlAOq.js +1 -0
  110. package/dist/dashboard/assets/quota-DJ1QlAOq.js.gz +0 -0
  111. package/dist/dashboard/assets/routing-fnCsxgNO.js +1 -0
  112. package/dist/dashboard/assets/routing-fnCsxgNO.js.gz +0 -0
  113. package/dist/dashboard/assets/{select-BIYIBkMU.js → select-BHqpd-bg.js} +2 -2
  114. package/dist/dashboard/assets/select-BHqpd-bg.js.gz +0 -0
  115. package/dist/dashboard/assets/settings-BxslsErn.js +1 -0
  116. package/dist/dashboard/assets/settings-BxslsErn.js.gz +0 -0
  117. package/dist/dashboard/assets/shield-BMQNxORh.js +1 -0
  118. package/dist/dashboard/assets/shield-BMQNxORh.js.gz +0 -0
  119. package/dist/dashboard/assets/{stack-DfgnFDWY.js → stack-j7cXjgYX.js} +1 -1
  120. package/dist/dashboard/assets/stack-j7cXjgYX.js.gz +0 -0
  121. package/dist/dashboard/assets/{state-CkfdYWel.js → state-DHmEnLa2.js} +1 -1
  122. package/dist/dashboard/assets/state-DHmEnLa2.js.gz +0 -0
  123. package/dist/dashboard/assets/{switch-SYm237HG.js → switch-DmZcyTIC.js} +1 -1
  124. package/dist/dashboard/assets/switch-DmZcyTIC.js.gz +0 -0
  125. package/dist/dashboard/assets/{useMutation-BQqCRNIi.js → useMutation-CbCP4w1o.js} +1 -1
  126. package/dist/dashboard/assets/useMutation-CbCP4w1o.js.gz +0 -0
  127. package/dist/dashboard/console-backgrounds.jpg +0 -0
  128. package/dist/dashboard/console-backgrounds_initial.jpg +0 -0
  129. package/dist/dashboard/index.html +2 -2
  130. package/dist/dashboard/index.html.gz +0 -0
  131. package/dist/dashboard/precompressed-assets.json +60 -59
  132. package/migrations/full-postgres/0036_system_prompt_presets_refs.sql +10 -0
  133. package/migrations/full-postgres/0037_share_concurrency_scope.sql +3 -0
  134. package/migrations/full-postgres/0038_add_accounts_quota_snapshot.sql +7 -0
  135. package/migrations/full-postgres/0039_add_telemetry_provider_model_indexes.sql +13 -0
  136. package/migrations/full-postgres/0040_add_models_dialect.sql +3 -0
  137. package/migrations/full-postgres/0041_add_daily_checkin_ledger_account_fk.sql +7 -0
  138. package/migrations/full-postgres/0042_add_telemetry_degenerate_stop.sql +14 -0
  139. package/migrations/lite-sqlite3/0036_system_prompt_presets_refs.sql +14 -0
  140. package/migrations/lite-sqlite3/0037_share_concurrency_scope.sql +3 -0
  141. package/migrations/lite-sqlite3/0038_add_accounts_quota_snapshot.sql +4 -0
  142. package/migrations/lite-sqlite3/0039_add_telemetry_provider_model_indexes.sql +6 -0
  143. package/migrations/lite-sqlite3/0040_add_models_dialect.sql +3 -0
  144. package/migrations/lite-sqlite3/0041_add_daily_checkin_ledger_account_fk.sql +26 -0
  145. package/migrations/lite-sqlite3/0042_add_telemetry_degenerate_stop.sql +12 -0
  146. package/package.json +4 -6
  147. package/scripts/cli/update.ts +15 -41
  148. package/src/boot/app.ts +58 -2
  149. package/src/boot/dependencies.ts +794 -732
  150. package/src/boot/lifecycle.ts +11 -12
  151. package/src/config/cooldowns.ts +34 -55
  152. package/src/config/network.ts +1 -1
  153. package/src/config/package-paths.ts +1 -1
  154. package/src/config/providers.ts +19 -14
  155. package/src/config/resources.ts +0 -5
  156. package/src/config/runtime.ts +2 -5
  157. package/src/config/spec.ts +33 -6
  158. package/src/config/usage.ts +27 -0
  159. package/src/console/api-keys/ban-constants.ts +38 -0
  160. package/src/console/api-keys/contracts.ts +71 -62
  161. package/src/console/api-keys/routes.ts +56 -24
  162. package/src/console/auth/access.ts +4 -4
  163. package/src/console/auth/account-reset.ts +1 -1
  164. package/src/console/auth/audit-recorder.ts +1 -1
  165. package/src/console/auth/lockout.ts +7 -3
  166. package/src/console/auth/row-coercion.ts +57 -12
  167. package/src/console/auth/session-resolver.ts +1 -1
  168. package/src/console/auth/session-service.ts +8 -2
  169. package/src/console/auth/session.ts +37 -31
  170. package/src/console/auth/setup-service.ts +4 -3
  171. package/src/console/auth/user-store.ts +1 -1
  172. package/src/console/backup/9router-map.ts +0 -3
  173. package/src/console/backup/contracts.ts +9 -9
  174. package/src/console/backup/export-endpoint.ts +5 -6
  175. package/src/console/backup/nine-router.ts +1 -7
  176. package/src/console/backup/routes.ts +29 -33
  177. package/src/console/backup/service.ts +468 -468
  178. package/src/console/backup/store.ts +47 -29
  179. package/src/console/backup/validate.ts +11 -1
  180. package/src/console/cli-tools/contracts.ts +43 -299
  181. package/src/console/cli-tools/fs-ops.ts +29 -0
  182. package/src/console/cli-tools/injectors/claude.ts +3 -3
  183. package/src/console/cli-tools/injectors/codex.ts +181 -181
  184. package/src/console/cli-tools/injectors/driver.ts +150 -247
  185. package/src/console/cli-tools/injectors/kilo.ts +146 -21
  186. package/src/console/cli-tools/injectors/opencode.ts +176 -153
  187. package/src/console/cli-tools/routes.ts +7 -1
  188. package/src/console/cli-tools/service.ts +8 -7
  189. package/src/console/cli-tools/store.ts +2 -2
  190. package/src/console/console-router.ts +32 -0
  191. package/src/console/dashboard-assets.ts +9 -9
  192. package/src/console/domain-registration.ts +16 -2
  193. package/src/console/inferhub/client.ts +0 -7
  194. package/src/console/inferhub/key-store.ts +1 -1
  195. package/src/console/inferhub/normalize.ts +2 -2
  196. package/src/console/inferhub/routes.ts +1 -1
  197. package/src/console/inferhub/types.ts +1 -1
  198. package/src/console/observability/contracts.ts +7 -1
  199. package/src/console/observability/live.ts +31 -18
  200. package/src/console/observability/logs.ts +1 -1
  201. package/src/console/observability/sse.ts +62 -8
  202. package/src/console/observability/store.ts +23 -4
  203. package/src/console/providers/catalog/catalog-projections.ts +1 -1
  204. package/src/console/providers/catalog/contracts.ts +39 -0
  205. package/src/console/providers/catalog/model-operations.ts +1 -1
  206. package/src/console/providers/catalog/provider-operations.ts +95 -17
  207. package/src/console/providers/catalog/public-model-store.ts +0 -0
  208. package/src/console/providers/catalog/routes.ts +27 -3
  209. package/src/console/providers/catalog/store.ts +147 -8
  210. package/src/console/providers/detail/routes.ts +1 -1
  211. package/src/console/providers/oauth/callback-listener.ts +100 -18
  212. package/src/console/providers/oauth/routes.ts +92 -13
  213. package/src/console/quota/account-quota-global.ts +41 -3
  214. package/src/console/quota/account-quota-shared.ts +10 -3
  215. package/src/console/quota/account-quota-tenant.ts +259 -66
  216. package/src/console/quota/daily-growth-job.ts +308 -0
  217. package/src/console/quota/native-identity.ts +2 -2
  218. package/src/console/quota/quota-view.ts +1 -1
  219. package/src/console/remote-support/routes.ts +1 -1
  220. package/src/console/remote-support/service.ts +2 -2
  221. package/src/console/routing/model/contracts.ts +1 -1
  222. package/src/console/routing/pools/contracts.ts +2 -2
  223. package/src/console/routing/pools/import-parse.ts +3 -3
  224. package/src/console/routing/pools/relay-deploy.ts +3 -3
  225. package/src/console/settings/contracts.ts +29 -2
  226. package/src/console/settings/store.ts +20 -0
  227. package/src/console/share/share-router.ts +100 -32
  228. package/src/console/share/share-stats.ts +11 -13
  229. package/src/console/share/share-usage.ts +1 -1
  230. package/src/console/shared/errors.ts +2 -2
  231. package/src/console/shared/object.ts +18 -0
  232. package/src/console/shared/read-through-cache.ts +4 -4
  233. package/src/console/shared/transaction.ts +14 -0
  234. package/src/console/system-prompt/contracts.ts +89 -12
  235. package/src/console/system-prompt/routes.ts +98 -45
  236. package/src/console/updates/apply-update.ts +244 -0
  237. package/src/console/updates/routes.ts +252 -97
  238. package/src/main.ts +11 -5
  239. package/src/network/http2-bidi.ts +1 -1
  240. package/src/network/ip-utils.ts +298 -0
  241. package/src/network/outbound-fetch.ts +1 -1
  242. package/src/network/pool/agent.ts +7 -6
  243. package/src/network/pool/byte-accounting.ts +1 -1
  244. package/src/network/pool/resolver.ts +16 -12
  245. package/src/network/pool/selector.ts +150 -136
  246. package/src/network/pool-health-machine.ts +1 -1
  247. package/src/network/rate-limit-verdict.ts +1 -1
  248. package/src/network/retry.ts +1 -1
  249. package/src/network/ssrf.ts +20 -71
  250. package/src/observability/event-log.ts +9 -2
  251. package/src/observability/event-query.ts +61 -291
  252. package/src/observability/log-sanitize.ts +1 -1
  253. package/src/observability/metrics.ts +4 -4
  254. package/src/observability/payload-capture.ts +6 -5
  255. package/src/observability/payload-store.ts +1 -1
  256. package/src/observability/runtime-metrics.ts +1 -1
  257. package/src/observability/telemetry-buffer.ts +19 -3
  258. package/src/observability/telemetry-reconciliation.ts +3 -3
  259. package/src/observability/token-speed.ts +1 -1
  260. package/src/persistence/connection-url.ts +1 -1
  261. package/src/persistence/db-handle.ts +1 -1
  262. package/src/persistence/db-pg.ts +2 -2
  263. package/src/persistence/db-sqlite.ts +3 -3
  264. package/src/persistence/dialect.ts +5 -1
  265. package/src/persistence/readiness.ts +1 -1
  266. package/src/persistence/redis.ts +1 -1
  267. package/src/persistence/schema.ts +121 -40
  268. package/src/persistence/shared-cache.ts +2 -2
  269. package/src/persistence/sort-order.ts +1 -1
  270. package/src/persistence/stores/api-key-store.ts +29 -2
  271. package/src/persistence/stores/share-store.ts +20 -8
  272. package/src/persistence/system-prompt-store.ts +63 -18
  273. package/src/persistence/tenant-preferences.ts +1 -1
  274. package/src/persistence/ttl-cache.ts +3 -3
  275. package/src/persistence/usage-flush.ts +7 -2
  276. package/src/protocol/canonical-model.ts +1 -1
  277. package/src/protocol/codecs.ts +6 -10
  278. package/src/protocol/dsml.ts +8 -125
  279. package/src/protocol/image-clamp.ts +2 -2
  280. package/src/protocol/reasoning-text.ts +1 -1
  281. package/src/protocol/reasoning.ts +7 -52
  282. package/src/protocol/registry.ts +50 -15
  283. package/src/protocol/request/chat.ts +46 -19
  284. package/src/protocol/request/codex.ts +1 -1
  285. package/src/protocol/request/messages.ts +46 -14
  286. package/src/protocol/request/responses.ts +19 -10
  287. package/src/protocol/response/chat.ts +25 -10
  288. package/src/protocol/response/codex.ts +1 -1
  289. package/src/protocol/response/messages.ts +1 -1
  290. package/src/protocol/response/omp-tool-call-markup.ts +1 -1
  291. package/src/protocol/response/responses.ts +1 -1
  292. package/src/protocol/sse-decoder.ts +50 -1
  293. package/src/protocol/stop-reason.ts +2 -2
  294. package/src/protocol/think-split.ts +347 -0
  295. package/src/protocol/translation/attachment-integrity.ts +3 -3
  296. package/src/protocol/translation/cache-controls.ts +8 -20
  297. package/src/protocol/translation/capabilities.ts +6 -6
  298. package/src/protocol/translation/dialect/deepseek.ts +10 -0
  299. package/src/protocol/translation/dialect/inband.ts +39 -0
  300. package/src/protocol/translation/dialect/model-thinking.ts +89 -0
  301. package/src/protocol/translation/dialect/qwen.ts +114 -0
  302. package/src/protocol/translation/dialect/resolver.ts +83 -0
  303. package/src/protocol/translation/dialect/rules.ts +93 -0
  304. package/src/protocol/translation/dialect/thinking-ladder.ts +245 -0
  305. package/src/protocol/translation/dialect/types.ts +58 -0
  306. package/src/protocol/translation/quirks.ts +18 -4
  307. package/src/protocol/translation/thinking.ts +12 -252
  308. package/src/protocol/translation/tool-id.ts +3 -3
  309. package/src/protocol/translation/tool-repair.ts +1 -1
  310. package/src/protocol/upstream/post-json.ts +6 -1
  311. package/src/providers/adapters/compatible-adapter.ts +71 -4
  312. package/src/providers/authentication/jwt-validator.ts +0 -5
  313. package/src/providers/authentication/oauth-client.ts +254 -253
  314. package/src/providers/authentication/oauth-flow-store.ts +42 -7
  315. package/src/providers/bundled-discovery-providers.ts +6 -30
  316. package/src/providers/configured-openai-providers.ts +82 -97
  317. package/src/providers/default-registry.ts +35 -37
  318. package/src/providers/discovery/catalog/catalog-format.ts +13 -2
  319. package/src/providers/discovery/discovery-types.ts +0 -1
  320. package/src/providers/discovery/openai-model-discovery.ts +2 -2
  321. package/src/providers/discovery/probe-phases.ts +48 -13
  322. package/src/providers/discovery/probing-service.ts +6 -8
  323. package/src/providers/integrations/agentrouter/agentrouter.ts +12 -2
  324. package/src/providers/integrations/antigravity/antigravity-oauth.ts +0 -1
  325. package/src/providers/integrations/antigravity/antigravity-protocol.ts +88 -28
  326. package/src/providers/integrations/antigravity/antigravity-quota.ts +18 -6
  327. package/src/providers/integrations/antigravity/antigravity.ts +22 -13
  328. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +1 -1
  329. package/src/providers/integrations/buddy/buddy-chat-shared.ts +20 -4
  330. package/src/providers/integrations/buddy/buddy-cli-headers.ts +19 -52
  331. package/src/providers/integrations/buddy/buddy-identity.ts +86 -0
  332. package/src/providers/integrations/buddy/buddy-oauth-shared.ts +1 -1
  333. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +1 -2
  334. package/src/providers/integrations/buddy/codebuddy/provider.ts +4 -15
  335. package/src/providers/integrations/buddy/codebuddy/shared.ts +30 -9
  336. package/src/providers/integrations/buddy/workbuddy/provider.ts +5 -9
  337. package/src/providers/integrations/buddy/workbuddy/shared.ts +21 -3
  338. package/src/providers/integrations/claude/claude-cch.ts +148 -148
  339. package/src/providers/integrations/claude/fallback-credit.ts +3 -7
  340. package/src/providers/integrations/cline/cline.ts +1 -1
  341. package/src/providers/integrations/codex/codex-device-code.ts +101 -101
  342. package/src/providers/integrations/codex/codex-oauth.ts +0 -1
  343. package/src/providers/integrations/commandcode/commandcode.ts +75 -115
  344. package/src/providers/integrations/commandcode/quota.ts +131 -51
  345. package/src/providers/integrations/cursor/cursor-http2.ts +0 -3
  346. package/src/providers/integrations/cursor/cursor-quota.ts +2 -6
  347. package/src/providers/integrations/cursor/cursor-request.ts +0 -87
  348. package/src/providers/integrations/cursor/cursor-shared.ts +0 -4
  349. package/src/providers/integrations/cursor/cursor.ts +35 -3
  350. package/src/providers/integrations/cursor/protocol/framing.ts +0 -56
  351. package/src/providers/integrations/cursor/protocol/messages.ts +0 -96
  352. package/src/providers/integrations/factory/factory.ts +9 -9
  353. package/src/providers/integrations/github/github-discovery.ts +70 -12
  354. package/src/providers/integrations/github/github-oauth.ts +184 -14
  355. package/src/providers/integrations/github/github.ts +134 -26
  356. package/src/providers/integrations/grok/grok-oauth.ts +261 -261
  357. package/src/providers/integrations/grok/grok-quota-frame.ts +1 -1
  358. package/src/providers/integrations/grok/grok-quota.ts +16 -7
  359. package/src/providers/integrations/grok/grok-ratelimit-store.ts +0 -5
  360. package/src/providers/integrations/grok/grok-ratelimit.ts +3 -0
  361. package/src/providers/integrations/grok/grok.ts +86 -36
  362. package/src/providers/integrations/kilo/kilo-oauth.ts +1 -1
  363. package/src/providers/integrations/kiro/kiro-cache.ts +31 -0
  364. package/src/providers/integrations/kiro/kiro-catalog.ts +31 -5
  365. package/src/providers/integrations/kiro/kiro-discovery.ts +47 -43
  366. package/src/providers/integrations/kiro/kiro-machine-id.ts +6 -3
  367. package/src/providers/integrations/kiro/kiro-oauth.ts +523 -255
  368. package/src/providers/integrations/kiro/kiro-quota.ts +35 -47
  369. package/src/providers/integrations/kiro/kiro-request.ts +8 -11
  370. package/src/providers/integrations/kiro/kiro-stream.ts +13 -0
  371. package/src/providers/integrations/kiro/kiro.ts +80 -68
  372. package/src/providers/integrations/muse/muse-oauth.ts +0 -1
  373. package/src/providers/integrations/muse/muse-quota.ts +0 -4
  374. package/src/providers/integrations/opencode/fingerprint.ts +7 -5
  375. package/src/providers/integrations/opencode/provider.ts +17 -24
  376. package/src/providers/integrations/qoder/qoder-catalog.ts +94 -11
  377. package/src/providers/integrations/qoder/qoder-oauth.ts +165 -25
  378. package/src/providers/integrations/qoder/qoder-quota.ts +48 -6
  379. package/src/providers/integrations/qoder/qoder-realm.ts +10 -31
  380. package/src/providers/integrations/qoder/qoder-sse.ts +159 -0
  381. package/src/providers/integrations/qoder/qoder-umid.ts +21 -25
  382. package/src/providers/integrations/qoder/qoder-virtual.ts +12 -0
  383. package/src/providers/integrations/qoder/qoder.ts +422 -71
  384. package/src/providers/integrations/shared/connect.ts +0 -54
  385. package/src/providers/integrations/shared/hash.ts +15 -0
  386. package/src/providers/integrations/shared/session-cache.ts +136 -0
  387. package/src/providers/integrations/trae/trae-cache.ts +7 -87
  388. package/src/providers/integrations/trae/trae-catalog.ts +1 -6
  389. package/src/providers/integrations/trae/trae-protocol.ts +17 -8
  390. package/src/providers/integrations/trae/trae-realm.ts +0 -7
  391. package/src/providers/integrations/trae/trae.ts +51 -8
  392. package/src/providers/integrations/xai/xai-oauth.ts +5 -5
  393. package/src/providers/integrations/zcode-start-plan/gifts.ts +265 -0
  394. package/src/providers/integrations/zcode-start-plan/oauth.ts +154 -0
  395. package/src/providers/integrations/zcode-start-plan/provider.ts +131 -0
  396. package/src/providers/integrations/zcode-start-plan/quota.ts +152 -0
  397. package/src/providers/integrations/zcode-start-plan/zcode_prompt.json +10 -0
  398. package/src/providers/operations/account-error-classification.ts +198 -133
  399. package/src/providers/operations/account-health-service.ts +31 -39
  400. package/src/providers/operations/account-reset-service.ts +58 -1
  401. package/src/providers/operations/byok-wire-profile.ts +19 -15
  402. package/src/providers/operations/cli-platform.ts +31 -5
  403. package/src/providers/operations/client-version-resolver.ts +4 -6
  404. package/src/providers/operations/client-versions.ts +10 -7
  405. package/src/providers/operations/provider-catalog-seeder.ts +2 -0
  406. package/src/providers/operations/provider-catalog-service.ts +18 -2
  407. package/src/providers/operations/provider-credential-service.ts +19 -2
  408. package/src/providers/provider-contracts.ts +18 -7
  409. package/src/providers/provider-metadata.ts +17 -2
  410. package/src/providers/provider-model-definition.ts +22 -1
  411. package/src/providers/provider-probe.ts +4 -2
  412. package/src/providers/provider-usage.ts +50 -19
  413. package/src/providers/quota/cache.ts +29 -0
  414. package/src/providers/quota/quota-contracts.ts +4 -0
  415. package/src/providers/quota/refresh.ts +27 -4
  416. package/src/providers/quota/snapshot.ts +94 -0
  417. package/src/providers/search/search-provider.ts +1 -3
  418. package/src/providers/search/search-providers.ts +2 -1
  419. package/src/security/abuse.ts +4 -4
  420. package/src/security/admission/service.ts +13 -6
  421. package/src/security/admission/ttl.ts +61 -61
  422. package/src/security/api-key-auth.ts +17 -3
  423. package/src/security/client-router-fingerprint.ts +4 -4
  424. package/src/security/credential-codec.ts +3 -3
  425. package/src/security/csrf.ts +2 -2
  426. package/src/security/ip-boundary.ts +16 -189
  427. package/src/security/model-access-rule.ts +1 -1
  428. package/src/security/outbound-headers.ts +18 -0
  429. package/src/shared/invoke-guard.ts +47 -0
  430. package/src/transport/contracts.ts +13 -4
  431. package/src/transport/dispatch/attempt-finalize.ts +70 -22
  432. package/src/transport/dispatch/attempt-loop.ts +22 -0
  433. package/src/transport/dispatch/capture-policy.ts +34 -37
  434. package/src/transport/dispatch/empty-stream.ts +23 -20
  435. package/src/transport/dispatch/generation.ts +25 -45
  436. package/src/transport/dispatch/native-shared.ts +127 -0
  437. package/src/transport/dispatch/proxy-request.ts +3 -1
  438. package/src/transport/dispatch/responses-compact.ts +25 -46
  439. package/src/transport/dispatch/streaming/encoder.ts +18 -72
  440. package/src/transport/dispatch/streaming/finalize.ts +2 -2
  441. package/src/transport/dispatch/streaming/stream.ts +7 -3
  442. package/src/transport/dispatch/streaming/thinking-loop.ts +1 -1
  443. package/src/transport/dispatch/systemone.ts +20 -48
  444. package/src/transport/dispatch/token-count.ts +41 -38
  445. package/src/transport/dispatch/websearch.ts +19 -45
  446. package/src/transport/drain-endpoint.ts +1 -1
  447. package/src/transport/failure-policy.ts +21 -1
  448. package/src/transport/guards/auth-guard.ts +0 -6
  449. package/src/transport/guards/body-policy.ts +3 -3
  450. package/src/transport/guards/error-lifecycle.ts +4 -0
  451. package/src/transport/guards/headroom-guard.ts +1 -1
  452. package/src/transport/guards/pipeline.ts +1 -1
  453. package/src/transport/guards/request-context.ts +12 -17
  454. package/src/transport/request/preparer.ts +286 -7
  455. package/src/transport/request/rtk/compress-request.ts +3 -3
  456. package/src/transport/request/rtk/rtk-constants.ts +2 -2
  457. package/src/transport/request/rtk/rtk-filters.ts +8 -8
  458. package/src/transport/request/state.ts +7 -13
  459. package/src/transport/request/system-prompt-injection.ts +74 -11
  460. package/src/transport/request/tenant-preferences.ts +22 -0
  461. package/src/transport/routing/candidate-index.ts +2 -2
  462. package/src/transport/routing/round-robin.ts +1 -1
  463. package/src/transport/routing/route-catalog.ts +17 -18
  464. package/src/transport/routing/route-model.ts +126 -33
  465. package/src/transport/routing/router.ts +159 -37
  466. package/src/transport/shutdown-notice.ts +2 -2
  467. package/src/transport/surface/adapters.ts +3 -3
  468. package/src/transport/surface/chat/encode.ts +7 -7
  469. package/src/transport/surface/chat/parse.ts +7 -7
  470. package/src/transport/surface/completion.ts +2 -2
  471. package/src/transport/surface/content-parts.ts +1 -1
  472. package/src/transport/surface/dialects.ts +2 -2
  473. package/src/transport/surface/messages/parse.ts +3 -3
  474. package/src/transport/surface/reasoning-summary.ts +9 -14
  475. package/src/transport/surface/responses/adapter.ts +1 -1
  476. package/src/transport/surface/responses/errors.ts +0 -9
  477. package/src/transport/vision-handoff-cache.ts +58 -0
  478. package/src/transport/vision-handoff.ts +431 -0
  479. package/src/workers/account-identity-sweep.ts +2 -2
  480. package/src/workers/buddy-growth-tasks.ts +392 -346
  481. package/src/workers/daily-checkin.ts +102 -49
  482. package/src/workers/maintenance-lease.ts +2 -35
  483. package/src/workers/oauth-refresh-sweep.ts +8 -1
  484. package/src/workers/quota-refresh-sweep.ts +120 -82
  485. package/src/workers/scheduler.ts +67 -4
  486. package/src/workers/sweep-runner.ts +24 -5
  487. package/src/workers/zcode-gift-claim.ts +108 -0
  488. package/dist/dashboard/assets/AccountCooldown-Bjo92Qat.js +0 -1
  489. package/dist/dashboard/assets/AccountCooldown-Bjo92Qat.js.gz +0 -0
  490. package/dist/dashboard/assets/BannedPage-ChG5svFb.js +0 -1
  491. package/dist/dashboard/assets/BannedPage-ChG5svFb.js.gz +0 -0
  492. package/dist/dashboard/assets/CliToolDetailPage-BIcWQun7.js +0 -1
  493. package/dist/dashboard/assets/CliToolDetailPage-BIcWQun7.js.gz +0 -0
  494. package/dist/dashboard/assets/CliToolsPage-Bf2VelDe.js +0 -1
  495. package/dist/dashboard/assets/CliToolsPage-Bf2VelDe.js.gz +0 -0
  496. package/dist/dashboard/assets/CombosPage-DCgm5g6g.js +0 -1
  497. package/dist/dashboard/assets/CombosPage-DCgm5g6g.js.gz +0 -0
  498. package/dist/dashboard/assets/ConsoleLogPage-a8mD0QF1.js +0 -1
  499. package/dist/dashboard/assets/ConsoleLogPage-a8mD0QF1.js.gz +0 -0
  500. package/dist/dashboard/assets/CustomizationPage-ggeMSjte.js +0 -1
  501. package/dist/dashboard/assets/CustomizationPage-ggeMSjte.js.gz +0 -0
  502. package/dist/dashboard/assets/HealthEventsModal-DzSWzE_j.js +0 -1
  503. package/dist/dashboard/assets/HealthEventsModal-DzSWzE_j.js.gz +0 -0
  504. package/dist/dashboard/assets/LoginPage-U6AKaJcF.js +0 -1
  505. package/dist/dashboard/assets/LoginPage-U6AKaJcF.js.gz +0 -0
  506. package/dist/dashboard/assets/ModelPicker-BMS2t8BA.js.gz +0 -0
  507. package/dist/dashboard/assets/OverviewPage-D2D4IiHl.js +0 -1
  508. package/dist/dashboard/assets/OverviewPage-D2D4IiHl.js.gz +0 -0
  509. package/dist/dashboard/assets/ProviderDetailPage-CrNf7j_X.js +0 -5
  510. package/dist/dashboard/assets/ProviderDetailPage-CrNf7j_X.js.gz +0 -0
  511. package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js +0 -1
  512. package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js.gz +0 -0
  513. package/dist/dashboard/assets/ProvidersPage-BSDbquc1.js +0 -1
  514. package/dist/dashboard/assets/ProvidersPage-BSDbquc1.js.gz +0 -0
  515. package/dist/dashboard/assets/ProxyPage-lsrx7NYg.js.gz +0 -0
  516. package/dist/dashboard/assets/QuotaPage-L0skzML8.js +0 -1
  517. package/dist/dashboard/assets/QuotaPage-L0skzML8.js.gz +0 -0
  518. package/dist/dashboard/assets/SettingsPage-CmAbjRHW.js +0 -1
  519. package/dist/dashboard/assets/SettingsPage-CmAbjRHW.js.gz +0 -0
  520. package/dist/dashboard/assets/SetupPage-f0DE1h0-.js +0 -1
  521. package/dist/dashboard/assets/SetupPage-f0DE1h0-.js.gz +0 -0
  522. package/dist/dashboard/assets/SortableList-ClzpwyET.js +0 -1
  523. package/dist/dashboard/assets/SortableList-ClzpwyET.js.gz +0 -0
  524. package/dist/dashboard/assets/StudioPage-Cq5tW7hW.js.gz +0 -0
  525. package/dist/dashboard/assets/ToolIcon-K1ckcDKg.js +0 -1
  526. package/dist/dashboard/assets/ToolIcon-K1ckcDKg.js.gz +0 -0
  527. package/dist/dashboard/assets/UsagePage-Dfc9rWQn.js +0 -56
  528. package/dist/dashboard/assets/UsagePage-Dfc9rWQn.js.gz +0 -0
  529. package/dist/dashboard/assets/api-keys-C38iXoO4.js +0 -1
  530. package/dist/dashboard/assets/api-keys-C38iXoO4.js.gz +0 -0
  531. package/dist/dashboard/assets/app-BayTTy01.js +0 -9
  532. package/dist/dashboard/assets/app-BayTTy01.js.gz +0 -0
  533. package/dist/dashboard/assets/app-a159k0ub.css +0 -2
  534. package/dist/dashboard/assets/app-a159k0ub.css.gz +0 -0
  535. package/dist/dashboard/assets/assert-DbRA8EG2.js +0 -15
  536. package/dist/dashboard/assets/assert-DbRA8EG2.js.gz +0 -0
  537. package/dist/dashboard/assets/badge-D9CS6dZk.js.gz +0 -0
  538. package/dist/dashboard/assets/banner-DympBNq4.js.gz +0 -0
  539. package/dist/dashboard/assets/card-ibRWU_uT.js.gz +0 -0
  540. package/dist/dashboard/assets/cli-tools-63qBGaRw.js +0 -1
  541. package/dist/dashboard/assets/cli-tools-63qBGaRw.js.gz +0 -0
  542. package/dist/dashboard/assets/clipboard-button-DJm-9pz7.js.gz +0 -0
  543. package/dist/dashboard/assets/dialog-8fiE6tec.js +0 -1
  544. package/dist/dashboard/assets/dialog-8fiE6tec.js.gz +0 -0
  545. package/dist/dashboard/assets/drawer-CqGYeOrd.js.gz +0 -0
  546. package/dist/dashboard/assets/entry-BSAsYsmV.js +0 -1
  547. package/dist/dashboard/assets/entry-BSAsYsmV.js.gz +0 -0
  548. package/dist/dashboard/assets/entry-CiIQULZD.js +0 -4
  549. package/dist/dashboard/assets/entry-CiIQULZD.js.gz +0 -0
  550. package/dist/dashboard/assets/entry-Daz0lJ-W.js +0 -1
  551. package/dist/dashboard/assets/entry-Daz0lJ-W.js.gz +0 -0
  552. package/dist/dashboard/assets/formatters-D-MTJxMr.js.gz +0 -0
  553. package/dist/dashboard/assets/github-badge-CPK47xQx.js +0 -1
  554. package/dist/dashboard/assets/github-badge-CPK47xQx.js.gz +0 -0
  555. package/dist/dashboard/assets/index-DBNPOfhV.js.gz +0 -0
  556. package/dist/dashboard/assets/inline-BT0zntEr.js.gz +0 -0
  557. package/dist/dashboard/assets/input-Da2ZViWg.js.gz +0 -0
  558. package/dist/dashboard/assets/layout-BXNeLw6M.js.gz +0 -0
  559. package/dist/dashboard/assets/list-row-BycOrrIV.js.gz +0 -0
  560. package/dist/dashboard/assets/live-DCQNZpF7.js +0 -1
  561. package/dist/dashboard/assets/live-DCQNZpF7.js.gz +0 -0
  562. package/dist/dashboard/assets/mutation-Rr0TIvcj.js.gz +0 -0
  563. package/dist/dashboard/assets/network-9Yj2ZyLo.js +0 -1
  564. package/dist/dashboard/assets/network-9Yj2ZyLo.js.gz +0 -0
  565. package/dist/dashboard/assets/numbers-CEagj6ZZ.js +0 -2
  566. package/dist/dashboard/assets/numbers-CEagj6ZZ.js.gz +0 -0
  567. package/dist/dashboard/assets/providers/zai-coding-plan.svg +0 -6
  568. package/dist/dashboard/assets/providers/zai-coding-plan.svg.gz +0 -0
  569. package/dist/dashboard/assets/quota-Caw7wqci.js +0 -1
  570. package/dist/dashboard/assets/quota-Caw7wqci.js.gz +0 -0
  571. package/dist/dashboard/assets/routing-CO9zCUus.js +0 -1
  572. package/dist/dashboard/assets/routing-CO9zCUus.js.gz +0 -0
  573. package/dist/dashboard/assets/select-BIYIBkMU.js.gz +0 -0
  574. package/dist/dashboard/assets/settings-jS3-PNSA.js +0 -1
  575. package/dist/dashboard/assets/settings-jS3-PNSA.js.gz +0 -0
  576. package/dist/dashboard/assets/stack-DfgnFDWY.js.gz +0 -0
  577. package/dist/dashboard/assets/state-CkfdYWel.js.gz +0 -0
  578. package/dist/dashboard/assets/stream-retry-5QnVuSSo.js +0 -1
  579. package/dist/dashboard/assets/stream-retry-5QnVuSSo.js.gz +0 -0
  580. package/dist/dashboard/assets/switch-SYm237HG.js.gz +0 -0
  581. package/dist/dashboard/assets/useMutation-BQqCRNIi.js.gz +0 -0
  582. package/src/console/cli-tools/injectors/cline.ts +0 -110
  583. package/src/console/cli-tools/injectors/copilot.ts +0 -108
  584. package/src/console/cli-tools/injectors/cowork.ts +0 -64
  585. package/src/console/cli-tools/injectors/deepseek-tui.ts +0 -85
  586. package/src/console/cli-tools/injectors/droid.ts +0 -96
  587. package/src/console/cli-tools/injectors/grok-build.ts +0 -70
  588. package/src/console/cli-tools/injectors/hermes.ts +0 -101
  589. package/src/console/cli-tools/injectors/jcode.ts +0 -115
  590. package/src/console/cli-tools/injectors/openclaw.ts +0 -161
  591. package/src/network/response-headers.ts +0 -39
  592. package/src/protocol/response/dsml.ts +0 -38
  593. package/src/providers/integrations/xiaomi-mimo/think-stream.ts +0 -229
  594. /package/dist/dashboard/assets/brand/{og_bansos.webp → og_bansos_initial.webp} +0 -0
  595. /package/dist/dashboard/assets/brand/{og_image.webp → og_image_initial.webp} +0 -0
@@ -12,12 +12,12 @@
12
12
  import type { CanonicalEvent, SourceSurface, UsageRecord } from "../../../protocol/canonical-model";
13
13
  import { canonicalTerminal } from "../../../protocol/codecs";
14
14
  import { reasoningTextOf } from "../../../protocol/reasoning-text";
15
- import { MimoThinkSplitter } from "../../../providers/integrations/xiaomi-mimo/think-stream";
15
+ import { ThinkSplitter } from "../../../protocol/think-split";
16
16
  import { createDispatchStreamEncoder, type DispatchStreamEncoder } from "../stream-bridge";
17
17
  import { THINKING_LOOP_MARKER, ThinkingLoopDetector } from "./thinking-loop";
18
18
 
19
19
  /** The terminal event shape every stream settles (or fails to settle) on. */
20
- export type StreamTerminalEvent = Extract<CanonicalEvent, { type: "terminal" }>;
20
+ type StreamTerminalEvent = Extract<CanonicalEvent, { type: "terminal" }>;
21
21
 
22
22
  /**
23
23
  * SSE comment frame: valid framing on every streamed surface, ignored by
@@ -46,7 +46,7 @@ interface TruncatedEventTranscript {
46
46
  }
47
47
 
48
48
  /** Error-frame input in the shape the dispatch stream encoder accepts. */
49
- export type StreamErrorFrameInput = Parameters<DispatchStreamEncoder["encodeError"]>[0];
49
+ type StreamErrorFrameInput = Parameters<DispatchStreamEncoder["encodeError"]>[0];
50
50
 
51
51
  /**
52
52
  * Per-event state and framing for one stream. Client framing and protocol
@@ -93,10 +93,11 @@ export class StreamEventPipeline {
93
93
  */
94
94
  private readonly diagnosticsCaptureMode: "none" | "metadata" | "full";
95
95
  private readonly streamEncoder: DispatchStreamEncoder;
96
- // One splitter per stream: a thinking tag can straddle two provider chunks,
97
- // and the text/idle bookkeeping below must see the split result, not the raw
98
- // tagged chunk.
99
- private readonly thinkSplitter = new MimoThinkSplitter();
96
+ // One splitter per stream, kept purely as a feeder for the loop guard: it
97
+ // reports the content currently inside an open `<think>` span so a runaway
98
+ // chain-of-thought is caught even when the provider wrote it into the text
99
+ // stream. It never re-routes the emitted events — text is forwarded verbatim.
100
+ private readonly thinkSplitter = new ThinkSplitter();
100
101
  /**
101
102
  * Content-level loop guard. A model that degenerates into a repeated
102
103
  * chain-of-thought keeps sending bytes, so the stall timeout — which fires
@@ -259,26 +260,17 @@ export class StreamEventPipeline {
259
260
  // `loopDetected`, so this only matters for an event already in flight;
260
261
  // dropping it is the alternative to throwing mid-stream.
261
262
  if (this.loopDetector.detected) return false;
262
- // The one place every surface shares. A provider that writes its reasoning
263
- // into the answer as `<think>…</think>` had that text forwarded verbatim:
264
- // the chat wire happened to split it in its own decoder, so chat looked
265
- // right while messages and responses — and any other provider wire whose
266
- // text carries the tags — showed the raw reasoning as the answer.
263
+ // Text is forwarded verbatim: inline thinking tags are a per-provider
264
+ // decoder concern, not a universal one. Splitting them here for every
265
+ // surface misclassified literal `<think>` in an answer as reasoning — the
266
+ // answer vanished into the thinking pane and the poisoned history stopped
267
+ // follow-up turns. The loop guard still watches reasoning a provider puts
268
+ // inside an open `<think>` span, without reclassifying the answer.
267
269
  if (event.type === "content_delta" && event.content.kind === "text") {
270
+ const enqueued = this.enqueueOne(event, controller);
268
271
  const split = this.thinkSplitter.push(event.content.text);
269
- let enqueued = false;
270
- if (split.reasoning.length > 0) {
271
- const reasoningEvent: CanonicalEvent = {
272
- ...event,
273
- content: { kind: "reasoning", payload: null, summary: split.reasoning },
274
- };
275
- enqueued = this.enqueueOne(reasoningEvent, controller) || enqueued;
276
- if (this.guardReasoning(split.reasoning, controller)) return enqueued;
277
- }
278
- if (split.text.length > 0) {
279
- enqueued =
280
- this.enqueueOne({ ...event, content: { kind: "text", text: split.text } }, controller) ||
281
- enqueued;
272
+ if (split.pendingReasoning.length > 0) {
273
+ if (this.guardReasoning(split.pendingReasoning, controller)) return true;
282
274
  }
283
275
  return enqueued;
284
276
  }
@@ -290,31 +282,8 @@ export class StreamEventPipeline {
290
282
  this.guardReasoning(reasoningTextOf(event.content) ?? "", controller);
291
283
  return enqueued;
292
284
  }
293
- // Text held back for a tag that never completed must be released *before*
294
- // the terminal: a content event after it is rejected by the surface
295
- // encoders, and the tail of the answer would be lost instead.
296
285
  if (event.type === "terminal") {
297
- const rest = this.thinkSplitter.flush();
298
- if (rest.reasoning.length > 0) {
299
- this.enqueueOne(
300
- {
301
- type: "content_delta",
302
- sequence_number: event.sequence_number,
303
- content: { kind: "reasoning", payload: null, summary: rest.reasoning },
304
- },
305
- controller,
306
- );
307
- }
308
- if (rest.text.length > 0) {
309
- this.enqueueOne(
310
- {
311
- type: "content_delta",
312
- sequence_number: event.sequence_number,
313
- content: { kind: "text", text: rest.text },
314
- },
315
- controller,
316
- );
317
- }
286
+ return this.enqueueOne(event, controller);
318
287
  }
319
288
  return this.enqueueOne(event, controller);
320
289
  }
@@ -335,29 +304,6 @@ export class StreamEventPipeline {
335
304
  ): boolean {
336
305
  const reason = text.length > 0 ? this.loopDetector.push(text) : undefined;
337
306
  if (reason === undefined) return false;
338
- // Flush before the terminal: content after it is rejected by the surface
339
- // encoders, and the held-back text would be lost.
340
- const rest = this.thinkSplitter.flush();
341
- if (rest.reasoning.length > 0) {
342
- this.enqueueOne(
343
- {
344
- type: "content_delta",
345
- sequence_number: this.lastSequenceNumber + 1,
346
- content: { kind: "reasoning", payload: null, summary: rest.reasoning },
347
- },
348
- controller,
349
- );
350
- }
351
- if (rest.text.length > 0) {
352
- this.enqueueOne(
353
- {
354
- type: "content_delta",
355
- sequence_number: this.lastSequenceNumber + 1,
356
- content: { kind: "text", text: rest.text },
357
- },
358
- controller,
359
- );
360
- }
361
307
  this.enqueueOne(
362
308
  canonicalTerminal({
363
309
  sequenceNumber: this.lastSequenceNumber + 1,
@@ -39,7 +39,7 @@ import {
39
39
  import { KEEPALIVE_COMMENT_BYTES, type StreamEventPipeline } from "./encoder";
40
40
 
41
41
  /** Collaborators the stream tail reads from the dispatch handler. */
42
- export interface StreamFinalizeDeps {
42
+ interface StreamFinalizeDeps {
43
43
  readonly db: CartethyiaDatabase;
44
44
  readonly poolSelector?: NetworkPoolSelector | undefined;
45
45
  readonly telemetryBuffer?: TelemetryBatchBuffer | undefined;
@@ -56,7 +56,7 @@ export interface StreamCandidateIdentity {
56
56
  }
57
57
 
58
58
  /** What the stream tail needs from the attempt's input. */
59
- export interface StreamFinalizeInput {
59
+ interface StreamFinalizeInput {
60
60
  readonly state: ProxyRequestState;
61
61
  readonly deps: StreamFinalizeDeps;
62
62
  readonly prepared: PreparedProxyRequest;
@@ -64,7 +64,7 @@ import { StreamEventPipeline } from "./encoder";
64
64
  import { StreamFinalizer, type StreamCandidateIdentity } from "./finalize";
65
65
 
66
66
  /** Collaborators the streaming branch reads from the dispatch handler. */
67
- export interface StreamingDispatchDeps {
67
+ interface StreamingDispatchDeps {
68
68
  readonly db: CartethyiaDatabase;
69
69
  readonly networkBindingFactory?: ValidatedNetworkBindingFactory | undefined;
70
70
  readonly poolSelector?: NetworkPoolSelector | undefined;
@@ -73,7 +73,7 @@ export interface StreamingDispatchDeps {
73
73
  }
74
74
 
75
75
  /** Everything one streamed attempt needs, all resolved by its caller. */
76
- export interface StreamingDispatchInput {
76
+ interface StreamingDispatchInput {
77
77
  readonly state: ProxyRequestState;
78
78
  readonly deps: StreamingDispatchDeps;
79
79
  readonly prepared: PreparedProxyRequest;
@@ -228,7 +228,11 @@ export async function dispatchStreamingAttempt(input: StreamingDispatchInput): P
228
228
  if (!isWrappedInjectionProvider(candidate.provider_id)) return {};
229
229
  const text = resolveTenantSystemPrompt(
230
230
  await readTenantSystemPromptsForDispatch(deps.db, prepared.authorization.snapshot.tenant_id),
231
- { providerId: candidate.provider_id, modelId: candidate.model_id },
231
+ {
232
+ providerId: candidate.provider_id,
233
+ modelId: candidate.model_id,
234
+ requestedModel: prepared.plan?.requested_model ?? candidate.model_id,
235
+ },
232
236
  )?.text;
233
237
  return text === undefined ? {} : { tenantSystemPrompt: text };
234
238
  })()),
@@ -70,7 +70,7 @@ const SEGMENT_MIN_CLUSTER = 4;
70
70
  * would hide every later prose paragraph from the heuristics. */
71
71
  const CODE_LINE = /^(?:[ \t]*```.*|(?: {2,}|\t)(?![ \t]*(?:[-*+]|\d+[.)])[ \t]).*|.*[{}[\](;,>][ \t]*)$/gm;
72
72
 
73
- export interface ThinkingLoopConfig {
73
+ interface ThinkingLoopConfig {
74
74
  readonly enabled?: boolean;
75
75
  }
76
76
 
@@ -1,11 +1,15 @@
1
1
  import type { ProviderAdapter, ProviderDispatchTarget } from "../../providers/provider-contracts";
2
2
  import { GatewayError } from "../../protocol/gateway-error";
3
- import { resolveCredentialForAccount } from "../../providers/operations/provider-credential-service";
4
3
  import { metrics } from "../../observability/metrics";
5
- import { parseThinkingSuffix } from "../../protocol/translation/thinking";
6
- import { completeAttempt, estimatedUsage, nativeCompletionBase } from "./attempt-finalize";
7
- import { repriceUsage, usageFromProvider } from "../../providers/provider-usage";
4
+ import { estimatedUsage, nativeCompletionBase, completeAttempt } from "./attempt-finalize";
8
5
  import { runAttemptLoop } from "./attempt-loop";
6
+ import {
7
+ networkFetchContext,
8
+ requireRecordBody,
9
+ resolveAttemptCredential,
10
+ stripThinkingSuffix,
11
+ usageFromUpstreamClone,
12
+ } from "./native-shared";
9
13
 
10
14
  /**
11
15
  * The native System One route (`POST /v1/systemone`) and the dependency surface
@@ -20,7 +24,7 @@ import { runAttemptLoop } from "./attempt-loop";
20
24
  */
21
25
  import type { GatewayHandlerDeps } from "./handler-deps";
22
26
 
23
- export type SystemoneHandlerDeps = GatewayHandlerDeps;
27
+ type SystemoneHandlerDeps = GatewayHandlerDeps;
24
28
 
25
29
  /** Adapter that can serve a System One decision request. */
26
30
  type SystemoneAdapter = ProviderAdapter & {
@@ -31,10 +35,10 @@ export function createSystemoneHandler(deps: SystemoneHandlerDeps) {
31
35
  return async ({ request }: { request: Request }): Promise<Response> => {
32
36
  const state = deps.stateStore.require(request);
33
37
  const authorization = state.authorization;
34
- const body = state.ingressBody;
35
- if (!authorization || body === null || typeof body !== "object" || Array.isArray(body))
36
- throw new GatewayError("invalid_request", 400, "systemone request body must be a JSON object (request {requestId}). Check the shape of what you sent — then resend 🪞");
37
- const decision = body as Record<string, unknown>;
38
+ const decision = requireRecordBody(
39
+ state,
40
+ "systemone request body must be a JSON object (request {requestId}). Check the shape of what you sent — then resend 🪞",
41
+ );
38
42
  if (typeof decision.model !== "string" || decision.model.trim().length === 0)
39
43
  throw new GatewayError("invalid_request", 400, "model is required (request {requestId}). Check the shape of what you sent — then resend 🪞");
40
44
  // A thinking suffix (`model(high)`) is stripped for the same reason the
@@ -42,9 +46,7 @@ export function createSystemoneHandler(deps: SystemoneHandlerDeps) {
42
46
  // allowlist and the route plan, and `(high)` is not part of any registered
43
47
  // id. The level itself is dropped — a decision request carries no reasoning
44
48
  // effort to shape, and the body is passed upstream verbatim.
45
- const { model: bareModel } = parseThinkingSuffix(decision.model);
46
- const decisionBody: Record<string, unknown> =
47
- bareModel === decision.model ? decision : { ...decision, model: bareModel };
49
+ const { bareModel, body: decisionBody } = stripThinkingSuffix(decision.model, decision);
48
50
  // Trust boundary only: the decision model's own question shapes are
49
51
  // upstream's to validate. `state` and a `questions` object are what make a
50
52
  // System One request a System One request at all.
@@ -60,7 +62,7 @@ export function createSystemoneHandler(deps: SystemoneHandlerDeps) {
60
62
  const prepared = await deps.proxyPreparer.prepareNativeService({
61
63
  model: bareModel,
62
64
  serviceKind: "systemone",
63
- authorization,
65
+ authorization: authorization!,
64
66
  ...(state.abortController.signal ? { signal: state.abortController.signal } : {}),
65
67
  ...(state.clientUserAgent === undefined ? {} : { clientUserAgent: state.clientUserAgent }),
66
68
  });
@@ -85,19 +87,7 @@ export function createSystemoneHandler(deps: SystemoneHandlerDeps) {
85
87
  "no eligible System One route (request {requestId}). The bouncer stepped out — come back shortly 🚪",
86
88
  ),
87
89
  prepare: async (candidate) => {
88
- const credential = candidate.provider_account_id
89
- ? await resolveCredentialForAccount(
90
- deps.db,
91
- candidate.provider_id,
92
- candidate.provider_account_id,
93
- deps.oauthRefreshService && deps.resolveOAuthRefresher
94
- ? { refreshService: deps.oauthRefreshService, resolveRefresher: deps.resolveOAuthRefresher }
95
- : undefined,
96
- )
97
- : {
98
- provider_id: candidate.provider_id as ProviderAdapter["provider_id"],
99
- credential_kind: "none" as const,
100
- };
90
+ const credential = await resolveAttemptCredential(deps, candidate);
101
91
  const adapter = (await deps.resolveProviderAdapter?.(candidate.provider_id)) as SystemoneAdapter | undefined;
102
92
  if (!adapter || typeof adapter.systemone !== "function")
103
93
  throw new GatewayError(
@@ -131,14 +121,7 @@ export function createSystemoneHandler(deps: SystemoneHandlerDeps) {
131
121
  deadline: state.deadlineMs,
132
122
  abort_signal: state.abortController.signal,
133
123
  ...(candidate.user_agent === undefined ? {} : { user_agent: candidate.user_agent }),
134
- ...(deps.networkBindingFactory
135
- ? {
136
- outbound_fetch: deps.networkBindingFactory.fetch(
137
- leases.proxySlot?.poolId,
138
- authorization.tenantId,
139
- ),
140
- }
141
- : {}),
124
+ ...networkFetchContext(deps, leases.proxySlot?.poolId, authorization!.tenantId),
142
125
  });
143
126
  if (!response.ok) {
144
127
  // Surface the upstream decision error verbatim: it is the operator's
@@ -154,20 +137,9 @@ export function createSystemoneHandler(deps: SystemoneHandlerDeps) {
154
137
  // Prefer the usage the decision endpoint reported; fall back to the
155
138
  // fixed native estimate. `response.clone()` leaves the body readable
156
139
  // for the client.
157
- let reportedUsage: ReturnType<typeof usageFromProvider>;
158
- try {
159
- const parsed: unknown = await response.clone().json();
160
- reportedUsage = usageFromProvider(
161
- typeof parsed === "object" && parsed !== null
162
- ? (parsed as Record<string, unknown>)["usage"]
163
- : undefined,
164
- );
165
- } catch {
166
- reportedUsage = undefined;
167
- }
168
- const usage = repriceUsage(
169
- reportedUsage ??
170
- estimatedUsage(prepared.estimatedInputTokens, prepared.estimatedOutputTokens),
140
+ const usage = await usageFromUpstreamClone(
141
+ response,
142
+ estimatedUsage(prepared.estimatedInputTokens, prepared.estimatedOutputTokens),
171
143
  candidate.provider_id,
172
144
  candidate.model_id,
173
145
  );
@@ -36,31 +36,33 @@
36
36
  import type { ProviderAdapter, ProviderDispatchTarget } from "../../providers/provider-contracts";
37
37
  import { GatewayError } from "../../protocol/gateway-error";
38
38
  import { isRecord } from "../../protocol/codecs";
39
- import { resolveCredentialForAccount } from "../../providers/operations/provider-credential-service";
40
39
  import { metrics } from "../../observability/metrics";
40
+ import type { TelemetryBatchBuffer } from "../../observability/telemetry-buffer";
41
41
  import type { ProxyRequestState } from "../request/state";
42
42
  import type { PreparedNativeRequest } from "../request/preparer";
43
43
  import { completeAttempt, estimatedUsage, nativeCompletionBase } from "./attempt-finalize";
44
+ import { finalizeRequestTelemetry } from "../guards/error-lifecycle";
44
45
  import { runAttemptLoop } from "./attempt-loop";
46
+ import { hasScope, networkFetchContext, requireRecordBody, resolveAttemptCredential } from "./native-shared";
45
47
 
46
48
  import type { GatewayHandlerDeps } from "./handler-deps";
47
49
 
48
- export type TokenCountHandlerDeps = GatewayHandlerDeps;
50
+ type TokenCountHandlerDeps = GatewayHandlerDeps;
49
51
 
50
52
  /** Builds the `POST /v1/messages/count_tokens` handler. */
51
53
  export function createTokenCountHandler(deps: TokenCountHandlerDeps) {
52
54
  return async ({ request }: { request: Request }): Promise<Response> => {
53
55
  const state = deps.stateStore.require(request);
54
56
  const authorization = state.authorization;
55
- const body = state.ingressBody;
56
- if (!authorization || body === null || typeof body !== "object" || Array.isArray(body)) {
57
- throw new GatewayError(
58
- "invalid_request",
59
- 400,
60
- "The count_tokens body must be a JSON object (request {requestId}), the same shape POST /v1/messages accepts. Check the shape of what you sent — then resend 🪞",
61
- );
57
+ const countBody = requireRecordBody(
58
+ state,
59
+ "The count_tokens body must be a JSON object (request {requestId}), the same shape POST /v1/messages accepts. Check the shape of what you sent — then resend 🪞",
60
+ );
61
+ // `requireRecordBody` already refused a missing authorization; this guard
62
+ // exists only to narrow the type for the code below.
63
+ if (!authorization) {
64
+ throw new GatewayError("invalid_request", 400, "authentication required (request {requestId})");
62
65
  }
63
- const countBody = body as Record<string, unknown>;
64
66
 
65
67
  // `model` names the route and `messages` is what gets counted; both are the
66
68
  // upstream's own required fields, so the gateway does not restate its own
@@ -71,7 +73,7 @@ export function createTokenCountHandler(deps: TokenCountHandlerDeps) {
71
73
  if (!Array.isArray(countBody.messages)) {
72
74
  throw new GatewayError("invalid_request", 400, "messages must be an array (request {requestId}) — there is nothing to count without it. Check the shape of what you sent — then resend 🪞");
73
75
  }
74
- if (!authorization.snapshot.scopes?.includes("routing:invoke" as never)) {
76
+ if (!hasScope(authorization?.snapshot.scopes, "routing:invoke")) {
75
77
  throw new GatewayError(
76
78
  "policy_rejected",
77
79
  403,
@@ -93,7 +95,7 @@ export function createTokenCountHandler(deps: TokenCountHandlerDeps) {
93
95
  // real 429/503 — a fabricated count would tell the client its context
94
96
  // fits when nothing is there to serve the turn that follows.
95
97
  if (!(error instanceof GatewayError) || error.code !== "capability_unsupported") throw error;
96
- return estimatedCountResponse(state, countBody);
98
+ return estimatedCountResponse(state, countBody, deps.telemetryBuffer, deps.db);
97
99
  }
98
100
 
99
101
  return runAttemptLoop<Response, ProviderAdapter>({
@@ -116,19 +118,7 @@ export function createTokenCountHandler(deps: TokenCountHandlerDeps) {
116
118
  "no eligible route can count tokens for this model (request {requestId}). The bouncer stepped out — come back shortly 🚪",
117
119
  ),
118
120
  prepare: async (candidate) => {
119
- const credential = candidate.provider_account_id
120
- ? await resolveCredentialForAccount(
121
- deps.db,
122
- candidate.provider_id,
123
- candidate.provider_account_id,
124
- deps.oauthRefreshService && deps.resolveOAuthRefresher
125
- ? { refreshService: deps.oauthRefreshService, resolveRefresher: deps.resolveOAuthRefresher }
126
- : undefined,
127
- )
128
- : {
129
- provider_id: candidate.provider_id as ProviderAdapter["provider_id"],
130
- credential_kind: "none" as const,
131
- };
121
+ const credential = await resolveAttemptCredential(deps, candidate);
132
122
  const adapter = await deps.resolveProviderAdapter?.(candidate.provider_id);
133
123
  if (!adapter) {
134
124
  throw new GatewayError("capability_unsupported", 400, "token counting is unavailable for this provider (request {requestId}). Know your tools — this one just isn't in the box 🧰");
@@ -167,14 +157,7 @@ export function createTokenCountHandler(deps: TokenCountHandlerDeps) {
167
157
  deadline: state.deadlineMs,
168
158
  abort_signal: state.abortController.signal,
169
159
  ...(candidate.user_agent === undefined ? {} : { user_agent: candidate.user_agent }),
170
- ...(deps.networkBindingFactory
171
- ? {
172
- outbound_fetch: deps.networkBindingFactory.fetch(
173
- leases.proxySlot?.poolId,
174
- authorization.tenantId,
175
- ),
176
- }
177
- : {}),
160
+ ...networkFetchContext(deps, leases.proxySlot?.poolId, authorization.tenantId),
178
161
  });
179
162
 
180
163
  // A count is free and generates nothing, so nothing is reserved or
@@ -221,20 +204,40 @@ export function createTokenCountHandler(deps: TokenCountHandlerDeps) {
221
204
  *
222
205
  * No attempt is finalized: nothing was leased, nothing was sent upstream, and
223
206
  * no provider produced this number, so recording it against an account would
224
- * invent usage the account never served. The request still gets an outcome —
225
- * without one the `afterResponse` hook finalizes it as a failure, and a count
226
- * that answered 200 would show up in Console Log as an error.
207
+ * invent usage the account never served. The request is still finalized through
208
+ * `finalizeRequestTelemetry` (the early-rejection pattern) — without it the
209
+ * latency histogram, the `request_complete` console event, and the durable
210
+ * telemetry row all miss this request — and `state.completed` prevents the
211
+ * `afterResponse` fallback from finalizing it a second time.
227
212
  */
228
213
  function estimatedCountResponse(
229
214
  state: ProxyRequestState,
230
215
  countBody: Record<string, unknown>,
216
+ telemetryBuffer: TelemetryBatchBuffer | undefined,
217
+ db: GatewayHandlerDeps["db"],
231
218
  ): Response {
232
219
  const inputTokens = estimateMessagesInputTokens(countBody);
233
220
  const payload = JSON.stringify({ input_tokens: inputTokens });
234
- state.outcome = { status: "completed", httpStatus: 200 };
221
+ // The request still gets an outcome — without one the `afterResponse` hook
222
+ // would finalize it as a failure, and a count that answered 200 would show up
223
+ // in Console Log as an error. `usage` is a zero-priced estimate: nothing was
224
+ // leased and no provider produced this number, so recording a cost against an
225
+ // account would invent usage the account never served (`estimated_cost: null`).
226
+ state.outcome = {
227
+ status: "completed",
228
+ httpStatus: 200,
229
+ usage: estimatedUsage(inputTokens, 0),
230
+ };
235
231
  state.completed = true;
236
232
  state.clientResponseText = payload;
237
233
  metrics.proxy_requests_total.inc(1, { status: "completed" });
234
+ // Finalize telemetry inline (the early-rejection pattern) instead of routing
235
+ // through `completeAttempt`: there was no lease and no provider — but the
236
+ // latency histogram, the `request_complete` console event, and the durable
237
+ // row must still happen, exactly once. `completed = true` also disables the
238
+ // `afterResponse` fallback, which is what used to make this request vanish
239
+ // from telemetry entirely.
240
+ if (telemetryBuffer) finalizeRequestTelemetry(state, telemetryBuffer, db);
238
241
  return new Response(payload, {
239
242
  headers: {
240
243
  "content-type": "application/json",
@@ -255,7 +258,7 @@ function estimatedCountResponse(
255
258
  * `/v1/messages?beta=true/count_tokens` — the counting segment lands inside the
256
259
  * query, so the upstream sees the chat path with junk after it and never counts.
257
260
  */
258
- export function joinCountTokensPath(endpoint: string): string {
261
+ function joinCountTokensPath(endpoint: string): string {
259
262
  const queryAt = endpoint.indexOf("?");
260
263
  const path = queryAt === -1 ? endpoint : endpoint.slice(0, queryAt);
261
264
  const query = queryAt === -1 ? "" : endpoint.slice(queryAt);
@@ -16,51 +16,44 @@
16
16
  */
17
17
  import type { ProviderAdapter, ProviderDispatchTarget } from "../../providers/provider-contracts";
18
18
  import { GatewayError } from "../../protocol/gateway-error";
19
- import { resolveCredentialForAccount } from "../../providers/operations/provider-credential-service";
20
19
  import { metrics } from "../../observability/metrics";
21
- import { parseThinkingSuffix } from "../../protocol/translation/thinking";
22
20
  import { completeAttempt, estimatedUsage, nativeCompletionBase } from "./attempt-finalize";
23
21
  import { runAttemptLoop } from "./attempt-loop";
22
+ import {
23
+ hasScope,
24
+ jsonResponse,
25
+ networkFetchContext,
26
+ requireRecordBody,
27
+ resolveAttemptCredential,
28
+ stripThinkingSuffix,
29
+ } from "./native-shared";
24
30
 
25
31
  import type { GatewayHandlerDeps } from "./handler-deps";
26
32
 
27
- export type WebsearchHandlerDeps = GatewayHandlerDeps;
28
-
29
-
33
+ type WebsearchHandlerDeps = GatewayHandlerDeps;
30
34
 
31
35
  /** Adapter that can serve a web-search request. */
32
36
  type WebsearchAdapter = ProviderAdapter & {
33
37
  websearch: NonNullable<ProviderAdapter["websearch"]>;
34
38
  };
35
39
 
36
- function jsonResponse(payload: unknown, status: number, requestId: string): Response {
37
- return new Response(JSON.stringify(payload), {
38
- status,
39
- headers: {
40
- "content-type": "application/json",
41
- "cache-control": "no-store",
42
- "x-request-id": requestId,
43
- },
44
- });
45
- }
46
-
47
40
  export function createWebsearchHandler(deps: WebsearchHandlerDeps) {
48
41
  return async ({ request }: { request: Request }): Promise<Response> => {
49
42
  const state = deps.stateStore.require(request);
50
43
  const authorization = state.authorization;
51
- const body = state.ingressBody;
52
- if (!authorization || body === null || typeof body !== "object" || Array.isArray(body))
53
- throw new GatewayError("invalid_request", 400, "The search body must be a JSON object (request {requestId}). Send one with at least a model and a query. Check the shape of what you sent — then resend 🪞");
54
- const search = body as Record<string, unknown>;
44
+ const search = requireRecordBody(state, "The search body must be a JSON object (request {requestId}). Send one with at least a model and a query. Check the shape of what you sent — then resend 🪞");
45
+ // `requireRecordBody` already refused a missing authorization; this guard
46
+ // exists only to narrow the type for the code below.
47
+ if (!authorization) {
48
+ throw new GatewayError("invalid_request", 400, "authentication required (request {requestId})");
49
+ }
55
50
  if (typeof search.model !== "string" || search.model.trim().length === 0)
56
51
  throw new GatewayError("invalid_request", 400, "A model is required (request {requestId}): it names the search provider to use. Check the shape of what you sent — then resend 🪞");
57
52
  // The search "model" names the provider (or an alias/combo of one). A
58
53
  // thinking suffix is stripped for the same reason the canonical preparer
59
54
  // strips it: it is not part of any registered id.
60
- const { model: bareModel } = parseThinkingSuffix(search.model);
61
- const searchBody: Record<string, unknown> =
62
- bareModel === search.model ? search : { ...search, model: bareModel };
63
- if (!authorization.snapshot.scopes?.includes("search:invoke" as never)) {
55
+ const { bareModel, body: searchBody } = stripThinkingSuffix(search.model, search);
56
+ if (!hasScope(authorization?.snapshot.scopes, "search:invoke")) {
64
57
  throw new GatewayError("policy_rejected", 403, "search not allowed for this key (request {requestId}) — enable search:invoke on the API key. Not allowed by policy — no hard feelings 🫂");
65
58
  }
66
59
  // Trust boundary only: the query must be a non-empty string. The adapter
@@ -91,19 +84,7 @@ export function createWebsearchHandler(deps: WebsearchHandlerDeps) {
91
84
  strictPoolSelection: false,
92
85
  exhaustedError: new GatewayError("route_unavailable", 503, "no eligible search route (request {requestId}). The bouncer stepped out — come back shortly 🚪"),
93
86
  prepare: async (candidate) => {
94
- const credential = candidate.provider_account_id
95
- ? await resolveCredentialForAccount(
96
- deps.db,
97
- candidate.provider_id,
98
- candidate.provider_account_id,
99
- deps.oauthRefreshService && deps.resolveOAuthRefresher
100
- ? { refreshService: deps.oauthRefreshService, resolveRefresher: deps.resolveOAuthRefresher }
101
- : undefined,
102
- )
103
- : {
104
- provider_id: candidate.provider_id as ProviderAdapter["provider_id"],
105
- credential_kind: "none" as const,
106
- };
87
+ const credential = await resolveAttemptCredential(deps, candidate);
107
88
  const adapter = (await deps.resolveProviderAdapter?.(candidate.provider_id)) as WebsearchAdapter | undefined;
108
89
  if (!adapter || typeof adapter.websearch !== "function")
109
90
  throw new GatewayError(
@@ -132,14 +113,7 @@ export function createWebsearchHandler(deps: WebsearchHandlerDeps) {
132
113
  deadline: state.deadlineMs,
133
114
  abort_signal: state.abortController.signal,
134
115
  ...(candidate.user_agent === undefined ? {} : { user_agent: candidate.user_agent }),
135
- ...(deps.networkBindingFactory
136
- ? {
137
- outbound_fetch: deps.networkBindingFactory.fetch(
138
- leases.proxySlot?.poolId,
139
- authorization.tenantId,
140
- ),
141
- }
142
- : {}),
116
+ ...networkFetchContext(deps, leases.proxySlot?.poolId, authorization.tenantId),
143
117
  });
144
118
  const payload = {
145
119
  provider: candidate.provider_id,
@@ -20,7 +20,7 @@
20
20
  import { timingSafeEqual } from "node:crypto";
21
21
  import { GatewayError } from "../protocol/gateway-error";
22
22
 
23
- export interface DrainEndpointDeps {
23
+ interface DrainEndpointDeps {
24
24
  /** Shared secret; the route is registered only when this is set. */
25
25
  readonly token: string;
26
26
  /** Begins the graceful drain (the process's own shutdown path). */
@@ -656,6 +656,12 @@ const QUOTA_ERROR_IDENTIFIERS: ReadonlySet<string> = new Set([
656
656
  // it is a spend/limit verdict, never an auth one, so it must classify as
657
657
  // quota (a cooldown) instead of `auth_invalidated`.
658
658
  "110",
659
+ // Qoder also reports quota exhaustion as `{"code":116,"error":"quota
660
+ // exceeded"}` (seen on the IDE model-server surface with a 499/stream
661
+ // close). The 499 special-case below already treats qoder 499 as quota;
662
+ // naming the code here makes any status that carries it classify as quota
663
+ // instead of `invalid_request`/`authentication_failed`.
664
+ "116",
659
665
  // InferHub publishes its own vocabulary in `error.code`. The two 403s below
660
666
  // are deliberate absences: `account_blocked` means the account was blocked
661
667
  // (an auth/entitlement verdict, not a spend limit) and
@@ -914,7 +920,21 @@ export async function mapUpstreamHttpError(response: Response, providerId: strin
914
920
  ? { code: "context_length_exceeded" as const, status: 413, origin: "upstream" as const }
915
921
  : classifyUpstreamError(response.status, identifier);
916
922
  const extracted = extractUpstreamMessage(parsedBody ?? text);
917
- const message = (extracted.length > 0 ? extracted : text.trim() || `Provider returned HTTP ${response.status}.`).slice(0, MAX_UPSTREAM_ERROR_BYTES).replace(/[\r\n]+/g, " ");
923
+ let message =
924
+ extracted.length > 0
925
+ ? extracted
926
+ : text.trim() || `Provider returned HTTP ${response.status}.`;
927
+ // A bare "Provider returned HTTP <5xx>" tells the operator nothing actionable;
928
+ // platform-unavailable fallbacks get the same human sign-off every other
929
+ // gateway error carries. The raw upstream body stays in `details.raw` for
930
+ // diagnostics either way.
931
+ if (
932
+ classification.code === "platform_unavailable" &&
933
+ message === `Provider returned HTTP ${response.status}.`
934
+ ) {
935
+ message = `The provider's service is temporarily unavailable (HTTP ${response.status}) (request {requestId}). Your request did not go through — wait a moment and try again. Hold tight ☁️`;
936
+ }
937
+ message = message.slice(0, MAX_UPSTREAM_ERROR_BYTES).replace(/[\r\n]+/g, " ");
918
938
  const headerWaitMs = parseUpstreamBackoff(response.headers);
919
939
  // A queue carries its own interval, which is the one the caller must honour:
920
940
  // it is what the upstream promised, while `Retry-After` may be absent or may
@@ -20,12 +20,6 @@ import { createAccessDecision } from "../../security/access-control";
20
20
  export function createApiKeyAuthenticationMiddleware(deps: {
21
21
  readonly db: CartethyiaDatabase;
22
22
  readonly stateStore: ProxyRequestStateStore;
23
- /**
24
- * Model-abuse ban gate. A banned IP or key is refused here, before the
25
- * canonical parse and route preparation, and before `state.authorization` is
26
- * assigned — so a banned caller's attempts produce no telemetry row and no
27
- * console error, which is exactly the noise this layer exists to stop.
28
- */
29
23
  /**
30
24
  * Tenant invoke kill switch. Checked after the key resolves, because the
31
25
  * tenant is a property of the key: the same gateway serves several tenants