@clinebot/core 0.0.35 → 0.0.36

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 (335) hide show
  1. package/README.md +1 -2
  2. package/dist/ClineCore.d.ts +53 -39
  3. package/dist/ClineCore.d.ts.map +1 -1
  4. package/dist/account/index.d.ts +1 -1
  5. package/dist/account/index.d.ts.map +1 -1
  6. package/dist/account/rpc.d.ts +6 -6
  7. package/dist/account/rpc.d.ts.map +1 -1
  8. package/dist/cron/index.d.ts +6 -0
  9. package/dist/cron/index.d.ts.map +1 -0
  10. package/dist/cron/resource-limiter.d.ts +9 -0
  11. package/dist/cron/resource-limiter.d.ts.map +1 -0
  12. package/dist/cron/schedule-command-service.d.ts +10 -0
  13. package/dist/cron/schedule-command-service.d.ts.map +1 -0
  14. package/dist/cron/schedule-service.d.ts +100 -0
  15. package/dist/cron/schedule-service.d.ts.map +1 -0
  16. package/dist/cron/scheduler.d.ts +66 -0
  17. package/dist/cron/scheduler.d.ts.map +1 -0
  18. package/dist/cron/sqlite-schedule-store.d.ts +52 -0
  19. package/dist/cron/sqlite-schedule-store.d.ts.map +1 -0
  20. package/dist/extensions/config/agent-config-loader.d.ts +4 -3
  21. package/dist/extensions/config/agent-config-loader.d.ts.map +1 -1
  22. package/dist/extensions/config/runtime-commands.d.ts +1 -0
  23. package/dist/extensions/config/runtime-commands.d.ts.map +1 -1
  24. package/dist/extensions/config/user-instruction-config-loader.d.ts +1 -0
  25. package/dist/extensions/config/user-instruction-config-loader.d.ts.map +1 -1
  26. package/dist/extensions/context/agentic-compaction.d.ts +2 -2
  27. package/dist/extensions/context/agentic-compaction.d.ts.map +1 -1
  28. package/dist/extensions/context/compaction-shared.d.ts +5 -4
  29. package/dist/extensions/context/compaction-shared.d.ts.map +1 -1
  30. package/dist/extensions/context/compaction.d.ts.map +1 -1
  31. package/dist/extensions/plugin/plugin-config-loader.d.ts +9 -2
  32. package/dist/extensions/plugin/plugin-config-loader.d.ts.map +1 -1
  33. package/dist/extensions/plugin/plugin-loader.d.ts +5 -3
  34. package/dist/extensions/plugin/plugin-loader.d.ts.map +1 -1
  35. package/dist/extensions/plugin/plugin-module-import.d.ts.map +1 -1
  36. package/dist/extensions/plugin/plugin-sandbox.d.ts +15 -2
  37. package/dist/extensions/plugin/plugin-sandbox.d.ts.map +1 -1
  38. package/dist/extensions/plugin/plugin-targeting.d.ts +7 -0
  39. package/dist/extensions/plugin/plugin-targeting.d.ts.map +1 -0
  40. package/dist/extensions/plugin-sandbox-bootstrap.js +211 -211
  41. package/dist/extensions/tools/definitions.d.ts +1 -1
  42. package/dist/extensions/tools/definitions.d.ts.map +1 -1
  43. package/dist/extensions/tools/executors/apply-patch.d.ts +3 -1
  44. package/dist/extensions/tools/executors/apply-patch.d.ts.map +1 -1
  45. package/dist/extensions/tools/executors/search.d.ts +1 -1
  46. package/dist/extensions/tools/executors/search.d.ts.map +1 -1
  47. package/dist/extensions/tools/index.d.ts +2 -0
  48. package/dist/extensions/tools/index.d.ts.map +1 -1
  49. package/dist/extensions/tools/presets.d.ts +26 -43
  50. package/dist/extensions/tools/presets.d.ts.map +1 -1
  51. package/dist/extensions/tools/runtime.d.ts +25 -0
  52. package/dist/extensions/tools/runtime.d.ts.map +1 -0
  53. package/dist/extensions/tools/schemas.d.ts.map +1 -1
  54. package/dist/extensions/tools/team/team-tools.d.ts +1 -0
  55. package/dist/extensions/tools/team/team-tools.d.ts.map +1 -1
  56. package/dist/hooks/hook-file-hooks.d.ts +4 -1
  57. package/dist/hooks/hook-file-hooks.d.ts.map +1 -1
  58. package/dist/hooks/index.d.ts +0 -1
  59. package/dist/hooks/index.d.ts.map +1 -1
  60. package/dist/hooks/subprocess.d.ts +8 -1
  61. package/dist/hooks/subprocess.d.ts.map +1 -1
  62. package/dist/hub/browser-websocket.d.ts +18 -0
  63. package/dist/hub/browser-websocket.d.ts.map +1 -0
  64. package/dist/hub/client.d.ts +45 -0
  65. package/dist/hub/client.d.ts.map +1 -0
  66. package/dist/hub/connect.d.ts +15 -0
  67. package/dist/hub/connect.d.ts.map +1 -0
  68. package/dist/hub/daemon-entry.d.ts +2 -0
  69. package/dist/hub/daemon-entry.d.ts.map +1 -0
  70. package/dist/hub/daemon-entry.js +1045 -0
  71. package/dist/hub/daemon.d.ts +5 -0
  72. package/dist/hub/daemon.d.ts.map +1 -0
  73. package/dist/hub/defaults.d.ts +13 -0
  74. package/dist/hub/defaults.d.ts.map +1 -0
  75. package/dist/hub/discovery.d.ts +29 -0
  76. package/dist/hub/discovery.d.ts.map +1 -0
  77. package/dist/hub/index.d.ts +15 -0
  78. package/dist/hub/index.d.ts.map +1 -0
  79. package/dist/hub/index.js +1044 -0
  80. package/dist/hub/native-transport.d.ts +17 -0
  81. package/dist/hub/native-transport.d.ts.map +1 -0
  82. package/dist/hub/runtime-handlers.d.ts +11 -0
  83. package/dist/hub/runtime-handlers.d.ts.map +1 -0
  84. package/dist/hub/server.d.ts +86 -0
  85. package/dist/hub/server.d.ts.map +1 -0
  86. package/dist/hub/session-client.d.ts +87 -0
  87. package/dist/hub/session-client.d.ts.map +1 -0
  88. package/dist/hub/start-shared-server.d.ts +19 -0
  89. package/dist/hub/start-shared-server.d.ts.map +1 -0
  90. package/dist/hub/transport.d.ts +8 -0
  91. package/dist/hub/transport.d.ts.map +1 -0
  92. package/dist/hub/ui-client.d.ts +44 -0
  93. package/dist/hub/ui-client.d.ts.map +1 -0
  94. package/dist/hub/workspace.d.ts +4 -0
  95. package/dist/hub/workspace.d.ts.map +1 -0
  96. package/dist/index.d.ts +26 -15
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +498 -476
  99. package/dist/llms/configured-provider-registry.d.ts +28 -0
  100. package/dist/llms/configured-provider-registry.d.ts.map +1 -0
  101. package/dist/llms/provider-defaults.d.ts +27 -0
  102. package/dist/llms/provider-defaults.d.ts.map +1 -0
  103. package/dist/llms/provider-settings.d.ts +202 -0
  104. package/dist/llms/provider-settings.d.ts.map +1 -0
  105. package/dist/llms/runtime-config.d.ts +4 -0
  106. package/dist/llms/runtime-config.d.ts.map +1 -0
  107. package/dist/llms/runtime-registry.d.ts +20 -0
  108. package/dist/llms/runtime-registry.d.ts.map +1 -0
  109. package/dist/llms/runtime-types.d.ts +85 -0
  110. package/dist/llms/runtime-types.d.ts.map +1 -0
  111. package/dist/runtime/host.d.ts +1 -2
  112. package/dist/runtime/host.d.ts.map +1 -1
  113. package/dist/runtime/rules.d.ts +1 -0
  114. package/dist/runtime/rules.d.ts.map +1 -1
  115. package/dist/runtime/runtime-builder.d.ts.map +1 -1
  116. package/dist/runtime/runtime-host.d.ts +22 -24
  117. package/dist/runtime/runtime-host.d.ts.map +1 -1
  118. package/dist/runtime/runtime-oauth-token-manager.d.ts.map +1 -1
  119. package/dist/runtime/session-runtime.d.ts +1 -19
  120. package/dist/runtime/session-runtime.d.ts.map +1 -1
  121. package/dist/services/global-settings.d.ts +12 -0
  122. package/dist/services/global-settings.d.ts.map +1 -0
  123. package/dist/services/local-runtime-bootstrap.d.ts +9 -3
  124. package/dist/services/local-runtime-bootstrap.d.ts.map +1 -1
  125. package/dist/services/plugin-tools.d.ts +16 -0
  126. package/dist/services/plugin-tools.d.ts.map +1 -0
  127. package/dist/services/providers/local-provider-registry.d.ts +4 -4
  128. package/dist/services/providers/local-provider-registry.d.ts.map +1 -1
  129. package/dist/services/providers/local-provider-service.d.ts +13 -13
  130. package/dist/services/providers/local-provider-service.d.ts.map +1 -1
  131. package/dist/services/session-data.d.ts +1 -1
  132. package/dist/services/session-data.d.ts.map +1 -1
  133. package/dist/services/storage/provider-settings-legacy-migration.d.ts +1 -1
  134. package/dist/services/storage/provider-settings-legacy-migration.d.ts.map +1 -1
  135. package/dist/services/telemetry/index.js +28 -15
  136. package/dist/services/workspace-manifest.d.ts +11 -0
  137. package/dist/services/workspace-manifest.d.ts.map +1 -1
  138. package/dist/session/persistence-service.d.ts +11 -23
  139. package/dist/session/persistence-service.d.ts.map +1 -1
  140. package/dist/session/session-manifest-store.d.ts +22 -0
  141. package/dist/session/session-manifest-store.d.ts.map +1 -0
  142. package/dist/session/session-row.d.ts +93 -0
  143. package/dist/session/session-row.d.ts.map +1 -0
  144. package/dist/session/session-service.d.ts +2 -102
  145. package/dist/session/session-service.d.ts.map +1 -1
  146. package/dist/session/subagent-session-manager.d.ts +36 -0
  147. package/dist/session/subagent-session-manager.d.ts.map +1 -0
  148. package/dist/session/team-persistence-store.d.ts +24 -0
  149. package/dist/session/team-persistence-store.d.ts.map +1 -0
  150. package/dist/transports/hub.d.ts +47 -0
  151. package/dist/transports/hub.d.ts.map +1 -0
  152. package/dist/transports/local.d.ts +10 -6
  153. package/dist/transports/local.d.ts.map +1 -1
  154. package/dist/transports/remote.d.ts +10 -0
  155. package/dist/transports/remote.d.ts.map +1 -0
  156. package/dist/transports/runtime-host-support.d.ts +3 -2
  157. package/dist/transports/runtime-host-support.d.ts.map +1 -1
  158. package/dist/types/chat-schema.d.ts +10 -12
  159. package/dist/types/chat-schema.d.ts.map +1 -1
  160. package/dist/types/config.d.ts +8 -7
  161. package/dist/types/config.d.ts.map +1 -1
  162. package/dist/types/provider-settings.d.ts +4 -5
  163. package/dist/types/provider-settings.d.ts.map +1 -1
  164. package/dist/types/session.d.ts +2 -1
  165. package/dist/types/session.d.ts.map +1 -1
  166. package/dist/types.d.ts +8 -1
  167. package/dist/types.d.ts.map +1 -1
  168. package/package.json +20 -6
  169. package/src/ClineCore.ts +68 -40
  170. package/src/account/index.ts +3 -3
  171. package/src/account/rpc.ts +12 -12
  172. package/src/cron/index.ts +5 -0
  173. package/src/cron/resource-limiter.ts +46 -0
  174. package/src/cron/schedule-command-service.ts +193 -0
  175. package/src/cron/schedule-service.ts +703 -0
  176. package/src/cron/scheduler.ts +637 -0
  177. package/src/cron/sqlite-schedule-store.ts +708 -0
  178. package/src/extensions/config/agent-config-loader.ts +17 -7
  179. package/src/extensions/config/runtime-commands.ts +6 -0
  180. package/src/extensions/config/user-instruction-config-loader.ts +1 -0
  181. package/src/extensions/context/agentic-compaction.ts +3 -3
  182. package/src/extensions/context/basic-compaction.ts +2 -2
  183. package/src/extensions/context/compaction-shared.ts +5 -4
  184. package/src/extensions/context/compaction.ts +3 -3
  185. package/src/extensions/plugin/plugin-config-loader.ts +17 -2
  186. package/src/extensions/plugin/plugin-loader.ts +48 -4
  187. package/src/extensions/plugin/plugin-module-import.ts +0 -2
  188. package/src/extensions/plugin/plugin-sandbox-bootstrap.ts +93 -39
  189. package/src/extensions/plugin/plugin-sandbox.ts +47 -27
  190. package/src/extensions/plugin/plugin-targeting.ts +32 -0
  191. package/src/extensions/tools/definitions.ts +30 -49
  192. package/src/extensions/tools/executors/apply-patch.ts +69 -80
  193. package/src/extensions/tools/executors/search.ts +195 -3
  194. package/src/extensions/tools/index.ts +10 -0
  195. package/src/extensions/tools/presets.ts +31 -46
  196. package/src/extensions/tools/runtime.ts +261 -0
  197. package/src/extensions/tools/schemas.ts +4 -2
  198. package/src/extensions/tools/team/team-tools.ts +21 -0
  199. package/src/hooks/hook-file-hooks.ts +8 -2
  200. package/src/hooks/index.ts +0 -7
  201. package/src/hooks/subprocess-runner.ts +1 -1
  202. package/src/hooks/subprocess.ts +9 -0
  203. package/src/hub/browser-websocket.ts +137 -0
  204. package/src/hub/client.ts +574 -0
  205. package/src/hub/connect.ts +156 -0
  206. package/src/hub/daemon-entry.ts +87 -0
  207. package/src/hub/daemon.ts +181 -0
  208. package/src/hub/defaults.ts +43 -0
  209. package/src/hub/discovery.ts +247 -0
  210. package/src/hub/index.ts +14 -0
  211. package/src/hub/native-transport.ts +31 -0
  212. package/src/hub/runtime-handlers.ts +140 -0
  213. package/src/hub/server.ts +1888 -0
  214. package/src/hub/session-client.ts +460 -0
  215. package/src/hub/start-shared-server.ts +58 -0
  216. package/src/hub/transport.ts +14 -0
  217. package/src/hub/ui-client.ts +122 -0
  218. package/src/hub/workspace.ts +19 -0
  219. package/src/index.ts +124 -68
  220. package/src/llms/configured-provider-registry.ts +193 -0
  221. package/src/llms/provider-defaults.ts +637 -0
  222. package/src/llms/provider-settings.ts +263 -0
  223. package/src/llms/runtime-config.ts +43 -0
  224. package/src/llms/runtime-registry.ts +171 -0
  225. package/src/llms/runtime-types.ts +121 -0
  226. package/src/runtime/host.ts +107 -269
  227. package/src/runtime/index.ts +1 -0
  228. package/src/runtime/rules.ts +12 -0
  229. package/src/runtime/runtime-builder.ts +24 -8
  230. package/src/runtime/runtime-host.ts +89 -61
  231. package/src/runtime/runtime-oauth-token-manager.ts +11 -15
  232. package/src/runtime/session-runtime.ts +0 -24
  233. package/src/services/global-settings.ts +122 -0
  234. package/src/services/local-runtime-bootstrap.ts +51 -13
  235. package/src/services/plugin-tools.ts +85 -0
  236. package/src/services/providers/local-provider-registry.ts +6 -6
  237. package/src/services/providers/local-provider-service.ts +42 -37
  238. package/src/services/session-data.ts +15 -9
  239. package/src/services/storage/provider-settings-legacy-migration.ts +6 -4
  240. package/src/services/storage/provider-settings-manager.ts +1 -1
  241. package/src/services/workspace-manifest.ts +18 -0
  242. package/src/session/file-session-service.ts +1 -1
  243. package/src/session/index.ts +6 -27
  244. package/src/session/persistence-service.ts +119 -504
  245. package/src/session/session-manifest-store.ts +158 -0
  246. package/src/session/session-row.ts +199 -0
  247. package/src/session/session-service.ts +17 -376
  248. package/src/session/session-team-coordination.ts +1 -1
  249. package/src/session/subagent-session-manager.ts +397 -0
  250. package/src/session/team-persistence-store.ts +176 -0
  251. package/src/transports/hub.ts +656 -0
  252. package/src/transports/local.ts +135 -40
  253. package/src/transports/remote.ts +26 -0
  254. package/src/transports/runtime-host-support.ts +63 -9
  255. package/src/types/chat-schema.ts +4 -5
  256. package/src/types/config.ts +8 -7
  257. package/src/types/provider-settings.ts +11 -7
  258. package/src/types/session.ts +2 -4
  259. package/src/types.ts +27 -1
  260. package/dist/hooks/persistent.d.ts +0 -64
  261. package/dist/hooks/persistent.d.ts.map +0 -1
  262. package/dist/runtime/rpc-runtime-ensure.d.ts +0 -65
  263. package/dist/runtime/rpc-runtime-ensure.d.ts.map +0 -1
  264. package/dist/runtime/rpc-spawn-lease.d.ts +0 -8
  265. package/dist/runtime/rpc-spawn-lease.d.ts.map +0 -1
  266. package/dist/session/rpc-session-service.d.ts +0 -16
  267. package/dist/session/rpc-session-service.d.ts.map +0 -1
  268. package/dist/session/sqlite-rpc-session-backend.d.ts +0 -31
  269. package/dist/session/sqlite-rpc-session-backend.d.ts.map +0 -1
  270. package/dist/transports/rpc.d.ts +0 -51
  271. package/dist/transports/rpc.d.ts.map +0 -1
  272. package/src/ClineCore.test.ts +0 -226
  273. package/src/account/cline-account-service.test.ts +0 -185
  274. package/src/account/featurebase-token.test.ts +0 -175
  275. package/src/account/rpc.test.ts +0 -63
  276. package/src/auth/bounded-ttl-cache.test.ts +0 -38
  277. package/src/auth/client.test.ts +0 -69
  278. package/src/auth/cline.test.ts +0 -267
  279. package/src/auth/codex.test.ts +0 -170
  280. package/src/auth/oca.test.ts +0 -340
  281. package/src/auth/server.test.ts +0 -287
  282. package/src/auth/utils.test.ts +0 -128
  283. package/src/extensions/config/agent-config-loader.test.ts +0 -236
  284. package/src/extensions/config/hooks-config-loader.test.ts +0 -20
  285. package/src/extensions/config/runtime-commands.test.ts +0 -115
  286. package/src/extensions/config/unified-config-file-watcher.test.ts +0 -196
  287. package/src/extensions/config/user-instruction-config-loader.test.ts +0 -246
  288. package/src/extensions/context/compaction.test.ts +0 -483
  289. package/src/extensions/mcp/config-loader.test.ts +0 -238
  290. package/src/extensions/mcp/manager.test.ts +0 -105
  291. package/src/extensions/plugin/plugin-config-loader.test.ts +0 -184
  292. package/src/extensions/plugin/plugin-loader.test.ts +0 -292
  293. package/src/extensions/plugin/plugin-sandbox.test.ts +0 -423
  294. package/src/extensions/tools/definitions.test.ts +0 -780
  295. package/src/extensions/tools/executors/bash.test.ts +0 -87
  296. package/src/extensions/tools/executors/editor.test.ts +0 -35
  297. package/src/extensions/tools/executors/file-read.test.ts +0 -125
  298. package/src/extensions/tools/model-tool-routing.test.ts +0 -86
  299. package/src/extensions/tools/presets.test.ts +0 -70
  300. package/src/extensions/tools/team/multi-agent.lifecycle.test.ts +0 -455
  301. package/src/extensions/tools/team/spawn-agent-tool.test.ts +0 -381
  302. package/src/extensions/tools/team/team-tools.test.ts +0 -918
  303. package/src/hooks/checkpoint-hooks.test.ts +0 -168
  304. package/src/hooks/hook-file-hooks.test.ts +0 -311
  305. package/src/hooks/persistent.ts +0 -661
  306. package/src/runtime/history.test.ts +0 -114
  307. package/src/runtime/host.test.ts +0 -230
  308. package/src/runtime/rpc-runtime-ensure.test.ts +0 -123
  309. package/src/runtime/rpc-runtime-ensure.ts +0 -659
  310. package/src/runtime/rpc-spawn-lease.test.ts +0 -81
  311. package/src/runtime/rpc-spawn-lease.ts +0 -156
  312. package/src/runtime/runtime-builder.team-persistence.test.ts +0 -245
  313. package/src/runtime/runtime-builder.test.ts +0 -615
  314. package/src/runtime/runtime-oauth-token-manager.test.ts +0 -137
  315. package/src/runtime/runtime-parity.test.ts +0 -143
  316. package/src/services/providers/local-provider-service.test.ts +0 -1062
  317. package/src/services/session-data.test.ts +0 -160
  318. package/src/services/storage/provider-settings-legacy-migration.test.ts +0 -424
  319. package/src/services/storage/provider-settings-manager.test.ts +0 -191
  320. package/src/services/telemetry/OpenTelemetryAdapter.test.ts +0 -157
  321. package/src/services/telemetry/OpenTelemetryProvider.test.ts +0 -326
  322. package/src/services/telemetry/TelemetryLoggerSink.test.ts +0 -42
  323. package/src/services/telemetry/TelemetryService.test.ts +0 -134
  324. package/src/services/telemetry/distinct-id.test.ts +0 -57
  325. package/src/services/workspace/file-indexer.d.ts +0 -11
  326. package/src/services/workspace/file-indexer.test.ts +0 -156
  327. package/src/services/workspace/mention-enricher.test.ts +0 -106
  328. package/src/session/persistence-service.test.ts +0 -300
  329. package/src/session/rpc-session-service.ts +0 -114
  330. package/src/session/session-service.team-persistence.test.ts +0 -48
  331. package/src/session/sqlite-rpc-session-backend.ts +0 -301
  332. package/src/transports/local.e2e.test.ts +0 -380
  333. package/src/transports/local.test.ts +0 -2559
  334. package/src/transports/rpc.test.ts +0 -82
  335. package/src/transports/rpc.ts +0 -665
@@ -1,1062 +0,0 @@
1
- import { mkdtempSync, rmSync } from "node:fs";
2
- import os from "node:os";
3
- import path from "node:path";
4
- import * as LlmsModels from "@clinebot/llms";
5
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
- import { ProviderSettingsManager } from "../storage/provider-settings-manager";
7
- import {
8
- readModelsFile,
9
- resolveModelsRegistryPath,
10
- } from "./local-provider-registry";
11
- import {
12
- addLocalProvider,
13
- deleteLocalProvider,
14
- getLocalProviderModels,
15
- listLocalProviders,
16
- normalizeOAuthProvider,
17
- resolveLocalClineAuthToken,
18
- saveLocalProviderSettings,
19
- updateLocalProvider,
20
- } from "./local-provider-service";
21
-
22
- // ---------------------------------------------------------------------------
23
- // Helpers
24
- // ---------------------------------------------------------------------------
25
-
26
- function makeTempManager(): {
27
- manager: ProviderSettingsManager;
28
- cleanup: () => void;
29
- } {
30
- const dir = mkdtempSync(
31
- path.join(os.tmpdir(), "local-provider-service-test-"),
32
- );
33
- const manager = new ProviderSettingsManager({
34
- filePath: path.join(dir, "providers.json"),
35
- });
36
- return {
37
- manager,
38
- cleanup: () => rmSync(dir, { recursive: true, force: true }),
39
- };
40
- }
41
-
42
- // ---------------------------------------------------------------------------
43
- // Shared state reset
44
- // ---------------------------------------------------------------------------
45
-
46
- afterEach(() => {
47
- LlmsModels.resetRegistry();
48
- vi.restoreAllMocks();
49
- vi.unstubAllGlobals();
50
- });
51
-
52
- // ===========================================================================
53
- // extractModelIdsFromPayload — tested indirectly via addLocalProvider
54
- // ===========================================================================
55
-
56
- describe("addLocalProvider – model ID parsing via modelsSourceUrl", () => {
57
- let manager: ProviderSettingsManager;
58
- let cleanup: () => void;
59
-
60
- beforeEach(() => {
61
- ({ manager, cleanup } = makeTempManager());
62
- });
63
-
64
- afterEach(() => cleanup());
65
-
66
- it("parses a flat array payload from modelsSourceUrl", async () => {
67
- const mockFetch = vi.fn().mockResolvedValue({
68
- ok: true,
69
- json: async () => ["alpha", "beta", "gamma"],
70
- });
71
- vi.stubGlobal("fetch", mockFetch);
72
-
73
- const result = await addLocalProvider(manager, {
74
- providerId: "flat-array-provider",
75
- name: "Flat Array",
76
- baseUrl: "https://example.invalid/v1",
77
- models: [],
78
- modelsSourceUrl: "https://example.invalid/models",
79
- });
80
-
81
- expect(result.modelsCount).toBe(3);
82
- const { models } = await getLocalProviderModels("flat-array-provider");
83
- expect(models.map((m) => m.id).sort()).toEqual(["alpha", "beta", "gamma"]);
84
- });
85
-
86
- it("parses a { data: [...] } payload from modelsSourceUrl", async () => {
87
- vi.stubGlobal(
88
- "fetch",
89
- vi.fn().mockResolvedValue({
90
- ok: true,
91
- json: async () => ({ data: [{ id: "model-x" }, { id: "model-y" }] }),
92
- }),
93
- );
94
-
95
- await addLocalProvider(manager, {
96
- providerId: "data-array-provider",
97
- name: "Data Array",
98
- baseUrl: "https://example.invalid/v1",
99
- models: [],
100
- modelsSourceUrl: "https://example.invalid/models",
101
- });
102
-
103
- const { models } = await getLocalProviderModels("data-array-provider");
104
- expect(models.map((m) => m.id).sort()).toEqual(["model-x", "model-y"]);
105
- });
106
-
107
- it("parses a { models: { id1: {}, id2: {} } } object-keyed payload", async () => {
108
- vi.stubGlobal(
109
- "fetch",
110
- vi.fn().mockResolvedValue({
111
- ok: true,
112
- json: async () => ({ models: { "key-a": {}, "key-b": {} } }),
113
- }),
114
- );
115
-
116
- await addLocalProvider(manager, {
117
- providerId: "obj-keys-provider",
118
- name: "Object Keys",
119
- baseUrl: "https://example.invalid/v1",
120
- models: [],
121
- modelsSourceUrl: "https://example.invalid/models",
122
- });
123
-
124
- const { models } = await getLocalProviderModels("obj-keys-provider");
125
- expect(models.map((m) => m.id).sort()).toEqual(["key-a", "key-b"]);
126
- });
127
-
128
- it("parses a { providers: { <id>: { models: [...] } } } nested payload", async () => {
129
- vi.stubGlobal(
130
- "fetch",
131
- vi.fn().mockResolvedValue({
132
- ok: true,
133
- json: async () => ({
134
- providers: {
135
- "nested-provider": { models: ["nested-a", "nested-b"] },
136
- },
137
- }),
138
- }),
139
- );
140
-
141
- await addLocalProvider(manager, {
142
- providerId: "nested-provider",
143
- name: "Nested Provider",
144
- baseUrl: "https://example.invalid/v1",
145
- models: [],
146
- modelsSourceUrl: "https://example.invalid/models",
147
- });
148
-
149
- const { models } = await getLocalProviderModels("nested-provider");
150
- expect(models.map((m) => m.id).sort()).toEqual(["nested-a", "nested-b"]);
151
- });
152
-
153
- it("merges manually specified models with fetched models and deduplicates", async () => {
154
- vi.stubGlobal(
155
- "fetch",
156
- vi.fn().mockResolvedValue({
157
- ok: true,
158
- json: async () => ["fetched-1", "fetched-2", "manual-1"],
159
- }),
160
- );
161
-
162
- const result = await addLocalProvider(manager, {
163
- providerId: "merged-provider",
164
- name: "Merged",
165
- baseUrl: "https://example.invalid/v1",
166
- models: ["manual-1", "manual-2"],
167
- modelsSourceUrl: "https://example.invalid/models",
168
- });
169
-
170
- // manual-1, manual-2, fetched-1, fetched-2, manual-1 → Set → 4
171
- expect(result.modelsCount).toBe(4);
172
- const { models } = await getLocalProviderModels("merged-provider");
173
- const ids = models.map((m) => m.id).sort();
174
- expect(ids).toEqual(["fetched-1", "fetched-2", "manual-1", "manual-2"]);
175
- });
176
-
177
- it("throws when fetch returns non-OK status", async () => {
178
- vi.stubGlobal(
179
- "fetch",
180
- vi.fn().mockResolvedValue({
181
- ok: false,
182
- status: 404,
183
- }),
184
- );
185
-
186
- await expect(
187
- addLocalProvider(manager, {
188
- providerId: "fail-fetch-provider",
189
- name: "Fail",
190
- baseUrl: "https://example.invalid/v1",
191
- models: [],
192
- modelsSourceUrl: "https://example.invalid/models",
193
- }),
194
- ).rejects.toThrow("HTTP 404");
195
- });
196
-
197
- it("ignores empty string entries in array payloads", async () => {
198
- vi.stubGlobal(
199
- "fetch",
200
- vi.fn().mockResolvedValue({
201
- ok: true,
202
- json: async () => ["good-model", "", " "],
203
- }),
204
- );
205
-
206
- await addLocalProvider(manager, {
207
- providerId: "empty-strings-provider",
208
- name: "Empty Strings",
209
- baseUrl: "https://example.invalid/v1",
210
- models: [],
211
- modelsSourceUrl: "https://example.invalid/models",
212
- });
213
-
214
- const { models } = await getLocalProviderModels("empty-strings-provider");
215
- expect(models.map((m) => m.id)).toEqual(["good-model"]);
216
- });
217
-
218
- it("ignores non-array, non-object payload shapes and falls back to manual models", async () => {
219
- vi.stubGlobal(
220
- "fetch",
221
- vi.fn().mockResolvedValue({
222
- ok: true,
223
- json: async () => "just a string",
224
- }),
225
- );
226
-
227
- await addLocalProvider(manager, {
228
- providerId: "fallback-provider",
229
- name: "Fallback",
230
- baseUrl: "https://example.invalid/v1",
231
- models: ["fallback-model"],
232
- modelsSourceUrl: "https://example.invalid/models",
233
- });
234
-
235
- const { models } = await getLocalProviderModels("fallback-provider");
236
- expect(models.map((m) => m.id)).toEqual(["fallback-model"]);
237
- });
238
- });
239
-
240
- // ===========================================================================
241
- // addLocalProvider – validation guards
242
- // ===========================================================================
243
-
244
- describe("addLocalProvider – validation", () => {
245
- let manager: ProviderSettingsManager;
246
- let cleanup: () => void;
247
-
248
- beforeEach(() => {
249
- ({ manager, cleanup } = makeTempManager());
250
- });
251
-
252
- afterEach(() => cleanup());
253
-
254
- it("throws when providerId is empty", async () => {
255
- await expect(
256
- addLocalProvider(manager, {
257
- providerId: " ",
258
- name: "X",
259
- baseUrl: "https://example.invalid",
260
- models: ["m"],
261
- }),
262
- ).rejects.toThrow("providerId is required");
263
- });
264
-
265
- it("throws when name is empty", async () => {
266
- await expect(
267
- addLocalProvider(manager, {
268
- providerId: "my-provider",
269
- name: " ",
270
- baseUrl: "https://example.invalid",
271
- models: ["m"],
272
- }),
273
- ).rejects.toThrow("name is required");
274
- });
275
-
276
- it("throws when baseUrl is empty and apiKey is provided", async () => {
277
- await expect(
278
- addLocalProvider(manager, {
279
- providerId: "my-provider2",
280
- name: "My Provider",
281
- baseUrl: " ",
282
- apiKey: "present-key",
283
- models: ["m"],
284
- }),
285
- ).rejects.toThrow("baseUrl is required");
286
- });
287
-
288
- it("throws when no models are provided and no modelsSourceUrl", async () => {
289
- await expect(
290
- addLocalProvider(manager, {
291
- providerId: "no-models-provider",
292
- name: "No Models",
293
- baseUrl: "https://example.invalid",
294
- models: [],
295
- }),
296
- ).rejects.toThrow("at least one model is required");
297
- });
298
-
299
- it("throws when provider already exists", async () => {
300
- // Register a provider first
301
- await addLocalProvider(manager, {
302
- providerId: "duplicate-provider",
303
- name: "First",
304
- baseUrl: "https://example.invalid/v1",
305
- models: ["m1"],
306
- });
307
-
308
- await expect(
309
- addLocalProvider(manager, {
310
- providerId: "duplicate-provider",
311
- name: "Second",
312
- baseUrl: "https://example.invalid/v1",
313
- models: ["m2"],
314
- }),
315
- ).rejects.toThrow('"duplicate-provider" already exists');
316
- });
317
- });
318
-
319
- // ===========================================================================
320
- // addLocalProvider – delete compatibility path
321
- // ===========================================================================
322
-
323
- describe("addLocalProvider – delete compatibility", () => {
324
- let manager: ProviderSettingsManager;
325
- let cleanup: () => void;
326
-
327
- beforeEach(async () => {
328
- ({ manager, cleanup } = makeTempManager());
329
- await addLocalProvider(manager, {
330
- providerId: "legacy-delete-provider",
331
- name: "Legacy Delete Provider",
332
- baseUrl: "https://example.invalid/v1",
333
- models: ["legacy-model"],
334
- });
335
- manager.saveProviderSettings(
336
- {
337
- provider: "legacy-delete-provider",
338
- baseUrl: "https://example.invalid/v1",
339
- model: "legacy-model",
340
- },
341
- { setLastUsed: true },
342
- );
343
- });
344
-
345
- afterEach(() => cleanup());
346
-
347
- it("treats empty baseUrl and apiKey as a delete request for existing provider", async () => {
348
- const result = await addLocalProvider(manager, {
349
- providerId: "legacy-delete-provider",
350
- name: "Ignored",
351
- baseUrl: " ",
352
- apiKey: " ",
353
- models: [],
354
- });
355
-
356
- expect(result.providerId).toBe("legacy-delete-provider");
357
- expect(result.modelsCount).toBe(0);
358
- expect(LlmsModels.hasProvider("legacy-delete-provider")).toBe(false);
359
- expect(
360
- manager.getProviderSettings("legacy-delete-provider"),
361
- ).toBeUndefined();
362
- expect(manager.getLastUsedProviderSettings()).toBeUndefined();
363
- });
364
-
365
- it("is a no-op delete when provider is already missing", async () => {
366
- await deleteLocalProvider(manager, {
367
- providerId: "legacy-delete-provider",
368
- });
369
-
370
- await expect(
371
- addLocalProvider(manager, {
372
- providerId: "legacy-delete-provider",
373
- name: "Ignored",
374
- baseUrl: " ",
375
- models: [],
376
- }),
377
- ).resolves.toMatchObject({
378
- providerId: "legacy-delete-provider",
379
- modelsCount: 0,
380
- });
381
- });
382
- });
383
-
384
- // ===========================================================================
385
- // addLocalProvider – defaultModelId selection
386
- // ===========================================================================
387
-
388
- describe("addLocalProvider – defaultModelId selection", () => {
389
- let manager: ProviderSettingsManager;
390
- let cleanup: () => void;
391
-
392
- beforeEach(() => {
393
- ({ manager, cleanup } = makeTempManager());
394
- });
395
-
396
- afterEach(() => cleanup());
397
-
398
- it("uses explicit defaultModelId when it is in the model list", async () => {
399
- await addLocalProvider(manager, {
400
- providerId: "default-model-provider",
401
- name: "Test",
402
- baseUrl: "https://example.invalid/v1",
403
- models: ["model-a", "model-b", "model-c"],
404
- defaultModelId: "model-b",
405
- });
406
-
407
- const settings = manager.getProviderSettings("default-model-provider");
408
- expect(settings?.model).toBe("model-b");
409
- });
410
-
411
- it("falls back to the first model when defaultModelId is not in the list", async () => {
412
- await addLocalProvider(manager, {
413
- providerId: "fallback-default-provider",
414
- name: "Test",
415
- baseUrl: "https://example.invalid/v1",
416
- models: ["model-a", "model-b"],
417
- defaultModelId: "not-in-list",
418
- });
419
-
420
- const settings = manager.getProviderSettings("fallback-default-provider");
421
- expect(settings?.model).toBe("model-a");
422
- });
423
- });
424
-
425
- // ===========================================================================
426
- // addLocalProvider – capabilities → vision / reasoning flags
427
- // ===========================================================================
428
-
429
- describe("addLocalProvider – capabilities", () => {
430
- let manager: ProviderSettingsManager;
431
- let cleanup: () => void;
432
-
433
- beforeEach(() => {
434
- ({ manager, cleanup } = makeTempManager());
435
- });
436
-
437
- afterEach(() => cleanup());
438
-
439
- it("sets supportsVision and supportsAttachments when capability is 'vision'", async () => {
440
- await addLocalProvider(manager, {
441
- providerId: "vision-provider",
442
- name: "Vision",
443
- baseUrl: "https://example.invalid/v1",
444
- models: ["vis-model"],
445
- capabilities: ["vision"],
446
- });
447
-
448
- const { models } = await getLocalProviderModels("vision-provider");
449
- expect(models).toHaveLength(1);
450
- expect(models[0].supportsVision).toBe(true);
451
- expect(models[0].supportsAttachments).toBe(true);
452
- });
453
-
454
- it("sets supportsReasoning when capability is 'reasoning'", async () => {
455
- await addLocalProvider(manager, {
456
- providerId: "reasoning-provider",
457
- name: "Reasoning",
458
- baseUrl: "https://example.invalid/v1",
459
- models: ["r-model"],
460
- capabilities: ["reasoning"],
461
- });
462
-
463
- const { models } = await getLocalProviderModels("reasoning-provider");
464
- expect(models[0].supportsReasoning).toBe(true);
465
- expect(models[0].supportsVision).toBeFalsy();
466
- });
467
-
468
- it("does not set vision/reasoning flags when capabilities are absent", async () => {
469
- await addLocalProvider(manager, {
470
- providerId: "plain-provider",
471
- name: "Plain",
472
- baseUrl: "https://example.invalid/v1",
473
- models: ["plain-model"],
474
- });
475
-
476
- const { models } = await getLocalProviderModels("plain-provider");
477
- expect(models[0].supportsVision).toBeFalsy();
478
- expect(models[0].supportsReasoning).toBeFalsy();
479
- });
480
-
481
- it("merges LiteLLM private models into the provider model listing when auth is configured", async () => {
482
- manager.saveProviderSettings(
483
- {
484
- provider: "litellm",
485
- apiKey: "test-key-catalog",
486
- baseUrl: "http://localhost:4010",
487
- model: "gpt-4o",
488
- },
489
- { setLastUsed: false },
490
- );
491
- vi.stubGlobal(
492
- "fetch",
493
- vi.fn().mockResolvedValue({
494
- ok: true,
495
- json: async () => ({
496
- data: [
497
- {
498
- model_name: "private-proxy-model",
499
- litellm_params: { model: "openai/gpt-4o-mini" },
500
- model_info: {
501
- supports_vision: true,
502
- supports_reasoning: true,
503
- },
504
- },
505
- ],
506
- }),
507
- }),
508
- );
509
-
510
- const { models } = await getLocalProviderModels(
511
- "litellm",
512
- manager.getProviderConfig("litellm"),
513
- );
514
-
515
- expect(models.map((model) => model.id)).toContain("private-proxy-model");
516
- expect(models.map((model) => model.id)).toContain("openai/gpt-4o-mini");
517
- expect(
518
- models.find((model) => model.id === "private-proxy-model"),
519
- ).toMatchObject({
520
- supportsVision: true,
521
- supportsReasoning: true,
522
- });
523
- });
524
- });
525
-
526
- // ===========================================================================
527
- // saveLocalProviderSettings
528
- // ===========================================================================
529
-
530
- describe("saveLocalProviderSettings", () => {
531
- let manager: ProviderSettingsManager;
532
- let cleanup: () => void;
533
-
534
- beforeEach(async () => {
535
- ({ manager, cleanup } = makeTempManager());
536
- // Seed a provider so there is something to operate on
537
- await addLocalProvider(manager, {
538
- providerId: "test-provider",
539
- name: "Test",
540
- baseUrl: "https://example.invalid/v1",
541
- models: ["m1"],
542
- });
543
- });
544
-
545
- afterEach(() => cleanup());
546
-
547
- it("disabling a provider removes it from settings", () => {
548
- const result = saveLocalProviderSettings(manager, {
549
- providerId: "test-provider",
550
- enabled: false,
551
- });
552
-
553
- expect(result.enabled).toBe(false);
554
- expect(manager.getProviderSettings("test-provider")).toBeUndefined();
555
- });
556
-
557
- it("updates apiKey", () => {
558
- saveLocalProviderSettings(manager, {
559
- providerId: "test-provider",
560
- enabled: true,
561
- apiKey: "new-key",
562
- });
563
-
564
- expect(manager.getProviderSettings("test-provider")?.apiKey).toBe(
565
- "new-key",
566
- );
567
- });
568
-
569
- it("clears apiKey when empty string is provided", () => {
570
- // First set a key
571
- saveLocalProviderSettings(manager, {
572
- providerId: "test-provider",
573
- enabled: true,
574
- apiKey: "some-key",
575
- });
576
- // Then clear it
577
- saveLocalProviderSettings(manager, {
578
- providerId: "test-provider",
579
- enabled: true,
580
- apiKey: "",
581
- });
582
-
583
- const settings = manager.getProviderSettings("test-provider");
584
- expect(settings).not.toHaveProperty("apiKey");
585
- });
586
-
587
- it("merges auth object rather than replacing it", () => {
588
- saveLocalProviderSettings(manager, {
589
- providerId: "test-provider",
590
- enabled: true,
591
- auth: { accessToken: "tok1" },
592
- });
593
- saveLocalProviderSettings(manager, {
594
- providerId: "test-provider",
595
- enabled: true,
596
- auth: { refreshToken: "ref1" },
597
- });
598
-
599
- const settings = manager.getProviderSettings("test-provider") as Record<
600
- string,
601
- unknown
602
- >;
603
- const auth = settings?.auth as Record<string, unknown>;
604
- expect(auth?.accessToken).toBe("tok1");
605
- expect(auth?.refreshToken).toBe("ref1");
606
- });
607
-
608
- it("passes through scalar fields like maxTokens and timeout", () => {
609
- saveLocalProviderSettings(manager, {
610
- providerId: "test-provider",
611
- enabled: true,
612
- maxTokens: 4096,
613
- timeout: 30_000,
614
- });
615
-
616
- const settings = manager.getProviderSettings("test-provider") as Record<
617
- string,
618
- unknown
619
- >;
620
- expect(settings?.maxTokens).toBe(4096);
621
- expect(settings?.timeout).toBe(30_000);
622
- });
623
-
624
- it("disabling a last-used provider also clears lastUsedProvider", async () => {
625
- // Set test-provider as last used
626
- manager.saveProviderSettings(
627
- { provider: "test-provider", model: "m1" },
628
- { setLastUsed: true },
629
- );
630
- expect(manager.getLastUsedProviderSettings()?.provider).toBe(
631
- "test-provider",
632
- );
633
-
634
- saveLocalProviderSettings(manager, {
635
- providerId: "test-provider",
636
- enabled: false,
637
- });
638
-
639
- expect(manager.getLastUsedProviderSettings()).toBeUndefined();
640
- });
641
- });
642
-
643
- // ===========================================================================
644
- // updateLocalProvider
645
- // ===========================================================================
646
-
647
- describe("updateLocalProvider", () => {
648
- let manager: ProviderSettingsManager;
649
- let cleanup: () => void;
650
-
651
- beforeEach(async () => {
652
- ({ manager, cleanup } = makeTempManager());
653
- await addLocalProvider(manager, {
654
- providerId: "editable-provider",
655
- name: "Editable Provider",
656
- baseUrl: "https://example.invalid/v1",
657
- apiKey: "seed-key",
658
- models: ["model-a", "model-b"],
659
- defaultModelId: "model-a",
660
- });
661
- });
662
-
663
- afterEach(() => cleanup());
664
-
665
- it("updates provider metadata and model registry", async () => {
666
- await updateLocalProvider(manager, {
667
- providerId: "editable-provider",
668
- name: "Renamed Provider",
669
- baseUrl: "https://api.example.invalid/v2",
670
- models: ["model-c", "model-d"],
671
- defaultModelId: "model-d",
672
- capabilities: ["vision", "reasoning"],
673
- });
674
-
675
- const provider = await LlmsModels.getProvider("editable-provider");
676
- expect(provider?.name).toBe("Renamed Provider");
677
- expect(provider?.baseUrl).toBe("https://api.example.invalid/v2");
678
- expect(provider?.defaultModelId).toBe("model-d");
679
-
680
- const { models } = await getLocalProviderModels("editable-provider");
681
- expect(models.map((model) => model.id).sort()).toEqual([
682
- "model-c",
683
- "model-d",
684
- ]);
685
- expect(models.find((model) => model.id === "model-c")?.supportsVision).toBe(
686
- true,
687
- );
688
- expect(
689
- models.find((model) => model.id === "model-c")?.supportsReasoning,
690
- ).toBe(true);
691
- });
692
-
693
- it("updates provider settings and can clear optional fields", async () => {
694
- await updateLocalProvider(manager, {
695
- providerId: "editable-provider",
696
- baseUrl: "https://api.example.invalid/v3",
697
- apiKey: "",
698
- headers: null,
699
- timeoutMs: null,
700
- });
701
-
702
- const settings = manager.getProviderSettings("editable-provider");
703
- expect(settings?.baseUrl).toBe("https://api.example.invalid/v3");
704
- expect(settings).not.toHaveProperty("apiKey");
705
- expect(settings).not.toHaveProperty("headers");
706
- expect(settings).not.toHaveProperty("timeout");
707
- });
708
-
709
- it("clears capabilities with null and does not treat [] as a clear sentinel", async () => {
710
- await updateLocalProvider(manager, {
711
- providerId: "editable-provider",
712
- capabilities: ["vision"],
713
- });
714
- const modelsPath = resolveModelsRegistryPath(manager);
715
- let modelsState = await readModelsFile(modelsPath);
716
- expect(
717
- modelsState.providers["editable-provider"]?.provider.capabilities,
718
- ).toEqual(["vision"]);
719
-
720
- await updateLocalProvider(manager, {
721
- providerId: "editable-provider",
722
- capabilities: [],
723
- });
724
- modelsState = await readModelsFile(modelsPath);
725
- expect(
726
- modelsState.providers["editable-provider"]?.provider.capabilities,
727
- ).toEqual([]);
728
-
729
- await updateLocalProvider(manager, {
730
- providerId: "editable-provider",
731
- capabilities: null,
732
- });
733
- modelsState = await readModelsFile(modelsPath);
734
- expect(
735
- modelsState.providers["editable-provider"]?.provider.capabilities,
736
- ).toBeUndefined();
737
- });
738
-
739
- it("allows modelsSourceUrl: null to clear URL while preserving existing models", async () => {
740
- const fetchMock = vi.fn().mockResolvedValue({
741
- ok: true,
742
- json: async () => ["remote-a", "remote-b"],
743
- });
744
- vi.stubGlobal("fetch", fetchMock);
745
-
746
- await updateLocalProvider(manager, {
747
- providerId: "editable-provider",
748
- models: ["model-a", "model-b"],
749
- modelsSourceUrl: "https://example.invalid/models",
750
- });
751
-
752
- await updateLocalProvider(manager, {
753
- providerId: "editable-provider",
754
- modelsSourceUrl: null,
755
- });
756
-
757
- const modelsState = await readModelsFile(
758
- resolveModelsRegistryPath(manager),
759
- );
760
- expect(
761
- modelsState.providers["editable-provider"]?.provider.modelsSourceUrl,
762
- ).toBeUndefined();
763
-
764
- const { models } = await getLocalProviderModels("editable-provider");
765
- expect(models.map((model) => model.id).sort()).toEqual([
766
- "model-a",
767
- "model-b",
768
- "remote-a",
769
- "remote-b",
770
- ]);
771
- expect(fetchMock).toHaveBeenCalledTimes(1);
772
- });
773
-
774
- it("throws when updating a provider that does not exist in custom registry", async () => {
775
- await expect(
776
- updateLocalProvider(manager, {
777
- providerId: "missing-provider",
778
- name: "Nope",
779
- }),
780
- ).rejects.toThrow('"missing-provider" does not exist');
781
- });
782
- });
783
-
784
- // ===========================================================================
785
- // deleteLocalProvider
786
- // ===========================================================================
787
-
788
- describe("deleteLocalProvider", () => {
789
- let manager: ProviderSettingsManager;
790
- let cleanup: () => void;
791
-
792
- beforeEach(async () => {
793
- ({ manager, cleanup } = makeTempManager());
794
- await addLocalProvider(manager, {
795
- providerId: "delete-me-provider",
796
- name: "Delete Me",
797
- baseUrl: "https://example.invalid/v1",
798
- models: ["delete-model"],
799
- });
800
- manager.saveProviderSettings(
801
- {
802
- provider: "delete-me-provider",
803
- baseUrl: "https://example.invalid/v1",
804
- model: "delete-model",
805
- },
806
- { setLastUsed: true },
807
- );
808
- });
809
-
810
- afterEach(() => cleanup());
811
-
812
- it("removes provider from registry and local settings", async () => {
813
- await deleteLocalProvider(manager, {
814
- providerId: "delete-me-provider",
815
- });
816
-
817
- expect(LlmsModels.hasProvider("delete-me-provider")).toBe(false);
818
- expect(manager.getProviderSettings("delete-me-provider")).toBeUndefined();
819
- expect(manager.getLastUsedProviderSettings()).toBeUndefined();
820
- });
821
-
822
- it("throws when deleting an unknown provider", async () => {
823
- await expect(
824
- deleteLocalProvider(manager, { providerId: "missing-provider" }),
825
- ).rejects.toThrow('"missing-provider" does not exist');
826
- });
827
- });
828
-
829
- // ===========================================================================
830
- // listLocalProviders
831
- // ===========================================================================
832
-
833
- describe("listLocalProviders", () => {
834
- let manager: ProviderSettingsManager;
835
- let cleanup: () => void;
836
-
837
- beforeEach(() => {
838
- ({ manager, cleanup } = makeTempManager());
839
- });
840
-
841
- afterEach(() => cleanup());
842
-
843
- it("includes all registered providers", async () => {
844
- await addLocalProvider(manager, {
845
- providerId: "list-provider-a",
846
- name: "Provider A",
847
- baseUrl: "https://example.invalid/a",
848
- models: ["ma1"],
849
- });
850
- await addLocalProvider(manager, {
851
- providerId: "list-provider-b",
852
- name: "Provider B",
853
- baseUrl: "https://example.invalid/b",
854
- models: ["mb1"],
855
- });
856
-
857
- const { providers } = await listLocalProviders(manager);
858
- const ids = providers.map((p) => p.id);
859
- expect(ids).toContain("list-provider-a");
860
- expect(ids).toContain("list-provider-b");
861
- });
862
-
863
- it("marks enabled providers correctly", async () => {
864
- await addLocalProvider(manager, {
865
- providerId: "enabled-check-provider",
866
- name: "Enabled Check",
867
- baseUrl: "https://example.invalid/v1",
868
- models: ["m1"],
869
- });
870
-
871
- const { providers } = await listLocalProviders(manager);
872
- const p = providers.find((x) => x.id === "enabled-check-provider");
873
- expect(p?.enabled).toBe(true);
874
- });
875
-
876
- it("exposes model count", async () => {
877
- await addLocalProvider(manager, {
878
- providerId: "count-provider",
879
- name: "Count",
880
- baseUrl: "https://example.invalid/v1",
881
- models: ["x", "y", "z"],
882
- });
883
-
884
- const { providers } = await listLocalProviders(manager);
885
- const p = providers.find((x) => x.id === "count-provider");
886
- expect(p?.models).toBe(3);
887
- });
888
-
889
- it("generates a stable color and letter for each provider", async () => {
890
- await addLocalProvider(manager, {
891
- providerId: "color-letter-provider",
892
- name: "Color Letter",
893
- baseUrl: "https://example.invalid/v1",
894
- models: ["m1"],
895
- });
896
-
897
- const { providers } = await listLocalProviders(manager);
898
- const p = providers.find((x) => x.id === "color-letter-provider");
899
- expect(p?.color).toMatch(/^#[0-9a-f]{6}$/i);
900
- expect(p?.letter).toBeTruthy();
901
- });
902
-
903
- it("includes built-in model lists in the provider catalog path", async () => {
904
- manager.saveProviderSettings(
905
- {
906
- provider: "openai-native",
907
- apiKey: "test-key",
908
- baseUrl: "https://api.openai.com/v1",
909
- model: "gpt-5.3-codex",
910
- },
911
- { setLastUsed: false },
912
- );
913
-
914
- const { providers } = await listLocalProviders(manager);
915
- const openai = providers.find(
916
- (provider) => provider.id === "openai-native",
917
- );
918
-
919
- expect(openai?.modelList?.length).toBeGreaterThan(0);
920
- expect(
921
- openai?.modelList?.some((model) => model.id === "gpt-5.3-codex"),
922
- ).toBe(true);
923
- });
924
-
925
- it("uses the same built-in model list for cline as openrouter", async () => {
926
- manager.saveProviderSettings(
927
- {
928
- provider: "cline",
929
- apiKey: "test-key",
930
- baseUrl: "https://api.cline.bot/api/v1",
931
- model: "anthropic/claude-sonnet-4.6",
932
- },
933
- { setLastUsed: false },
934
- );
935
-
936
- const { providers } = await listLocalProviders(manager);
937
- const cline = providers.find((provider) => provider.id === "cline");
938
- const openrouter = providers.find(
939
- (provider) => provider.id === "openrouter",
940
- );
941
-
942
- expect(cline?.modelList?.length).toBeGreaterThan(0);
943
- expect(cline?.modelList).toEqual(openrouter?.modelList);
944
- });
945
-
946
- it("does not eagerly fetch LiteLLM private models while listing providers", async () => {
947
- manager.saveProviderSettings(
948
- {
949
- provider: "litellm",
950
- apiKey: "test-key",
951
- baseUrl: "http://localhost:4000",
952
- model: "gpt-4o",
953
- },
954
- { setLastUsed: false },
955
- );
956
- const fetchMock = vi.fn().mockResolvedValue({
957
- ok: true,
958
- json: async () => ({
959
- data: [
960
- {
961
- model_name: "team-private-model",
962
- litellm_params: { model: "team/private-model" },
963
- model_info: {},
964
- },
965
- ],
966
- }),
967
- });
968
- vi.stubGlobal("fetch", fetchMock);
969
-
970
- const { providers } = await listLocalProviders(manager);
971
- const litellm = providers.find((provider) => provider.id === "litellm");
972
-
973
- expect(fetchMock).not.toHaveBeenCalled();
974
- expect(litellm?.modelList?.length).toBeGreaterThan(0);
975
- expect(
976
- litellm?.modelList?.some((model) => model.id === "team/private-model"),
977
- ).toBe(false);
978
- });
979
- });
980
-
981
- // ===========================================================================
982
- // normalizeOAuthProvider
983
- // ===========================================================================
984
-
985
- describe("normalizeOAuthProvider", () => {
986
- it("normalizes 'cline' to 'cline'", () => {
987
- expect(normalizeOAuthProvider("cline")).toBe("cline");
988
- expect(normalizeOAuthProvider(" CLINE ")).toBe("cline");
989
- });
990
-
991
- it("normalizes 'oca' to 'oca'", () => {
992
- expect(normalizeOAuthProvider("oca")).toBe("oca");
993
- expect(normalizeOAuthProvider("OCA")).toBe("oca");
994
- });
995
-
996
- it("normalizes 'codex' and 'openai-codex' to 'openai-codex'", () => {
997
- expect(normalizeOAuthProvider("codex")).toBe("openai-codex");
998
- expect(normalizeOAuthProvider("openai-codex")).toBe("openai-codex");
999
- expect(normalizeOAuthProvider("OPENAI-CODEX")).toBe("openai-codex");
1000
- });
1001
-
1002
- it("throws for unsupported providers", () => {
1003
- expect(() => normalizeOAuthProvider("anthropic")).toThrow(
1004
- "does not support OAuth login",
1005
- );
1006
- expect(() => normalizeOAuthProvider("")).toThrow();
1007
- });
1008
- });
1009
-
1010
- // ===========================================================================
1011
- // resolveLocalClineAuthToken
1012
- // ===========================================================================
1013
-
1014
- describe("resolveLocalClineAuthToken", () => {
1015
- it("returns undefined when settings is undefined", () => {
1016
- expect(resolveLocalClineAuthToken(undefined)).toBeUndefined();
1017
- });
1018
-
1019
- it("returns accessToken when present", () => {
1020
- expect(
1021
- resolveLocalClineAuthToken({
1022
- provider: "cline" as never,
1023
- auth: { accessToken: "tok123" },
1024
- }),
1025
- ).toBe("tok123");
1026
- });
1027
-
1028
- it("falls back to apiKey when accessToken is absent", () => {
1029
- expect(
1030
- resolveLocalClineAuthToken({
1031
- provider: "cline" as never,
1032
- apiKey: "api-key-456",
1033
- }),
1034
- ).toBe("api-key-456");
1035
- });
1036
-
1037
- it("prefers accessToken over apiKey", () => {
1038
- expect(
1039
- resolveLocalClineAuthToken({
1040
- provider: "cline" as never,
1041
- apiKey: "api-key",
1042
- auth: { accessToken: "access-token" },
1043
- }),
1044
- ).toBe("access-token");
1045
- });
1046
-
1047
- it("returns undefined when both accessToken and apiKey are empty strings", () => {
1048
- expect(
1049
- resolveLocalClineAuthToken({
1050
- provider: "cline" as never,
1051
- apiKey: " ",
1052
- auth: { accessToken: " " },
1053
- }),
1054
- ).toBeUndefined();
1055
- });
1056
-
1057
- it("returns undefined when both fields are absent", () => {
1058
- expect(
1059
- resolveLocalClineAuthToken({ provider: "cline" as never }),
1060
- ).toBeUndefined();
1061
- });
1062
- });