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,89 @@
1
+ import { Check } from 'lucide-preact';
2
+ import { PROVIDER_THINKING_EFFORTS, type ProviderThinkingEffort } from '@/shared/lib/models/provider/dialect';
3
+
4
+ interface EffortLadderProps {
5
+ /** Selected levels, in any order; the component renders them in omp's order. */
6
+ selected: ProviderThinkingEffort[];
7
+ onToggle: (effort: ProviderThinkingEffort) => void;
8
+ onSelectAll: () => void;
9
+ onClear: () => void;
10
+ }
11
+
12
+ /**
13
+ * The reasoning ladder a model accepts, as checkboxes.
14
+ *
15
+ * omp reads this as `thinking.efforts` and refuses to start a level that is not
16
+ * on the list, so the control is a capability declaration rather than a
17
+ * preference: ticking `low` and `high` means the model may be asked for those
18
+ * two and nothing else. `off` is deliberately not offered — omp's `EffortSchema`
19
+ * rejects it, and turning thinking off is a per-turn choice the chat composer
20
+ * already owns.
21
+ *
22
+ * An EMPTY selection is a valid and distinct state: it means "do not declare a
23
+ * ladder", which leaves omp on its own default for the model. That is why the
24
+ * hint changes with the selection instead of always describing a ladder.
25
+ */
26
+ export function EffortLadder({ selected, onToggle, onSelectAll, onClear }: EffortLadderProps) {
27
+ const isAll = selected.length === PROVIDER_THINKING_EFFORTS.length;
28
+
29
+ return (
30
+ <div>
31
+ <div className="flex items-center justify-between mb-1.5">
32
+ <label className="block text-xs font-semibold text-ink">Reasoning levels</label>
33
+ <div className="flex items-center gap-2 text-[10px]">
34
+ <button
35
+ type="button"
36
+ onClick={onSelectAll}
37
+ disabled={isAll}
38
+ className="text-ink/50 hover:text-ink transition-colors cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed"
39
+ >
40
+ all
41
+ </button>
42
+ <span className="text-ink/20">·</span>
43
+ <button
44
+ type="button"
45
+ onClick={onClear}
46
+ disabled={selected.length === 0}
47
+ className="text-ink/50 hover:text-ink transition-colors cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed"
48
+ >
49
+ none
50
+ </button>
51
+ </div>
52
+ </div>
53
+
54
+ <div className="grid grid-cols-3 gap-2">
55
+ {PROVIDER_THINKING_EFFORTS.map((effort) => {
56
+ const isOn = selected.includes(effort);
57
+ return (
58
+ <button
59
+ key={effort}
60
+ type="button"
61
+ onClick={() => onToggle(effort)}
62
+ aria-pressed={isOn}
63
+ className={`flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg border text-left text-[11px] transition-colors cursor-pointer ${
64
+ isOn
65
+ ? 'border-ink/50 bg-ink/5 font-semibold text-ink'
66
+ : 'border-ink/15 hover:border-ink/30 text-ink/70'
67
+ }`}
68
+ >
69
+ <span
70
+ className={`w-3 h-3 rounded-[3px] border flex items-center justify-center shrink-0 ${
71
+ isOn ? 'border-ink bg-ink text-canvas' : 'border-ink/30'
72
+ }`}
73
+ >
74
+ {isOn && <Check size={9} strokeWidth={3} />}
75
+ </span>
76
+ <span className="truncate">{effort}</span>
77
+ </button>
78
+ );
79
+ })}
80
+ </div>
81
+
82
+ <p className="text-[10px] text-ink/40 mt-1">
83
+ {selected.length === 0
84
+ ? 'No ladder declared — omp keeps its own default levels for this model.'
85
+ : `Only these levels will be offered: ${PROVIDER_THINKING_EFFORTS.filter((e) => selected.includes(e)).join(', ')}.`}
86
+ </p>
87
+ </div>
88
+ );
89
+ }
@@ -1,5 +1,5 @@
1
1
  import type { ProviderItem } from '@/shared/types';
2
- import { ProviderIcon } from '@/client/components/settings/categories/provider-settings/Icons';
2
+ import { ProviderIcon } from '@/client/components/common/provider-icon';
3
3
 
4
4
  interface ProviderHeaderProps {
5
5
  provider: ProviderItem;
@@ -9,7 +9,7 @@ export function ProviderHeader({ provider }: ProviderHeaderProps) {
9
9
  return (
10
10
  <div className="flex flex-col gap-1 pb-4">
11
11
  <div className="flex items-center gap-2.5">
12
- <ProviderIcon icon={provider.icon} size={22} />
12
+ <ProviderIcon icon={provider.icon} slug={provider.slug} name={provider.name} size={22} />
13
13
  <h2 className="text-xl font-bold tracking-tight text-ink">
14
14
  {provider.name}
15
15
  </h2>
@@ -1,7 +1,7 @@
1
1
  import { useEffect, useRef, useState } from 'preact/hooks';
2
2
  import { AlertCircle, CheckCircle2, Globe, Key, Loader2 } from 'lucide-preact';
3
3
  import type { ProviderItem } from '@/shared/types';
4
- import { ProviderIcon } from '@/client/components/settings/categories/provider-settings/Icons';
4
+ import { ProviderIcon } from '@/client/components/common/provider-icon';
5
5
  import { Modal } from '@/client/components/common/Modal';
6
6
 
7
7
  interface LoginModalProps {
@@ -132,7 +132,7 @@ export function LoginModal({ isOpen, provider, onClose, onAuthenticated }: Login
132
132
  onClose={onClose}
133
133
  header={
134
134
  <div className="flex items-center gap-2.5">
135
- <ProviderIcon icon={provider.icon} size={18} />
135
+ <ProviderIcon icon={provider.icon} slug={provider.slug} name={provider.name} size={18} />
136
136
  <div>
137
137
  <h3 className="text-sm font-semibold text-ink">Sign in to {provider.name}</h3>
138
138
  <p className="text-[11px] text-ink/50 font-mono">{provider.slug}</p>
@@ -1,15 +1,19 @@
1
1
  import { useState } from 'preact/hooks';
2
- import { Eye, EyeOff, Image as ImageIcon, RefreshCw, Search, Settings2, SlidersHorizontal } from 'lucide-preact';
2
+ import { Eye, EyeOff, Image as ImageIcon, Plus, RefreshCw, Search, Settings2, SlidersHorizontal } from 'lucide-preact';
3
3
  import type { ProviderModel } from '@/shared/types';
4
4
  import { formatContextWindow } from '@/shared/lib/code/format';
5
5
  import { formatPrice } from '@/shared/lib/format/number';
6
6
 
7
7
  interface ProviderModelsListProps {
8
8
  models: ProviderModel[];
9
+ /** Why the list is empty, when it legitimately is — never a generic excuse. */
10
+ emptyReason?: string;
9
11
  onToggleModelVisibility: (modelId: string) => void;
10
12
  onHideAll: () => void;
11
13
  onShowAll: () => void;
12
14
  onFetchModels: () => Promise<void>;
15
+ /** Opens the manual "Add Model" dialog. */
16
+ onAddModel: () => void;
13
17
  canFetchModels: boolean;
14
18
  isFetchingModels: boolean;
15
19
  onOpenModelConfig: (model: ProviderModel) => void;
@@ -18,10 +22,12 @@ interface ProviderModelsListProps {
18
22
 
19
23
  export function ProviderModelsList({
20
24
  models,
25
+ emptyReason,
21
26
  onToggleModelVisibility,
22
27
  onHideAll,
23
28
  onShowAll,
24
29
  onFetchModels,
30
+ onAddModel,
25
31
  canFetchModels,
26
32
  isFetchingModels,
27
33
  onOpenModelConfig,
@@ -52,6 +58,18 @@ export function ProviderModelsList({
52
58
  <RefreshCw size={12} className={isFetchingModels ? 'animate-spin' : ''} />
53
59
  <span>{isFetchingModels ? 'fetching...' : 'fetch models'}</span>
54
60
  </button>
61
+ {/* Manual registration, for the ids no listing endpoint reports:
62
+ a gateway model its /models omits, an Azure deployment name, a
63
+ provider with no listing route at all (Bedrock, Vertex). */}
64
+ <button
65
+ type="button"
66
+ onClick={onAddModel}
67
+ title="Register a model by hand"
68
+ className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded bg-ink/5 hover:bg-ink/10 text-[11px] font-medium text-ink/80 hover:text-ink transition-colors cursor-pointer border border-ink/10"
69
+ >
70
+ <Plus size={12} />
71
+ <span>add model</span>
72
+ </button>
55
73
  <button
56
74
  type="button"
57
75
  onClick={onHideAll}
@@ -94,7 +112,9 @@ export function ProviderModelsList({
94
112
  <div className="space-y-1 pt-1">
95
113
  {filteredModels.length === 0 ? (
96
114
  <div className="py-8 text-center text-xs text-ink/40">
97
- No models found matching "{searchQuery}"
115
+ {searchQuery
116
+ ? `No models found matching "${searchQuery}"`
117
+ : emptyReason || 'No models registered for this provider yet.'}
98
118
  </div>
99
119
  ) : (
100
120
  filteredModels.map((model) => (
@@ -0,0 +1,82 @@
1
+ import { useState } from 'preact/hooks';
2
+ import { Check, ChevronDown } from 'lucide-preact';
3
+ import type { PresetProviderOption } from '@/shared/types/settings/provider';
4
+ import { groupProviderPresets } from '@/shared/lib/models/provider/presets';
5
+ import { ProviderIcon } from '@/client/components/common/provider-icon';
6
+
7
+ interface PresetPickerProps {
8
+ presets: PresetProviderOption[];
9
+ selectedPresetId: string;
10
+ onSelectPreset: (preset: PresetProviderOption) => void;
11
+ }
12
+
13
+ /**
14
+ * The provider picker, grouped by kind.
15
+ *
16
+ * Groups matter because the presets are not interchangeable: a local engine is
17
+ * keyless and discovered by omp, a gateway is fetched and written into
18
+ * models.yml, and an "Other dialects" entry exists precisely because the
19
+ * endpoint is NOT OpenAI-shaped. Listing them in one flat grid made a keyless
20
+ * Ollama row look like a hosted API that would ask for a key.
21
+ */
22
+ export function PresetPicker({ presets, selectedPresetId, onSelectPreset }: PresetPickerProps) {
23
+ const [collapsed, setCollapsed] = useState<Record<string, boolean>>({});
24
+ const groups = groupProviderPresets(presets);
25
+
26
+ if (presets.length === 0) {
27
+ return (
28
+ <p className="px-2 py-3 text-xs text-ink/50">
29
+ Every known provider is already configured.
30
+ </p>
31
+ );
32
+ }
33
+
34
+ return (
35
+ <div className="max-h-52 scrollbar-overlay-container scrollbar-overlay-static pr-1 space-y-2">
36
+ {groups.map((entry) => {
37
+ const isCollapsed = collapsed[entry.group] === true;
38
+ return (
39
+ <div key={entry.group}>
40
+ <button
41
+ type="button"
42
+ onClick={() => setCollapsed((prev) => ({ ...prev, [entry.group]: !isCollapsed }))}
43
+ className="w-full flex items-center gap-1.5 px-0.5 py-1 text-[10px] font-semibold tracking-wider text-ink/50 uppercase hover:text-ink/80 transition-colors cursor-pointer"
44
+ >
45
+ <ChevronDown
46
+ size={11}
47
+ className={`transition-transform ${isCollapsed ? '-rotate-90' : ''}`}
48
+ />
49
+ <span>{entry.group}</span>
50
+ <span className="text-ink/30 normal-case tracking-normal">{entry.options.length}</span>
51
+ </button>
52
+
53
+ {!isCollapsed && (
54
+ <div className="grid grid-cols-2 gap-2 pt-1">
55
+ {entry.options.map((preset) => {
56
+ const isSelected = preset.id === selectedPresetId;
57
+ return (
58
+ <button
59
+ key={preset.id}
60
+ type="button"
61
+ onClick={() => onSelectPreset(preset)}
62
+ title={preset.defaultUrl}
63
+ className={`flex items-center gap-2 p-2 rounded-lg border text-left text-xs transition-colors cursor-pointer ${
64
+ isSelected
65
+ ? 'border-ink/50 bg-ink/5 font-semibold text-ink shadow-2xs'
66
+ : 'border-ink/15 hover:border-ink/30 text-ink/70'
67
+ }`}
68
+ >
69
+ <ProviderIcon icon={preset.icon} slug={preset.slug} name={preset.name} size={14} />
70
+ <span className="truncate flex-1">{preset.name}</span>
71
+ {isSelected && <Check size={12} className="shrink-0 text-ink" />}
72
+ </button>
73
+ );
74
+ })}
75
+ </div>
76
+ )}
77
+ </div>
78
+ );
79
+ })}
80
+ </div>
81
+ );
82
+ }
@@ -0,0 +1,107 @@
1
+ import { DollarSign } from 'lucide-preact';
2
+
3
+ interface PriceFieldsProps {
4
+ costInput: string;
5
+ costOutput: string;
6
+ costCacheRead: string;
7
+ costCacheWrite: string;
8
+ onCostInputChange: (value: string) => void;
9
+ onCostOutputChange: (value: string) => void;
10
+ onCostCacheReadChange: (value: string) => void;
11
+ onCostCacheWriteChange: (value: string) => void;
12
+ error?: string;
13
+ }
14
+
15
+ /** One labelled price box. `hint` distinguishes the four rates. */
16
+ function PriceField({
17
+ value,
18
+ onChange,
19
+ label,
20
+ hint,
21
+ }: {
22
+ value: string;
23
+ onChange: (value: string) => void;
24
+ label: string;
25
+ hint: string;
26
+ }) {
27
+ return (
28
+ <div>
29
+ <label className="block text-[11px] font-medium text-ink/70 mb-1" title={hint}>
30
+ {label}
31
+ </label>
32
+ <div className="relative flex items-center">
33
+ <DollarSign size={12} className="absolute left-2.5 text-ink/40 pointer-events-none" />
34
+ <input
35
+ type="text"
36
+ inputMode="decimal"
37
+ value={value}
38
+ onChange={(e) => onChange(e.currentTarget.value)}
39
+ placeholder="0"
40
+ className="w-full bg-paper border border-ink/20 rounded-md pl-7 pr-2 py-1.5 text-xs text-ink outline-none focus:border-ink/60 font-mono"
41
+ />
42
+ </div>
43
+ </div>
44
+ );
45
+ }
46
+
47
+ /**
48
+ * The four `models.yml` cost rates, in USD per 1M tokens.
49
+ *
50
+ * omp requires all four fields together, so input/output are the pair that
51
+ * decides whether a price is written at all; the two cache rates then default
52
+ * to 0, which is the honest value for "no cached-token discount". Cache rates
53
+ * matter for accounting rather than routing: without them every cached turn is
54
+ * billed at the full input rate, which is why they are worth asking for
55
+ * separately instead of deriving from input.
56
+ */
57
+ export function PriceFields({
58
+ costInput,
59
+ costOutput,
60
+ costCacheRead,
61
+ costCacheWrite,
62
+ onCostInputChange,
63
+ onCostOutputChange,
64
+ onCostCacheReadChange,
65
+ onCostCacheWriteChange,
66
+ error,
67
+ }: PriceFieldsProps) {
68
+ return (
69
+ <div>
70
+ <label className="block text-xs font-semibold text-ink mb-1.5">
71
+ Price <span className="font-normal text-ink/40">— USD per 1M tokens, optional</span>
72
+ </label>
73
+ <div className="grid grid-cols-2 gap-3">
74
+ <PriceField
75
+ value={costInput}
76
+ onChange={onCostInputChange}
77
+ label="Input"
78
+ hint="Uncached prompt tokens"
79
+ />
80
+ <PriceField
81
+ value={costOutput}
82
+ onChange={onCostOutputChange}
83
+ label="Output"
84
+ hint="Generated tokens"
85
+ />
86
+ <PriceField
87
+ value={costCacheRead}
88
+ onChange={onCostCacheReadChange}
89
+ label="Cache read"
90
+ hint="Prompt tokens served from the provider's cache — usually a fraction of the input rate"
91
+ />
92
+ <PriceField
93
+ value={costCacheWrite}
94
+ onChange={onCostCacheWriteChange}
95
+ label="Cache write"
96
+ hint="Prompt tokens written into the provider's cache"
97
+ />
98
+ </div>
99
+ {error && <p className="text-[11px] text-error mt-1">{error}</p>}
100
+ <p className="text-[10px] text-ink/40 mt-1">
101
+ Used for omp's cost accounting. Input and output decide whether a price is
102
+ written at all; the cache rates fall back to 0, which bills cached turns at
103
+ the full input rate.
104
+ </p>
105
+ </div>
106
+ );
107
+ }
@@ -1,9 +1,9 @@
1
1
  import { useState } from 'preact/hooks';
2
2
  import type { FormEvent } from 'preact/compat';
3
- import { AlertCircle, CheckCircle2, DownloadCloud, Globe, Key, RefreshCw } from 'lucide-preact';
3
+ import { AlertCircle, CheckCircle2, DownloadCloud, Globe, Info, Key, RefreshCw } from 'lucide-preact';
4
4
  import type { ProviderItem } from '@/shared/types';
5
- import { fetchProviderModelsRemote } from '@/shared/lib/models/provider-models';
6
- import { ProviderIcon } from '@/client/components/settings/categories/provider-settings/Icons';
5
+ import { fetchProviderModelsRemote } from '@/shared/lib/models/provider/models';
6
+ import { ProviderIcon } from '@/client/components/common/provider-icon';
7
7
  import { Modal } from '@/client/components/common/Modal';
8
8
 
9
9
  interface ReconnectModalProps {
@@ -23,6 +23,7 @@ export function ReconnectModal({
23
23
  onReconnect,
24
24
  onFetchModels,
25
25
  }: ReconnectModalProps) {
26
+ const isKeyless = provider.auth === 'none';
26
27
  const [apiKey, setApiKey] = useState(provider.apiKey || 'sk-••••••••••••••••••••••••');
27
28
  const [baseUrl, setBaseUrl] = useState(provider.baseUrl || 'https://api.openai.com/v1');
28
29
  const [isTesting, setIsTesting] = useState(false);
@@ -39,7 +40,11 @@ export function ReconnectModal({
39
40
  const handleTestConnection = async () => {
40
41
  setIsTesting(true);
41
42
  setTestResult(null);
42
- const result = await fetchProviderModelsRemote(baseUrl, draftCredentials.apiKey);
43
+ const result = await fetchProviderModelsRemote({
44
+ baseUrl,
45
+ ...(draftCredentials.apiKey ? { apiKey: draftCredentials.apiKey } : {}),
46
+ ...(provider.api ? { api: provider.api } : {}),
47
+ });
43
48
  setIsTesting(false);
44
49
  setTestResult(result.ok ? 'success' : 'failed');
45
50
  };
@@ -47,7 +52,10 @@ export function ReconnectModal({
47
52
  const handleSave = async (e: FormEvent) => {
48
53
  e.preventDefault();
49
54
  onReconnect({
50
- apiKey,
55
+ // A keyless provider is configured by `auth: none`; storing the masked
56
+ // placeholder as its credential would show a key that authenticates
57
+ // nothing.
58
+ ...(isKeyless ? {} : { apiKey }),
51
59
  baseUrl,
52
60
  status: 'connected',
53
61
  });
@@ -60,7 +68,7 @@ export function ReconnectModal({
60
68
  onClose={onClose}
61
69
  header={
62
70
  <div className="flex items-center gap-2.5">
63
- <ProviderIcon icon={provider.icon} size={18} />
71
+ <ProviderIcon icon={provider.icon} slug={provider.slug} name={provider.name} size={18} />
64
72
  <div>
65
73
  <h3 className="text-sm font-semibold text-ink">
66
74
  Authentication & Credentials
@@ -108,21 +116,33 @@ export function ReconnectModal({
108
116
  </div>
109
117
  </div>
110
118
 
111
- {/* API Key */}
112
- <div>
113
- <label className="block text-xs font-semibold text-ink mb-1">
114
- API Key / Auth Token
115
- </label>
116
- <div className="relative flex items-center">
117
- <Key size={13} className="absolute left-2.5 text-ink/40 pointer-events-none" />
118
- <input
119
- type="text"
120
- value={apiKey}
121
- onChange={(e) => setApiKey(e.currentTarget.value)}
122
- className="w-full bg-paper border border-ink/20 rounded-md pl-8 pr-3 py-1.5 text-xs text-ink outline-none focus:border-ink/60 font-mono"
123
- />
119
+ {/* API Key — omitted for a keyless provider, which omp configures by
120
+ `auth: none` and which has no credential to store. */}
121
+ {isKeyless ? (
122
+ <div className="flex items-start gap-2 px-3 py-2 rounded-md bg-ink/5 border border-ink/10 text-[11px] text-ink/60">
123
+ <Info size={13} className="mt-0.5 flex-shrink-0 text-ink/50" />
124
+ <span>
125
+ This provider is registered with <span className="font-mono">auth: none</span>, so omp
126
+ sends no credential. Set Auth to “API key” in the provider’s own entry if the endpoint
127
+ starts requiring one.
128
+ </span>
124
129
  </div>
125
- </div>
130
+ ) : (
131
+ <div>
132
+ <label className="block text-xs font-semibold text-ink mb-1">
133
+ API Key / Auth Token
134
+ </label>
135
+ <div className="relative flex items-center">
136
+ <Key size={13} className="absolute left-2.5 text-ink/40 pointer-events-none" />
137
+ <input
138
+ type="text"
139
+ value={apiKey}
140
+ onChange={(e) => setApiKey(e.currentTarget.value)}
141
+ className="w-full bg-paper border border-ink/20 rounded-md pl-8 pr-3 py-1.5 text-xs text-ink outline-none focus:border-ink/60 font-mono"
142
+ />
143
+ </div>
144
+ </div>
145
+ )}
126
146
 
127
147
  {/* Test connection indicator */}
128
148
  <div className="flex items-center justify-between pt-1">
@@ -154,8 +174,9 @@ export function ReconnectModal({
154
174
  <div className="flex items-start gap-2 px-3 py-2 rounded-md bg-ink/5 border border-ink/10 text-[11px] text-ink/60">
155
175
  <DownloadCloud size={13} className="mt-0.5 flex-shrink-0 text-ink/50" />
156
176
  <span>
157
- On save, models are fetched from the endpoint and merged into the
158
- list below — existing models are kept, only new ones are added.
177
+ {isKeyless
178
+ ? 'On save, models are fetched from the endpoint and merged into the list below — a keyless server needs none to answer.'
179
+ : 'On save, models are fetched from the endpoint and merged into the list below — existing models are kept, only new ones are added.'}
159
180
  </span>
160
181
  </div>
161
182
  </Modal>
@@ -2,7 +2,7 @@
2
2
  import { useRef, useState } from 'preact/hooks';
3
3
  import { Check, ChevronDown, Folder, Plus } from 'lucide-preact';
4
4
  import type { ProviderItem } from '@/shared/types';
5
- import { ProviderIcon } from '@/client/components/settings/categories/provider-settings/Icons';
5
+ import { ProviderIcon } from '@/client/components/common/provider-icon';
6
6
  import { useOnClickOutside } from '@/client/hooks/ui/on-click-outside';
7
7
 
8
8
  interface ProviderSidebarListProps {
@@ -115,7 +115,7 @@ export function ProviderSidebarList({
115
115
  }`}
116
116
  >
117
117
  <div className="flex items-center gap-2.5 min-w-0">
118
- <ProviderIcon icon={item.icon} size={15} className="flex-shrink-0" />
118
+ <ProviderIcon icon={item.icon} slug={item.slug} name={item.name} size={15} className="flex-shrink-0" />
119
119
  <span className="text-[12px] truncate">{item.name}</span>
120
120
  </div>
121
121
 
@@ -0,0 +1,115 @@
1
+ import type { UsageLimitWindow } from '@/shared/types';
2
+ import { formatDateTime, formatFractionPercent, formatUsageAmount } from '@/client/components/settings/categories/usage-settings/format';
3
+
4
+ interface LimitRowProps {
5
+ limit: UsageLimitWindow;
6
+ }
7
+
8
+ /** One provider-reported quota window: label, bar, used/remaining, reset. */
9
+ function LimitRow({ limit }: LimitRowProps) {
10
+ const percent = Math.min(100, Math.max(0, (limit.usedFraction ?? 0) * 100));
11
+ const scope = limit.modelId ?? limit.accountId;
12
+ const usedText = limit.used !== undefined ? formatUsageAmount(limit.used, limit.unit) : undefined;
13
+ const limitText = limit.limit !== undefined ? formatUsageAmount(limit.limit, limit.unit) : undefined;
14
+ const remainingText = limit.remaining !== undefined
15
+ ? formatUsageAmount(limit.remaining, limit.unit)
16
+ : undefined;
17
+
18
+ return (
19
+ <div className="bg-paper border border-ink/15 rounded-lg p-3 shadow-xs space-y-2">
20
+ <div className="flex items-start justify-between gap-2">
21
+ <div className="min-w-0">
22
+ <div className="text-[11px] font-semibold text-ink truncate">{limit.label}</div>
23
+ {scope && <div className="text-[10px] text-ink/50 truncate">{scope}</div>}
24
+ </div>
25
+ <span className="text-[10px] text-ink/50 flex-shrink-0 whitespace-nowrap">
26
+ {limit.resetsAt !== undefined ? `Resets ${formatDateTime(new Date(limit.resetsAt).toISOString())}` : ''}
27
+ </span>
28
+ </div>
29
+
30
+ <div className="h-1.5 w-full bg-ink/10 rounded-full overflow-hidden">
31
+ <div
32
+ className={`h-full rounded-full ${limit.status === 'exhausted' ? 'bg-error/70' : 'bg-ink/80'}`}
33
+ style={{ width: `${percent}%` }}
34
+ />
35
+ </div>
36
+
37
+ <div className="flex flex-wrap items-center justify-between gap-x-3 gap-y-0.5 text-[10px] text-ink/60">
38
+ <span>
39
+ Used{' '}
40
+ <span className="font-semibold text-ink">
41
+ {limit.unit === 'percent' || !usedText ? formatFractionPercent(limit.usedFraction ?? 0) : usedText}
42
+ </span>
43
+ {/* A percent unit is already a share of 100, so naming the limit
44
+ again would print "42% of 100%". */}
45
+ {limit.unit !== 'percent' && usedText && limitText ? ` of ${limitText}` : ''}
46
+ </span>
47
+ <span className="flex items-center gap-2">
48
+ {limit.status !== 'ok' && limit.status !== 'unknown' && (
49
+ <span className="text-[10px] font-mono uppercase px-1.5 py-0.5 rounded bg-ink/10 text-ink/70">
50
+ {limit.status}
51
+ </span>
52
+ )}
53
+ {remainingText && (
54
+ <span>
55
+ Remaining <span className="font-semibold text-ink">{remainingText}</span>
56
+ </span>
57
+ )}
58
+ </span>
59
+ </div>
60
+
61
+ {limit.notes && limit.notes.length > 0 && (
62
+ <ul className="text-[10px] text-ink/50 leading-relaxed space-y-0.5">
63
+ {limit.notes.map((note) => (
64
+ <li key={note}>{note}</li>
65
+ ))}
66
+ </ul>
67
+ )}
68
+ </div>
69
+ );
70
+ }
71
+
72
+ interface LimitsSectionProps {
73
+ limits: UsageLimitWindow[];
74
+ /** Whether omp ships a usage adapter for this provider. */
75
+ tracked: boolean;
76
+ /** omp tracks this provider but its endpoint returned nothing. */
77
+ unavailable?: boolean;
78
+ /** Provider-wide caveats shown above the windows. */
79
+ notes?: string[];
80
+ }
81
+
82
+ /** Generic quota windows for any provider omp reports usage for. */
83
+ export function LimitsSection({ limits, tracked, unavailable, notes }: LimitsSectionProps) {
84
+ return (
85
+ <div className="space-y-2">
86
+ <h4 className="text-[10px] font-mono font-semibold uppercase tracking-wider text-ink/40">
87
+ Quota windows
88
+ </h4>
89
+
90
+ {notes && notes.length > 0 && (
91
+ <ul className="text-[10px] text-ink/50 leading-relaxed space-y-0.5">
92
+ {notes.map((note) => (
93
+ <li key={note}>{note}</li>
94
+ ))}
95
+ </ul>
96
+ )}
97
+
98
+ {limits.length === 0 ? (
99
+ <p className="text-[11px] text-ink/50">
100
+ {!tracked
101
+ ? 'No quota source for this provider: omp ships no usage adapter for it and it exposes no balance endpoint the chamber knows.'
102
+ : unavailable
103
+ ? 'omp tracks this provider but its usage endpoint returned no data — an invalid or expired credential, an unreachable endpoint, or a plan without quota windows.'
104
+ : 'No quota windows were reported for this provider.'}
105
+ </p>
106
+ ) : (
107
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-2.5">
108
+ {limits.map((limit) => (
109
+ <LimitRow key={limit.id} limit={limit} />
110
+ ))}
111
+ </div>
112
+ )}
113
+ </div>
114
+ );
115
+ }