ompchamber 2.0.2 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (329) hide show
  1. package/README.md +7 -6
  2. package/bunfig.toml +11 -0
  3. package/dist/client/index.html +16 -5
  4. package/dist/client/index.js +1393 -0
  5. package/dist/client/static/js/index-prbmx3yj.js +5123 -0
  6. package/dist/client/static/js/index-ranztes9.css +1 -0
  7. package/index.html +54 -0
  8. package/package.json +9 -10
  9. package/public/apple-touch-icon.png +0 -0
  10. package/public/icon-192x192.png +0 -0
  11. package/public/icon.svg +60 -0
  12. package/public/manifest.webmanifest +38 -0
  13. package/src/cli/lib/commands/serve.js +9 -5
  14. package/src/cli/lib/runtime.js +18 -7
  15. package/src/client/components/common/provider-icon/glyphs.ts +118 -0
  16. package/src/client/components/common/provider-icon/index.tsx +85 -0
  17. package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +4 -0
  18. package/src/client/components/layout/desktop-layout/index.tsx +2 -0
  19. package/src/client/components/mobile/LayoutWrapper.tsx +16 -0
  20. package/src/client/components/mobile/SessionSidebar.tsx +10 -0
  21. package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +0 -1
  22. package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +16 -1
  23. package/src/client/components/mobile/mobile-session-sidebar/List.tsx +3 -0
  24. package/src/client/components/settings/categories/ProviderSettings.tsx +45 -4
  25. package/src/client/components/settings/categories/UsageSettings.tsx +22 -7
  26. package/src/client/components/settings/categories/provider-settings/AddModelModal.tsx +322 -0
  27. package/src/client/components/settings/categories/provider-settings/AddProviderModal.tsx +152 -56
  28. package/src/client/components/settings/categories/provider-settings/AuthSection.tsx +52 -1
  29. package/src/client/components/settings/categories/provider-settings/DeleteProviderModal.tsx +133 -0
  30. package/src/client/components/settings/categories/provider-settings/DialectFields.tsx +136 -0
  31. package/src/client/components/settings/categories/provider-settings/EffortLadder.tsx +89 -0
  32. package/src/client/components/settings/categories/provider-settings/Header.tsx +2 -2
  33. package/src/client/components/settings/categories/provider-settings/LoginModal.tsx +2 -2
  34. package/src/client/components/settings/categories/provider-settings/ModelsList.tsx +22 -2
  35. package/src/client/components/settings/categories/provider-settings/PresetPicker.tsx +82 -0
  36. package/src/client/components/settings/categories/provider-settings/PriceFields.tsx +107 -0
  37. package/src/client/components/settings/categories/provider-settings/ReconnectModal.tsx +43 -22
  38. package/src/client/components/settings/categories/provider-settings/SidebarList.tsx +2 -2
  39. package/src/client/components/settings/categories/usage-settings/LimitsSection.tsx +115 -0
  40. package/src/client/components/settings/categories/usage-settings/ProviderCard.tsx +19 -26
  41. package/src/client/components/settings/categories/usage-settings/ProviderDetail.tsx +29 -22
  42. package/src/client/components/settings/categories/usage-settings/SidebarList.tsx +60 -48
  43. package/src/client/components/settings/categories/usage-settings/format.ts +36 -0
  44. package/src/client/components/settings/categories/usage-settings/providers.ts +52 -28
  45. package/src/client/components/workspace/chat-timeline/AiMessageFooter.tsx +1 -1
  46. package/src/client/components/workspace/chat-timeline/GeneratingIndicator.tsx +1 -1
  47. package/src/client/components/workspace/diff-panel/Toolbar.tsx +71 -38
  48. package/src/client/components/workspace/diff-panel/index.tsx +139 -53
  49. package/src/client/components/workspace/editor/Tabs.tsx +2 -2
  50. package/src/client/components/workspace/editor/Toolbar.tsx +111 -26
  51. package/src/client/components/workspace/editor/index.tsx +27 -9
  52. package/src/client/components/workspace/model-dropdown/Item.tsx +13 -23
  53. package/src/client/components/workspace/model-dropdown/Panel.tsx +1 -1
  54. package/src/client/components/workspace/model-dropdown/Section.tsx +13 -4
  55. package/src/client/components/workspace/model-dropdown/index.tsx +7 -3
  56. package/src/client/components/workspace/usage-panel/ProviderSelect.tsx +10 -5
  57. package/src/client/components/workspace/usage-panel/index.tsx +36 -18
  58. package/src/client/data/samples/dialogue-session.ts +1 -1
  59. package/src/client/data/samples/sample-tools/other.ts +1 -1
  60. package/src/client/data/settings/provider.ts +141 -9
  61. package/src/client/hooks/editor/use-file-editor.ts +24 -27
  62. package/src/client/hooks/models/use-provider-names.ts +1 -1
  63. package/src/client/hooks/settings/providers/actions.ts +337 -0
  64. package/src/client/hooks/settings/providers/api.ts +69 -0
  65. package/src/client/hooks/settings/providers/index.ts +138 -0
  66. package/src/client/hooks/settings/providers/messages.ts +77 -0
  67. package/src/client/hooks/settings/useUsageReport.ts +8 -3
  68. package/src/client/hooks/ui/theme.ts +3 -3
  69. package/src/client/hooks/workspace/file-tabs.ts +62 -11
  70. package/src/client/stubs/elkjs.ts +6 -1
  71. package/src/server/index.ts +20 -2
  72. package/src/server/lib/assets/font-css.server.ts +86 -0
  73. package/src/server/lib/assets/fonts.server.ts +117 -0
  74. package/src/server/lib/bundler/css.ts +116 -0
  75. package/src/server/lib/lifecycle/shell-route.ts +17 -0
  76. package/src/server/lib/models/provider-listing.server.ts +257 -0
  77. package/src/server/lib/models/provider-registry.server.test.ts +122 -0
  78. package/src/server/lib/models/provider-registry.server.ts +76 -25
  79. package/src/server/lib/omp/config/models-config.ts +11 -0
  80. package/src/server/lib/omp/config/models-validation.test.ts +137 -0
  81. package/src/server/lib/omp/config/models-validation.ts +111 -2
  82. package/src/server/lib/omp/config/provider-seeds.test.ts +167 -0
  83. package/src/server/lib/omp/config/provider-seeds.ts +176 -0
  84. package/src/server/lib/omp/config/providers.ts +94 -100
  85. package/src/server/lib/omp/core/auth-credentials.ts +45 -0
  86. package/src/server/lib/omp/session/{usage.ts → usage/aggregate.ts} +1 -118
  87. package/src/server/lib/omp/session/usage/shaping.ts +130 -0
  88. package/src/server/lib/usage/deepseek.ts +4 -5
  89. package/src/server/lib/usage/kenari.ts +7 -6
  90. package/src/server/lib/usage/omp-usage.server.test.ts +126 -0
  91. package/src/server/lib/usage/omp-usage.server.ts +194 -0
  92. package/src/server/lib/usage/provider-key.ts +81 -3
  93. package/src/server/lib/usage/providers.server.test.ts +47 -0
  94. package/src/server/lib/usage/providers.server.ts +208 -0
  95. package/src/server/lib/usage/vendor-balance.server.test.ts +104 -0
  96. package/src/server/lib/usage/vendor-balance.server.ts +284 -0
  97. package/src/server/plugins/shell.server.ts +56 -0
  98. package/src/server/plugins/ssr.ts +24 -36
  99. package/src/server/plugins/static.test.ts +26 -29
  100. package/src/server/plugins/static.ts +40 -19
  101. package/src/server/routes/settings/index.ts +2 -0
  102. package/src/server/routes/settings/provider-model.ts +107 -0
  103. package/src/server/routes/settings/provider-models.ts +65 -202
  104. package/src/server/routes/settings/providers.ts +22 -7
  105. package/src/server/routes/settings/usage.ts +125 -45
  106. package/src/server/routes/telemetry/tokens.ts +2 -1
  107. package/src/shared/lib/fs/diff-parser.test.ts +116 -0
  108. package/src/shared/lib/fs/diff-parser.ts +86 -33
  109. package/src/shared/lib/models/{provider-connection.ts → provider/connection.ts} +37 -0
  110. package/src/shared/lib/models/provider/dialect.ts +179 -0
  111. package/src/shared/lib/models/provider/glyph.test.ts +116 -0
  112. package/src/shared/lib/models/provider/glyph.ts +200 -0
  113. package/src/shared/lib/models/{provider-models.ts → provider/models.ts} +91 -6
  114. package/src/shared/lib/models/provider/presets.test.ts +172 -0
  115. package/src/shared/lib/models/provider/presets.ts +83 -0
  116. package/src/shared/lib/workspace/file-tab-id.ts +29 -0
  117. package/src/shared/types/settings/provider.ts +81 -0
  118. package/src/shared/types/settings/usage.ts +88 -14
  119. package/tsconfig.json +3 -1
  120. package/dist/client/static/css/index.5f630421b7.css +0 -2
  121. package/dist/client/static/font/KaTeX_AMS-Regular.a79f1c3119.woff2 +0 -0
  122. package/dist/client/static/font/KaTeX_Caligraphic-Bold.ec17d13264.woff2 +0 -0
  123. package/dist/client/static/font/KaTeX_Caligraphic-Regular.55fac25845.woff2 +0 -0
  124. package/dist/client/static/font/KaTeX_Fraktur-Bold.d42a5579b0.woff2 +0 -0
  125. package/dist/client/static/font/KaTeX_Fraktur-Regular.d3c882a649.woff2 +0 -0
  126. package/dist/client/static/font/KaTeX_Main-Bold.c3fb5ac22f.woff2 +0 -0
  127. package/dist/client/static/font/KaTeX_Main-BoldItalic.6f2bb1dff2.woff2 +0 -0
  128. package/dist/client/static/font/KaTeX_Main-Italic.8916142bec.woff2 +0 -0
  129. package/dist/client/static/font/KaTeX_Main-Regular.0462f03bdf.woff2 +0 -0
  130. package/dist/client/static/font/KaTeX_Math-BoldItalic.572d331f69.woff2 +0 -0
  131. package/dist/client/static/font/KaTeX_Math-Italic.f28c23acad.woff2 +0 -0
  132. package/dist/client/static/font/KaTeX_SansSerif-Bold.8c5b5494b6.woff2 +0 -0
  133. package/dist/client/static/font/KaTeX_SansSerif-Italic.3b1e59b3ba.woff2 +0 -0
  134. package/dist/client/static/font/KaTeX_SansSerif-Regular.ba21ed5f84.woff2 +0 -0
  135. package/dist/client/static/font/KaTeX_Script-Regular.03e9641d6f.woff2 +0 -0
  136. package/dist/client/static/font/KaTeX_Size1-Regular.eae34984b3.woff2 +0 -0
  137. package/dist/client/static/font/KaTeX_Size2-Regular.5916a24fa3.woff2 +0 -0
  138. package/dist/client/static/font/KaTeX_Size4-Regular.10d95fd3a2.woff2 +0 -0
  139. package/dist/client/static/font/KaTeX_Typewriter-Regular.a8709e3622.woff2 +0 -0
  140. package/dist/client/static/font/fira-code-latin-400-normal.2655b10c55.woff2 +0 -0
  141. package/dist/client/static/font/fira-code-latin-500-normal.92a991f204.woff2 +0 -0
  142. package/dist/client/static/font/fira-code-latin-600-normal.43788eb7f1.woff2 +0 -0
  143. package/dist/client/static/font/fira-code-latin-700-normal.2667215d33.woff2 +0 -0
  144. package/dist/client/static/js/77.0d81c74cf0.js +0 -139
  145. package/dist/client/static/js/77.0d81c74cf0.js.LICENSE.txt +0 -8
  146. package/dist/client/static/js/async/0.8e59f2eb5b.js +0 -1
  147. package/dist/client/static/js/async/0k.5852e6b4fa.js +0 -1
  148. package/dist/client/static/js/async/0w.9ed36d5860.js +0 -1
  149. package/dist/client/static/js/async/1.f387c12b63.js +0 -1
  150. package/dist/client/static/js/async/12.d091f91c99.js +0 -1
  151. package/dist/client/static/js/async/13.8f810a8a6c.js +0 -1
  152. package/dist/client/static/js/async/19.6fe909d55a.js +0 -160
  153. package/dist/client/static/js/async/1b.23f1141a38.js +0 -1
  154. package/dist/client/static/js/async/1k.973e8e21fc.js +0 -2
  155. package/dist/client/static/js/async/1k.973e8e21fc.js.LICENSE.txt +0 -11
  156. package/dist/client/static/js/async/1p.0d5cddda96.js +0 -1
  157. package/dist/client/static/js/async/2.b589f1733b.js +0 -1
  158. package/dist/client/static/js/async/2v.96165101de.js +0 -1
  159. package/dist/client/static/js/async/3.0cacc08423.js +0 -1
  160. package/dist/client/static/js/async/37.b28b2f61cd.js +0 -1
  161. package/dist/client/static/js/async/3d.5085fb913f.js +0 -1
  162. package/dist/client/static/js/async/3j.357c80ff38.js +0 -2
  163. package/dist/client/static/js/async/3j.357c80ff38.js.LICENSE.txt +0 -11
  164. package/dist/client/static/js/async/3p.bcf32a0983.js +0 -1
  165. package/dist/client/static/js/async/3r.3a69e98ee0.js +0 -64
  166. package/dist/client/static/js/async/3u.1228460e2a.js +0 -1
  167. package/dist/client/static/js/async/3y.3530b824ad.js +0 -198
  168. package/dist/client/static/js/async/3y.3530b824ad.js.LICENSE.txt +0 -5
  169. package/dist/client/static/js/async/4.9442eeb472.js +0 -1
  170. package/dist/client/static/js/async/4e.678285ca55.js +0 -1
  171. package/dist/client/static/js/async/4k.33a0c7eeb4.js +0 -1
  172. package/dist/client/static/js/async/4m.928a1dde85.js +0 -31
  173. package/dist/client/static/js/async/4m.928a1dde85.js.LICENSE.txt +0 -11
  174. package/dist/client/static/js/async/4q.aad20dab25.js +0 -1
  175. package/dist/client/static/js/async/5.ebaf8b89cc.js +0 -36
  176. package/dist/client/static/js/async/51.95bcae98af.js +0 -2
  177. package/dist/client/static/js/async/51.95bcae98af.js.LICENSE.txt +0 -6
  178. package/dist/client/static/js/async/5b.d88304bc13.js +0 -1
  179. package/dist/client/static/js/async/5j.d6407c4248.js +0 -287
  180. package/dist/client/static/js/async/5o.88316d4460.js +0 -62
  181. package/dist/client/static/js/async/6.f034c9b528.js +0 -1
  182. package/dist/client/static/js/async/66.5dfa29f957.js +0 -93
  183. package/dist/client/static/js/async/6o.5b1a06a382.js +0 -1
  184. package/dist/client/static/js/async/7.e4a9288dd0.js +0 -1
  185. package/dist/client/static/js/async/73.308dff4da8.js +0 -1
  186. package/dist/client/static/js/async/7e.a1cc9bfd88.js +0 -1
  187. package/dist/client/static/js/async/7g.a7814350c3.js +0 -1
  188. package/dist/client/static/js/async/7l.fadbbc0583.js +0 -1
  189. package/dist/client/static/js/async/8.5067439d8c.js +0 -1
  190. package/dist/client/static/js/async/8j.ea110663fb.js +0 -1
  191. package/dist/client/static/js/async/a.bce4542392.js +0 -1
  192. package/dist/client/static/js/async/aa.65d3852576.js +0 -1
  193. package/dist/client/static/js/async/ar.9303e5f2e8.js +0 -2
  194. package/dist/client/static/js/async/ar.9303e5f2e8.js.LICENSE.txt +0 -11
  195. package/dist/client/static/js/async/az.d656062b0f.js +0 -1
  196. package/dist/client/static/js/async/b.b4f6f06515.js +0 -1
  197. package/dist/client/static/js/async/bi.09688ba75c.js +0 -1
  198. package/dist/client/static/js/async/bq.586bc738d1.js +0 -1
  199. package/dist/client/static/js/async/c.797d933882.js +0 -1
  200. package/dist/client/static/js/async/c0.0af1e640d3.js +0 -24
  201. package/dist/client/static/js/async/ca.8a52cfd447.js +0 -1
  202. package/dist/client/static/js/async/cf.d34fff5d67.js +0 -1
  203. package/dist/client/static/js/async/ct.b74928f7a3.js +0 -1
  204. package/dist/client/static/js/async/cx.3cb0242a3a.js +0 -226
  205. package/dist/client/static/js/async/d.a5fc8fe1b3.js +0 -1
  206. package/dist/client/static/js/async/da.f62c70f6c8.js +0 -1
  207. package/dist/client/static/js/async/dp.1725d6432c.js +0 -1
  208. package/dist/client/static/js/async/e.93214d622b.js +0 -79
  209. package/dist/client/static/js/async/f.ab50467719.js +0 -19
  210. package/dist/client/static/js/async/f.ab50467719.js.LICENSE.txt +0 -12
  211. package/dist/client/static/js/async/f7.46971e9d6d.js +0 -2
  212. package/dist/client/static/js/async/f7.46971e9d6d.js.LICENSE.txt +0 -6
  213. package/dist/client/static/js/async/f8.bbf0dbae70.js +0 -1
  214. package/dist/client/static/js/async/g.cb81980498.js +0 -2
  215. package/dist/client/static/js/async/g.cb81980498.js.LICENSE.txt +0 -11
  216. package/dist/client/static/js/async/h.4e790471e4.js +0 -27
  217. package/dist/client/static/js/async/h1.393f066d09.js +0 -1
  218. package/dist/client/static/js/async/h3.216e9798ab.js +0 -148
  219. package/dist/client/static/js/async/hd.0da931b622.js +0 -1
  220. package/dist/client/static/js/async/hk.ba7fb04d41.js +0 -1
  221. package/dist/client/static/js/async/hn.b645041e00.js +0 -1
  222. package/dist/client/static/js/async/ho.475a5e1ae4.js +0 -1
  223. package/dist/client/static/js/async/hp.e9fe2e395a.js +0 -2
  224. package/dist/client/static/js/async/hp.e9fe2e395a.js.LICENSE.txt +0 -17
  225. package/dist/client/static/js/async/hs.e7c59d8090.js +0 -41
  226. package/dist/client/static/js/async/i.7c148e8440.js +0 -1
  227. package/dist/client/static/js/async/id.573df20f29.js +0 -1
  228. package/dist/client/static/js/async/j.ab925882d8.js +0 -1
  229. package/dist/client/static/js/async/jd.63886a1f09.js +0 -1
  230. package/dist/client/static/js/async/jg.d86ebd4013.js +0 -86
  231. package/dist/client/static/js/async/jl.874d352278.js +0 -1
  232. package/dist/client/static/js/async/jll.11313b455a.js +0 -1
  233. package/dist/client/static/js/async/js.67182759ef.js +0 -1
  234. package/dist/client/static/js/async/jsc.23b70d6f40.js +0 -1
  235. package/dist/client/static/js/async/jse.334588ff5d.js +0 -1
  236. package/dist/client/static/js/async/jz.72eb929106.js +0 -77
  237. package/dist/client/static/js/async/k.3210286401.js +0 -217
  238. package/dist/client/static/js/async/ka.c1990ecaa3.js +0 -115
  239. package/dist/client/static/js/async/katex-vendor.de6a87c308.js +0 -1
  240. package/dist/client/static/js/async/ku.29c8a59d0c.js +0 -1
  241. package/dist/client/static/js/async/ky.8dab1dade0.js +0 -1
  242. package/dist/client/static/js/async/l.12dd34e1be.js +0 -1
  243. package/dist/client/static/js/async/l9.949710adaf.js +0 -83
  244. package/dist/client/static/js/async/l9.949710adaf.js.LICENSE.txt +0 -13
  245. package/dist/client/static/js/async/ls.9d9a01026c.js +0 -104
  246. package/dist/client/static/js/async/m.eb2e6752e2.js +0 -28
  247. package/dist/client/static/js/async/m.eb2e6752e2.js.LICENSE.txt +0 -11
  248. package/dist/client/static/js/async/m3.d2b05f9604.js +0 -2
  249. package/dist/client/static/js/async/m3.d2b05f9604.js.LICENSE.txt +0 -6
  250. package/dist/client/static/js/async/ma.2c261b04dd.js +0 -2
  251. package/dist/client/static/js/async/ma.2c261b04dd.js.LICENSE.txt +0 -6
  252. package/dist/client/static/js/async/me.8ace414f80.js +0 -23
  253. package/dist/client/static/js/async/ms.98960af4eb.js +0 -67
  254. package/dist/client/static/js/async/ms.98960af4eb.js.LICENSE.txt +0 -12
  255. package/dist/client/static/js/async/mt.0352e976d8.js +0 -1
  256. package/dist/client/static/js/async/n.a9f1c51701.js +0 -1
  257. package/dist/client/static/js/async/na.a36cd532d6.js +0 -1
  258. package/dist/client/static/js/async/nk.fc7e58325e.js +0 -1
  259. package/dist/client/static/js/async/o.36353dfb88.js +0 -1
  260. package/dist/client/static/js/async/oc.b73042e8bd.js +0 -8
  261. package/dist/client/static/js/async/og.8eb577893a.js +0 -193
  262. package/dist/client/static/js/async/p.48ecfcd14e.js +0 -1
  263. package/dist/client/static/js/async/px.fc6877924b.js +0 -1
  264. package/dist/client/static/js/async/q.22588845a9.js +0 -21
  265. package/dist/client/static/js/async/q.22588845a9.js.LICENSE.txt +0 -6
  266. package/dist/client/static/js/async/q5.97dc1481b6.js +0 -286
  267. package/dist/client/static/js/async/qs.2a9ddb3c72.js +0 -1
  268. package/dist/client/static/js/async/r.51010ad87c.js +0 -1
  269. package/dist/client/static/js/async/rd.cb6bf495bb.js +0 -2
  270. package/dist/client/static/js/async/rd.cb6bf495bb.js.LICENSE.txt +0 -4
  271. package/dist/client/static/js/async/s.0f395cdda5.js +0 -94
  272. package/dist/client/static/js/async/t.4dca0e2ba8.js +0 -1
  273. package/dist/client/static/js/async/t9.d44f57d518.js +0 -1
  274. package/dist/client/static/js/async/ta.973534bbec.js +0 -1
  275. package/dist/client/static/js/async/tr.e878658140.js +0 -309
  276. package/dist/client/static/js/async/u.0871ac5b22.js +0 -1
  277. package/dist/client/static/js/async/ud.3065bfee31.js +0 -2
  278. package/dist/client/static/js/async/ud.3065bfee31.js.LICENSE.txt +0 -4
  279. package/dist/client/static/js/async/up.ea990da0ff.js +0 -32
  280. package/dist/client/static/js/async/uw.8890abbabc.js +0 -1
  281. package/dist/client/static/js/async/v.7d28d3c241.js +0 -34
  282. package/dist/client/static/js/async/vp.9b13bf8610.js +0 -1
  283. package/dist/client/static/js/async/vt.42416b707d.js +0 -1
  284. package/dist/client/static/js/async/w.08ac210f60.js +0 -1
  285. package/dist/client/static/js/async/wt.ad92393a90.js +0 -1
  286. package/dist/client/static/js/async/x.563f72ca6b.js +0 -1
  287. package/dist/client/static/js/async/x0.475516418f.js +0 -1
  288. package/dist/client/static/js/async/x1.da8687159b.js +0 -90
  289. package/dist/client/static/js/async/xe.da8dcc2015.js +0 -2
  290. package/dist/client/static/js/async/xe.da8dcc2015.js.LICENSE.txt +0 -11
  291. package/dist/client/static/js/async/xl.a3c562a58c.js +0 -1
  292. package/dist/client/static/js/async/xn.6510c0abe2.js +0 -2
  293. package/dist/client/static/js/async/xn.6510c0abe2.js.LICENSE.txt +0 -6
  294. package/dist/client/static/js/async/xn1.83a7784fe1.js +0 -10
  295. package/dist/client/static/js/async/xn1.83a7784fe1.js.LICENSE.txt +0 -11
  296. package/dist/client/static/js/async/xnl.feb0021e50.js +0 -2
  297. package/dist/client/static/js/async/xnl.feb0021e50.js.LICENSE.txt +0 -12
  298. package/dist/client/static/js/async/y.1a9ed7a5d8.js +0 -1
  299. package/dist/client/static/js/async/y6.43c57defe4.js +0 -1
  300. package/dist/client/static/js/async/yb.8c1342401d.js +0 -1
  301. package/dist/client/static/js/async/ybq.4491f2f2dc.js +0 -1
  302. package/dist/client/static/js/async/yg.35bebd6d0c.js +0 -149
  303. package/dist/client/static/js/async/yq.917ddd7ba2.js +0 -1
  304. package/dist/client/static/js/async/ys.00687cf82d.js +0 -38
  305. package/dist/client/static/js/async/yv.d99d829136.js +0 -1
  306. package/dist/client/static/js/async/z.56f61a4374.js +0 -1
  307. package/dist/client/static/js/async/zi.5fdb07b86a.js +0 -24
  308. package/dist/client/static/js/async/zj.654f37e8ff.js +0 -1
  309. package/dist/client/static/js/async/zn.966d6f1cc6.js +0 -1
  310. package/dist/client/static/js/async/zo.26213b40c0.js +0 -1
  311. package/dist/client/static/js/async/zx.e9f4105e00.js +0 -1
  312. package/dist/client/static/js/index.e6a257402a.js +0 -38
  313. package/dist/client/static/js/index.e6a257402a.js.LICENSE.txt +0 -4
  314. package/dist/client/static/js/markdown-vendor.71e43278b3.js +0 -144
  315. package/dist/client/static/js/shiki-vendor.cb8452ddcd.js +0 -1
  316. package/src/client/components/settings/categories/provider-settings/Icons.tsx +0 -35
  317. package/src/client/hooks/settings/providers.ts +0 -326
  318. package/src/server/plugins/dev-assets.ts +0 -49
  319. package/src/shared/lib/code/editor-utils.ts +0 -16
  320. package/src/shared/lib/models/provider-presets.ts +0 -29
  321. /package/dist/client/{apple-touch-icon.png → static/apple-touch-icon-dskx5sr3.png} +0 -0
  322. /package/dist/client/{icon-192x192.png → static/icon-192x192-6gn5cr2f.png} +0 -0
  323. /package/dist/client/{icon.svg → static/icon-d07z9dtz.svg} +0 -0
  324. /package/dist/client/{manifest.webmanifest → static/manifest-bnyy75kt.webmanifest} +0 -0
  325. /package/{dist/client → public}/icon-512x512.png +0 -0
  326. /package/{dist/client → public}/icon-maskable-512x512.png +0 -0
  327. /package/{dist/client → public}/sw.js +0 -0
  328. /package/src/shared/lib/models/{provider-cleanup.ts → provider/cleanup.ts} +0 -0
  329. /package/src/shared/lib/models/{provider-label.ts → provider/label.ts} +0 -0
@@ -0,0 +1,116 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The whitespace toggle's contract. Trimming the displayed text left a
8
+ * whitespace-only rewrite painted red/green and counted in `+n/-n`, so the
9
+ * toggle changed the glyphs and nothing else — these cases pin the behavior a
10
+ * reader actually expects from it.
11
+ */
12
+
13
+ import { describe, expect, test } from 'bun:test';
14
+
15
+ import { parseUnifiedDiff } from '@/shared/lib/fs/diff-parser';
16
+
17
+ const WHITESPACE_ONLY = [
18
+ 'diff --git a/ws.ts b/ws.ts',
19
+ 'index 46378b4..c824424 100644',
20
+ '--- a/ws.ts',
21
+ '+++ b/ws.ts',
22
+ '@@ -1,3 +1,3 @@',
23
+ ' function ws() {',
24
+ '- return 1;',
25
+ '+ return 1;',
26
+ ' }',
27
+ '',
28
+ ].join('\n');
29
+
30
+ const REAL_CHANGE = [
31
+ 'diff --git a/main.ts b/main.ts',
32
+ '--- a/main.ts',
33
+ '+++ b/main.ts',
34
+ '@@ -1,2 +1,2 @@',
35
+ ' export const a = 1;',
36
+ '-export function f() { return a; }',
37
+ '+export function f() { return a + 1; }',
38
+ '',
39
+ ].join('\n');
40
+
41
+ const MIXED = [
42
+ 'diff --git a/mixed.ts b/mixed.ts',
43
+ '--- a/mixed.ts',
44
+ '+++ b/mixed.ts',
45
+ '@@ -1,3 +1,3 @@',
46
+ '- const a = 1;',
47
+ '+ const a = 1;',
48
+ '- const b = 2;',
49
+ '+ const b = 3;',
50
+ '',
51
+ ].join('\n');
52
+
53
+ describe('parseUnifiedDiff', () => {
54
+ test('counts and marks a whitespace-only rewrite by default', () => {
55
+ const { lines, additions, deletions } = parseUnifiedDiff(WHITESPACE_ONLY);
56
+
57
+ expect(additions).toBe(1);
58
+ expect(deletions).toBe(1);
59
+ expect(lines.filter((l) => l.type === 'add')).toHaveLength(1);
60
+ expect(lines.filter((l) => l.type === 'del')).toHaveLength(1);
61
+ });
62
+
63
+ test('folds a whitespace-only rewrite into context when ignoring whitespace', () => {
64
+ const { lines, splitRows, additions, deletions } = parseUnifiedDiff(WHITESPACE_ONLY, true);
65
+
66
+ // The counts the toolbar renders must match what is on screen.
67
+ expect(additions).toBe(0);
68
+ expect(deletions).toBe(0);
69
+ expect(lines.some((l) => l.type === 'add' || l.type === 'del')).toBe(false);
70
+
71
+ const folded = lines.find((l) => l.type === 'context' && l.oldLineNumber === 2);
72
+ expect(folded?.newLineNumber).toBe(2);
73
+ expect(folded?.text).toBe(' return 1;');
74
+
75
+ // Split view reads the same decision — it is built by the same flush.
76
+ const row = splitRows.find((r) => r.left?.lineNumber === 2);
77
+ expect(row?.left?.type).toBe('context');
78
+ expect(row?.right?.type).toBe('context');
79
+ expect(row?.left?.text).toBe(' return 1;');
80
+ expect(row?.right?.text).toBe(' return 1;');
81
+ });
82
+
83
+ test('keeps a real change when ignoring whitespace', () => {
84
+ const { lines, additions, deletions } = parseUnifiedDiff(REAL_CHANGE, true);
85
+
86
+ expect(additions).toBe(1);
87
+ expect(deletions).toBe(1);
88
+ expect(lines.find((l) => l.type === 'del')?.text).toBe('export function f() { return a; }');
89
+ expect(lines.find((l) => l.type === 'add')?.text).toBe('export function f() { return a + 1; }');
90
+ });
91
+
92
+ test('folds only the whitespace pair of a mixed hunk', () => {
93
+ const { lines, additions, deletions } = parseUnifiedDiff(MIXED, true);
94
+
95
+ // Line 1 differs by indentation only; line 2 is a genuine edit.
96
+ expect(additions).toBe(1);
97
+ expect(deletions).toBe(1);
98
+ expect(lines.filter((l) => l.type === 'del').map((l) => l.text)).toEqual([' const b = 2;']);
99
+ expect(lines.filter((l) => l.type === 'add').map((l) => l.text)).toEqual([' const b = 3;']);
100
+ });
101
+
102
+ test('emits unified lines in diff order (deletions before additions)', () => {
103
+ const { lines } = parseUnifiedDiff(REAL_CHANGE);
104
+ const kinds = lines.map((l) => l.type);
105
+
106
+ // A git diff ends with a newline, which `split` turns into one trailing
107
+ // empty context line — pre-existing, and the same row the view has always
108
+ // drawn under the last change.
109
+ expect(kinds.slice(0, 4)).toEqual(['meta', 'context', 'del', 'add']);
110
+ });
111
+
112
+ test('returns empty results for an empty diff', () => {
113
+ expect(parseUnifiedDiff('', true)).toEqual({ lines: [], splitRows: [], additions: 0, deletions: 0 });
114
+ expect(parseUnifiedDiff(' \n', false)).toEqual({ lines: [], splitRows: [], additions: 0, deletions: 0 });
115
+ });
116
+ });
@@ -6,20 +6,31 @@ export interface DiffLine {
6
6
  }
7
7
 
8
8
  export interface SplitDiffRow {
9
- left?: {
10
- type: 'del' | 'context';
11
- text: string;
12
- lineNumber: number;
13
- };
14
- right?: {
15
- type: 'add' | 'context';
16
- text: string;
17
- lineNumber: number;
18
- };
9
+ left?: { type: 'del' | 'context'; text: string; lineNumber: number };
10
+ right?: { type: 'add' | 'context'; text: string; lineNumber: number };
19
11
  isMeta?: boolean;
20
12
  metaText?: string;
21
13
  }
22
14
 
15
+ /** A buffered `-`/`+` line, before it is decided whether it is a real change. */
16
+ interface PendingChange {
17
+ text: string;
18
+ lineNumber: number;
19
+ }
20
+
21
+ /**
22
+ * Parse a unified diff into the flat line list (Unified view) and the paired
23
+ * row list (Split view), plus the addition/deletion counts the toolbar shows.
24
+ *
25
+ * Both outputs are produced by the same flush, so a decision made here cannot
26
+ * land in one view and not the other.
27
+ *
28
+ * With `ignoreWhitespace`, a `-`/`+` pair whose text differs only in whitespace
29
+ * is folded back into context: the change is still in the file, but the reader
30
+ * asked not to be shown formatting noise. Merely trimming the displayed text —
31
+ * which is what this used to do — left the row painted red/green and counted in
32
+ * `+n/-n`, so the toggle changed the glyphs and nothing else.
33
+ */
23
34
  export function parseUnifiedDiff(rawDiff: string, ignoreWhitespace: boolean = false): {
24
35
  lines: DiffLine[];
25
36
  splitRows: SplitDiffRow[];
@@ -44,16 +55,60 @@ export function parseUnifiedDiff(rawDiff: string, ignoreWhitespace: boolean = fa
44
55
  let oldCounter = 1;
45
56
  let newCounter = 1;
46
57
 
47
- // Buffer for pairing consecutive deletions and additions in split view
48
- let delBuffer: { text: string; lineNumber: number }[] = [];
49
- let addBuffer: { text: string; lineNumber: number }[] = [];
58
+ // Consecutive deletions and additions, paired up in split view.
59
+ let delBuffer: PendingChange[] = [];
60
+ let addBuffer: PendingChange[] = [];
50
61
 
51
62
  const flushBuffers = () => {
52
63
  const maxLen = Math.max(delBuffer.length, addBuffer.length);
64
+ // A whitespace-only rewrite is decided per pair, but the flat line list
65
+ // keeps unified-diff order (every `-` of the group, then every `+`), which
66
+ // is what the original loop produced as it walked the raw diff.
67
+ const folded = new Array<boolean>(maxLen).fill(false);
68
+ if (ignoreWhitespace) {
69
+ for (let i = 0; i < maxLen; i++) {
70
+ const del = delBuffer[i];
71
+ const add = addBuffer[i];
72
+ folded[i] = Boolean(del && add && del.text.trim() === add.text.trim());
73
+ }
74
+ }
75
+
76
+ for (let i = 0; i < delBuffer.length; i++) {
77
+ const del = delBuffer[i];
78
+ const add = addBuffer[i];
79
+ if (folded[i] && add) {
80
+ // Show it as unchanged on both sides: the change is still in the file,
81
+ // the reader asked not to be shown formatting noise.
82
+ lines.push({
83
+ type: 'context',
84
+ // The new form, so the row reads as the file's current state.
85
+ text: add.text,
86
+ oldLineNumber: del.lineNumber,
87
+ newLineNumber: add.lineNumber,
88
+ });
89
+ continue;
90
+ }
91
+ lines.push({ type: 'del', text: del.text, oldLineNumber: del.lineNumber });
92
+ }
93
+ for (let i = 0; i < addBuffer.length; i++) {
94
+ const add = addBuffer[i];
95
+ if (folded[i]) continue;
96
+ lines.push({ type: 'add', text: add.text, newLineNumber: add.lineNumber });
97
+ }
98
+
53
99
  for (let i = 0; i < maxLen; i++) {
100
+ const del = delBuffer[i];
101
+ const add = addBuffer[i];
102
+ if (folded[i] && del && add) {
103
+ splitRows.push({
104
+ left: { type: 'context', text: del.text, lineNumber: del.lineNumber },
105
+ right: { type: 'context', text: add.text, lineNumber: add.lineNumber },
106
+ });
107
+ continue;
108
+ }
54
109
  splitRows.push({
55
- left: delBuffer[i] ? { type: 'del', text: delBuffer[i].text, lineNumber: delBuffer[i].lineNumber } : undefined,
56
- right: addBuffer[i] ? { type: 'add', text: addBuffer[i].text, lineNumber: addBuffer[i].lineNumber } : undefined,
110
+ left: del ? { type: 'del', text: del.text, lineNumber: del.lineNumber } : undefined,
111
+ right: add ? { type: 'add', text: add.text, lineNumber: add.lineNumber } : undefined,
57
112
  });
58
113
  }
59
114
  delBuffer = [];
@@ -90,39 +145,26 @@ export function parseUnifiedDiff(rawDiff: string, ignoreWhitespace: boolean = fa
90
145
 
91
146
  if (rawLine.startsWith('+')) {
92
147
  const text = rawLine.slice(1);
93
- const normalizedText = ignoreWhitespace ? text.trim() : text;
94
148
  additions++;
95
- lines.push({
96
- type: 'add',
97
- text: normalizedText,
98
- newLineNumber: newCounter,
99
- });
100
- addBuffer.push({ text: normalizedText, lineNumber: newCounter });
149
+ addBuffer.push({ text, lineNumber: newCounter });
101
150
  newCounter++;
102
151
  } else if (rawLine.startsWith('-')) {
103
152
  const text = rawLine.slice(1);
104
- const normalizedText = ignoreWhitespace ? text.trim() : text;
105
153
  deletions++;
106
- lines.push({
107
- type: 'del',
108
- text: normalizedText,
109
- oldLineNumber: oldCounter,
110
- });
111
- delBuffer.push({ text: normalizedText, lineNumber: oldCounter });
154
+ delBuffer.push({ text, lineNumber: oldCounter });
112
155
  oldCounter++;
113
156
  } else {
114
157
  flushBuffers();
115
158
  const text = rawLine.startsWith(' ') ? rawLine.slice(1) : rawLine;
116
- const normalizedText = ignoreWhitespace ? text.trim() : text;
117
159
  lines.push({
118
160
  type: 'context',
119
- text: normalizedText,
161
+ text,
120
162
  oldLineNumber: oldCounter,
121
163
  newLineNumber: newCounter,
122
164
  });
123
165
  splitRows.push({
124
- left: { type: 'context', text: normalizedText, lineNumber: oldCounter },
125
- right: { type: 'context', text: normalizedText, lineNumber: newCounter },
166
+ left: { type: 'context', text, lineNumber: oldCounter },
167
+ right: { type: 'context', text, lineNumber: newCounter },
126
168
  });
127
169
  oldCounter++;
128
170
  newCounter++;
@@ -131,5 +173,16 @@ export function parseUnifiedDiff(rawDiff: string, ignoreWhitespace: boolean = fa
131
173
 
132
174
  flushBuffers();
133
175
 
176
+ if (ignoreWhitespace) {
177
+ // The counts were tallied before the pairs could be folded, so re-derive
178
+ // them from what is actually shown.
179
+ additions = 0;
180
+ deletions = 0;
181
+ for (const line of lines) {
182
+ if (line.type === 'add') additions++;
183
+ else if (line.type === 'del') deletions++;
184
+ }
185
+ }
186
+
134
187
  return { lines, splitRows, additions, deletions };
135
188
  }
@@ -39,3 +39,40 @@ export function saveProviderOverlay(providers: ProviderItem[]): Promise<Provider
39
39
  export function setProviderEnabled(slug: string, enabled: boolean): Promise<ProviderItem[] | null> {
40
40
  return postProviders(enabled ? { enableProvider: slug } : { disableProvider: slug });
41
41
  }
42
+
43
+ export interface ProviderDeleteResult {
44
+ ok: boolean;
45
+ providers?: ProviderItem[];
46
+ /** True when a `models.yml` entry was actually removed. */
47
+ removedFromOmp?: boolean;
48
+ /** How many model entries went with it. */
49
+ modelsRemoved?: number;
50
+ /** The overlay row went but the file cleanup failed — surfaced, not swallowed. */
51
+ warning?: string;
52
+ error?: string;
53
+ }
54
+
55
+ /**
56
+ * Delete a provider: its chamber overlay row AND its `models.yml` entry.
57
+ *
58
+ * The provider is identified by the id the registry served, because a provider
59
+ * that exists only in models.yml has no overlay row to match on — the server
60
+ * resolves the id against the merged registry to find the slug to unregister.
61
+ */
62
+ export async function deleteProvider(id: string): Promise<ProviderDeleteResult> {
63
+ try {
64
+ const response = await fetch(`${PROVIDERS_ENDPOINT}?id=${encodeURIComponent(id)}`, { method: 'DELETE' });
65
+ const data = await response.json() as ProviderDeleteResult;
66
+ if (!response.ok) {
67
+ return { ok: false, error: data?.error || `Delete failed (HTTP ${response.status})` };
68
+ }
69
+ // The provider — and every model it served — leaves the chat picker.
70
+ notifyModelsUpdated();
71
+ return { ...data, ok: true };
72
+ } catch (error) {
73
+ return {
74
+ ok: false,
75
+ error: error instanceof Error ? error.message : 'Network error while deleting the provider',
76
+ };
77
+ }
78
+ }
@@ -0,0 +1,179 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The provider DIALECT — which wire API a custom provider speaks, how it
8
+ * authenticates, and how its model list is obtained. All three are omp
9
+ * `models.yml` provider fields, and all three change what a provider can do:
10
+ *
11
+ * - `api` selects the request/response shape. A gateway that speaks
12
+ * `anthropic-messages` is not reachable through the OpenAI-compatible path,
13
+ * and omp rejects an unknown `api` by disabling EVERY custom provider in the
14
+ * file — so the value must come from omp's own `ApiSchema`.
15
+ * - `auth: none` is what makes a local server keyless; without it omp demands
16
+ * an `apiKey` and a keyless provider is rejected as unconfigured.
17
+ * - `discovery` lets OMP list the models live from the endpoint instead of the
18
+ * chamber writing a snapshot of ids into `models.yml`.
19
+ *
20
+ * Pure data + pure functions, shared by the server (which validates and writes
21
+ * the file) and the client (which offers the choices), so the two cannot
22
+ * disagree about what omp accepts.
23
+ */
24
+
25
+ import type { OmpProviderApi, ProviderAuthMode, ProviderDiscoveryType, ProviderModelSource } from '@/shared/types/settings/provider';
26
+
27
+ /** Every `api` omp accepts, in omp's own `ApiSchema` order. */
28
+ export const PROVIDER_APIS: readonly OmpProviderApi[] = [
29
+ 'openai-completions',
30
+ 'openai-responses',
31
+ 'openai-codex-responses',
32
+ 'azure-openai-responses',
33
+ 'anthropic-messages',
34
+ 'bedrock-converse-stream',
35
+ 'google-generative-ai',
36
+ 'google-gemini-cli',
37
+ 'google-vertex',
38
+ 'openrouter-decisions',
39
+ 'typesafe',
40
+ ];
41
+
42
+ /** One-line explanation of what each dialect is, for the picker. */
43
+ export const PROVIDER_API_LABELS: Readonly<Record<OmpProviderApi, string>> = {
44
+ 'openai-completions': 'OpenAI Chat Completions — the default for gateways and local servers',
45
+ 'openai-responses': 'OpenAI Responses API — newer OpenAI-shaped endpoints',
46
+ 'openai-codex-responses': 'OpenAI Codex Responses — Codex-style endpoints',
47
+ 'azure-openai-responses': 'Azure OpenAI Responses — api-key header, api-version query',
48
+ 'anthropic-messages': 'Anthropic Messages — Claude-shaped proxies (x-api-key)',
49
+ 'bedrock-converse-stream': 'Amazon Bedrock Converse — AWS credential chain',
50
+ 'google-generative-ai': 'Google Generative AI — Gemini API (x-goog-api-key)',
51
+ 'google-gemini-cli': 'Google Gemini CLI — OAuth-backed Gemini route',
52
+ 'google-vertex': 'Google Vertex AI — service-account / ADC route',
53
+ 'openrouter-decisions': 'OpenRouter decisions — judge role only',
54
+ typesafe: 'Typesafe System One — judge role only',
55
+ };
56
+
57
+ /**
58
+ * omp's `EffortSchema` — the reasoning ladder, lowest first, in omp's own
59
+ * `EFFORT_ORDER`.
60
+ *
61
+ * `off` is deliberately absent: omp rejects it inside `thinking.efforts`
62
+ * ("must be minimal, low, medium, high, xhigh or max"), because turning thinking
63
+ * off is a per-turn choice rather than a declared capability. A model that
64
+ * declares no effort ladder at all still gets omp's default one.
65
+ */
66
+ export const PROVIDER_THINKING_EFFORTS = ['minimal', 'low', 'medium', 'high', 'xhigh', 'max'] as const;
67
+
68
+ export type ProviderThinkingEffort = (typeof PROVIDER_THINKING_EFFORTS)[number];
69
+
70
+ /** True when `value` is an effort omp's `EffortSchema` accepts. */
71
+ export function isProviderThinkingEffort(value: unknown): value is ProviderThinkingEffort {
72
+ return typeof value === 'string' && (PROVIDER_THINKING_EFFORTS as readonly string[]).includes(value);
73
+ }
74
+
75
+ /** Auth modes omp accepts, with the meaning the picker shows. */
76
+ export const PROVIDER_AUTH_MODES: ReadonlyArray<{
77
+ value: ProviderAuthMode;
78
+ label: string;
79
+ hint: string;
80
+ }> = [
81
+ { value: 'apiKey', label: 'API key', hint: 'omp requires a key for this provider' },
82
+ { value: 'none', label: 'No auth (keyless)', hint: 'Local server that needs no credential' },
83
+ { value: 'oauth', label: 'OAuth', hint: 'Claude-Code-style proxy expecting the cloaked request shape' },
84
+ ];
85
+
86
+ /** Discovery types omp can drive itself, with what each one probes. */
87
+ export const PROVIDER_DISCOVERY_TYPES: ReadonlyArray<{
88
+ value: ProviderDiscoveryType;
89
+ label: string;
90
+ hint: string;
91
+ }> = [
92
+ { value: 'openai-models-list', label: 'OpenAI /v1/models', hint: 'Any OpenAI-compatible endpoint' },
93
+ { value: 'ollama', label: 'Ollama', hint: 'Native Ollama /api/tags + /api/show' },
94
+ { value: 'llama.cpp', label: 'llama.cpp', hint: 'llama.cpp server model endpoints' },
95
+ { value: 'lm-studio', label: 'LM Studio', hint: 'LM Studio /v1/models + native metadata' },
96
+ { value: 'litellm', label: 'LiteLLM proxy', hint: 'LiteLLM proxy with rich model metadata' },
97
+ { value: 'proxy', label: 'New-API / One-API proxy', hint: 'Wire api read per model from the proxy' },
98
+ {
99
+ value: 'apple-foundation-models',
100
+ label: 'Apple Foundation Models',
101
+ hint: 'On-device model (Apple silicon only)',
102
+ },
103
+ ];
104
+
105
+ /** True when `value` is a dialect omp's `ApiSchema` accepts. */
106
+ export function isOmpProviderApi(value: unknown): value is OmpProviderApi {
107
+ return typeof value === 'string' && (PROVIDER_APIS as readonly string[]).includes(value);
108
+ }
109
+
110
+ /**
111
+ * True when the value is a credential the UI masked rather than a real key.
112
+ *
113
+ * The settings UI echoes a stored key back as `sk-••••…`; that placeholder must
114
+ * never be persisted, or a provider looks configured while it cannot
115
+ * authenticate. Shared by the client (which produces the mask) and the server
116
+ * (which is the last gate before the file).
117
+ */
118
+ export function isMaskedApiKey(value: unknown): boolean {
119
+ return typeof value === 'string' && /•{3,}/.test(value);
120
+ }
121
+
122
+ /** True when `value` is an auth mode omp accepts. */
123
+ export function isProviderAuthMode(value: unknown): value is ProviderAuthMode {
124
+ return value === 'apiKey' || value === 'none' || value === 'oauth';
125
+ }
126
+
127
+ /** True when `value` is a discovery type omp accepts. */
128
+ export function isProviderDiscoveryType(value: unknown): value is ProviderDiscoveryType {
129
+ return PROVIDER_DISCOVERY_TYPES.some((entry) => entry.value === value);
130
+ }
131
+
132
+ /**
133
+ * omp's `api` for a provider whose endpoint is only recognizable by URL. omp
134
+ * accepts eleven dialects and no endpoint can be asked which one it speaks, so
135
+ * only the two hosts that are unambiguous by name are claimed here; everything
136
+ * else gets the OpenAI-compatible default that the model-listing probe already
137
+ * assumes. Anything else (Azure, Bedrock, Vertex, a Codex proxy) has to be
138
+ * picked by hand — guessing would write a dialect that misroutes every request.
139
+ */
140
+ export function inferProviderApi(baseUrl: string): OmpProviderApi {
141
+ if (/anthropic\./i.test(baseUrl)) return 'anthropic-messages';
142
+ if (/generativelanguage\.googleapis\.com/i.test(baseUrl)) return 'google-generative-ai';
143
+ if (/openai\.azure\.com/i.test(baseUrl)) return 'azure-openai-responses';
144
+ if (/openrouter\.ai/i.test(baseUrl)) return 'openai-completions';
145
+ return 'openai-completions';
146
+ }
147
+
148
+ /**
149
+ * Why a provider's model list is empty — or `undefined` when the emptiness is
150
+ * unexplained and should read as a problem.
151
+ *
152
+ * A provider with no models is not automatically broken: an endpoint override
153
+ * leaves the model list to omp's own catalog, a discovery provider is listed by
154
+ * omp at runtime, and an authenticated login provider's models arrive with the
155
+ * agent's own snapshot. Printing "no models" for all of them made a correctly
156
+ * configured proxy look like a failed setup.
157
+ */
158
+ export function providerEmptyReason(provider: {
159
+ modelSource?: ProviderModelSource;
160
+ discovery?: ProviderDiscoveryType;
161
+ auth?: ProviderAuthMode;
162
+ }): string | undefined {
163
+ if (provider.discovery || provider.modelSource === 'discovery') {
164
+ return 'omp discovers this provider’s models live — they appear in the chat model picker, not here.';
165
+ }
166
+ if (provider.modelSource === 'override') {
167
+ // An override entry with no models and no credential is the one shape that
168
+ // genuinely cannot serve anything: omp has no key to reach the endpoint
169
+ // with. Naming the credential is the honest instruction.
170
+ return provider.auth === 'none'
171
+ ? 'Endpoint override — omp keeps its own model list for this provider. Use “fetch models” to add specific ids.'
172
+ : 'Endpoint override — omp keeps its own model list for this provider. It still needs a credential: use “connect” to supply one, or “fetch models” to add specific ids.';
173
+ }
174
+ if (provider.auth === 'none') {
175
+ return 'No models registered yet. This server is keyless, so use “fetch models” to list what it serves.';
176
+ }
177
+ return undefined;
178
+ }
179
+
@@ -0,0 +1,116 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The provider mark resolver is shared by the server (which builds provider
8
+ * rows) and the client (which renders them), and its two failure modes are both
9
+ * silent: a provider whose spelling does not fold onto its brand draws the wrong
10
+ * logo, and a provider with no mark at all would draw nothing. These lock the
11
+ * folding rules and the initials fallback.
12
+ */
13
+
14
+ import { describe, expect, test } from 'bun:test';
15
+
16
+ import { PROVIDER_GLYPHS } from '@/client/components/common/provider-icon/glyphs';
17
+ import { providerInitials, resolveProviderGlyph } from '@/shared/lib/models/provider/glyph';
18
+
19
+ /**
20
+ * Every provider id omp 18.3.0 ships (`KnownProvider`) plus the chamber's own
21
+ * additions. A provider added upstream appears here as one more id, and the
22
+ * coverage test below is what makes its missing mark a loud failure instead of a
23
+ * blank badge in the picker.
24
+ */
25
+ const PROVIDER_IDS = [
26
+ 'abliteration', 'aiand', 'aimlapi', 'alibaba-coding-plan', 'alibaba-token-plan', 'amazon-bedrock',
27
+ 'anthropic', 'apple', 'azure', 'baseten', 'bedrock-mantle', 'cerebras', 'charm-hyper', 'cline-pass',
28
+ 'cloudflare-ai-gateway', 'commandcode', 'coreweave', 'cursor', 'deepinfra', 'deepseek', 'devin',
29
+ 'firepass', 'fireworks', 'github-copilot', 'gitlab-duo', 'gitlab-duo-agent', 'gmi-cloud', 'google',
30
+ 'google-antigravity', 'google-gemini-cli', 'google-vertex', 'groq', 'huggingface', 'kilo', 'kimi-code',
31
+ 'litellm', 'llama.cpp', 'lm-studio', 'local', 'meta', 'minimax', 'minimax-code', 'minimax-code-cn',
32
+ 'mistral', 'moonshot', 'muse-code', 'nanogpt', 'novita', 'nvidia', 'ollama', 'ollama-cloud', 'openai',
33
+ 'openai-codex', 'opencode-go', 'opencode-zen', 'openrouter', 'qianfan', 'qwen-portal', 'sakana',
34
+ 'siliconflow', 'siliconflow-cn', 'singularityapi-dev', 'singularityapi-tech', 'stepfun', 'synthetic',
35
+ 'together', 'typesafe', 'umans', 'venice', 'vercel-ai-gateway', 'vllm', 'wafer-serverless', 'web',
36
+ 'xai', 'xai-oauth', 'xiaomi', 'xiaomi-token-plan-ams', 'xiaomi-token-plan-cn', 'xiaomi-token-plan-sgp',
37
+ 'yolo-auto', 'zai', 'zenmux', 'zhipu-coding-plan',
38
+ // Chamber-side spellings that are not omp provider ids, plus the OAuth
39
+ // flavour ids the providers API actually serves (verified against a live
40
+ // /api/settings/providers response).
41
+ 'kenari', 'agentrouter', 'claude', 'gemini', 'opencode', 'custom',
42
+ 'zai-coding-plan', 'openai-codex-device', 'perplexity', 'tavily', 'kagi', 'exa', 'stencil',
43
+ ];
44
+
45
+ describe('resolveProviderGlyph', () => {
46
+ test('a resolved mark exists in the glyph table', () => {
47
+ for (const id of PROVIDER_IDS) {
48
+ const name = resolveProviderGlyph(id);
49
+ if (name) expect(PROVIDER_GLYPHS[name]).toBeDefined();
50
+ }
51
+ });
52
+
53
+ test('every glyph draws something', () => {
54
+ // A bake that produced an empty body renders an invisible icon with no
55
+ // error anywhere — the provider simply loses its mark.
56
+ for (const [name, glyph] of Object.entries(PROVIDER_GLYPHS)) {
57
+ expect(glyph.viewBox, name).toMatch(/^[-\d.]+ [-\d.]+ [\d.]+ [\d.]+$/);
58
+ expect(/<(path|circle|rect|g)\b/.test(glyph.body), name).toBe(true);
59
+ }
60
+ });
61
+
62
+ test('no provider id leaves a blank badge', () => {
63
+ // The one promise of this module: a provider is drawn as its brand mark or
64
+ // as readable initials, never as an empty box.
65
+ for (const id of PROVIDER_IDS) {
66
+ const mark = resolveProviderGlyph(id);
67
+ expect(mark !== null || providerInitials(id).length > 0).toBe(true);
68
+ }
69
+ });
70
+
71
+ test('variants of one brand share that brand mark', () => {
72
+ expect(resolveProviderGlyph('minimax-code-cn')).toBe(resolveProviderGlyph('minimax'));
73
+ expect(resolveProviderGlyph('xiaomi-token-plan-ams')).toBe(resolveProviderGlyph('xiaomi'));
74
+ expect(resolveProviderGlyph('google-vertex')).toBe(resolveProviderGlyph('vertexai'));
75
+ expect(resolveProviderGlyph('openai-codex')).toBe(resolveProviderGlyph('codex'));
76
+ expect(resolveProviderGlyph('opencode-zen')).toBe(resolveProviderGlyph('opencode-go'));
77
+ expect(resolveProviderGlyph('gitlab-duo-agent')).toBe(resolveProviderGlyph('gitlab-duo'));
78
+ });
79
+
80
+ test('spelling differences fold to the same key', () => {
81
+ // omp reports ids, a models.yml slug is user-typed, and a display name is
82
+ // prose — all three reach this function.
83
+ for (const spelling of ['DeepSeek', 'deepseek', 'deep-seek', 'DEEP_SEEK', 'deepseek ']) {
84
+ expect(resolveProviderGlyph(spelling)).toBe('deepseek');
85
+ }
86
+ });
87
+
88
+ test('a stored icon key wins over the slug', () => {
89
+ // Legacy preset rows carry `icon: 'claude'` while their slug moved on.
90
+ expect(resolveProviderGlyph('claude', 'anthropic')).toBe('claude');
91
+ });
92
+
93
+ test('an unknown provider resolves to null rather than a wrong brand', () => {
94
+ expect(resolveProviderGlyph('my-private-gateway')).toBeNull();
95
+ expect(resolveProviderGlyph('')).toBeNull();
96
+ expect(resolveProviderGlyph(undefined, null)).toBeNull();
97
+ });
98
+ });
99
+
100
+ describe('providerInitials', () => {
101
+ test('takes one initial per word, joined with a single trailing dot', () => {
102
+ expect(providerInitials('Tooker')).toBe('t.');
103
+ expect(providerInitials('Toktok ID')).toBe('ti.');
104
+ expect(providerInitials('My Local Gateway')).toBe('ml.');
105
+ });
106
+
107
+ test('a hyphenated or underscored slug reads as separate words', () => {
108
+ expect(providerInitials('toktok-id')).toBe('ti.');
109
+ expect(providerInitials('tok_tok')).toBe('tt.');
110
+ });
111
+
112
+ test('an unlabelled provider still yields a visible badge', () => {
113
+ expect(providerInitials('')).toBe('?');
114
+ expect(providerInitials(' ')).toBe('?');
115
+ });
116
+ });