@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.
- package/README.md +10 -1
- package/dist/caddy/index.js +35 -1
- package/dist/caddy/index.js.map +3 -3
- package/dist/launchd/index.js +35 -1
- package/dist/launchd/index.js.map +3 -3
- package/dist/linux/index.js +35 -1
- package/dist/linux/index.js.map +3 -3
- package/dist/litellm/access-group-form.d.ts +26 -0
- package/dist/litellm/access-group-form.d.ts.map +1 -0
- package/dist/litellm/access-group-operations.d.ts +37 -0
- package/dist/litellm/access-group-operations.d.ts.map +1 -0
- package/dist/litellm/access-group-types.d.ts +49 -0
- package/dist/litellm/access-group-types.d.ts.map +1 -0
- package/dist/litellm/access-group.d.ts +55 -0
- package/dist/litellm/access-group.d.ts.map +1 -0
- package/dist/litellm/credential-errors.d.ts +131 -0
- package/dist/litellm/credential-errors.d.ts.map +1 -0
- package/dist/litellm/credential-form.d.ts +79 -0
- package/dist/litellm/credential-form.d.ts.map +1 -0
- package/dist/litellm/credential-operations.d.ts +84 -0
- package/dist/litellm/credential-operations.d.ts.map +1 -0
- package/dist/litellm/credential-rewrite.d.ts +6 -0
- package/dist/litellm/credential-rewrite.d.ts.map +1 -0
- package/dist/litellm/credential-types.d.ts +87 -0
- package/dist/litellm/credential-types.d.ts.map +1 -0
- package/dist/litellm/credential-values.d.ts +53 -0
- package/dist/litellm/credential-values.d.ts.map +1 -0
- package/dist/litellm/credential.d.ts +83 -0
- package/dist/litellm/credential.d.ts.map +1 -0
- package/dist/litellm/fake-access-group-litellm.d.ts +34 -0
- package/dist/litellm/fake-access-group-litellm.d.ts.map +1 -0
- package/dist/litellm/fake-credential-litellm.d.ts +54 -0
- package/dist/litellm/fake-credential-litellm.d.ts.map +1 -0
- package/dist/litellm/fake-keys.d.ts +53 -0
- package/dist/litellm/fake-keys.d.ts.map +1 -0
- package/dist/litellm/fake-litellm.d.ts +4 -1
- package/dist/litellm/fake-litellm.d.ts.map +1 -1
- package/dist/litellm/fake-mcp-litellm.d.ts +67 -0
- package/dist/litellm/fake-mcp-litellm.d.ts.map +1 -0
- package/dist/litellm/fake-model-litellm-post-update.d.ts +49 -0
- package/dist/litellm/fake-model-litellm-post-update.d.ts.map +1 -0
- package/dist/litellm/fake-model-litellm.d.ts +48 -0
- package/dist/litellm/fake-model-litellm.d.ts.map +1 -0
- package/dist/litellm/fake-policy-attachment-litellm.d.ts +33 -0
- package/dist/litellm/fake-policy-attachment-litellm.d.ts.map +1 -0
- package/dist/litellm/fake-policy-litellm.d.ts +32 -0
- package/dist/litellm/fake-policy-litellm.d.ts.map +1 -0
- package/dist/litellm/fake-registry-base.d.ts +42 -0
- package/dist/litellm/fake-registry-base.d.ts.map +1 -0
- package/dist/litellm/fake-stack.d.ts +8 -1
- package/dist/litellm/fake-stack.d.ts.map +1 -1
- package/dist/litellm/fake-team-litellm.d.ts +47 -0
- package/dist/litellm/fake-team-litellm.d.ts.map +1 -0
- package/dist/litellm/fake-tool-litellm.d.ts +28 -0
- package/dist/litellm/fake-tool-litellm.d.ts.map +1 -0
- package/dist/litellm/fake-toolset-litellm.d.ts +29 -0
- package/dist/litellm/fake-toolset-litellm.d.ts.map +1 -0
- package/dist/litellm/index.d.ts +18 -0
- package/dist/litellm/index.d.ts.map +1 -1
- package/dist/litellm/index.js +3078 -2
- package/dist/litellm/index.js.map +65 -4
- package/dist/litellm/key-errors.d.ts +169 -0
- package/dist/litellm/key-errors.d.ts.map +1 -0
- package/dist/litellm/key-form.d.ts +119 -0
- package/dist/litellm/key-form.d.ts.map +1 -0
- package/dist/litellm/key-harness.d.ts +41 -0
- package/dist/litellm/key-harness.d.ts.map +1 -0
- package/dist/litellm/key-metadata-errors.d.ts +30 -0
- package/dist/litellm/key-metadata-errors.d.ts.map +1 -0
- package/dist/litellm/key-metadata.d.ts +22 -0
- package/dist/litellm/key-metadata.d.ts.map +1 -0
- package/dist/litellm/key-operations.d.ts +63 -0
- package/dist/litellm/key-operations.d.ts.map +1 -0
- package/dist/litellm/key-secret.d.ts +33 -0
- package/dist/litellm/key-secret.d.ts.map +1 -0
- package/dist/litellm/key.d.ts +78 -0
- package/dist/litellm/key.d.ts.map +1 -0
- package/dist/litellm/mcp-server-credential.d.ts +50 -0
- package/dist/litellm/mcp-server-credential.d.ts.map +1 -0
- package/dist/litellm/mcp-server-errors.d.ts +101 -0
- package/dist/litellm/mcp-server-errors.d.ts.map +1 -0
- package/dist/litellm/mcp-server-form.d.ts +56 -0
- package/dist/litellm/mcp-server-form.d.ts.map +1 -0
- package/dist/litellm/mcp-server-locate.d.ts +26 -0
- package/dist/litellm/mcp-server-locate.d.ts.map +1 -0
- package/dist/litellm/mcp-server-operations.d.ts +63 -0
- package/dist/litellm/mcp-server-operations.d.ts.map +1 -0
- package/dist/litellm/mcp-server-shadow.d.ts +24 -0
- package/dist/litellm/mcp-server-shadow.d.ts.map +1 -0
- package/dist/litellm/mcp-server-types.d.ts +118 -0
- package/dist/litellm/mcp-server-types.d.ts.map +1 -0
- package/dist/litellm/mcp-server-url.d.ts +11 -0
- package/dist/litellm/mcp-server-url.d.ts.map +1 -0
- package/dist/litellm/mcp-server.d.ts +95 -0
- package/dist/litellm/mcp-server.d.ts.map +1 -0
- package/dist/litellm/model-claim.d.ts +24 -0
- package/dist/litellm/model-claim.d.ts.map +1 -0
- package/dist/litellm/model-credential.d.ts +12 -0
- package/dist/litellm/model-credential.d.ts.map +1 -0
- package/dist/litellm/model-errors.d.ts +119 -0
- package/dist/litellm/model-errors.d.ts.map +1 -0
- package/dist/litellm/model-form.d.ts +78 -0
- package/dist/litellm/model-form.d.ts.map +1 -0
- package/dist/litellm/model-locate.d.ts +13 -0
- package/dist/litellm/model-locate.d.ts.map +1 -0
- package/dist/litellm/model-operations.d.ts +59 -0
- package/dist/litellm/model-operations.d.ts.map +1 -0
- package/dist/litellm/model-types.d.ts +91 -0
- package/dist/litellm/model-types.d.ts.map +1 -0
- package/dist/litellm/model.d.ts +88 -0
- package/dist/litellm/model.d.ts.map +1 -0
- package/dist/litellm/policy-attachment-form.d.ts +19 -0
- package/dist/litellm/policy-attachment-form.d.ts.map +1 -0
- package/dist/litellm/policy-attachment-operations.d.ts +31 -0
- package/dist/litellm/policy-attachment-operations.d.ts.map +1 -0
- package/dist/litellm/policy-attachment-types.d.ts +48 -0
- package/dist/litellm/policy-attachment-types.d.ts.map +1 -0
- package/dist/litellm/policy-attachment.d.ts +61 -0
- package/dist/litellm/policy-attachment.d.ts.map +1 -0
- package/dist/litellm/policy-form.d.ts +22 -0
- package/dist/litellm/policy-form.d.ts.map +1 -0
- package/dist/litellm/policy-operations.d.ts +42 -0
- package/dist/litellm/policy-operations.d.ts.map +1 -0
- package/dist/litellm/policy-types.d.ts +57 -0
- package/dist/litellm/policy-types.d.ts.map +1 -0
- package/dist/litellm/policy.d.ts +65 -0
- package/dist/litellm/policy.d.ts.map +1 -0
- package/dist/litellm/providers.d.ts +1 -1
- package/dist/litellm/providers.d.ts.map +1 -1
- package/dist/litellm/registry-errors.d.ts +62 -0
- package/dist/litellm/registry-errors.d.ts.map +1 -0
- package/dist/litellm/registry-support.d.ts +42 -0
- package/dist/litellm/registry-support.d.ts.map +1 -0
- package/dist/litellm/team-form.d.ts +26 -0
- package/dist/litellm/team-form.d.ts.map +1 -0
- package/dist/litellm/team-members.d.ts +10 -0
- package/dist/litellm/team-members.d.ts.map +1 -0
- package/dist/litellm/team-operations.d.ts +38 -0
- package/dist/litellm/team-operations.d.ts.map +1 -0
- package/dist/litellm/team-permission.d.ts +35 -0
- package/dist/litellm/team-permission.d.ts.map +1 -0
- package/dist/litellm/team-types.d.ts +101 -0
- package/dist/litellm/team-types.d.ts.map +1 -0
- package/dist/litellm/team.d.ts +67 -0
- package/dist/litellm/team.d.ts.map +1 -0
- package/dist/litellm/tool-policy-form.d.ts +21 -0
- package/dist/litellm/tool-policy-form.d.ts.map +1 -0
- package/dist/litellm/tool-policy-operations.d.ts +30 -0
- package/dist/litellm/tool-policy-operations.d.ts.map +1 -0
- package/dist/litellm/tool-policy-types.d.ts +46 -0
- package/dist/litellm/tool-policy-types.d.ts.map +1 -0
- package/dist/litellm/tool-policy.d.ts +62 -0
- package/dist/litellm/tool-policy.d.ts.map +1 -0
- package/dist/litellm/toolset-form.d.ts +20 -0
- package/dist/litellm/toolset-form.d.ts.map +1 -0
- package/dist/litellm/toolset-operations.d.ts +39 -0
- package/dist/litellm/toolset-operations.d.ts.map +1 -0
- package/dist/litellm/toolset-types.d.ts +46 -0
- package/dist/litellm/toolset-types.d.ts.map +1 -0
- package/dist/litellm/toolset.d.ts +55 -0
- package/dist/litellm/toolset.d.ts.map +1 -0
- package/dist/openbao/index.js +35 -1
- package/dist/openbao/index.js.map +3 -3
- package/dist/ownership/rows.d.ts +11 -0
- package/dist/ownership/rows.d.ts.map +1 -1
- package/dist/postgres/connection.d.ts +14 -2
- package/dist/postgres/connection.d.ts.map +1 -1
- package/dist/postgres/database-sql.d.ts +27 -8
- package/dist/postgres/database-sql.d.ts.map +1 -1
- package/dist/postgres/fake-grants-parse.d.ts +37 -0
- package/dist/postgres/fake-grants-parse.d.ts.map +1 -0
- package/dist/postgres/fake-grants-read.d.ts +61 -0
- package/dist/postgres/fake-grants-read.d.ts.map +1 -0
- package/dist/postgres/fake-grants-sql.d.ts +46 -0
- package/dist/postgres/fake-grants-sql.d.ts.map +1 -0
- package/dist/postgres/fake-grants-write.d.ts +29 -0
- package/dist/postgres/fake-grants-write.d.ts.map +1 -0
- package/dist/postgres/fake-role-sql.d.ts +41 -0
- package/dist/postgres/fake-role-sql.d.ts.map +1 -0
- package/dist/postgres/fake-schema-sql.d.ts +42 -0
- package/dist/postgres/fake-schema-sql.d.ts.map +1 -0
- package/dist/postgres/fake-sql-parse.d.ts +23 -0
- package/dist/postgres/fake-sql-parse.d.ts.map +1 -0
- package/dist/postgres/fake-sql-quote.d.ts +12 -0
- package/dist/postgres/fake-sql-quote.d.ts.map +1 -0
- package/dist/postgres/fake-sql.d.ts +42 -0
- package/dist/postgres/fake-sql.d.ts.map +1 -1
- package/dist/postgres/grants-attrs.d.ts +115 -0
- package/dist/postgres/grants-attrs.d.ts.map +1 -0
- package/dist/postgres/grants-declare.d.ts +88 -0
- package/dist/postgres/grants-declare.d.ts.map +1 -0
- package/dist/postgres/grants-diff.d.ts +25 -0
- package/dist/postgres/grants-diff.d.ts.map +1 -0
- package/dist/postgres/grants-errors.d.ts +137 -0
- package/dist/postgres/grants-errors.d.ts.map +1 -0
- package/dist/postgres/grants-existence.d.ts +14 -0
- package/dist/postgres/grants-existence.d.ts.map +1 -0
- package/dist/postgres/grants-ops.d.ts +65 -0
- package/dist/postgres/grants-ops.d.ts.map +1 -0
- package/dist/postgres/grants-ownership.d.ts +18 -0
- package/dist/postgres/grants-ownership.d.ts.map +1 -0
- package/dist/postgres/grants-plan-restore.d.ts +22 -0
- package/dist/postgres/grants-plan-restore.d.ts.map +1 -0
- package/dist/postgres/grants-plan.d.ts +65 -0
- package/dist/postgres/grants-plan.d.ts.map +1 -0
- package/dist/postgres/grants-read-defaults.d.ts +17 -0
- package/dist/postgres/grants-read-defaults.d.ts.map +1 -0
- package/dist/postgres/grants-read.d.ts +77 -0
- package/dist/postgres/grants-read.d.ts.map +1 -0
- package/dist/postgres/grants-refuse.d.ts +25 -0
- package/dist/postgres/grants-refuse.d.ts.map +1 -0
- package/dist/postgres/grants-sql.d.ts +42 -0
- package/dist/postgres/grants-sql.d.ts.map +1 -0
- package/dist/postgres/grants-words.d.ts +29 -0
- package/dist/postgres/grants-words.d.ts.map +1 -0
- package/dist/postgres/grants.d.ts +37 -0
- package/dist/postgres/grants.d.ts.map +1 -0
- package/dist/postgres/index.d.ts +31 -6
- package/dist/postgres/index.d.ts.map +1 -1
- package/dist/postgres/index.js +1998 -37
- package/dist/postgres/index.js.map +46 -7
- package/dist/postgres/installed-classifier.d.ts +13 -0
- package/dist/postgres/installed-classifier.d.ts.map +1 -0
- package/dist/postgres/providers.d.ts +16 -8
- package/dist/postgres/providers.d.ts.map +1 -1
- package/dist/postgres/psql-executor.d.ts +4 -0
- package/dist/postgres/psql-executor.d.ts.map +1 -1
- package/dist/postgres/role-attrs.d.ts +112 -0
- package/dist/postgres/role-attrs.d.ts.map +1 -0
- package/dist/postgres/role-diff.d.ts +22 -0
- package/dist/postgres/role-diff.d.ts.map +1 -0
- package/dist/postgres/role-errors.d.ts +132 -0
- package/dist/postgres/role-errors.d.ts.map +1 -0
- package/dist/postgres/role-membership-sql.d.ts +78 -0
- package/dist/postgres/role-membership-sql.d.ts.map +1 -0
- package/dist/postgres/role-provider.d.ts +11 -0
- package/dist/postgres/role-provider.d.ts.map +1 -0
- package/dist/postgres/role-scram.d.ts +8 -0
- package/dist/postgres/role-scram.d.ts.map +1 -0
- package/dist/postgres/role-secrets.d.ts +38 -0
- package/dist/postgres/role-secrets.d.ts.map +1 -0
- package/dist/postgres/role-sql.d.ts +68 -0
- package/dist/postgres/role-sql.d.ts.map +1 -0
- package/dist/postgres/role.d.ts +57 -0
- package/dist/postgres/role.d.ts.map +1 -0
- package/dist/postgres/schema-assert.d.ts +50 -0
- package/dist/postgres/schema-assert.d.ts.map +1 -0
- package/dist/postgres/schema-attrs.d.ts +51 -0
- package/dist/postgres/schema-attrs.d.ts.map +1 -0
- package/dist/postgres/schema-delete.d.ts +28 -0
- package/dist/postgres/schema-delete.d.ts.map +1 -0
- package/dist/postgres/schema-diff.d.ts +14 -0
- package/dist/postgres/schema-diff.d.ts.map +1 -0
- package/dist/postgres/schema-errors.d.ts +139 -0
- package/dist/postgres/schema-errors.d.ts.map +1 -0
- package/dist/postgres/schema-identity.d.ts +8 -0
- package/dist/postgres/schema-identity.d.ts.map +1 -0
- package/dist/postgres/schema-reconcile.d.ts +12 -0
- package/dist/postgres/schema-reconcile.d.ts.map +1 -0
- package/dist/postgres/schema-sql.d.ts +45 -0
- package/dist/postgres/schema-sql.d.ts.map +1 -0
- package/dist/postgres/schema-test-kit.d.ts +67 -0
- package/dist/postgres/schema-test-kit.d.ts.map +1 -0
- package/dist/postgres/schema.d.ts +54 -0
- package/dist/postgres/schema.d.ts.map +1 -0
- package/dist/proxmox/index.js +35 -1
- package/dist/proxmox/index.js.map +3 -3
- package/dist/release/fake-engine.d.ts +10 -0
- package/dist/release/fake-engine.d.ts.map +1 -1
- package/dist/release/index.js +35 -1
- package/dist/release/index.js.map +3 -3
- package/dist/valkey/acl-attrs.d.ts +94 -0
- package/dist/valkey/acl-attrs.d.ts.map +1 -0
- package/dist/valkey/acl-compare.d.ts +28 -0
- package/dist/valkey/acl-compare.d.ts.map +1 -0
- package/dist/valkey/acl-connection.d.ts +6 -0
- package/dist/valkey/acl-connection.d.ts.map +1 -0
- package/dist/valkey/acl-form.d.ts +62 -0
- package/dist/valkey/acl-form.d.ts.map +1 -0
- package/dist/valkey/acl-managed.d.ts +4 -0
- package/dist/valkey/acl-managed.d.ts.map +1 -0
- package/dist/valkey/acl-ops.d.ts +37 -0
- package/dist/valkey/acl-ops.d.ts.map +1 -0
- package/dist/valkey/acl-profiles.d.ts +53 -0
- package/dist/valkey/acl-profiles.d.ts.map +1 -0
- package/dist/valkey/acl-reconcile.d.ts +26 -0
- package/dist/valkey/acl-reconcile.d.ts.map +1 -0
- package/dist/valkey/acl.d.ts +24 -0
- package/dist/valkey/acl.d.ts.map +1 -0
- package/dist/valkey/connection-service.d.ts +22 -0
- package/dist/valkey/connection-service.d.ts.map +1 -0
- package/dist/valkey/connection.d.ts +59 -0
- package/dist/valkey/connection.d.ts.map +1 -0
- package/dist/valkey/errors.d.ts +165 -0
- package/dist/valkey/errors.d.ts.map +1 -0
- package/dist/valkey/fake-acl.d.ts +29 -0
- package/dist/valkey/fake-acl.d.ts.map +1 -0
- package/dist/valkey/fake-valkey.d.ts +35 -0
- package/dist/valkey/fake-valkey.d.ts.map +1 -0
- package/dist/valkey/handler-test-fixture.d.ts +20 -0
- package/dist/valkey/handler-test-fixture.d.ts.map +1 -0
- package/dist/valkey/index.d.ts +18 -0
- package/dist/valkey/index.d.ts.map +1 -0
- package/dist/valkey/index.js +964 -0
- package/dist/valkey/index.js.map +27 -0
- package/dist/valkey/instance-attrs.d.ts +46 -0
- package/dist/valkey/instance-attrs.d.ts.map +1 -0
- package/dist/valkey/instance-form.d.ts +22 -0
- package/dist/valkey/instance-form.d.ts.map +1 -0
- package/dist/valkey/instance.d.ts +48 -0
- package/dist/valkey/instance.d.ts.map +1 -0
- package/dist/valkey/provider-socket-fixture.d.ts +8 -0
- package/dist/valkey/provider-socket-fixture.d.ts.map +1 -0
- package/dist/valkey/providers.d.ts +16 -0
- package/dist/valkey/providers.d.ts.map +1 -0
- package/dist/valkey/resp.d.ts +38 -0
- package/dist/valkey/resp.d.ts.map +1 -0
- package/dist/valkey/transport-error.d.ts +36 -0
- package/dist/valkey/transport-error.d.ts.map +1 -0
- package/dist/valkey/transport.d.ts +50 -0
- package/dist/valkey/transport.d.ts.map +1 -0
- package/docs/litellm-access-group.md +105 -0
- package/docs/litellm-credential.md +60 -0
- package/docs/litellm-key-metadata.md +56 -0
- package/docs/litellm-key.md +194 -0
- package/docs/litellm-mcp.md +187 -0
- package/docs/litellm-model.md +111 -0
- package/docs/litellm-policy-attachment.md +86 -0
- package/docs/litellm-policy.md +103 -0
- package/docs/litellm-team.md +117 -0
- package/docs/litellm-tool-policy.md +90 -0
- package/docs/litellm-toolset.md +92 -0
- package/docs/litellm.md +16 -9
- package/docs/peers.md +15 -6
- package/docs/postgres-grants-example.md +55 -0
- package/docs/postgres-grants.md +157 -0
- package/docs/postgres-role.md +126 -0
- package/docs/postgres-schema.md +139 -0
- package/docs/postgres.md +20 -3
- package/docs/valkey-transport.md +29 -0
- package/docs/valkey.md +187 -0
- package/package.json +6 -2
- package/src/ceph/ceph-argv.test.ts +1 -1
- package/src/launchd/sudo-allowlist.test.ts +2 -2
- package/src/linux/sudo-allowlist.test.ts +3 -3
- package/src/linux/sudo-runner.test.ts +2 -2
- package/src/litellm/access-group-form.ts +105 -0
- package/src/litellm/access-group-operations.ts +92 -0
- package/src/litellm/access-group-types.ts +49 -0
- package/src/litellm/access-group.test.ts +197 -0
- package/src/litellm/access-group.ts +151 -0
- package/src/litellm/credential-errors.ts +183 -0
- package/src/litellm/credential-form.test.ts +151 -0
- package/src/litellm/credential-form.ts +211 -0
- package/src/litellm/credential-guards.test.ts +241 -0
- package/src/litellm/credential-operations.ts +208 -0
- package/src/litellm/credential-recovery.test.ts +93 -0
- package/src/litellm/credential-rewrite.test.ts +164 -0
- package/src/litellm/credential-rewrite.ts +50 -0
- package/src/litellm/credential-types.ts +117 -0
- package/src/litellm/credential-values.ts +94 -0
- package/src/litellm/credential.test.ts +218 -0
- package/src/litellm/credential.ts +227 -0
- package/src/litellm/fake-access-group-litellm.ts +127 -0
- package/src/litellm/fake-credential-litellm.ts +216 -0
- package/src/litellm/fake-keys.ts +190 -0
- package/src/litellm/fake-litellm.ts +19 -9
- package/src/litellm/fake-mcp-litellm.ts +214 -0
- package/src/litellm/fake-model-litellm-post-update.ts +103 -0
- package/src/litellm/fake-model-litellm.ts +233 -0
- package/src/litellm/fake-policy-attachment-litellm.ts +97 -0
- package/src/litellm/fake-policy-litellm.ts +186 -0
- package/src/litellm/fake-registry-base.ts +69 -0
- package/src/litellm/fake-stack.ts +17 -1
- package/src/litellm/fake-team-litellm.ts +229 -0
- package/src/litellm/fake-tool-litellm.ts +88 -0
- package/src/litellm/fake-toolset-litellm.ts +110 -0
- package/src/litellm/index.ts +140 -0
- package/src/litellm/key-delete.test.ts +212 -0
- package/src/litellm/key-errors.ts +241 -0
- package/src/litellm/key-form.ts +200 -0
- package/src/litellm/key-harness.ts +121 -0
- package/src/litellm/key-metadata-errors.ts +49 -0
- package/src/litellm/key-metadata.test.ts +156 -0
- package/src/litellm/key-metadata.ts +74 -0
- package/src/litellm/key-operations.ts +184 -0
- package/src/litellm/key-rotation.test.ts +110 -0
- package/src/litellm/key-scope.test.ts +135 -0
- package/src/litellm/key-secret.test.ts +248 -0
- package/src/litellm/key-secret.ts +137 -0
- package/src/litellm/key-transport.test.ts +166 -0
- package/src/litellm/key.test.ts +231 -0
- package/src/litellm/key.ts +213 -0
- package/src/litellm/mcp-server-adopt.test.ts +86 -0
- package/src/litellm/mcp-server-alias.test.ts +184 -0
- package/src/litellm/mcp-server-credential.ts +85 -0
- package/src/litellm/mcp-server-errors.ts +141 -0
- package/src/litellm/mcp-server-exports.test.ts +15 -0
- package/src/litellm/mcp-server-form.test.ts +241 -0
- package/src/litellm/mcp-server-form.ts +233 -0
- package/src/litellm/mcp-server-handlers.test.ts +150 -0
- package/src/litellm/mcp-server-locate.ts +66 -0
- package/src/litellm/mcp-server-operations.ts +111 -0
- package/src/litellm/mcp-server-registry.test.ts +146 -0
- package/src/litellm/mcp-server-retype.test.ts +114 -0
- package/src/litellm/mcp-server-shadow.test.ts +91 -0
- package/src/litellm/mcp-server-shadow.ts +45 -0
- package/src/litellm/mcp-server-tools.test.ts +85 -0
- package/src/litellm/mcp-server-types.ts +143 -0
- package/src/litellm/mcp-server-url.ts +66 -0
- package/src/litellm/mcp-server.test.ts +213 -0
- package/src/litellm/mcp-server.ts +225 -0
- package/src/litellm/model-claim.ts +69 -0
- package/src/litellm/model-credential.ts +45 -0
- package/src/litellm/model-errors.ts +168 -0
- package/src/litellm/model-foreign-row.test.ts +173 -0
- package/src/litellm/model-form.test.ts +228 -0
- package/src/litellm/model-form.ts +223 -0
- package/src/litellm/model-locate.test.ts +128 -0
- package/src/litellm/model-locate.ts +67 -0
- package/src/litellm/model-operations.test.ts +93 -0
- package/src/litellm/model-operations.ts +146 -0
- package/src/litellm/model-params-preserve.test.ts +119 -0
- package/src/litellm/model-recovery.test.ts +57 -0
- package/src/litellm/model-types.ts +112 -0
- package/src/litellm/model.test.ts +222 -0
- package/src/litellm/model.ts +237 -0
- package/src/litellm/policy-attachment-form.ts +103 -0
- package/src/litellm/policy-attachment-operations.ts +74 -0
- package/src/litellm/policy-attachment-types.ts +48 -0
- package/src/litellm/policy-attachment.test.ts +198 -0
- package/src/litellm/policy-attachment.ts +178 -0
- package/src/litellm/policy-form.ts +122 -0
- package/src/litellm/policy-operations.ts +112 -0
- package/src/litellm/policy-types.ts +58 -0
- package/src/litellm/policy.test.ts +214 -0
- package/src/litellm/policy.ts +146 -0
- package/src/litellm/providers.ts +24 -3
- package/src/litellm/registry-errors.ts +88 -0
- package/src/litellm/registry-exports.test.ts +66 -0
- package/src/litellm/registry-support.ts +109 -0
- package/src/litellm/team-form.ts +140 -0
- package/src/litellm/team-members.test.ts +150 -0
- package/src/litellm/team-members.ts +58 -0
- package/src/litellm/team-operations.ts +90 -0
- package/src/litellm/team-permission.ts +159 -0
- package/src/litellm/team-read.test.ts +89 -0
- package/src/litellm/team-types.ts +106 -0
- package/src/litellm/team.test.ts +206 -0
- package/src/litellm/team.ts +159 -0
- package/src/litellm/tool-policy-form.ts +94 -0
- package/src/litellm/tool-policy-operations.ts +58 -0
- package/src/litellm/tool-policy-types.ts +48 -0
- package/src/litellm/tool-policy.test.ts +172 -0
- package/src/litellm/tool-policy.ts +125 -0
- package/src/litellm/toolset-form.ts +104 -0
- package/src/litellm/toolset-operations.ts +90 -0
- package/src/litellm/toolset-types.ts +47 -0
- package/src/litellm/toolset.test.ts +198 -0
- package/src/litellm/toolset.ts +151 -0
- package/src/ownership/ownership.test.ts +33 -1
- package/src/ownership/rows.ts +53 -0
- package/src/postgres/connection.ts +42 -7
- package/src/postgres/database-bigint-serialization.test.ts +1 -0
- package/src/postgres/database-refusals.test.ts +2 -2
- package/src/postgres/database-sql.ts +36 -9
- package/src/postgres/fake-grants-parse.ts +189 -0
- package/src/postgres/fake-grants-read.ts +250 -0
- package/src/postgres/fake-grants-sql.ts +207 -0
- package/src/postgres/fake-grants-write.ts +90 -0
- package/src/postgres/fake-role-sql.ts +240 -0
- package/src/postgres/fake-schema-semantics.test.ts +36 -0
- package/src/postgres/fake-schema-sql.ts +146 -0
- package/src/postgres/fake-sql-parse.ts +86 -0
- package/src/postgres/fake-sql-quote.ts +36 -0
- package/src/postgres/fake-sql.ts +166 -46
- package/src/postgres/fixtures/pg-REL_18_6-acl.h.txt +147 -0
- package/src/postgres/fixtures/pg-REL_18_6-aclexplode-func.txt +31 -0
- package/src/postgres/fixtures/pg-REL_18_6-alter-default-privileges-synopsis.txt +82 -0
- package/src/postgres/fixtures/pg-REL_18_6-alter_role.sgml.txt +44 -0
- package/src/postgres/fixtures/pg-REL_18_6-create_role.sgml.txt +26 -0
- package/src/postgres/fixtures/pg-REL_18_6-grant-synopsis.txt +95 -0
- package/src/postgres/fixtures/pg-REL_18_6-pg_attribute.h.txt +24 -0
- package/src/postgres/fixtures/pg-REL_18_6-pg_auth_members.h.txt +62 -0
- package/src/postgres/fixtures/pg-REL_18_6-pg_authid.h.txt +70 -0
- package/src/postgres/fixtures/pg-REL_18_6-pg_class.h.txt +33 -0
- package/src/postgres/fixtures/pg-REL_18_6-pg_default_acl.h.txt +81 -0
- package/src/postgres/fixtures/pg-REL_18_6-pg_namespace.h.txt +28 -0
- package/src/postgres/fixtures/pg-REL_18_6-revoke-synopsis.txt +124 -0
- package/src/postgres/grants-acl.test.ts +61 -0
- package/src/postgres/grants-attrs.ts +185 -0
- package/src/postgres/grants-database.test.ts +158 -0
- package/src/postgres/grants-declare.ts +172 -0
- package/src/postgres/grants-delete-proof.test.ts +154 -0
- package/src/postgres/grants-diff.ts +101 -0
- package/src/postgres/grants-errors.ts +215 -0
- package/src/postgres/grants-existence-guards.test.ts +113 -0
- package/src/postgres/grants-existence.ts +101 -0
- package/src/postgres/grants-lifecycle-guards.test.ts +208 -0
- package/src/postgres/grants-lifecycle-repair.test.ts +208 -0
- package/src/postgres/grants-lifecycle.test.ts +181 -0
- package/src/postgres/grants-ops.ts +229 -0
- package/src/postgres/grants-owner-revoke.test.ts +121 -0
- package/src/postgres/grants-ownership.ts +49 -0
- package/src/postgres/grants-plan-collateral.test.ts +202 -0
- package/src/postgres/grants-plan-diff.test.ts +204 -0
- package/src/postgres/grants-plan-restore.ts +43 -0
- package/src/postgres/grants-plan-revocations.test.ts +69 -0
- package/src/postgres/grants-plan.test.ts +148 -0
- package/src/postgres/grants-plan.ts +245 -0
- package/src/postgres/grants-provenance.test.ts +134 -0
- package/src/postgres/grants-psql-lifecycle.test.ts +105 -0
- package/src/postgres/grants-read-absent.test.ts +91 -0
- package/src/postgres/grants-read-defaults.ts +51 -0
- package/src/postgres/grants-read.ts +250 -0
- package/src/postgres/grants-refusals.test.ts +136 -0
- package/src/postgres/grants-refuse.ts +119 -0
- package/src/postgres/grants-relkind-wire.test.ts +111 -0
- package/src/postgres/grants-repair-plan.test.ts +201 -0
- package/src/postgres/grants-sql.test.ts +128 -0
- package/src/postgres/grants-sql.ts +144 -0
- package/src/postgres/grants-words.ts +34 -0
- package/src/postgres/grants.ts +110 -0
- package/src/postgres/index.ts +129 -5
- package/src/postgres/installed-classifier.ts +36 -0
- package/src/postgres/providers.ts +33 -9
- package/src/postgres/psql-executor-escaping.test.ts +67 -0
- package/src/postgres/psql-executor.test.ts +63 -2
- package/src/postgres/psql-executor.ts +74 -34
- package/src/postgres/role-adopt.test.ts +98 -0
- package/src/postgres/role-attrs.ts +137 -0
- package/src/postgres/role-diff.ts +93 -0
- package/src/postgres/role-errors.ts +206 -0
- package/src/postgres/role-guard.test.ts +60 -0
- package/src/postgres/role-identity.test.ts +197 -0
- package/src/postgres/role-lifecycle.test.ts +228 -0
- package/src/postgres/role-membership-options.test.ts +73 -0
- package/src/postgres/role-membership-repair.test.ts +153 -0
- package/src/postgres/role-membership-sql.ts +223 -0
- package/src/postgres/role-password-ascii.test.ts +53 -0
- package/src/postgres/role-password-transports.test.ts +123 -0
- package/src/postgres/role-password.test.ts +158 -0
- package/src/postgres/role-privilege.test.ts +107 -0
- package/src/postgres/role-provenance.test.ts +200 -0
- package/src/postgres/role-provider.ts +105 -0
- package/src/postgres/role-refusals.test.ts +225 -0
- package/src/postgres/role-scram.test.ts +38 -0
- package/src/postgres/role-scram.ts +40 -0
- package/src/postgres/role-secrets.ts +61 -0
- package/src/postgres/role-sql.ts +213 -0
- package/src/postgres/role-valid-until.test.ts +68 -0
- package/src/postgres/role.ts +226 -0
- package/src/postgres/schema-assert.ts +139 -0
- package/src/postgres/schema-attrs.ts +71 -0
- package/src/postgres/schema-delete-proofs.test.ts +192 -0
- package/src/postgres/schema-delete.ts +96 -0
- package/src/postgres/schema-diff.ts +44 -0
- package/src/postgres/schema-drop-2bp01.test.ts +146 -0
- package/src/postgres/schema-errors.ts +209 -0
- package/src/postgres/schema-handlers.test.ts +193 -0
- package/src/postgres/schema-identity.ts +39 -0
- package/src/postgres/schema-lifecycle.test.ts +246 -0
- package/src/postgres/schema-race.test.ts +70 -0
- package/src/postgres/schema-reconcile.ts +100 -0
- package/src/postgres/schema-refusals.test.ts +144 -0
- package/src/postgres/schema-socket.test.ts +162 -0
- package/src/postgres/schema-sql.ts +130 -0
- package/src/postgres/schema-takeover.test.ts +149 -0
- package/src/postgres/schema-test-kit.ts +85 -0
- package/src/postgres/schema.ts +130 -0
- package/src/release/binary-alias.test.ts +3 -3
- package/src/release/binary-claims.test.ts +3 -3
- package/src/release/binary-directory-umask.test.ts +65 -0
- package/src/release/fake-engine.ts +15 -0
- package/src/valkey/acl-attrs.ts +98 -0
- package/src/valkey/acl-compare.ts +57 -0
- package/src/valkey/acl-connection.ts +13 -0
- package/src/valkey/acl-delete.test.ts +127 -0
- package/src/valkey/acl-form.ts +239 -0
- package/src/valkey/acl-managed.ts +13 -0
- package/src/valkey/acl-ops.ts +122 -0
- package/src/valkey/acl-ownership.test.ts +118 -0
- package/src/valkey/acl-prefix.test.ts +38 -0
- package/src/valkey/acl-profiles.ts +81 -0
- package/src/valkey/acl-reconcile.ts +180 -0
- package/src/valkey/acl-reserved.test.ts +69 -0
- package/src/valkey/acl-revocation.test.ts +94 -0
- package/src/valkey/acl-seat-prefix.test.ts +111 -0
- package/src/valkey/acl-validation.test.ts +54 -0
- package/src/valkey/acl.test.ts +215 -0
- package/src/valkey/acl.ts +169 -0
- package/src/valkey/connection-service.ts +46 -0
- package/src/valkey/connection.ts +180 -0
- package/src/valkey/errors.ts +239 -0
- package/src/valkey/fake-acl.ts +128 -0
- package/src/valkey/fake-valkey.ts +224 -0
- package/src/valkey/handler-test-fixture.ts +43 -0
- package/src/valkey/handlers.test.ts +205 -0
- package/src/valkey/index.ts +54 -0
- package/src/valkey/instance-attrs.ts +48 -0
- package/src/valkey/instance-form.ts +104 -0
- package/src/valkey/instance.test.ts +108 -0
- package/src/valkey/instance.ts +160 -0
- package/src/valkey/provider-socket-fixture.ts +71 -0
- package/src/valkey/providers.test.ts +129 -0
- package/src/valkey/providers.ts +21 -0
- package/src/valkey/resp.ts +212 -0
- package/src/valkey/transport-bounds.test.ts +150 -0
- package/src/valkey/transport-error.ts +37 -0
- package/src/valkey/transport.test.ts +158 -0
- package/src/valkey/transport.ts +109 -0
- package/src/valkey/upstream-bounds.test.ts +14 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Refusals, wire bodies, the read shape and the comparison for `LiteLLM.Model`, testable without
|
|
3
|
+
* a server. Credential handling is model-credential.ts's; the URL rules are mcp-server-url.ts's.
|
|
4
|
+
*
|
|
5
|
+
* ⛔ WHAT IS COMPARED, WITH ENCRYPTION IN MIND. LiteLLM stores `litellm_params` encrypted when the
|
|
6
|
+
* proxy runs with a database master key (`prisma_client.py`, `encrypt_value`, tag v1.103.0) and
|
|
7
|
+
* answers a row whose `litellm_params` are decrypted, with `api_key` stripped (measured at
|
|
8
|
+
* v1.103.0) — `/v2/model/info`'s docstring still says api keys and api_base are left out. So:
|
|
9
|
+
* - `model_name`, `model_info.id`, `mode`, `base_model` and `access_groups` are compared from
|
|
10
|
+
* the fields the read returns.
|
|
11
|
+
* - `model`, `api_base` and `api_key` are compared through a DIGEST of the DECLARED values
|
|
12
|
+
* (`declaredDigest`), carried in attributes as `paramsSeal`, never a digest of the live row.
|
|
13
|
+
* A plan therefore reports drift when the declaration changes and stays quiet when the proxy
|
|
14
|
+
* hides a field it was told before — the false-drift answer the encrypted row leaves no other
|
|
15
|
+
* way to. A field the proxy DOES return (`model`) is additionally compared when the read
|
|
16
|
+
* carries it; the seal is what decides when the read says nothing.
|
|
17
|
+
* ⛔ A LIVE `api_key` IS NEVER OVERWRITTEN BY AN ADOPTED ROW'S NOTHING. `apiKey` omitted on an
|
|
18
|
+
* adopted row sends no `api_key` and the live reference is kept; `differing` reports no drift
|
|
19
|
+
* for it. Declaring `apiKey` is what moves the row onto `os.environ/NAME`.
|
|
20
|
+
* ⛔ EVERYTHING AN EXISTING ROW NEEDS GOES TO ONE PATCH `/model/{model_id}/update`. At the tag
|
|
21
|
+
* the SDK was generated from (OpenAPI 1.103.0), POST `/model/update`'s `update_model` REBUILDS
|
|
22
|
+
* `litellm_params` from the parsed request model and REPLACES the column: a key the model does
|
|
23
|
+
* not declare (`extra_headers`, `weight`, `order`) is DELETED from the stored row, and a
|
|
24
|
+
* non-`None` pydantic default (`false` on the five flags in `LEAVE_ALONE_PARAMS`) overwrites
|
|
25
|
+
* it — measured on a throwaway 1.103.0 proxy, and the reason a params POST is never sent. The
|
|
26
|
+
* PATCH route's `update_db_model` instead MERGES: sent keys land, the rest of the row is
|
|
27
|
+
* preserved, and a JSON `null` keeps the stored value. So one PATCH carries the managed
|
|
28
|
+
* `litellm_params` (with `null` for those five flags), the `model_name` when it changed, and
|
|
29
|
+
* the `model_info` fields — `access_groups`, `mode`, `base_model`, and the row's own `id`, so
|
|
30
|
+
* an adopt-by-id re-affirms the row it pinned. `model_name` is sent only when it changed,
|
|
31
|
+
* because a same-name write would collide with another deployment in the group (model.ts).
|
|
32
|
+
*/
|
|
33
|
+
import type * as models from '@distilled.cloud/litellm/model_management';
|
|
34
|
+
import {
|
|
35
|
+
type ModelAttributes,
|
|
36
|
+
type ModelProps,
|
|
37
|
+
hasProviderPrefix,
|
|
38
|
+
isBlank,
|
|
39
|
+
} from './model-types.ts';
|
|
40
|
+
import { redactUrl, urlProblem } from './mcp-server-url.ts';
|
|
41
|
+
import { declaredValues, sealFromValues } from './model-credential.ts';
|
|
42
|
+
|
|
43
|
+
/** The first reason a declaration is refused, or `undefined`. Pure: no environment, no server. */
|
|
44
|
+
export const firstProblem = (props: ModelProps): string | undefined => {
|
|
45
|
+
if (isBlank(props.modelName) || props.modelName !== props.modelName.trim()) {
|
|
46
|
+
return '`modelName` must be a non-empty string without leading or trailing whitespace';
|
|
47
|
+
}
|
|
48
|
+
if (props.id !== undefined && isBlank(props.id)) {
|
|
49
|
+
return '`id` must be a non-empty string when declared';
|
|
50
|
+
}
|
|
51
|
+
if (isBlank(props.model)) return '`model` must be a non-empty string';
|
|
52
|
+
if (!hasProviderPrefix(props.model)) {
|
|
53
|
+
return `\`model\` must carry a provider prefix, e.g. "xai/${props.model}": LiteLLM routes by it`;
|
|
54
|
+
}
|
|
55
|
+
if (
|
|
56
|
+
props.model.toLowerCase().startsWith('openai/') &&
|
|
57
|
+
props.modelName.toLowerCase().startsWith('grok')
|
|
58
|
+
) {
|
|
59
|
+
return '`model` must not use the "openai/" prefix on a grok group: Grok is xAI, declare "xai/…"';
|
|
60
|
+
}
|
|
61
|
+
if (props.apiBase !== undefined) {
|
|
62
|
+
const url = urlProblem(props.apiBase);
|
|
63
|
+
if (url !== undefined) return url;
|
|
64
|
+
}
|
|
65
|
+
if (
|
|
66
|
+
props.apiKey !== undefined &&
|
|
67
|
+
(typeof props.apiKey !== 'object' || typeof props.apiKey.fromEnv !== 'string')
|
|
68
|
+
) {
|
|
69
|
+
return "`apiKey` takes `{ fromEnv: 'NAME' }`, never a value: the value would land in Alchemy's unencrypted state";
|
|
70
|
+
}
|
|
71
|
+
if (props.apiKey !== undefined && isBlank(props.apiKey.fromEnv)) {
|
|
72
|
+
return '`apiKey.fromEnv` must name an environment variable';
|
|
73
|
+
}
|
|
74
|
+
return undefined;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/** The digest of the DECLARED values, for `paramsSeal`. Pure: no environment, no lookup. */
|
|
78
|
+
export const declaredDigest = (props: ModelProps): string => sealFromValues(declaredValues(props));
|
|
79
|
+
|
|
80
|
+
const sameSet = (live: readonly string[], declared: readonly string[]): boolean =>
|
|
81
|
+
new Set(live).size === new Set(declared).size && declared.every((entry) => live.includes(entry));
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Wire names of the fields where the live row and the declaration disagree.
|
|
85
|
+
*
|
|
86
|
+
* `model` is compared only when the read carries it (an encrypted row omits it); the seal decides
|
|
87
|
+
* otherwise, and model.ts's diff turns a changed seal into an update. `model_name` mismatch on a
|
|
88
|
+
* pinned id is identity drift — replace in model.ts, update here only when the name changed and
|
|
89
|
+
* the id stayed.
|
|
90
|
+
*/
|
|
91
|
+
export const differing = (live: ModelAttributes, props: ModelProps): readonly string[] => {
|
|
92
|
+
const out: string[] = [];
|
|
93
|
+
if (live.modelName !== props.modelName) out.push('model_name');
|
|
94
|
+
if (live.model !== null && live.model !== props.model) {
|
|
95
|
+
out.push('model');
|
|
96
|
+
}
|
|
97
|
+
if (props.mode !== undefined && live.mode !== props.mode) out.push('mode');
|
|
98
|
+
if (props.baseModel !== undefined && live.baseModel !== props.baseModel) out.push('base_model');
|
|
99
|
+
if (!sameSet(live.accessGroups, props.accessGroups ?? [])) out.push('access_groups');
|
|
100
|
+
return out;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* One row of `/model/info`, as attributes. ⛔ Copies no `litellm_params` field but `model`, and
|
|
105
|
+
* only when the read returns an OBJECT: the fields a row hides (`api_key` is stripped at
|
|
106
|
+
* v1.103.0) are never mistaken for declared values — `api_base` and any `api_key` are never
|
|
107
|
+
* copied at all. `dbModel` records `model_info.db_model` (false = served from the proxy's config
|
|
108
|
+
* file), defaulting to true when the read omits it. The digest is the caller's (model.ts fills
|
|
109
|
+
* `paramsSeal` from state, a row cannot supply one).
|
|
110
|
+
*/
|
|
111
|
+
export const toAttributes = (row: Record<string, unknown>): ModelAttributes => {
|
|
112
|
+
const info = (row['model_info'] ?? {}) as Record<string, unknown>;
|
|
113
|
+
const params = row['litellm_params'];
|
|
114
|
+
const carriedModel =
|
|
115
|
+
typeof params === 'object' && params !== null
|
|
116
|
+
? (params as Record<string, unknown>)['model']
|
|
117
|
+
: undefined;
|
|
118
|
+
const id = row['id'] ?? info['id'];
|
|
119
|
+
return {
|
|
120
|
+
accessGroups: Array.isArray(info['access_groups'])
|
|
121
|
+
? (info['access_groups'] as string[]).map(String)
|
|
122
|
+
: [],
|
|
123
|
+
baseModel: typeof info['base_model'] === 'string' ? info['base_model'] : null,
|
|
124
|
+
dbModel: info['db_model'] === false ? false : true,
|
|
125
|
+
id: String(id),
|
|
126
|
+
mode: typeof info['mode'] === 'string' ? info['mode'] : null,
|
|
127
|
+
model: typeof carriedModel === 'string' ? redactUrl(carriedModel) : null,
|
|
128
|
+
modelName: String(row['model_name'] ?? ''),
|
|
129
|
+
paramsSeal: '',
|
|
130
|
+
};
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* `litellm_params` fields whose pydantic default is `False`, not `None` (`litellm/types/router.py`
|
|
135
|
+
* at v1.103.0). A params write parses the request model, which fills every unset field, so an
|
|
136
|
+
* omitted key is written `false`; JSON `null` is the `None` that keeps the stored value. Create
|
|
137
|
+
* does not send them: a new row should take the vendor default.
|
|
138
|
+
*/
|
|
139
|
+
const LEAVE_ALONE_PARAMS = [
|
|
140
|
+
'allow_client_keepalive_override',
|
|
141
|
+
'merge_reasoning_content_in_choices',
|
|
142
|
+
'use_in_pass_through',
|
|
143
|
+
'use_litellm_proxy',
|
|
144
|
+
'use_xai_oauth',
|
|
145
|
+
] as const;
|
|
146
|
+
|
|
147
|
+
/** The managed `litellm_params` a create sends — with the credential in reference form. */
|
|
148
|
+
const managedParams = (props: ModelProps) => {
|
|
149
|
+
const params: Record<string, unknown> = { model: props.model };
|
|
150
|
+
if (props.apiBase !== undefined) params['api_base'] = props.apiBase;
|
|
151
|
+
if (props.apiKey !== undefined) params['api_key'] = `os.environ/${props.apiKey.fromEnv}`;
|
|
152
|
+
return params;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The same set for a PATCH, plus `null` for every non-`None` default this resource does not own.
|
|
157
|
+
* The PATCH merge keeps the stored value for `None`, so the nulls are what stop a params write
|
|
158
|
+
* clobbering those five flags.
|
|
159
|
+
*/
|
|
160
|
+
const patchParams = (props: ModelProps): Record<string, unknown> => ({
|
|
161
|
+
...managedParams(props),
|
|
162
|
+
...Object.fromEntries(LEAVE_ALONE_PARAMS.map((key) => [key, null])),
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* ⚠️ `Record`, then a widening cast in the callers, ON PURPOSE: the generated 1.103.0 request
|
|
167
|
+
* schema declares neither `access_groups` nor `mode` on `model_info`, while the vendor routes
|
|
168
|
+
* on both (`model/types.py`'s `access_groups`, the read's own docstring) and the read ANSWERS
|
|
169
|
+
* both. `modelInfoOf` returns the plain object and the wire probe's runtime encode carries
|
|
170
|
+
* unknown keys through (measured: the fake receives `model_info.id` and `access_groups` the
|
|
171
|
+
* typed struct does not declare).
|
|
172
|
+
*/
|
|
173
|
+
const modelInfoOf = (props: ModelProps): Record<string, unknown> => ({
|
|
174
|
+
...(props.baseModel === undefined ? {} : { base_model: props.baseModel }),
|
|
175
|
+
...(props.mode === undefined ? {} : { mode: props.mode }),
|
|
176
|
+
// ★ ALWAYS SENT: `accessGroups` is compared with a default empty set (model-types.ts), so an
|
|
177
|
+
// omitted list is a declared "no groups" — a partial update must be able to clear them, the
|
|
178
|
+
// way a budget's soft-only declaration clears max_budget. `mode`/`base_model` are compared
|
|
179
|
+
// only when declared, so their omission really does leave the live value alone.
|
|
180
|
+
access_groups: [...(props.accessGroups ?? [])],
|
|
181
|
+
id: null,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
/** A create names the group and pins the id it asked for on `model_info` of the POST body. */
|
|
185
|
+
export const createBody = (
|
|
186
|
+
props: ModelProps,
|
|
187
|
+
modelId: string,
|
|
188
|
+
): models.AddNewModelModelNewPostRequest => {
|
|
189
|
+
const body: Record<string, unknown> = {
|
|
190
|
+
litellm_params: managedParams(props),
|
|
191
|
+
model_info: { ...modelInfoOf(props), id: modelId },
|
|
192
|
+
model_name: props.modelName,
|
|
193
|
+
};
|
|
194
|
+
return body as unknown as models.AddNewModelModelNewPostRequest;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* PATCH `/model/{model_id}/update`: everything an existing row needs, in the ONE write a
|
|
199
|
+
* converge fires (model.ts). `update_db_model` MERGES (v1.103.0, measured), so:
|
|
200
|
+
* - `litellm_params` carries the managed keys (with `null` for the five non-`None` defaults
|
|
201
|
+
* this resource does not own); the merge keeps every stored key the declaration does not
|
|
202
|
+
* own (`extra_headers`, `weight`, `order`), which is exactly what POST `/model/update`
|
|
203
|
+
* would DELETE by rebuilding the column.
|
|
204
|
+
* - `model_info` carries `access_groups` (always present: an omitted list is "no groups" and
|
|
205
|
+
* must be able to clear), `mode` and `base_model` only when declared, and the row's own
|
|
206
|
+
* `id`, so an adopt-by-id re-affirms the row it pinned.
|
|
207
|
+
* - `model_name` only when it changed: a same-name write would collide with a sibling
|
|
208
|
+
* deployment in the group (model.ts). Not a seal stamp: a matching row records `paramsSeal`
|
|
209
|
+
* locally (model.ts).
|
|
210
|
+
*
|
|
211
|
+
* ⚠️ `Record`, then a widening cast, for the same reason as {@link modelInfoOf}: the generated
|
|
212
|
+
* 1.103.0 `LitellmTypesRouterModelInfo` declares neither `access_groups` nor `mode`, while the
|
|
213
|
+
* vendor model allows both (`extra="allow"`) and the read answers both.
|
|
214
|
+
*/
|
|
215
|
+
export const patchBody = (props: ModelProps, live: ModelAttributes): Record<string, unknown> => {
|
|
216
|
+
const renamed = live.modelName !== props.modelName;
|
|
217
|
+
return {
|
|
218
|
+
model_id: live.id,
|
|
219
|
+
litellm_params: patchParams(props),
|
|
220
|
+
model_info: { ...modelInfoOf(props), id: live.id },
|
|
221
|
+
...(renamed ? { model_name: props.modelName } : {}),
|
|
222
|
+
};
|
|
223
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which live row a `LiteLLM.Model` declaration means — `model-locate.ts`'s rules, through
|
|
3
|
+
* Alchemy's real Plan and Apply over the fake proxy (`fake-model-litellm.ts`): a shared group
|
|
4
|
+
* name is refused rather than guessed at, a pinned `id` selects exactly one row, a rename is a
|
|
5
|
+
* new group unless the id is pinned, and a changed id is a replace.
|
|
6
|
+
*
|
|
7
|
+
* ★ EVERY VALUE IS `FAKE-*`; no environment variable is set anywhere (model.test.ts's header).
|
|
8
|
+
*/
|
|
9
|
+
import { expect, test } from 'bun:test';
|
|
10
|
+
import * as Effect from 'effect/Effect';
|
|
11
|
+
import { FAKE_BASE } from './fake-litellm.ts';
|
|
12
|
+
import { type FakeModelLitellm, modelRow, startFakeModelLitellm } from './fake-model-litellm.ts';
|
|
13
|
+
import { fakeStack, writesOf } from './fake-stack.ts';
|
|
14
|
+
import { LiteLLMModel } from './model.ts';
|
|
15
|
+
import type { ModelProps } from './model-types.ts';
|
|
16
|
+
|
|
17
|
+
type Row = Record<string, unknown>;
|
|
18
|
+
|
|
19
|
+
const KEY = 'sk-test-master';
|
|
20
|
+
const stack = (fake: FakeModelLitellm) =>
|
|
21
|
+
fakeStack({ apiKey: KEY, baseUrl: FAKE_BASE }, fake.fetch);
|
|
22
|
+
const declare = (props: ModelProps, name = 'Grok') =>
|
|
23
|
+
Effect.gen(function* () {
|
|
24
|
+
yield* LiteLLMModel(name, props);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const grok: ModelProps = {
|
|
28
|
+
apiKey: { fromEnv: 'FAKE_XAI_KEY' },
|
|
29
|
+
model: 'xai/grok-4.7',
|
|
30
|
+
modelName: 'grok',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
test('two live rows with the declared name are refused, never guessed at', async () => {
|
|
34
|
+
const seed = [
|
|
35
|
+
modelRow({
|
|
36
|
+
litellm_params: { model: 'xai/grok-4.7' },
|
|
37
|
+
model_info: { access_groups: [], id: 'FAKE-live-id-a' },
|
|
38
|
+
model_name: 'grok',
|
|
39
|
+
}),
|
|
40
|
+
modelRow({
|
|
41
|
+
litellm_params: { model: 'xai/grok-4.7' },
|
|
42
|
+
model_info: { access_groups: [], id: 'FAKE-live-id-b' },
|
|
43
|
+
model_name: 'grok',
|
|
44
|
+
}),
|
|
45
|
+
];
|
|
46
|
+
const fake = startFakeModelLitellm({ masterKey: KEY, seed });
|
|
47
|
+
await expect(stack(fake).deploy(declare(grok), { adopt: true })).rejects.toThrow();
|
|
48
|
+
expect(writesOf(fake.requests())).toEqual([]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('a declared id pins one of two same-named rows', async () => {
|
|
52
|
+
const fake = startFakeModelLitellm({
|
|
53
|
+
masterKey: KEY,
|
|
54
|
+
seed: [
|
|
55
|
+
modelRow({
|
|
56
|
+
litellm_params: { model: 'xai/grok-4.7' },
|
|
57
|
+
model_info: { access_groups: [], id: 'FAKE-live-id-a' },
|
|
58
|
+
model_name: 'grok',
|
|
59
|
+
}),
|
|
60
|
+
modelRow({
|
|
61
|
+
litellm_params: { model: 'xai/grok-4.7' },
|
|
62
|
+
model_info: { access_groups: [], id: 'FAKE-live-id-b' },
|
|
63
|
+
model_name: 'grok',
|
|
64
|
+
}),
|
|
65
|
+
],
|
|
66
|
+
});
|
|
67
|
+
const pinned: ModelProps = { ...grok, id: 'FAKE-live-id-b' };
|
|
68
|
+
expect(await stack(fake).deploy(declare(pinned), { adopt: true })).toEqual({ Grok: 'adopted' });
|
|
69
|
+
// The pin's declaration manages a credential the read never shows, so adopt converges it with
|
|
70
|
+
// one params PATCH (the five nulls keep unmanaged flags — model-form.ts). The sibling with the
|
|
71
|
+
// same group name is left alone: the adopted row is the one the id named.
|
|
72
|
+
expect(writesOf(fake.requests())).toContain('PATCH /model/FAKE-live-id-b/update');
|
|
73
|
+
expect(fake.models().map((row) => (row['model_info'] as Row)['id'])).toEqual([
|
|
74
|
+
'FAKE-live-id-a',
|
|
75
|
+
'FAKE-live-id-b',
|
|
76
|
+
]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('a renamed group without a pinned id is a new group: the old row is retained', async () => {
|
|
80
|
+
const fake = startFakeModelLitellm({ masterKey: KEY });
|
|
81
|
+
const same = stack(fake);
|
|
82
|
+
await same.deploy(declare(grok));
|
|
83
|
+
const planned = await same.deploy(declare({ ...grok, modelName: 'grok-fast' }));
|
|
84
|
+
expect(planned).toEqual({ Grok: 'replace' });
|
|
85
|
+
// ★ `defaultRemovalPolicy: 'retain'`: nothing removes the old group unless the stack opts in
|
|
86
|
+
expect(fake.models().map((row) => row['model_name'])).toEqual(['grok', 'grok-fast']);
|
|
87
|
+
const ids = fake.models().map((row) => String((row['model_info'] as Row)['id']));
|
|
88
|
+
expect(ids[0]).not.toBe(ids[1]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('a rename with a pinned id renames the same row', async () => {
|
|
92
|
+
const fake = startFakeModelLitellm({
|
|
93
|
+
masterKey: KEY,
|
|
94
|
+
seed: [
|
|
95
|
+
modelRow({
|
|
96
|
+
litellm_params: { model: 'xai/grok-4.7' },
|
|
97
|
+
model_info: { access_groups: [], id: 'FAKE-live-id' },
|
|
98
|
+
model_name: 'grok',
|
|
99
|
+
}),
|
|
100
|
+
],
|
|
101
|
+
});
|
|
102
|
+
const engine = stack(fake);
|
|
103
|
+
const pinned: ModelProps = { ...grok, id: 'FAKE-live-id' };
|
|
104
|
+
await engine.deploy(declare(pinned), { adopt: true });
|
|
105
|
+
expect(
|
|
106
|
+
await engine.deploy(declare({ ...pinned, modelName: 'grok-fast' }), { adopt: true }),
|
|
107
|
+
).toEqual({
|
|
108
|
+
Grok: 'update',
|
|
109
|
+
});
|
|
110
|
+
expect(fake.models()).toHaveLength(1);
|
|
111
|
+
expect(fake.models()[0]?.['model_name']).toBe('grok-fast');
|
|
112
|
+
expect(fake.bodies().at(-1)).toMatchObject({
|
|
113
|
+
model_info: { id: 'FAKE-live-id' },
|
|
114
|
+
model_name: 'grok-fast',
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('a changed declared id is a replace, create-first, and the old row is retained', async () => {
|
|
119
|
+
const fake = startFakeModelLitellm({ masterKey: KEY });
|
|
120
|
+
const same = stack(fake);
|
|
121
|
+
await same.deploy(declare({ ...grok, id: 'FAKE-id-a' }));
|
|
122
|
+
const planned = await same.deploy(declare({ ...grok, id: 'FAKE-id-b' }));
|
|
123
|
+
expect(planned).toEqual({ Grok: 'replace' });
|
|
124
|
+
expect(fake.models().map((row) => String((row['model_info'] as Row)['id']))).toEqual([
|
|
125
|
+
'FAKE-id-a',
|
|
126
|
+
'FAKE-id-b',
|
|
127
|
+
]);
|
|
128
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which live row a `LiteLLM.Model` declaration means, and which id a new row would ask for.
|
|
3
|
+
*
|
|
4
|
+
* ★ THE LIST DECIDES FIRST. `GET /model/info` answers every deployment row in one call, so a
|
|
5
|
+
* group name, an id and absence are all decided from it (model-operations.ts). The by-id read
|
|
6
|
+
* is the same route with the `litellm_model_id` filter, kept only for the write-back on a row
|
|
7
|
+
* the list may not show yet — after a create, the read-back falls back to it exactly like
|
|
8
|
+
* `mcp-server-locate.ts` does for the registry.
|
|
9
|
+
* ⚠️ ADOPT-BY-NAME NEEDS THE LIST: a name cannot be read from the table by this route family. A
|
|
10
|
+
* row the list lacks is adoptable only by declaring its `id`.
|
|
11
|
+
* ⛔ A NAME IS NOT UNIQUE (unlike an MCPServer): LiteLLM runs several deployments under one group
|
|
12
|
+
* name on purpose, so a name that matches more than one listed row is refused
|
|
13
|
+
* (`LitellmModelAmbiguousNameError`), never guessed at.
|
|
14
|
+
*/
|
|
15
|
+
import { createPhysicalName } from 'alchemy/PhysicalName';
|
|
16
|
+
import * as Effect from 'effect/Effect';
|
|
17
|
+
import { LitellmModelAmbiguousNameError } from './model-errors.ts';
|
|
18
|
+
import { listModels, readModelRow } from './model-operations.ts';
|
|
19
|
+
import type { ModelAttributes, ModelProps } from './model-types.ts';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The live row among the LISTED ones, or `undefined`. A pinned id (from state, else declared) wins;
|
|
23
|
+
* otherwise the name decides, and an ambiguous name is refused.
|
|
24
|
+
*/
|
|
25
|
+
export const locate = (
|
|
26
|
+
rows: readonly ModelAttributes[],
|
|
27
|
+
props: ModelProps,
|
|
28
|
+
output: ModelAttributes | undefined,
|
|
29
|
+
) => {
|
|
30
|
+
const pinned = output?.id ?? props.id;
|
|
31
|
+
if (pinned !== undefined) return Effect.succeed(rows.find((row) => row.id === pinned));
|
|
32
|
+
const named = rows.filter((row) => row.modelName === props.modelName);
|
|
33
|
+
return named.length > 1
|
|
34
|
+
? Effect.fail(
|
|
35
|
+
new LitellmModelAmbiguousNameError({
|
|
36
|
+
ids: named.map((row) => row.id),
|
|
37
|
+
modelName: props.modelName,
|
|
38
|
+
}),
|
|
39
|
+
)
|
|
40
|
+
: Effect.succeed(named[0]);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** The id a create asks for: the recorded one, else the declared one, else a deterministic name. */
|
|
44
|
+
export const wantedId = (
|
|
45
|
+
id: string,
|
|
46
|
+
instanceId: string,
|
|
47
|
+
props: ModelProps,
|
|
48
|
+
output: ModelAttributes | undefined,
|
|
49
|
+
) =>
|
|
50
|
+
output?.id !== undefined
|
|
51
|
+
? Effect.succeed(output.id)
|
|
52
|
+
: props.id !== undefined
|
|
53
|
+
? Effect.succeed(props.id)
|
|
54
|
+
: createPhysicalName({ id, instanceId, lowercase: true, maxLength: 64 });
|
|
55
|
+
|
|
56
|
+
/** The live row this declaration means, from the list, else from the table by the id it would use. */
|
|
57
|
+
export const findLive = (
|
|
58
|
+
id: string,
|
|
59
|
+
instanceId: string,
|
|
60
|
+
props: ModelProps,
|
|
61
|
+
output: ModelAttributes | undefined,
|
|
62
|
+
) =>
|
|
63
|
+
Effect.gen(function* () {
|
|
64
|
+
const listed = yield* locate(yield* listModels(), props, output);
|
|
65
|
+
if (listed !== undefined) return listed;
|
|
66
|
+
return yield* readModelRow(yield* wantedId(id, instanceId, props, output));
|
|
67
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `LiteLLM.Model`'s operations against the fake proxy: what absence really is, and which delete
|
|
3
|
+
* failures are allowed to be swallowed. The rule under test is S21's — never decide absence from
|
|
4
|
+
* an error's text, only from a read.
|
|
5
|
+
*
|
|
6
|
+
* ⚠️ The fake's 400 on a delete of a missing id is its own choice, not a measurement of LiteLLM
|
|
7
|
+
* 1.103.0. A PATCH of a missing id is the measured 404 (fake-model-litellm.ts).
|
|
8
|
+
*/
|
|
9
|
+
import { credentials } from '@distilled.cloud/litellm/Credentials';
|
|
10
|
+
import { describe, expect, test } from 'bun:test';
|
|
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 { modelRow, startFakeModelLitellm } from './fake-model-litellm.ts';
|
|
16
|
+
import { deleteModel, listModels, readModelRow } from './model-operations.ts';
|
|
17
|
+
import { LitellmModelUnreadableError } from './model-errors.ts';
|
|
18
|
+
import type { LitellmOpContext } from './operations.ts';
|
|
19
|
+
|
|
20
|
+
const KEY = 'sk-test-master';
|
|
21
|
+
|
|
22
|
+
const run = <A, E>(
|
|
23
|
+
fetchFn: typeof globalThis.fetch,
|
|
24
|
+
effect: Effect.Effect<A, E, LitellmOpContext>,
|
|
25
|
+
) =>
|
|
26
|
+
Effect.runPromise(
|
|
27
|
+
effect.pipe(
|
|
28
|
+
Effect.provide(FetchHttpClient.layer),
|
|
29
|
+
Effect.provide(Layer.succeed(FetchHttpClient.Fetch, fetchFn)),
|
|
30
|
+
Effect.provide(credentials({ apiKey: KEY, baseUrl: FAKE_BASE })),
|
|
31
|
+
) as Effect.Effect<A, E, never>,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const live = modelRow({
|
|
35
|
+
litellm_params: { model: 'xai/grok-4.7' },
|
|
36
|
+
model_info: { access_groups: [], id: 'FAKE-live-id' },
|
|
37
|
+
model_name: 'grok',
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('absence', () => {
|
|
41
|
+
test('a missing id is a 400, re-listed, and absent only when the list lacks it', async () => {
|
|
42
|
+
const fake = startFakeModelLitellm({ masterKey: KEY });
|
|
43
|
+
await expect(run(fake.fetch, readModelRow('FAKE-never-existed'))).resolves.toBeUndefined();
|
|
44
|
+
expect(fake.requests().map((request) => request.path)).toEqual([
|
|
45
|
+
'/model/info?litellm_model_id=FAKE-never-existed',
|
|
46
|
+
'/model/info',
|
|
47
|
+
]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('a 400 on an id the list still holds is not read as absence', async () => {
|
|
51
|
+
const fake = startFakeModelLitellm({
|
|
52
|
+
byIdRefused: true,
|
|
53
|
+
masterKey: KEY,
|
|
54
|
+
seed: [live],
|
|
55
|
+
});
|
|
56
|
+
await expect(run(fake.fetch, readModelRow('FAKE-live-id'))).rejects.toThrow();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('a row the by-id read answers is the one asked for', async () => {
|
|
60
|
+
const fake = startFakeModelLitellm({ masterKey: KEY, seed: [live] });
|
|
61
|
+
const row = await run(fake.fetch, readModelRow('FAKE-live-id'));
|
|
62
|
+
expect(row?.id).toBe('FAKE-live-id');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('a list answer without ids is refused, never adopted', async () => {
|
|
66
|
+
const fetchFn = (async () =>
|
|
67
|
+
new Response(JSON.stringify({ data: [{ model_name: 'grok' }] }), {
|
|
68
|
+
headers: { 'content-type': 'application/json' },
|
|
69
|
+
status: 200,
|
|
70
|
+
})) as unknown as typeof globalThis.fetch;
|
|
71
|
+
const result = run(fetchFn, listModels());
|
|
72
|
+
await expect(result).rejects.toBeInstanceOf(LitellmModelUnreadableError);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('deleting a deployment', () => {
|
|
77
|
+
test('a 400 on a row that is genuinely still live is not swallowed', async () => {
|
|
78
|
+
const fake = startFakeModelLitellm({ forbidDelete: true, masterKey: KEY, seed: [live] });
|
|
79
|
+
await expect(run(fake.fetch, deleteModel('FAKE-live-id'))).rejects.toThrow();
|
|
80
|
+
expect(fake.models()).toHaveLength(1);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('a genuinely-gone id still succeeds with no error (idempotent delete preserved)', async () => {
|
|
84
|
+
const fake = startFakeModelLitellm({ forbidDelete: true, masterKey: KEY });
|
|
85
|
+
await expect(run(fake.fetch, deleteModel('FAKE-never-existed'))).resolves.toBeUndefined();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('a delete that lands removes the row', async () => {
|
|
89
|
+
const fake = startFakeModelLitellm({ masterKey: KEY, seed: [live] });
|
|
90
|
+
await expect(run(fake.fetch, deleteModel('FAKE-live-id'))).resolves.toBeUndefined();
|
|
91
|
+
expect(fake.models()).toEqual([]);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The five LiteLLM `/model/*` calls `LiteLLM.Model` uses, through the SDK's typed operations in
|
|
3
|
+
* `@distilled.cloud/litellm/model_management` (LiteLLM 1.103.0; names verified in the generated
|
|
4
|
+
* source, never guessed):
|
|
5
|
+
*
|
|
6
|
+
* list `getModelInfoV1ModelInfo` GET /model/info
|
|
7
|
+
* read `getModelInfoV1ModelInfo` GET /model/info?litellm_model_id={id}
|
|
8
|
+
* create `addNewModelModelNewPost` POST /model/new
|
|
9
|
+
* patch `patchModelModelModelIdUpdatePatch` PATCH /model/{model_id}/update
|
|
10
|
+
* delete `deleteModelModelDeletePost` POST /model/delete
|
|
11
|
+
*
|
|
12
|
+
* The SDK is generated from LiteLLM OpenAPI **1.103.0** (`model_management.ts`'s header). At that
|
|
13
|
+
* tag, and at v1.100.0, `model_info_v1` raises HTTP 400 when the router has no such deployment
|
|
14
|
+
* (`proxy_server.py`, "Model id = … not found on litellm proxy"), not 404. The same 400 is also
|
|
15
|
+
* "no rights" or a bad filter. The PATCH route's `update_db_model` MERGES: `litellm_params`,
|
|
16
|
+
* `model_name` and `model_info` (`access_groups`, `mode`, `base_model`) all land without
|
|
17
|
+
* rebuilding the column, which is the one write `LiteLLM.Model` converges with (model-form.ts).
|
|
18
|
+
* POST `/model/update` REBUILDS `litellm_params` and deletes stored keys the request model does
|
|
19
|
+
* not declare, so this resource never sends it.
|
|
20
|
+
*
|
|
21
|
+
* ⛔ LIST-FIRST READS. One GET of the whole list answers a group, an id and absence unambiguously,
|
|
22
|
+
* the way `budget-operations.ts` reads. The by-id read is the same route with the
|
|
23
|
+
* `litellm_model_id` filter, used only where the list cannot decide (a write-back on a row the
|
|
24
|
+
* list may not show yet).
|
|
25
|
+
* ⛔ NO MESSAGE SNIFFING (S21). The by-id read's 400 is undeclared on the generated operation, so
|
|
26
|
+
* it decodes at run time as the shared `BadRequest` (`HTTP_STATUS_MAP`). `catchTag` matches the
|
|
27
|
+
* tag — the same match `deleteModel` uses — so a later declared class with that tag is still
|
|
28
|
+
* seen. A 400 is absence only when a re-list also lacks the id. A 404 is the same absence.
|
|
29
|
+
* Any other failure propagates.
|
|
30
|
+
* ⛔ THE ROW'S PARAMS ARE NEVER READ INTO STATE: `toAttributes` (model-form.ts) copies `model`
|
|
31
|
+
* only when `litellm_params` is an object, and never `api_base` or `api_key` — the v1.103.0
|
|
32
|
+
* read answers params DECRYPTED but STRIPS `api_key` (measured). What was declared is
|
|
33
|
+
* remembered as a digest instead.
|
|
34
|
+
* ⚠️ Unlike pass-through endpoints, each deployment is its own DB row: no semaphore.
|
|
35
|
+
*/
|
|
36
|
+
import * as models from '@distilled.cloud/litellm/model_management';
|
|
37
|
+
import type { BadRequest, NotFound } from '@distilled.cloud/litellm/Errors';
|
|
38
|
+
import * as Effect from 'effect/Effect';
|
|
39
|
+
import { type LitellmOpContext, throughFetch } from './operations.ts';
|
|
40
|
+
import { toAttributes } from './model-form.ts';
|
|
41
|
+
import type { ModelAttributes } from './model-types.ts';
|
|
42
|
+
import { LitellmModelUnreadableError } from './model-errors.ts';
|
|
43
|
+
|
|
44
|
+
const hasRowId = (row: unknown): boolean => {
|
|
45
|
+
if (typeof row !== 'object' || row === null) return false;
|
|
46
|
+
const record = row as Record<string, unknown>;
|
|
47
|
+
const info =
|
|
48
|
+
typeof record['model_info'] === 'object' && record['model_info'] !== null
|
|
49
|
+
? (record['model_info'] as Record<string, unknown>)
|
|
50
|
+
: {};
|
|
51
|
+
return typeof record['id'] === 'string' || typeof info['id'] === 'string';
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The rows the answer carries. ⚠️ MEASURED with the fake: the SDK types
|
|
56
|
+
* `GetModelInfoV1ModelInfoResponse` as `{ body: unknown }` but hands the decoded JSON back
|
|
57
|
+
* directly (a bare array, or the proxy's `{ data: [...] }` wrapper), so accept all three shapes
|
|
58
|
+
* and refuse anything else — the same rule budget-operations.ts / mcp-server-operations.ts keep.
|
|
59
|
+
*/
|
|
60
|
+
const rowsOf = (
|
|
61
|
+
response: unknown,
|
|
62
|
+
): Effect.Effect<readonly ModelAttributes[], LitellmModelUnreadableError> => {
|
|
63
|
+
const rows: unknown = Array.isArray(response)
|
|
64
|
+
? response
|
|
65
|
+
: Array.isArray((response as { data?: unknown } | null)?.data)
|
|
66
|
+
? (response as { data: unknown[] }).data
|
|
67
|
+
: (response as { body?: unknown } | null)?.body;
|
|
68
|
+
return Array.isArray(rows) && rows.every(hasRowId)
|
|
69
|
+
? Effect.succeed((rows as Record<string, unknown>[]).map(toAttributes))
|
|
70
|
+
: Effect.fail(
|
|
71
|
+
new LitellmModelUnreadableError({
|
|
72
|
+
reason: Array.isArray(rows) ? 'a row has no model id' : 'not a list of deployments',
|
|
73
|
+
}),
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/** Every deployment row, config-file ones included. This is the read that locates and diffs. */
|
|
78
|
+
export const listModels = (): Effect.Effect<
|
|
79
|
+
readonly ModelAttributes[],
|
|
80
|
+
models.GetModelInfoV1ModelInfoError | LitellmModelUnreadableError,
|
|
81
|
+
LitellmOpContext
|
|
82
|
+
> => throughFetch(models.getModelInfoV1ModelInfo({})).pipe(Effect.flatMap(rowsOf));
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* One row by id, or `undefined` when the proxy has no such deployment.
|
|
86
|
+
*
|
|
87
|
+
* ⛔ A 400 IS NOT ABSENCE BY ITSELF. v1.103.0 `model_info_v1` raises 400 for a missing id, and the
|
|
88
|
+
* same status is "no rights" or a bad filter. Re-list, and treat the id as absent only when the
|
|
89
|
+
* list does not contain it (`deleteModel`'s rule). A 401, a 500 or a dead network says nothing
|
|
90
|
+
* about whether the row exists and must never be read as absence. The id is re-checked on a
|
|
91
|
+
* successful answer: a proxy that ignored the filter must not hand back some other row.
|
|
92
|
+
* ⚠️ 400 and 404 are undeclared on this GET, so they are not in its error union, but the status
|
|
93
|
+
* map still produces those tags. Widening here is what lets `catchTag` see them.
|
|
94
|
+
*/
|
|
95
|
+
type ModelInfoFailure = models.GetModelInfoV1ModelInfoError | BadRequest | NotFound;
|
|
96
|
+
|
|
97
|
+
export const readModelRow = (modelId: string) =>
|
|
98
|
+
throughFetch(models.getModelInfoV1ModelInfo({ litellm_model_id: modelId })).pipe(
|
|
99
|
+
Effect.mapError((error): ModelInfoFailure => error),
|
|
100
|
+
Effect.catchTag('NotFound', () => Effect.succeed(undefined)),
|
|
101
|
+
Effect.catchTag('BadRequest', (error) =>
|
|
102
|
+
listModels().pipe(
|
|
103
|
+
Effect.flatMap((rows) =>
|
|
104
|
+
rows.some((row) => row.id === modelId) ? Effect.fail(error) : Effect.succeed(undefined),
|
|
105
|
+
),
|
|
106
|
+
),
|
|
107
|
+
),
|
|
108
|
+
Effect.flatMap((response) =>
|
|
109
|
+
response === undefined
|
|
110
|
+
? Effect.succeed(undefined)
|
|
111
|
+
: rowsOf(response).pipe(Effect.map((rows) => rows.find((row) => row.id === modelId))),
|
|
112
|
+
),
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
/** POST /model/new. The answer carries no usable row (the fake answers `{}`), so the id asked for is the one tracked. */
|
|
116
|
+
export const createModel = (body: models.AddNewModelModelNewPostRequest) =>
|
|
117
|
+
throughFetch(models.addNewModelModelNewPost(body)).pipe(Effect.asVoid);
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* PATCH `/model/{model_id}/update`. The ONE write a converge of an existing row fires: its merge
|
|
121
|
+
* carries `litellm_params`, `model_name` and `model_info` together (model-form.ts).
|
|
122
|
+
*/
|
|
123
|
+
export const patchModel = (body: Record<string, unknown>) =>
|
|
124
|
+
throughFetch(
|
|
125
|
+
models.patchModelModelModelIdUpdatePatch(
|
|
126
|
+
body as unknown as models.PatchModelModelModelIdUpdatePatchRequest,
|
|
127
|
+
),
|
|
128
|
+
).pipe(Effect.asVoid);
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Idempotent BY THIS FUNCTION, not by the vendor. A `BadRequest` re-lists and is swallowed only
|
|
132
|
+
* when the id is genuinely absent — the same delete can also be refused for lack of rights or a
|
|
133
|
+
* disconnected DB, which leaves the row live and must never be reported as done
|
|
134
|
+
* (budget-operations.ts's rule; never the message text, S21).
|
|
135
|
+
*/
|
|
136
|
+
export const deleteModel = (modelId: string) =>
|
|
137
|
+
throughFetch(models.deleteModelModelDeletePost({ id: modelId })).pipe(
|
|
138
|
+
Effect.asVoid,
|
|
139
|
+
Effect.catchTag('BadRequest', (original) =>
|
|
140
|
+
listModels().pipe(
|
|
141
|
+
Effect.flatMap((rows) =>
|
|
142
|
+
rows.some((row) => row.id === modelId) ? Effect.fail(original) : Effect.void,
|
|
143
|
+
),
|
|
144
|
+
),
|
|
145
|
+
),
|
|
146
|
+
);
|