@homeflare/alchemy 0.43.0 → 0.45.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 (612) hide show
  1. package/README.md +10 -1
  2. package/dist/caddy/index.js +35 -1
  3. package/dist/caddy/index.js.map +3 -3
  4. package/dist/launchd/index.js +35 -1
  5. package/dist/launchd/index.js.map +3 -3
  6. package/dist/linux/index.js +35 -1
  7. package/dist/linux/index.js.map +3 -3
  8. package/dist/litellm/access-group-form.d.ts +26 -0
  9. package/dist/litellm/access-group-form.d.ts.map +1 -0
  10. package/dist/litellm/access-group-operations.d.ts +37 -0
  11. package/dist/litellm/access-group-operations.d.ts.map +1 -0
  12. package/dist/litellm/access-group-types.d.ts +49 -0
  13. package/dist/litellm/access-group-types.d.ts.map +1 -0
  14. package/dist/litellm/access-group.d.ts +55 -0
  15. package/dist/litellm/access-group.d.ts.map +1 -0
  16. package/dist/litellm/credential-errors.d.ts +131 -0
  17. package/dist/litellm/credential-errors.d.ts.map +1 -0
  18. package/dist/litellm/credential-form.d.ts +79 -0
  19. package/dist/litellm/credential-form.d.ts.map +1 -0
  20. package/dist/litellm/credential-operations.d.ts +84 -0
  21. package/dist/litellm/credential-operations.d.ts.map +1 -0
  22. package/dist/litellm/credential-rewrite.d.ts +6 -0
  23. package/dist/litellm/credential-rewrite.d.ts.map +1 -0
  24. package/dist/litellm/credential-types.d.ts +87 -0
  25. package/dist/litellm/credential-types.d.ts.map +1 -0
  26. package/dist/litellm/credential-values.d.ts +53 -0
  27. package/dist/litellm/credential-values.d.ts.map +1 -0
  28. package/dist/litellm/credential.d.ts +83 -0
  29. package/dist/litellm/credential.d.ts.map +1 -0
  30. package/dist/litellm/fake-access-group-litellm.d.ts +34 -0
  31. package/dist/litellm/fake-access-group-litellm.d.ts.map +1 -0
  32. package/dist/litellm/fake-credential-litellm.d.ts +54 -0
  33. package/dist/litellm/fake-credential-litellm.d.ts.map +1 -0
  34. package/dist/litellm/fake-keys.d.ts +53 -0
  35. package/dist/litellm/fake-keys.d.ts.map +1 -0
  36. package/dist/litellm/fake-litellm.d.ts +4 -1
  37. package/dist/litellm/fake-litellm.d.ts.map +1 -1
  38. package/dist/litellm/fake-mcp-litellm.d.ts +67 -0
  39. package/dist/litellm/fake-mcp-litellm.d.ts.map +1 -0
  40. package/dist/litellm/fake-model-litellm-post-update.d.ts +49 -0
  41. package/dist/litellm/fake-model-litellm-post-update.d.ts.map +1 -0
  42. package/dist/litellm/fake-model-litellm.d.ts +48 -0
  43. package/dist/litellm/fake-model-litellm.d.ts.map +1 -0
  44. package/dist/litellm/fake-policy-attachment-litellm.d.ts +33 -0
  45. package/dist/litellm/fake-policy-attachment-litellm.d.ts.map +1 -0
  46. package/dist/litellm/fake-policy-litellm.d.ts +32 -0
  47. package/dist/litellm/fake-policy-litellm.d.ts.map +1 -0
  48. package/dist/litellm/fake-registry-base.d.ts +42 -0
  49. package/dist/litellm/fake-registry-base.d.ts.map +1 -0
  50. package/dist/litellm/fake-stack.d.ts +8 -1
  51. package/dist/litellm/fake-stack.d.ts.map +1 -1
  52. package/dist/litellm/fake-team-litellm.d.ts +47 -0
  53. package/dist/litellm/fake-team-litellm.d.ts.map +1 -0
  54. package/dist/litellm/fake-tool-litellm.d.ts +28 -0
  55. package/dist/litellm/fake-tool-litellm.d.ts.map +1 -0
  56. package/dist/litellm/fake-toolset-litellm.d.ts +29 -0
  57. package/dist/litellm/fake-toolset-litellm.d.ts.map +1 -0
  58. package/dist/litellm/index.d.ts +18 -0
  59. package/dist/litellm/index.d.ts.map +1 -1
  60. package/dist/litellm/index.js +3078 -2
  61. package/dist/litellm/index.js.map +65 -4
  62. package/dist/litellm/key-errors.d.ts +169 -0
  63. package/dist/litellm/key-errors.d.ts.map +1 -0
  64. package/dist/litellm/key-form.d.ts +119 -0
  65. package/dist/litellm/key-form.d.ts.map +1 -0
  66. package/dist/litellm/key-harness.d.ts +41 -0
  67. package/dist/litellm/key-harness.d.ts.map +1 -0
  68. package/dist/litellm/key-metadata-errors.d.ts +30 -0
  69. package/dist/litellm/key-metadata-errors.d.ts.map +1 -0
  70. package/dist/litellm/key-metadata.d.ts +22 -0
  71. package/dist/litellm/key-metadata.d.ts.map +1 -0
  72. package/dist/litellm/key-operations.d.ts +63 -0
  73. package/dist/litellm/key-operations.d.ts.map +1 -0
  74. package/dist/litellm/key-secret.d.ts +33 -0
  75. package/dist/litellm/key-secret.d.ts.map +1 -0
  76. package/dist/litellm/key.d.ts +78 -0
  77. package/dist/litellm/key.d.ts.map +1 -0
  78. package/dist/litellm/mcp-server-credential.d.ts +50 -0
  79. package/dist/litellm/mcp-server-credential.d.ts.map +1 -0
  80. package/dist/litellm/mcp-server-errors.d.ts +101 -0
  81. package/dist/litellm/mcp-server-errors.d.ts.map +1 -0
  82. package/dist/litellm/mcp-server-form.d.ts +56 -0
  83. package/dist/litellm/mcp-server-form.d.ts.map +1 -0
  84. package/dist/litellm/mcp-server-locate.d.ts +26 -0
  85. package/dist/litellm/mcp-server-locate.d.ts.map +1 -0
  86. package/dist/litellm/mcp-server-operations.d.ts +63 -0
  87. package/dist/litellm/mcp-server-operations.d.ts.map +1 -0
  88. package/dist/litellm/mcp-server-shadow.d.ts +24 -0
  89. package/dist/litellm/mcp-server-shadow.d.ts.map +1 -0
  90. package/dist/litellm/mcp-server-types.d.ts +118 -0
  91. package/dist/litellm/mcp-server-types.d.ts.map +1 -0
  92. package/dist/litellm/mcp-server-url.d.ts +11 -0
  93. package/dist/litellm/mcp-server-url.d.ts.map +1 -0
  94. package/dist/litellm/mcp-server.d.ts +95 -0
  95. package/dist/litellm/mcp-server.d.ts.map +1 -0
  96. package/dist/litellm/model-claim.d.ts +24 -0
  97. package/dist/litellm/model-claim.d.ts.map +1 -0
  98. package/dist/litellm/model-credential.d.ts +12 -0
  99. package/dist/litellm/model-credential.d.ts.map +1 -0
  100. package/dist/litellm/model-errors.d.ts +119 -0
  101. package/dist/litellm/model-errors.d.ts.map +1 -0
  102. package/dist/litellm/model-form.d.ts +78 -0
  103. package/dist/litellm/model-form.d.ts.map +1 -0
  104. package/dist/litellm/model-locate.d.ts +13 -0
  105. package/dist/litellm/model-locate.d.ts.map +1 -0
  106. package/dist/litellm/model-operations.d.ts +59 -0
  107. package/dist/litellm/model-operations.d.ts.map +1 -0
  108. package/dist/litellm/model-types.d.ts +91 -0
  109. package/dist/litellm/model-types.d.ts.map +1 -0
  110. package/dist/litellm/model.d.ts +88 -0
  111. package/dist/litellm/model.d.ts.map +1 -0
  112. package/dist/litellm/policy-attachment-form.d.ts +19 -0
  113. package/dist/litellm/policy-attachment-form.d.ts.map +1 -0
  114. package/dist/litellm/policy-attachment-operations.d.ts +31 -0
  115. package/dist/litellm/policy-attachment-operations.d.ts.map +1 -0
  116. package/dist/litellm/policy-attachment-types.d.ts +48 -0
  117. package/dist/litellm/policy-attachment-types.d.ts.map +1 -0
  118. package/dist/litellm/policy-attachment.d.ts +61 -0
  119. package/dist/litellm/policy-attachment.d.ts.map +1 -0
  120. package/dist/litellm/policy-form.d.ts +22 -0
  121. package/dist/litellm/policy-form.d.ts.map +1 -0
  122. package/dist/litellm/policy-operations.d.ts +42 -0
  123. package/dist/litellm/policy-operations.d.ts.map +1 -0
  124. package/dist/litellm/policy-types.d.ts +57 -0
  125. package/dist/litellm/policy-types.d.ts.map +1 -0
  126. package/dist/litellm/policy.d.ts +65 -0
  127. package/dist/litellm/policy.d.ts.map +1 -0
  128. package/dist/litellm/providers.d.ts +1 -1
  129. package/dist/litellm/providers.d.ts.map +1 -1
  130. package/dist/litellm/registry-errors.d.ts +62 -0
  131. package/dist/litellm/registry-errors.d.ts.map +1 -0
  132. package/dist/litellm/registry-support.d.ts +42 -0
  133. package/dist/litellm/registry-support.d.ts.map +1 -0
  134. package/dist/litellm/team-form.d.ts +26 -0
  135. package/dist/litellm/team-form.d.ts.map +1 -0
  136. package/dist/litellm/team-members.d.ts +10 -0
  137. package/dist/litellm/team-members.d.ts.map +1 -0
  138. package/dist/litellm/team-operations.d.ts +38 -0
  139. package/dist/litellm/team-operations.d.ts.map +1 -0
  140. package/dist/litellm/team-permission.d.ts +35 -0
  141. package/dist/litellm/team-permission.d.ts.map +1 -0
  142. package/dist/litellm/team-types.d.ts +101 -0
  143. package/dist/litellm/team-types.d.ts.map +1 -0
  144. package/dist/litellm/team.d.ts +67 -0
  145. package/dist/litellm/team.d.ts.map +1 -0
  146. package/dist/litellm/tool-policy-form.d.ts +21 -0
  147. package/dist/litellm/tool-policy-form.d.ts.map +1 -0
  148. package/dist/litellm/tool-policy-operations.d.ts +30 -0
  149. package/dist/litellm/tool-policy-operations.d.ts.map +1 -0
  150. package/dist/litellm/tool-policy-types.d.ts +46 -0
  151. package/dist/litellm/tool-policy-types.d.ts.map +1 -0
  152. package/dist/litellm/tool-policy.d.ts +62 -0
  153. package/dist/litellm/tool-policy.d.ts.map +1 -0
  154. package/dist/litellm/toolset-form.d.ts +20 -0
  155. package/dist/litellm/toolset-form.d.ts.map +1 -0
  156. package/dist/litellm/toolset-operations.d.ts +39 -0
  157. package/dist/litellm/toolset-operations.d.ts.map +1 -0
  158. package/dist/litellm/toolset-types.d.ts +46 -0
  159. package/dist/litellm/toolset-types.d.ts.map +1 -0
  160. package/dist/litellm/toolset.d.ts +55 -0
  161. package/dist/litellm/toolset.d.ts.map +1 -0
  162. package/dist/openbao/index.js +35 -1
  163. package/dist/openbao/index.js.map +3 -3
  164. package/dist/ownership/rows.d.ts +11 -0
  165. package/dist/ownership/rows.d.ts.map +1 -1
  166. package/dist/postgres/connection.d.ts +14 -2
  167. package/dist/postgres/connection.d.ts.map +1 -1
  168. package/dist/postgres/database-sql.d.ts +27 -8
  169. package/dist/postgres/database-sql.d.ts.map +1 -1
  170. package/dist/postgres/fake-grants-parse.d.ts +37 -0
  171. package/dist/postgres/fake-grants-parse.d.ts.map +1 -0
  172. package/dist/postgres/fake-grants-read.d.ts +61 -0
  173. package/dist/postgres/fake-grants-read.d.ts.map +1 -0
  174. package/dist/postgres/fake-grants-sql.d.ts +46 -0
  175. package/dist/postgres/fake-grants-sql.d.ts.map +1 -0
  176. package/dist/postgres/fake-grants-write.d.ts +29 -0
  177. package/dist/postgres/fake-grants-write.d.ts.map +1 -0
  178. package/dist/postgres/fake-role-sql.d.ts +41 -0
  179. package/dist/postgres/fake-role-sql.d.ts.map +1 -0
  180. package/dist/postgres/fake-schema-sql.d.ts +42 -0
  181. package/dist/postgres/fake-schema-sql.d.ts.map +1 -0
  182. package/dist/postgres/fake-sql-parse.d.ts +23 -0
  183. package/dist/postgres/fake-sql-parse.d.ts.map +1 -0
  184. package/dist/postgres/fake-sql-quote.d.ts +12 -0
  185. package/dist/postgres/fake-sql-quote.d.ts.map +1 -0
  186. package/dist/postgres/fake-sql.d.ts +42 -0
  187. package/dist/postgres/fake-sql.d.ts.map +1 -1
  188. package/dist/postgres/grants-attrs.d.ts +115 -0
  189. package/dist/postgres/grants-attrs.d.ts.map +1 -0
  190. package/dist/postgres/grants-declare.d.ts +88 -0
  191. package/dist/postgres/grants-declare.d.ts.map +1 -0
  192. package/dist/postgres/grants-diff.d.ts +25 -0
  193. package/dist/postgres/grants-diff.d.ts.map +1 -0
  194. package/dist/postgres/grants-errors.d.ts +137 -0
  195. package/dist/postgres/grants-errors.d.ts.map +1 -0
  196. package/dist/postgres/grants-existence.d.ts +14 -0
  197. package/dist/postgres/grants-existence.d.ts.map +1 -0
  198. package/dist/postgres/grants-ops.d.ts +65 -0
  199. package/dist/postgres/grants-ops.d.ts.map +1 -0
  200. package/dist/postgres/grants-ownership.d.ts +18 -0
  201. package/dist/postgres/grants-ownership.d.ts.map +1 -0
  202. package/dist/postgres/grants-plan-restore.d.ts +22 -0
  203. package/dist/postgres/grants-plan-restore.d.ts.map +1 -0
  204. package/dist/postgres/grants-plan.d.ts +65 -0
  205. package/dist/postgres/grants-plan.d.ts.map +1 -0
  206. package/dist/postgres/grants-read-defaults.d.ts +17 -0
  207. package/dist/postgres/grants-read-defaults.d.ts.map +1 -0
  208. package/dist/postgres/grants-read.d.ts +77 -0
  209. package/dist/postgres/grants-read.d.ts.map +1 -0
  210. package/dist/postgres/grants-refuse.d.ts +25 -0
  211. package/dist/postgres/grants-refuse.d.ts.map +1 -0
  212. package/dist/postgres/grants-sql.d.ts +42 -0
  213. package/dist/postgres/grants-sql.d.ts.map +1 -0
  214. package/dist/postgres/grants-words.d.ts +29 -0
  215. package/dist/postgres/grants-words.d.ts.map +1 -0
  216. package/dist/postgres/grants.d.ts +37 -0
  217. package/dist/postgres/grants.d.ts.map +1 -0
  218. package/dist/postgres/index.d.ts +31 -6
  219. package/dist/postgres/index.d.ts.map +1 -1
  220. package/dist/postgres/index.js +1998 -37
  221. package/dist/postgres/index.js.map +46 -7
  222. package/dist/postgres/installed-classifier.d.ts +13 -0
  223. package/dist/postgres/installed-classifier.d.ts.map +1 -0
  224. package/dist/postgres/providers.d.ts +16 -8
  225. package/dist/postgres/providers.d.ts.map +1 -1
  226. package/dist/postgres/psql-executor.d.ts +4 -0
  227. package/dist/postgres/psql-executor.d.ts.map +1 -1
  228. package/dist/postgres/role-attrs.d.ts +112 -0
  229. package/dist/postgres/role-attrs.d.ts.map +1 -0
  230. package/dist/postgres/role-diff.d.ts +22 -0
  231. package/dist/postgres/role-diff.d.ts.map +1 -0
  232. package/dist/postgres/role-errors.d.ts +132 -0
  233. package/dist/postgres/role-errors.d.ts.map +1 -0
  234. package/dist/postgres/role-membership-sql.d.ts +78 -0
  235. package/dist/postgres/role-membership-sql.d.ts.map +1 -0
  236. package/dist/postgres/role-provider.d.ts +11 -0
  237. package/dist/postgres/role-provider.d.ts.map +1 -0
  238. package/dist/postgres/role-scram.d.ts +8 -0
  239. package/dist/postgres/role-scram.d.ts.map +1 -0
  240. package/dist/postgres/role-secrets.d.ts +38 -0
  241. package/dist/postgres/role-secrets.d.ts.map +1 -0
  242. package/dist/postgres/role-sql.d.ts +68 -0
  243. package/dist/postgres/role-sql.d.ts.map +1 -0
  244. package/dist/postgres/role.d.ts +57 -0
  245. package/dist/postgres/role.d.ts.map +1 -0
  246. package/dist/postgres/schema-assert.d.ts +50 -0
  247. package/dist/postgres/schema-assert.d.ts.map +1 -0
  248. package/dist/postgres/schema-attrs.d.ts +51 -0
  249. package/dist/postgres/schema-attrs.d.ts.map +1 -0
  250. package/dist/postgres/schema-delete.d.ts +28 -0
  251. package/dist/postgres/schema-delete.d.ts.map +1 -0
  252. package/dist/postgres/schema-diff.d.ts +14 -0
  253. package/dist/postgres/schema-diff.d.ts.map +1 -0
  254. package/dist/postgres/schema-errors.d.ts +139 -0
  255. package/dist/postgres/schema-errors.d.ts.map +1 -0
  256. package/dist/postgres/schema-identity.d.ts +8 -0
  257. package/dist/postgres/schema-identity.d.ts.map +1 -0
  258. package/dist/postgres/schema-reconcile.d.ts +12 -0
  259. package/dist/postgres/schema-reconcile.d.ts.map +1 -0
  260. package/dist/postgres/schema-sql.d.ts +45 -0
  261. package/dist/postgres/schema-sql.d.ts.map +1 -0
  262. package/dist/postgres/schema-test-kit.d.ts +67 -0
  263. package/dist/postgres/schema-test-kit.d.ts.map +1 -0
  264. package/dist/postgres/schema.d.ts +54 -0
  265. package/dist/postgres/schema.d.ts.map +1 -0
  266. package/dist/proxmox/index.js +35 -1
  267. package/dist/proxmox/index.js.map +3 -3
  268. package/dist/release/fake-engine.d.ts +10 -0
  269. package/dist/release/fake-engine.d.ts.map +1 -1
  270. package/dist/release/index.js +35 -1
  271. package/dist/release/index.js.map +3 -3
  272. package/dist/valkey/acl-attrs.d.ts +94 -0
  273. package/dist/valkey/acl-attrs.d.ts.map +1 -0
  274. package/dist/valkey/acl-compare.d.ts +28 -0
  275. package/dist/valkey/acl-compare.d.ts.map +1 -0
  276. package/dist/valkey/acl-connection.d.ts +6 -0
  277. package/dist/valkey/acl-connection.d.ts.map +1 -0
  278. package/dist/valkey/acl-form.d.ts +62 -0
  279. package/dist/valkey/acl-form.d.ts.map +1 -0
  280. package/dist/valkey/acl-managed.d.ts +4 -0
  281. package/dist/valkey/acl-managed.d.ts.map +1 -0
  282. package/dist/valkey/acl-ops.d.ts +37 -0
  283. package/dist/valkey/acl-ops.d.ts.map +1 -0
  284. package/dist/valkey/acl-profiles.d.ts +53 -0
  285. package/dist/valkey/acl-profiles.d.ts.map +1 -0
  286. package/dist/valkey/acl-reconcile.d.ts +26 -0
  287. package/dist/valkey/acl-reconcile.d.ts.map +1 -0
  288. package/dist/valkey/acl.d.ts +24 -0
  289. package/dist/valkey/acl.d.ts.map +1 -0
  290. package/dist/valkey/connection-service.d.ts +22 -0
  291. package/dist/valkey/connection-service.d.ts.map +1 -0
  292. package/dist/valkey/connection.d.ts +59 -0
  293. package/dist/valkey/connection.d.ts.map +1 -0
  294. package/dist/valkey/errors.d.ts +165 -0
  295. package/dist/valkey/errors.d.ts.map +1 -0
  296. package/dist/valkey/fake-acl.d.ts +29 -0
  297. package/dist/valkey/fake-acl.d.ts.map +1 -0
  298. package/dist/valkey/fake-valkey.d.ts +35 -0
  299. package/dist/valkey/fake-valkey.d.ts.map +1 -0
  300. package/dist/valkey/handler-test-fixture.d.ts +20 -0
  301. package/dist/valkey/handler-test-fixture.d.ts.map +1 -0
  302. package/dist/valkey/index.d.ts +18 -0
  303. package/dist/valkey/index.d.ts.map +1 -0
  304. package/dist/valkey/index.js +964 -0
  305. package/dist/valkey/index.js.map +27 -0
  306. package/dist/valkey/instance-attrs.d.ts +46 -0
  307. package/dist/valkey/instance-attrs.d.ts.map +1 -0
  308. package/dist/valkey/instance-form.d.ts +22 -0
  309. package/dist/valkey/instance-form.d.ts.map +1 -0
  310. package/dist/valkey/instance.d.ts +48 -0
  311. package/dist/valkey/instance.d.ts.map +1 -0
  312. package/dist/valkey/provider-socket-fixture.d.ts +8 -0
  313. package/dist/valkey/provider-socket-fixture.d.ts.map +1 -0
  314. package/dist/valkey/providers.d.ts +16 -0
  315. package/dist/valkey/providers.d.ts.map +1 -0
  316. package/dist/valkey/resp.d.ts +38 -0
  317. package/dist/valkey/resp.d.ts.map +1 -0
  318. package/dist/valkey/transport-error.d.ts +36 -0
  319. package/dist/valkey/transport-error.d.ts.map +1 -0
  320. package/dist/valkey/transport.d.ts +50 -0
  321. package/dist/valkey/transport.d.ts.map +1 -0
  322. package/docs/litellm-access-group.md +105 -0
  323. package/docs/litellm-credential.md +60 -0
  324. package/docs/litellm-key-metadata.md +56 -0
  325. package/docs/litellm-key.md +194 -0
  326. package/docs/litellm-mcp.md +187 -0
  327. package/docs/litellm-model.md +111 -0
  328. package/docs/litellm-policy-attachment.md +86 -0
  329. package/docs/litellm-policy.md +103 -0
  330. package/docs/litellm-team.md +117 -0
  331. package/docs/litellm-tool-policy.md +90 -0
  332. package/docs/litellm-toolset.md +92 -0
  333. package/docs/litellm.md +16 -9
  334. package/docs/peers.md +15 -6
  335. package/docs/postgres-grants-example.md +55 -0
  336. package/docs/postgres-grants.md +157 -0
  337. package/docs/postgres-role.md +126 -0
  338. package/docs/postgres-schema.md +139 -0
  339. package/docs/postgres.md +20 -3
  340. package/docs/valkey-transport.md +29 -0
  341. package/docs/valkey.md +187 -0
  342. package/package.json +6 -2
  343. package/src/ceph/ceph-argv.test.ts +1 -1
  344. package/src/launchd/sudo-allowlist.test.ts +2 -2
  345. package/src/linux/sudo-allowlist.test.ts +3 -3
  346. package/src/linux/sudo-runner.test.ts +2 -2
  347. package/src/litellm/access-group-form.ts +105 -0
  348. package/src/litellm/access-group-operations.ts +92 -0
  349. package/src/litellm/access-group-types.ts +49 -0
  350. package/src/litellm/access-group.test.ts +197 -0
  351. package/src/litellm/access-group.ts +151 -0
  352. package/src/litellm/credential-errors.ts +183 -0
  353. package/src/litellm/credential-form.test.ts +151 -0
  354. package/src/litellm/credential-form.ts +211 -0
  355. package/src/litellm/credential-guards.test.ts +241 -0
  356. package/src/litellm/credential-operations.ts +208 -0
  357. package/src/litellm/credential-recovery.test.ts +93 -0
  358. package/src/litellm/credential-rewrite.test.ts +164 -0
  359. package/src/litellm/credential-rewrite.ts +50 -0
  360. package/src/litellm/credential-types.ts +117 -0
  361. package/src/litellm/credential-values.ts +94 -0
  362. package/src/litellm/credential.test.ts +218 -0
  363. package/src/litellm/credential.ts +227 -0
  364. package/src/litellm/fake-access-group-litellm.ts +127 -0
  365. package/src/litellm/fake-credential-litellm.ts +216 -0
  366. package/src/litellm/fake-keys.ts +190 -0
  367. package/src/litellm/fake-litellm.ts +19 -9
  368. package/src/litellm/fake-mcp-litellm.ts +214 -0
  369. package/src/litellm/fake-model-litellm-post-update.ts +103 -0
  370. package/src/litellm/fake-model-litellm.ts +233 -0
  371. package/src/litellm/fake-policy-attachment-litellm.ts +97 -0
  372. package/src/litellm/fake-policy-litellm.ts +186 -0
  373. package/src/litellm/fake-registry-base.ts +69 -0
  374. package/src/litellm/fake-stack.ts +17 -1
  375. package/src/litellm/fake-team-litellm.ts +229 -0
  376. package/src/litellm/fake-tool-litellm.ts +88 -0
  377. package/src/litellm/fake-toolset-litellm.ts +110 -0
  378. package/src/litellm/index.ts +140 -0
  379. package/src/litellm/key-delete.test.ts +212 -0
  380. package/src/litellm/key-errors.ts +241 -0
  381. package/src/litellm/key-form.ts +200 -0
  382. package/src/litellm/key-harness.ts +121 -0
  383. package/src/litellm/key-metadata-errors.ts +49 -0
  384. package/src/litellm/key-metadata.test.ts +156 -0
  385. package/src/litellm/key-metadata.ts +74 -0
  386. package/src/litellm/key-operations.ts +184 -0
  387. package/src/litellm/key-rotation.test.ts +110 -0
  388. package/src/litellm/key-scope.test.ts +135 -0
  389. package/src/litellm/key-secret.test.ts +248 -0
  390. package/src/litellm/key-secret.ts +137 -0
  391. package/src/litellm/key-transport.test.ts +166 -0
  392. package/src/litellm/key.test.ts +231 -0
  393. package/src/litellm/key.ts +213 -0
  394. package/src/litellm/mcp-server-adopt.test.ts +86 -0
  395. package/src/litellm/mcp-server-alias.test.ts +184 -0
  396. package/src/litellm/mcp-server-credential.ts +85 -0
  397. package/src/litellm/mcp-server-errors.ts +141 -0
  398. package/src/litellm/mcp-server-exports.test.ts +15 -0
  399. package/src/litellm/mcp-server-form.test.ts +241 -0
  400. package/src/litellm/mcp-server-form.ts +233 -0
  401. package/src/litellm/mcp-server-handlers.test.ts +150 -0
  402. package/src/litellm/mcp-server-locate.ts +66 -0
  403. package/src/litellm/mcp-server-operations.ts +111 -0
  404. package/src/litellm/mcp-server-registry.test.ts +146 -0
  405. package/src/litellm/mcp-server-retype.test.ts +114 -0
  406. package/src/litellm/mcp-server-shadow.test.ts +91 -0
  407. package/src/litellm/mcp-server-shadow.ts +45 -0
  408. package/src/litellm/mcp-server-tools.test.ts +85 -0
  409. package/src/litellm/mcp-server-types.ts +143 -0
  410. package/src/litellm/mcp-server-url.ts +66 -0
  411. package/src/litellm/mcp-server.test.ts +213 -0
  412. package/src/litellm/mcp-server.ts +225 -0
  413. package/src/litellm/model-claim.ts +69 -0
  414. package/src/litellm/model-credential.ts +45 -0
  415. package/src/litellm/model-errors.ts +168 -0
  416. package/src/litellm/model-foreign-row.test.ts +173 -0
  417. package/src/litellm/model-form.test.ts +228 -0
  418. package/src/litellm/model-form.ts +223 -0
  419. package/src/litellm/model-locate.test.ts +128 -0
  420. package/src/litellm/model-locate.ts +67 -0
  421. package/src/litellm/model-operations.test.ts +93 -0
  422. package/src/litellm/model-operations.ts +146 -0
  423. package/src/litellm/model-params-preserve.test.ts +119 -0
  424. package/src/litellm/model-recovery.test.ts +57 -0
  425. package/src/litellm/model-types.ts +112 -0
  426. package/src/litellm/model.test.ts +222 -0
  427. package/src/litellm/model.ts +237 -0
  428. package/src/litellm/policy-attachment-form.ts +103 -0
  429. package/src/litellm/policy-attachment-operations.ts +74 -0
  430. package/src/litellm/policy-attachment-types.ts +48 -0
  431. package/src/litellm/policy-attachment.test.ts +198 -0
  432. package/src/litellm/policy-attachment.ts +178 -0
  433. package/src/litellm/policy-form.ts +122 -0
  434. package/src/litellm/policy-operations.ts +112 -0
  435. package/src/litellm/policy-types.ts +58 -0
  436. package/src/litellm/policy.test.ts +214 -0
  437. package/src/litellm/policy.ts +146 -0
  438. package/src/litellm/providers.ts +24 -3
  439. package/src/litellm/registry-errors.ts +88 -0
  440. package/src/litellm/registry-exports.test.ts +66 -0
  441. package/src/litellm/registry-support.ts +109 -0
  442. package/src/litellm/team-form.ts +140 -0
  443. package/src/litellm/team-members.test.ts +150 -0
  444. package/src/litellm/team-members.ts +58 -0
  445. package/src/litellm/team-operations.ts +90 -0
  446. package/src/litellm/team-permission.ts +159 -0
  447. package/src/litellm/team-read.test.ts +89 -0
  448. package/src/litellm/team-types.ts +106 -0
  449. package/src/litellm/team.test.ts +206 -0
  450. package/src/litellm/team.ts +159 -0
  451. package/src/litellm/tool-policy-form.ts +94 -0
  452. package/src/litellm/tool-policy-operations.ts +58 -0
  453. package/src/litellm/tool-policy-types.ts +48 -0
  454. package/src/litellm/tool-policy.test.ts +172 -0
  455. package/src/litellm/tool-policy.ts +125 -0
  456. package/src/litellm/toolset-form.ts +104 -0
  457. package/src/litellm/toolset-operations.ts +90 -0
  458. package/src/litellm/toolset-types.ts +47 -0
  459. package/src/litellm/toolset.test.ts +198 -0
  460. package/src/litellm/toolset.ts +151 -0
  461. package/src/ownership/ownership.test.ts +33 -1
  462. package/src/ownership/rows.ts +53 -0
  463. package/src/postgres/connection.ts +42 -7
  464. package/src/postgres/database-bigint-serialization.test.ts +1 -0
  465. package/src/postgres/database-refusals.test.ts +2 -2
  466. package/src/postgres/database-sql.ts +36 -9
  467. package/src/postgres/fake-grants-parse.ts +189 -0
  468. package/src/postgres/fake-grants-read.ts +250 -0
  469. package/src/postgres/fake-grants-sql.ts +207 -0
  470. package/src/postgres/fake-grants-write.ts +90 -0
  471. package/src/postgres/fake-role-sql.ts +240 -0
  472. package/src/postgres/fake-schema-semantics.test.ts +36 -0
  473. package/src/postgres/fake-schema-sql.ts +146 -0
  474. package/src/postgres/fake-sql-parse.ts +86 -0
  475. package/src/postgres/fake-sql-quote.ts +36 -0
  476. package/src/postgres/fake-sql.ts +166 -46
  477. package/src/postgres/fixtures/pg-REL_18_6-acl.h.txt +147 -0
  478. package/src/postgres/fixtures/pg-REL_18_6-aclexplode-func.txt +31 -0
  479. package/src/postgres/fixtures/pg-REL_18_6-alter-default-privileges-synopsis.txt +82 -0
  480. package/src/postgres/fixtures/pg-REL_18_6-alter_role.sgml.txt +44 -0
  481. package/src/postgres/fixtures/pg-REL_18_6-create_role.sgml.txt +26 -0
  482. package/src/postgres/fixtures/pg-REL_18_6-grant-synopsis.txt +95 -0
  483. package/src/postgres/fixtures/pg-REL_18_6-pg_attribute.h.txt +24 -0
  484. package/src/postgres/fixtures/pg-REL_18_6-pg_auth_members.h.txt +62 -0
  485. package/src/postgres/fixtures/pg-REL_18_6-pg_authid.h.txt +70 -0
  486. package/src/postgres/fixtures/pg-REL_18_6-pg_class.h.txt +33 -0
  487. package/src/postgres/fixtures/pg-REL_18_6-pg_default_acl.h.txt +81 -0
  488. package/src/postgres/fixtures/pg-REL_18_6-pg_namespace.h.txt +28 -0
  489. package/src/postgres/fixtures/pg-REL_18_6-revoke-synopsis.txt +124 -0
  490. package/src/postgres/grants-acl.test.ts +61 -0
  491. package/src/postgres/grants-attrs.ts +185 -0
  492. package/src/postgres/grants-database.test.ts +158 -0
  493. package/src/postgres/grants-declare.ts +172 -0
  494. package/src/postgres/grants-delete-proof.test.ts +154 -0
  495. package/src/postgres/grants-diff.ts +101 -0
  496. package/src/postgres/grants-errors.ts +215 -0
  497. package/src/postgres/grants-existence-guards.test.ts +113 -0
  498. package/src/postgres/grants-existence.ts +101 -0
  499. package/src/postgres/grants-lifecycle-guards.test.ts +208 -0
  500. package/src/postgres/grants-lifecycle-repair.test.ts +208 -0
  501. package/src/postgres/grants-lifecycle.test.ts +181 -0
  502. package/src/postgres/grants-ops.ts +229 -0
  503. package/src/postgres/grants-owner-revoke.test.ts +121 -0
  504. package/src/postgres/grants-ownership.ts +49 -0
  505. package/src/postgres/grants-plan-collateral.test.ts +202 -0
  506. package/src/postgres/grants-plan-diff.test.ts +204 -0
  507. package/src/postgres/grants-plan-restore.ts +43 -0
  508. package/src/postgres/grants-plan-revocations.test.ts +69 -0
  509. package/src/postgres/grants-plan.test.ts +148 -0
  510. package/src/postgres/grants-plan.ts +245 -0
  511. package/src/postgres/grants-provenance.test.ts +134 -0
  512. package/src/postgres/grants-psql-lifecycle.test.ts +105 -0
  513. package/src/postgres/grants-read-absent.test.ts +91 -0
  514. package/src/postgres/grants-read-defaults.ts +51 -0
  515. package/src/postgres/grants-read.ts +250 -0
  516. package/src/postgres/grants-refusals.test.ts +136 -0
  517. package/src/postgres/grants-refuse.ts +119 -0
  518. package/src/postgres/grants-relkind-wire.test.ts +111 -0
  519. package/src/postgres/grants-repair-plan.test.ts +201 -0
  520. package/src/postgres/grants-sql.test.ts +128 -0
  521. package/src/postgres/grants-sql.ts +144 -0
  522. package/src/postgres/grants-words.ts +34 -0
  523. package/src/postgres/grants.ts +110 -0
  524. package/src/postgres/index.ts +129 -5
  525. package/src/postgres/installed-classifier.ts +36 -0
  526. package/src/postgres/providers.ts +33 -9
  527. package/src/postgres/psql-executor-escaping.test.ts +67 -0
  528. package/src/postgres/psql-executor.test.ts +63 -2
  529. package/src/postgres/psql-executor.ts +74 -34
  530. package/src/postgres/role-adopt.test.ts +98 -0
  531. package/src/postgres/role-attrs.ts +137 -0
  532. package/src/postgres/role-diff.ts +93 -0
  533. package/src/postgres/role-errors.ts +206 -0
  534. package/src/postgres/role-guard.test.ts +60 -0
  535. package/src/postgres/role-identity.test.ts +197 -0
  536. package/src/postgres/role-lifecycle.test.ts +228 -0
  537. package/src/postgres/role-membership-options.test.ts +73 -0
  538. package/src/postgres/role-membership-repair.test.ts +153 -0
  539. package/src/postgres/role-membership-sql.ts +223 -0
  540. package/src/postgres/role-password-ascii.test.ts +53 -0
  541. package/src/postgres/role-password-transports.test.ts +123 -0
  542. package/src/postgres/role-password.test.ts +158 -0
  543. package/src/postgres/role-privilege.test.ts +107 -0
  544. package/src/postgres/role-provenance.test.ts +200 -0
  545. package/src/postgres/role-provider.ts +105 -0
  546. package/src/postgres/role-refusals.test.ts +225 -0
  547. package/src/postgres/role-scram.test.ts +38 -0
  548. package/src/postgres/role-scram.ts +40 -0
  549. package/src/postgres/role-secrets.ts +61 -0
  550. package/src/postgres/role-sql.ts +213 -0
  551. package/src/postgres/role-valid-until.test.ts +68 -0
  552. package/src/postgres/role.ts +226 -0
  553. package/src/postgres/schema-assert.ts +139 -0
  554. package/src/postgres/schema-attrs.ts +71 -0
  555. package/src/postgres/schema-delete-proofs.test.ts +192 -0
  556. package/src/postgres/schema-delete.ts +96 -0
  557. package/src/postgres/schema-diff.ts +44 -0
  558. package/src/postgres/schema-drop-2bp01.test.ts +146 -0
  559. package/src/postgres/schema-errors.ts +209 -0
  560. package/src/postgres/schema-handlers.test.ts +193 -0
  561. package/src/postgres/schema-identity.ts +39 -0
  562. package/src/postgres/schema-lifecycle.test.ts +246 -0
  563. package/src/postgres/schema-race.test.ts +70 -0
  564. package/src/postgres/schema-reconcile.ts +100 -0
  565. package/src/postgres/schema-refusals.test.ts +144 -0
  566. package/src/postgres/schema-socket.test.ts +162 -0
  567. package/src/postgres/schema-sql.ts +130 -0
  568. package/src/postgres/schema-takeover.test.ts +149 -0
  569. package/src/postgres/schema-test-kit.ts +85 -0
  570. package/src/postgres/schema.ts +130 -0
  571. package/src/release/binary-alias.test.ts +3 -3
  572. package/src/release/binary-claims.test.ts +3 -3
  573. package/src/release/binary-directory-umask.test.ts +65 -0
  574. package/src/release/fake-engine.ts +15 -0
  575. package/src/valkey/acl-attrs.ts +98 -0
  576. package/src/valkey/acl-compare.ts +57 -0
  577. package/src/valkey/acl-connection.ts +13 -0
  578. package/src/valkey/acl-delete.test.ts +127 -0
  579. package/src/valkey/acl-form.ts +239 -0
  580. package/src/valkey/acl-managed.ts +13 -0
  581. package/src/valkey/acl-ops.ts +122 -0
  582. package/src/valkey/acl-ownership.test.ts +118 -0
  583. package/src/valkey/acl-prefix.test.ts +38 -0
  584. package/src/valkey/acl-profiles.ts +81 -0
  585. package/src/valkey/acl-reconcile.ts +180 -0
  586. package/src/valkey/acl-reserved.test.ts +69 -0
  587. package/src/valkey/acl-revocation.test.ts +94 -0
  588. package/src/valkey/acl-seat-prefix.test.ts +111 -0
  589. package/src/valkey/acl-validation.test.ts +54 -0
  590. package/src/valkey/acl.test.ts +215 -0
  591. package/src/valkey/acl.ts +169 -0
  592. package/src/valkey/connection-service.ts +46 -0
  593. package/src/valkey/connection.ts +180 -0
  594. package/src/valkey/errors.ts +239 -0
  595. package/src/valkey/fake-acl.ts +128 -0
  596. package/src/valkey/fake-valkey.ts +224 -0
  597. package/src/valkey/handler-test-fixture.ts +43 -0
  598. package/src/valkey/handlers.test.ts +205 -0
  599. package/src/valkey/index.ts +54 -0
  600. package/src/valkey/instance-attrs.ts +48 -0
  601. package/src/valkey/instance-form.ts +104 -0
  602. package/src/valkey/instance.test.ts +108 -0
  603. package/src/valkey/instance.ts +160 -0
  604. package/src/valkey/provider-socket-fixture.ts +71 -0
  605. package/src/valkey/providers.test.ts +129 -0
  606. package/src/valkey/providers.ts +21 -0
  607. package/src/valkey/resp.ts +212 -0
  608. package/src/valkey/transport-bounds.test.ts +150 -0
  609. package/src/valkey/transport-error.ts +37 -0
  610. package/src/valkey/transport.test.ts +158 -0
  611. package/src/valkey/transport.ts +109 -0
  612. package/src/valkey/upstream-bounds.test.ts +14 -0
@@ -0,0 +1,237 @@
1
+ /**
2
+ * `LiteLLM.Model` — one deployment row of LiteLLM's model registry (`/model/info`), an upstream
3
+ * model the proxy's router serves under a group name. A group name can carry SEVERAL deployments
4
+ * (that is LiteLLM's load balancing); this resource owns exactly one row.
5
+ *
6
+ * ★ ADOPT BY NAME. `modelName` finds the live row whose group name matches; declare `id` to pin one
7
+ * instead. A live row with no state is `Unowned`, so it needs `--adopt`. A name matching more
8
+ * than one listed row is refused, never guessed at (`LitellmModelAmbiguousNameError`).
9
+ * ★ `defaultRemovalPolicy: 'retain'` — a deployment's removal takes it out of its routing group for
10
+ * every key that reaches it. Opt in with `.pipe(RemovalPolicy.destroy())`.
11
+ * ⛔ THE CREDENTIAL IS `{ fromEnv: 'NAME' }`, NEVER A VALUE (S25): it is sent in LiteLLM's own
12
+ * reference form `os.environ/NAME` and the proxy resolves it in its own environment — nothing
13
+ * here reads `process.env` (model-credential.ts), so a value could never be sealed truthfully.
14
+ * A row's stored params come back DECRYPTED at v1.103.0 but with `api_key` STRIPPED (measured),
15
+ * so a plan compares the visible fields plus a digest of the DECLARED values (`paramsSeal`) —
16
+ * never the values themselves, which belong in the proxy's DB only (model-form.ts).
17
+ * ⚠️ A RENAME IS A NEW GROUP when the id is not pinned: the old row survives under 'retain', the
18
+ * new group gets a fresh deterministic id. With a pinned id the SAME row is renamed — the caller
19
+ * chose the row (model-types.ts).
20
+ * ⚠️ UNMEASURED AT 1.103.0, guarded by a read back: whether `/model/new` honours a supplied
21
+ * `model_info.id`. The answer is an untyped body, so the id ASKED FOR is the one tracked
22
+ * (model-operations.ts); a proxy that issued its own id fails loudly
23
+ * (`LitellmModelAbsentAfterWriteError`) instead of recording a row it cannot identify.
24
+ */
25
+ import { Resource } from 'alchemy';
26
+ import { Unowned } from 'alchemy/AdoptPolicy';
27
+ import { isResolved } from 'alchemy/Diff';
28
+ import type { Input } from 'alchemy/Input';
29
+ import { createPhysicalName } from 'alchemy/PhysicalName';
30
+ import * as Provider from 'alchemy/Provider';
31
+ import * as Effect from 'effect/Effect';
32
+ import * as Predicate from 'effect/Predicate';
33
+ import { noteUnfinished } from '../ownership/resume.ts';
34
+ import {
35
+ LitellmModelAbsentAfterWriteError,
36
+ LitellmModelConfigFileRowError,
37
+ LitellmModelInvalidError,
38
+ LitellmModelNotConvergedError,
39
+ } from './model-errors.ts';
40
+ import { claimStatelessRow } from './model-claim.ts';
41
+ import { createBody, declaredDigest, differing, firstProblem, patchBody } from './model-form.ts';
42
+ import { sealState } from './model-credential.ts';
43
+ import { findLive, wantedId } from './model-locate.ts';
44
+ import {
45
+ createModel,
46
+ deleteModel,
47
+ listModels,
48
+ patchModel,
49
+ readModelRow,
50
+ } from './model-operations.ts';
51
+ import type { ModelAttributes, ModelProps } from './model-types.ts';
52
+
53
+ export type { ModelAttributes, ModelProps };
54
+ export type { ModelError } from './model-errors.ts';
55
+
56
+ export interface LiteLLMModel extends Resource<'LiteLLM.Model', ModelProps, ModelAttributes> {}
57
+
58
+ export const LiteLLMModel = Resource<LiteLLMModel>('LiteLLM.Model', {
59
+ defaultRemovalPolicy: 'retain',
60
+ });
61
+
62
+ export const isLiteLLMModel = (value: unknown): value is LiteLLMModel =>
63
+ Predicate.hasProperty(value, 'Type') && value.Type === 'LiteLLM.Model';
64
+
65
+ const refuse = (props: ModelProps) => {
66
+ const problem = firstProblem(props);
67
+ return problem === undefined
68
+ ? Effect.void
69
+ : Effect.fail(new LitellmModelInvalidError({ problem, modelName: String(props.modelName) }));
70
+ };
71
+
72
+ type Args<P> = {
73
+ fqn: string;
74
+ id: string;
75
+ instanceId: string;
76
+ output: ModelAttributes | undefined;
77
+ } & P;
78
+
79
+ /** ⛔ TEST-ONLY EXPORT. Not on index.ts's barrel. */
80
+ export const modelHandlers = {
81
+ /**
82
+ * ⛔ WITH NO `output` THIS IS ALCHEMY'S ADOPTION PROBE, AND THE ONLY PLAN-TIME HOOK A NEW MODEL
83
+ * GETS — so the refusals run here too, as a defect (mcp-server.ts: the cold-start probe must
84
+ * fail the plan, and a recovery read must degrade to "nothing recovered", which only a defect
85
+ * does). The v1.103.0 read answers params decrypted with `api_key` stripped, so this refusal
86
+ * is the declaration's (`firstProblem`), before Apply commits props into unencrypted state.
87
+ */
88
+ read: ({ id, instanceId, olds, output }: Args<{ olds: ModelProps }>) =>
89
+ Effect.gen(function* () {
90
+ const problem = output === undefined ? firstProblem(olds) : undefined;
91
+ if (problem !== undefined) {
92
+ return yield* Effect.die(
93
+ new LitellmModelInvalidError({ problem, modelName: String(olds.modelName) }),
94
+ );
95
+ }
96
+ // ★ THE LIST FIRST: one GET answers a name, an id and absence (model-locate.ts).
97
+ const found = yield* findLive(id, instanceId, olds, output);
98
+ if (found === undefined) return undefined;
99
+ // ★ AN INTERRUPTED UNPINNED CREATE. The recovery read reuses the `creating` row's instance
100
+ // id, which is the one `POST /model/new` was given, so the live id matches the physical
101
+ // name. The probe's instance id is minted fresh (Plan.ts `generateInstanceId`) and cannot
102
+ // match a row this declaration created. A declared id can collide, so it stays `Unowned`.
103
+ if (output === undefined && olds.id === undefined) {
104
+ const physical = yield* createPhysicalName({
105
+ id,
106
+ instanceId,
107
+ lowercase: true,
108
+ maxLength: 64,
109
+ });
110
+ if (found.id === physical) return found;
111
+ }
112
+ // The row cannot supply a digest of what was declared; state can (model-form.ts).
113
+ return output === undefined ? Unowned(found) : { ...found, paramsSeal: output.paramsSeal };
114
+ }),
115
+
116
+ diff: ({
117
+ instanceId,
118
+ news,
119
+ output,
120
+ }: {
121
+ instanceId: string;
122
+ news: Input<ModelProps>;
123
+ output: ModelAttributes | undefined;
124
+ }) =>
125
+ Effect.gen(function* () {
126
+ if (!isResolved(news)) return undefined;
127
+ yield* refuse(news);
128
+ // ★ No attributes: an unfinished generation of our own. `--adopt` may resume it
129
+ // (ownership/resume.ts). A fresh replace's new instance is never diffed this way.
130
+ if (output === undefined) return yield* noteUnfinished(instanceId);
131
+ // The id is identity: a different declared id is a different row.
132
+ if (news.id !== undefined && news.id !== output.id) {
133
+ return { action: 'replace', deleteFirst: false } as const;
134
+ }
135
+ // A rename without a pinned id is a new group (model.ts header): old row retained.
136
+ if (news.modelName !== output.modelName && news.id === undefined) {
137
+ return { action: 'replace', deleteFirst: false } as const;
138
+ }
139
+ // Visible drift, or a stale seal, is an update: the read strips `api_key`, so the
140
+ // seal is the only signal a declared credential moved (model-credential.ts).
141
+ // Reconcile then decides the write: a bare matching adopt records its seal
142
+ // locally, and a declaration that manages an invisible param PATCHes once to
143
+ // converge it.
144
+ return differing(output, news).length === 0 && sealState(news, output.paramsSeal) === 'match'
145
+ ? ({ action: 'noop' } as const)
146
+ : ({ action: 'update' } as const);
147
+ }),
148
+
149
+ reconcile: ({ fqn, id, instanceId, news, output }: Args<{ news: ModelProps }>) =>
150
+ Effect.gen(function* () {
151
+ yield* refuse(news);
152
+ const before = yield* findLive(id, instanceId, news, output);
153
+ let sealed = output?.paramsSeal ?? '';
154
+ let modelId: string;
155
+
156
+ if (before === undefined) {
157
+ const wanted = yield* wantedId(id, instanceId, news, output);
158
+ yield* createModel(createBody(news, wanted));
159
+ // The id ASKED FOR is the one tracked; the read back refuses to claim a row the
160
+ // proxy filed elsewhere (model.ts header's unmeasured note, model-operations.ts).
161
+ modelId = wanted;
162
+ sealed = declaredDigest(news);
163
+ } else {
164
+ // ⛔ CONFIG-FILE ROWS ARE ALWAYS REFUSED. The DB API cannot manage a row the proxy
165
+ // serves from its config file (`model_info.db_model: false`), regardless of whether
166
+ // this stack already holds state for it.
167
+ if (before.dbModel === false) {
168
+ return yield* Effect.fail(
169
+ new LitellmModelConfigFileRowError({ id: before.id, modelName: news.modelName }),
170
+ );
171
+ }
172
+ // ⛔ NOTHING IS WRITTEN OVER A ROW THIS STACK HOLDS NO STATE FOR, unless the gate says
173
+ // so (model-claim.ts): a deterministic id is ours, an older generation's id is refused
174
+ // with a state-level recovery, and anything else needs `--adopt` on a create or an
175
+ // unfinished generation. `output` may be the live attributes the plan stripped from
176
+ // `Unowned(found)` (`paramsSeal` is empty until a deploy commits a seal), so the gate
177
+ // runs both when output is undefined and when it is only a probe result.
178
+ const stateless = output === undefined || output.paramsSeal === '';
179
+ if (stateless) {
180
+ yield* claimStatelessRow({
181
+ beforeId: before.id,
182
+ fqn,
183
+ instanceId,
184
+ news,
185
+ output,
186
+ wanted: yield* wantedId(id, instanceId, news, output),
187
+ });
188
+ }
189
+ modelId = before.id;
190
+ const drifted = differing(before, news);
191
+ const sealStale = sealState(news, sealed) === 'stale';
192
+ // ⛔ A STAMPING ADOPT, AND ONE MORE REASON TO WRITE. A live row starts `paramsSeal: ''`
193
+ // (model-form.ts), and the PATCH merge rewrites the managed `litellm_params`: a bare
194
+ // adopt of an already-matching row records the digest locally and writes nothing. A
195
+ // declaration that manages an invisible param (`apiKey` / `apiBase` — never on the
196
+ // read) must still write once: a matching-looking row can carry a DIFFERENT stored
197
+ // reference, and a local seal over it would freeze that divergence forever. A seal
198
+ // that EXISTS and no longer matches is a declaration this resource already owns —
199
+ // that one writes too, with nulls for the five non-`None` defaults (model-form.ts).
200
+ const sealNeedsStamp =
201
+ sealStale && (sealed !== '' || news.apiKey !== undefined || news.apiBase !== undefined);
202
+ if (drifted.length > 0 || sealNeedsStamp) {
203
+ yield* patchModel(patchBody(news, before));
204
+ }
205
+ if (drifted.length > 0 || sealStale) sealed = declaredDigest(news);
206
+ }
207
+
208
+ // ★ THE LIST FIRST, THEN BY ID: a row the list does not show yet (create commits, list
209
+ // refreshes lazily) is still a live row if the by-id read answers it.
210
+ const after =
211
+ (yield* listModels()).find((row) => row.id === modelId) ?? (yield* readModelRow(modelId));
212
+ if (after === undefined) {
213
+ return yield* Effect.fail(
214
+ new LitellmModelAbsentAfterWriteError({ id: modelId, modelName: news.modelName }),
215
+ );
216
+ }
217
+ const left = differing(after, news);
218
+ if (left.length > 0) {
219
+ return yield* Effect.fail(
220
+ new LitellmModelNotConvergedError({
221
+ fields: left,
222
+ id: modelId,
223
+ modelName: news.modelName,
224
+ }),
225
+ );
226
+ }
227
+ return { ...after, paramsSeal: sealed };
228
+ }),
229
+
230
+ delete: ({ output }: { output: ModelAttributes }) => deleteModel(output.id),
231
+
232
+ /** ⛔ EMPTY: adoption is an explicit act (`--adopt`), and no ownership mark exists to filter by. */
233
+ list: () => Effect.succeed([]),
234
+ };
235
+
236
+ export const LiteLLMModelProvider = () =>
237
+ Provider.effect(LiteLLMModel, Effect.succeed(LiteLLMModel.Provider.of(modelHandlers)));
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Wire bodies, comparison and refusals for `LiteLLM.PolicyAttachment`, testable without a server.
3
+ *
4
+ * ★ TWO ATTACHMENTS ARE "THE SAME" WHEN EVERY FIELD IS: the policy, the scope, the four selector SETS
5
+ * and the priority. That is the only identity an attachment has (policy-attachment-types.ts), and
6
+ * it is what adoption and the read back both use.
7
+ */
8
+ import type * as policies from '@distilled.cloud/litellm/policy_engine';
9
+ import {
10
+ asRow,
11
+ canonical,
12
+ firstBadEntry,
13
+ firstDuplicate,
14
+ isBlank,
15
+ numberOrNull,
16
+ sameSet,
17
+ stringOrNull,
18
+ stringsOf,
19
+ } from './registry-support.ts';
20
+ import type {
21
+ PolicyAttachmentAttributes,
22
+ PolicyAttachmentProps,
23
+ } from './policy-attachment-types.ts';
24
+
25
+ const SELECTORS = ['teams', 'keys', 'models', 'tags'] as const;
26
+
27
+ /** The first reason a declaration is refused, or `undefined`. Runs before any request. */
28
+ export const firstProblem = (props: PolicyAttachmentProps): string | undefined => {
29
+ if (isBlank(props.policyName) || props.policyName !== props.policyName.trim()) {
30
+ return '`policyName` must be a non-blank name with no leading or trailing space';
31
+ }
32
+ if (props.scope !== undefined && props.scope !== '*') return "`scope` can only be '*'";
33
+ for (const field of SELECTORS) {
34
+ const values = props[field] ?? [];
35
+ const bad = firstBadEntry(values);
36
+ if (bad !== undefined)
37
+ return `\`${field}\` has a blank or padded entry (${JSON.stringify(bad)})`;
38
+ const twice = firstDuplicate(values);
39
+ if (twice !== undefined) return `\`${field}\` lists ${JSON.stringify(twice)} twice`;
40
+ }
41
+ if (props.scope === undefined && SELECTORS.every((field) => (props[field] ?? []).length === 0)) {
42
+ return "no `scope`, `teams`, `keys`, `models` or `tags`: LiteLLM treats an attachment with no selector as GLOBAL, so declare `scope: '*'` if that is meant";
43
+ }
44
+ if (
45
+ props.priority !== undefined &&
46
+ !(
47
+ Number.isInteger(props.priority) &&
48
+ props.priority >= -2147483648 &&
49
+ props.priority <= 2147483647
50
+ )
51
+ ) {
52
+ return '`priority` must be a 32-bit integer';
53
+ }
54
+ return undefined;
55
+ };
56
+
57
+ /** One attachment row. `undefined` is "not an attachment", and so is a config-file one. */
58
+ export const toAttributes = (value: unknown): PolicyAttachmentAttributes | undefined => {
59
+ const row = asRow(value);
60
+ if (row === undefined) return undefined;
61
+ const attachmentId = row['attachment_id'];
62
+ const policyName = row['policy_name'];
63
+ if (typeof attachmentId !== 'string' || typeof policyName !== 'string') return undefined;
64
+ return {
65
+ attachmentId,
66
+ keys: canonical(stringsOf(row['keys'])),
67
+ models: canonical(stringsOf(row['models'])),
68
+ policyName,
69
+ priority: numberOrNull(row['priority']),
70
+ scope: stringOrNull(row['scope']),
71
+ tags: canonical(stringsOf(row['tags'])),
72
+ teams: canonical(stringsOf(row['teams'])),
73
+ };
74
+ };
75
+
76
+ /** Whether a live row is defined in config.yaml (its synthetic id is `config-<n>`): out of reach of the API. */
77
+ export const isConfigRow = (value: unknown): boolean =>
78
+ asRow(value)?.['definition_location'] === 'config';
79
+
80
+ /** Wire names of the fields where live and declared disagree. Empty means "the same attachment". */
81
+ export const differing = (
82
+ live: PolicyAttachmentAttributes,
83
+ props: PolicyAttachmentProps,
84
+ ): readonly string[] => {
85
+ const fields: string[] = [];
86
+ if (live.policyName !== props.policyName) fields.push('policy_name');
87
+ if ((live.scope ?? null) !== (props.scope ?? null)) fields.push('scope');
88
+ for (const field of SELECTORS) if (!sameSet(live[field], props[field] ?? [])) fields.push(field);
89
+ if (live.priority !== (props.priority ?? null)) fields.push('priority');
90
+ return fields;
91
+ };
92
+
93
+ export const createBody = (
94
+ props: PolicyAttachmentProps,
95
+ ): policies.CreatePolicyAttachmentPoliciesAttachmentsPostRequest => ({
96
+ policy_name: props.policyName,
97
+ ...(props.scope === undefined ? {} : { scope: props.scope }),
98
+ ...(props.teams === undefined ? {} : { teams: [...props.teams] }),
99
+ ...(props.keys === undefined ? {} : { keys: [...props.keys] }),
100
+ ...(props.models === undefined ? {} : { models: [...props.models] }),
101
+ ...(props.tags === undefined ? {} : { tags: [...props.tags] }),
102
+ ...(props.priority === undefined ? {} : { priority: props.priority }),
103
+ });
@@ -0,0 +1,74 @@
1
+ /**
2
+ * The three LiteLLM `/policies/attachments*` calls `LiteLLM.PolicyAttachment` uses, through the SDK's
3
+ * typed operations in `@distilled.cloud/litellm/policy_engine` (LiteLLM 1.103.0):
4
+ *
5
+ * list `listPolicyAttachmentsPoliciesAttachmentsListGet` GET /policies/attachments/list
6
+ * create `createPolicyAttachmentPoliciesAttachmentsPost` POST /policies/attachments
7
+ * delete `deletePolicyAttachmentPoliciesAttachmentsAttachmentIdDelete` DELETE /policies/attachments/{id}
8
+ *
9
+ * ★ THERE IS NO UPDATE OPERATION, because the API has none (policy-attachment-types.ts). The by-id
10
+ * `GET` is not used either: the list carries every field and answers absence without a 404.
11
+ * ★ THE LIST MERGES IN CONFIG-FILE ATTACHMENTS (`definition_location: "config"`, ids `config-<n>`),
12
+ * which the API cannot delete. `listAttachments` returns database rows only.
13
+ * ⚠️ MOST FAILURES ARE A PLAIN 500 (`policy_endpoints.py` wraps everything in `except Exception` and
14
+ * answers `HTTPException(500, detail=str(e))`), and the SDK retries 5xx with backoff. The two
15
+ * answers that are not: a policy with no production version is a 404, and a concrete team, key or model
16
+ * selector that resolves to nothing is a 400 (`PolicyValidator.find_invalid_scope_entries`), so a
17
+ * `teams` entry must exist BEFORE its attachment: declare it as `team.teamAlias`. Wildcard patterns
18
+ * are let through, and may match nothing today.
19
+ * ★ DELETE IS IDEMPOTENT BY A REAL READ: a failed DELETE (a missing id is a 404) is swallowed only when
20
+ * the list no longer has the id; otherwise the ORIGINAL error is re-raised.
21
+ */
22
+ import * as policies from '@distilled.cloud/litellm/policy_engine';
23
+ import * as Effect from 'effect/Effect';
24
+ import { throughFetch } from './operations.ts';
25
+ import { isConfigRow, toAttributes } from './policy-attachment-form.ts';
26
+ import type { PolicyAttachmentAttributes } from './policy-attachment-types.ts';
27
+ import { LitellmRegistryUnreadableError } from './registry-errors.ts';
28
+ import { asRow, bodyOf } from './registry-support.ts';
29
+
30
+ const RESOURCE = 'LiteLLM.PolicyAttachment';
31
+
32
+ const unreadable = (reason: string) =>
33
+ new LitellmRegistryUnreadableError({ reason, resource: RESOURCE });
34
+
35
+ /** Every DATABASE attachment. Config-file ones are dropped here, once. */
36
+ export const listAttachments = () =>
37
+ throughFetch(policies.listPolicyAttachmentsPoliciesAttachmentsListGet({})).pipe(
38
+ Effect.flatMap((response) => {
39
+ const rows = asRow(bodyOf(response))?.['attachments'];
40
+ if (rows === undefined) return Effect.succeed<readonly PolicyAttachmentAttributes[]>([]);
41
+ if (!Array.isArray(rows)) return Effect.fail(unreadable('attachments is not an array'));
42
+ const parsed = rows.filter((row) => !isConfigRow(row)).map(toAttributes);
43
+ return parsed.every((each) => each !== undefined)
44
+ ? Effect.succeed(parsed as readonly PolicyAttachmentAttributes[])
45
+ : Effect.fail(unreadable('an attachment has no id or policy name'));
46
+ }),
47
+ );
48
+
49
+ /** The created row, which carries the id LiteLLM issued. `undefined` when the answer is not one. */
50
+ export const createAttachment = (
51
+ body: policies.CreatePolicyAttachmentPoliciesAttachmentsPostRequest,
52
+ ) =>
53
+ throughFetch(policies.createPolicyAttachmentPoliciesAttachmentsPost(body)).pipe(
54
+ Effect.map((response) => toAttributes(bodyOf(response))),
55
+ );
56
+
57
+ export const deleteAttachment = (attachmentId: string) =>
58
+ throughFetch(
59
+ policies.deletePolicyAttachmentPoliciesAttachmentsAttachmentIdDelete({
60
+ attachment_id: attachmentId,
61
+ }),
62
+ ).pipe(
63
+ Effect.asVoid,
64
+ Effect.catch((original) =>
65
+ listAttachments().pipe(
66
+ Effect.catch(() => Effect.fail(original)),
67
+ Effect.flatMap((rows) =>
68
+ rows.some((row) => row.attachmentId === attachmentId)
69
+ ? Effect.fail(original)
70
+ : Effect.void,
71
+ ),
72
+ ),
73
+ ),
74
+ );
@@ -0,0 +1,48 @@
1
+ /**
2
+ * What a `LiteLLM.PolicyAttachment` declares (props) and what is remembered about it (attributes).
3
+ *
4
+ * ★ AN ATTACHMENT SAYS WHERE A POLICY APPLIES (`LiteLLM_PolicyAttachmentTable`,
5
+ * `/policies/attachments`): a request gets the policy when it matches ALL of the attachment's
6
+ * selectors (`PolicyMatcher.scope_matches`, `policy_engine/policy_matcher.py`, 1.103.0). Teams, keys
7
+ * and models are matched by ALIAS or model name with `*` wildcards (`prefix-*`); a request's team
8
+ * alias is `LiteLLM.Team`'s `teamAlias`, not its id.
9
+ * ⛔ AN ATTACHMENT WITH NO SELECTOR IS GLOBAL. `PolicyScope.get_teams()`, `get_keys()` and
10
+ * `get_models()` each default to `["*"]` when empty, and empty `tags` are simply not checked
11
+ * (`policy_types.py`), so an attachment that names only a policy applies to EVERY request. It is
12
+ * refused unless `scope: '*'` says so. The other three empty selectors are "any", which is why
13
+ * `{ teams: ['x'] }` covers every key and every model of team x.
14
+ * ⛔ IMMUTABLE: THERE IS NO UPDATE ROUTE (`policy_endpoints.py` has create, get, list and delete only).
15
+ * Any change is a REPLACE: the new attachment is created first, then the old one is deleted, so for a
16
+ * moment both apply. Guardrails are additive, so overlap can only add enforcement, never drop it.
17
+ * ★ IDENTITY IS CONTENT. The id is issued by the proxy and there is no name, so a live attachment is
18
+ * adopted by matching every field (policy, scope, the four selector sets, priority).
19
+ * ⚠️ CONFIG-FILE ATTACHMENTS (`config-<n>` ids) are never adopted or deleted: they cannot be removed
20
+ * through the API.
21
+ */
22
+ export interface PolicyAttachmentProps {
23
+ /** The policy to attach, by name. The proxy refuses a name with no PRODUCTION version (404). */
24
+ readonly policyName: string;
25
+ /** `'*'` for a global attachment. Declare this OR a selector; neither is refused (see above). */
26
+ readonly scope?: '*';
27
+ /** Team ALIASES or patterns. A set. */
28
+ readonly teams?: readonly string[];
29
+ /** Key aliases or patterns. A set. */
30
+ readonly keys?: readonly string[];
31
+ /** Model names or patterns. A set. */
32
+ readonly models?: readonly string[];
33
+ /** Tag patterns matched against key and team `metadata.tags`. A set. */
34
+ readonly tags?: readonly string[];
35
+ /** Explicit execution order, lower first; an attachment with one runs before those without. 32-bit int. */
36
+ readonly priority?: number;
37
+ }
38
+
39
+ export interface PolicyAttachmentAttributes {
40
+ readonly attachmentId: string;
41
+ readonly policyName: string;
42
+ readonly scope: string | null;
43
+ readonly teams: readonly string[];
44
+ readonly keys: readonly string[];
45
+ readonly models: readonly string[];
46
+ readonly tags: readonly string[];
47
+ readonly priority: number | null;
48
+ }
@@ -0,0 +1,198 @@
1
+ /**
2
+ * `LiteLLM.PolicyAttachment` through Alchemy's real Plan and Apply over the fake proxy
3
+ * (`fake-policy-attachment-litellm.ts`): create, no-op, adopt by content, the create-first replace,
4
+ * removal, and the refusals.
5
+ *
6
+ * ★ EVERY VALUE IS `FAKE-*`.
7
+ * ⚠️ WHAT THE FAKE MODELS IS READ FROM THE 1.103.0 SOURCE, NOT MEASURED ON A LIVE PROXY (its header).
8
+ */
9
+ import { expect, test } from 'bun:test';
10
+ import { credentials } from '@distilled.cloud/litellm/Credentials';
11
+ import * as Effect from 'effect/Effect';
12
+ import * as Layer from 'effect/Layer';
13
+ import * as FetchHttpClient from 'effect/unstable/http/FetchHttpClient';
14
+ import { FAKE_BASE } from './fake-litellm.ts';
15
+ import {
16
+ type FakePolicyAttachmentLitellm,
17
+ attachmentRow,
18
+ startFakePolicyAttachmentLitellm,
19
+ } from './fake-policy-attachment-litellm.ts';
20
+ import { FAKE_KEY } from './fake-registry-base.ts';
21
+ import { fakeStack, writesOf } from './fake-stack.ts';
22
+ import { LiteLLMPolicyAttachment } from './policy-attachment.ts';
23
+ import { deleteAttachment } from './policy-attachment-operations.ts';
24
+ import type { PolicyAttachmentProps } from './policy-attachment-types.ts';
25
+
26
+ const stack = (fake: FakePolicyAttachmentLitellm) =>
27
+ fakeStack({ apiKey: FAKE_KEY, baseUrl: FAKE_BASE }, fake.fetch);
28
+ const declare = (props: PolicyAttachmentProps, name = 'Attach') =>
29
+ Effect.gen(function* () {
30
+ yield* LiteLLMPolicyAttachment(name, props);
31
+ });
32
+
33
+ const options = { knownTeams: ['fake-team'], policies: ['FAKE-baseline'] };
34
+ const global: PolicyAttachmentProps = { policyName: 'FAKE-baseline', scope: '*' };
35
+ const scoped: PolicyAttachmentProps = {
36
+ models: ['fake-code', 'fake-flash'],
37
+ policyName: 'FAKE-baseline',
38
+ priority: 10,
39
+ teams: ['fake-team'],
40
+ };
41
+
42
+ test('creates a global attachment, then a second deploy writes nothing', async () => {
43
+ const fake = startFakePolicyAttachmentLitellm(options);
44
+ const same = stack(fake);
45
+ expect(await same.deploy(declare(global))).toEqual({ Attach: 'create' });
46
+ expect(fake.attachments()[0]).toMatchObject({
47
+ policy_name: 'FAKE-baseline',
48
+ scope: '*',
49
+ teams: [],
50
+ });
51
+ const before = fake.requests().length;
52
+ expect(await same.deploy(declare(global))).toEqual({ Attach: 'noop' });
53
+ expect(writesOf(fake.requests().slice(before))).toEqual([]);
54
+ });
55
+
56
+ test('sends every declared selector and the priority, and only those', async () => {
57
+ const fake = startFakePolicyAttachmentLitellm(options);
58
+ await stack(fake).deploy(declare(scoped));
59
+ expect(fake.bodies()[0]).toEqual({
60
+ models: ['fake-code', 'fake-flash'],
61
+ policy_name: 'FAKE-baseline',
62
+ priority: 10,
63
+ teams: ['fake-team'],
64
+ });
65
+ });
66
+
67
+ test('a change is a create-first REPLACE and the old attachment is deleted afterwards', async () => {
68
+ const fake = startFakePolicyAttachmentLitellm(options);
69
+ const same = stack(fake);
70
+ await same.deploy(declare(scoped));
71
+ const before = fake.requests().length;
72
+ expect(await same.deploy(declare({ ...scoped, models: ['fake-code'] }))).toEqual({
73
+ Attach: 'replace',
74
+ });
75
+ // ★ create first, delete second: for a moment both applied, and none of that window was unattached
76
+ expect(writesOf(fake.requests().slice(before))).toEqual([
77
+ 'POST /policies/attachments',
78
+ 'DELETE /policies/attachments/FAKE-attachment-0001',
79
+ ]);
80
+ expect(fake.attachments()).toHaveLength(1);
81
+ expect(fake.attachments()[0]).toMatchObject({
82
+ attachment_id: 'FAKE-attachment-0002',
83
+ models: ['fake-code'],
84
+ });
85
+ });
86
+
87
+ test('removing the declaration DELETES the attachment: it holds no data, and a retained one would keep the scope attached', async () => {
88
+ const fake = startFakePolicyAttachmentLitellm(options);
89
+ const same = stack(fake);
90
+ await same.deploy(declare(scoped));
91
+ await same.deploy(Effect.void);
92
+ expect(fake.attachments()).toEqual([]);
93
+ });
94
+
95
+ test('a live attachment is adopted by CONTENT: Unowned without --adopt, adopted with it, and a match writes nothing', async () => {
96
+ const live = attachmentRow({
97
+ attachment_id: 'FAKE-live-1',
98
+ models: ['fake-flash', 'fake-code'],
99
+ policy_name: 'FAKE-baseline',
100
+ priority: 10,
101
+ teams: ['fake-team'],
102
+ });
103
+ const fake = startFakePolicyAttachmentLitellm({ ...options, seed: [live] });
104
+ const engine = stack(fake);
105
+ await expect(engine.deploy(declare(scoped))).rejects.toThrow();
106
+ expect(writesOf(fake.requests())).toEqual([]);
107
+ expect(Object.values(await engine.deploy(declare(scoped), { adopt: true }))).toEqual(['adopted']);
108
+ expect(writesOf(fake.requests())).toEqual([]);
109
+ });
110
+
111
+ test('two identical live attachments are refused, never guessed at', async () => {
112
+ const twin = (id: string) =>
113
+ attachmentRow({ attachment_id: id, policy_name: 'FAKE-baseline', scope: '*' });
114
+ const fake = startFakePolicyAttachmentLitellm({
115
+ ...options,
116
+ seed: [twin('FAKE-live-a'), twin('FAKE-live-b')],
117
+ });
118
+ await expect(stack(fake).deploy(declare(global), { adopt: true })).rejects.toThrow();
119
+ expect(writesOf(fake.requests())).toEqual([]);
120
+ });
121
+
122
+ test('a config-file attachment is never adopted: an equal one is ignored and a database one is created', async () => {
123
+ const fake = startFakePolicyAttachmentLitellm({
124
+ ...options,
125
+ configAttachments: [{ policy_name: 'FAKE-baseline', scope: '*' }],
126
+ });
127
+ expect(await stack(fake).deploy(declare(global))).toEqual({ Attach: 'create' });
128
+ expect(fake.attachments()).toHaveLength(1);
129
+ });
130
+
131
+ test('an attachment with no selector is refused before any request: LiteLLM would make it GLOBAL', async () => {
132
+ const fake = startFakePolicyAttachmentLitellm(options);
133
+ await expect(stack(fake).deploy(declare({ policyName: 'FAKE-baseline' }))).rejects.toThrow();
134
+ expect(fake.requests()).toEqual([]);
135
+ });
136
+
137
+ test('other refused declarations fail the plan before a single request', async () => {
138
+ for (const props of [
139
+ { ...scoped, policyName: ' ' },
140
+ { ...scoped, scope: 'FAKE' as never },
141
+ { ...scoped, teams: ['fake-team', 'fake-team'] },
142
+ { ...scoped, models: [''] },
143
+ { ...scoped, priority: 1.5 },
144
+ { ...scoped, priority: 2147483648 },
145
+ ]) {
146
+ const fake = startFakePolicyAttachmentLitellm(options);
147
+ await expect(stack(fake).deploy(declare(props))).rejects.toThrow();
148
+ expect(fake.requests()).toEqual([]);
149
+ }
150
+ });
151
+
152
+ test('a team that does not exist yet fails the create (declare the alias from the Team), a wildcard is let through', async () => {
153
+ const fake = startFakePolicyAttachmentLitellm(options);
154
+ await expect(
155
+ stack(fake).deploy(declare({ ...scoped, teams: ['fake-missing'] })),
156
+ ).rejects.toThrow();
157
+ expect(fake.attachments()).toEqual([]);
158
+ await stack(fake).deploy(declare({ ...scoped, teams: ['fake-*'] }, 'Wild'));
159
+ expect(fake.attachments()).toHaveLength(1);
160
+ });
161
+
162
+ test('a policy with no production version fails the create and leaves nothing behind', async () => {
163
+ const fake = startFakePolicyAttachmentLitellm({ knownTeams: [], policies: [] });
164
+ await expect(stack(fake).deploy(declare(global))).rejects.toThrow();
165
+ expect(fake.attachments()).toEqual([]);
166
+ });
167
+
168
+ const run = <A, E>(fetchFn: typeof globalThis.fetch, effect: Effect.Effect<A, E, unknown>) =>
169
+ Effect.runPromise(
170
+ effect.pipe(
171
+ Effect.provide(FetchHttpClient.layer),
172
+ Effect.provide(Layer.succeed(FetchHttpClient.Fetch, fetchFn)),
173
+ Effect.provide(credentials({ apiKey: FAKE_KEY, baseUrl: FAKE_BASE })),
174
+ ) as Effect.Effect<A, E, never>,
175
+ );
176
+
177
+ test('delete of a missing id is swallowed only because the list says it is gone', async () => {
178
+ const live = attachmentRow({
179
+ attachment_id: 'FAKE-live-2',
180
+ policy_name: 'FAKE-baseline',
181
+ scope: '*',
182
+ });
183
+ const fake = startFakePolicyAttachmentLitellm({ ...options, seed: [live] });
184
+ await run(fake.fetch, deleteAttachment('FAKE-live-2'));
185
+ expect(fake.attachments()).toEqual([]);
186
+ await run(fake.fetch, deleteAttachment('FAKE-live-2'));
187
+ });
188
+
189
+ test('a delete refused for lack of rights re-raises the ORIGINAL error and the row stays', async () => {
190
+ const live = attachmentRow({
191
+ attachment_id: 'FAKE-live-3',
192
+ policy_name: 'FAKE-baseline',
193
+ scope: '*',
194
+ });
195
+ const fake = startFakePolicyAttachmentLitellm({ ...options, forbidDelete: true, seed: [live] });
196
+ await expect(run(fake.fetch, deleteAttachment('FAKE-live-3'))).rejects.toThrow();
197
+ expect(fake.attachments()).toHaveLength(1);
198
+ });