@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,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `LiteLLM.Key`'s `metadata`, over the fake: a MERGE that never strips what LiteLLM keeps inside the
|
|
3
|
+
* column (guardrails, per-model limits, passthrough routes), and callback credentials that never
|
|
4
|
+
* reach state (key-metadata.ts has the measured reasons). The fake replaces `metadata` on update, as
|
|
5
|
+
* LiteLLM does, so a body that dropped a folded key would lose it here too.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, expect, test } from 'bun:test';
|
|
8
|
+
import { differing, toAttributes, updateBody } from './key-form.ts';
|
|
9
|
+
import {
|
|
10
|
+
FAKE_KEY,
|
|
11
|
+
VAR,
|
|
12
|
+
dashboardEdit,
|
|
13
|
+
declare,
|
|
14
|
+
failureOf,
|
|
15
|
+
keyStack,
|
|
16
|
+
liveRow,
|
|
17
|
+
newFake,
|
|
18
|
+
withEnv,
|
|
19
|
+
} from './key-harness.ts';
|
|
20
|
+
import { callbackSlotsIn, mergedMetadata, splitMetadata } from './key-metadata.ts';
|
|
21
|
+
|
|
22
|
+
/** What LiteLLM 1.103 keeps in a key's metadata besides what a person declared. */
|
|
23
|
+
const folded = {
|
|
24
|
+
allowed_passthrough_routes: ['/x'],
|
|
25
|
+
guardrails: ['pii-mask'],
|
|
26
|
+
model_rpm_limit: { m: 5 },
|
|
27
|
+
tags: ['prod'],
|
|
28
|
+
};
|
|
29
|
+
/** A legacy row's plaintext callback config: 1.103 encrypts new ones, old ones pass through. */
|
|
30
|
+
const SECRET = 'sk-lf-FAKE-callback-secret-000001';
|
|
31
|
+
const logging = [{ callback_name: 'langfuse', callback_vars: { langfuse_secret_key: SECRET } }];
|
|
32
|
+
const seat = (metadata: Record<string, unknown>) =>
|
|
33
|
+
liveRow({ metadata: { seat: 'a', ...metadata } });
|
|
34
|
+
|
|
35
|
+
describe('metadata is a merge', () => {
|
|
36
|
+
test('a declaration that matches what it names writes nothing, however much else the row holds', async () => {
|
|
37
|
+
const fake = newFake({ keySeed: [seat(folded)] });
|
|
38
|
+
const planned = await keyStack(fake).deploy(
|
|
39
|
+
declare({ keyAlias: 'seat-a', metadata: { seat: 'a' } }),
|
|
40
|
+
{
|
|
41
|
+
adopt: true,
|
|
42
|
+
},
|
|
43
|
+
);
|
|
44
|
+
expect(Object.values(planned)).toEqual(['adopted']);
|
|
45
|
+
expect(fake.keys.writes()).toEqual([]);
|
|
46
|
+
expect(fake.keys.rows()[0]?.['metadata']).toEqual({ seat: 'a', ...folded });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('changing a declared key resends every live key with it', async () => {
|
|
50
|
+
const fake = newFake({ keySeed: [seat({ ...folded, owner: 'ops' })] });
|
|
51
|
+
await keyStack(fake).deploy(declare({ keyAlias: 'seat-a', metadata: { seat: 'b' } }), {
|
|
52
|
+
adopt: true,
|
|
53
|
+
});
|
|
54
|
+
const expected = { seat: 'b', ...folded, owner: 'ops' };
|
|
55
|
+
expect(fake.keys.writes()).toEqual([
|
|
56
|
+
{ body: { key_alias: 'seat-a', metadata: expected }, path: '/key/update' },
|
|
57
|
+
]);
|
|
58
|
+
expect(fake.keys.rows()[0]?.['metadata']).toEqual(expected);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('a setting someone adds to a key the stack owns survives the next update', async () => {
|
|
62
|
+
const fake = newFake();
|
|
63
|
+
const stack = keyStack(fake);
|
|
64
|
+
const props = { key: { fromEnv: VAR }, keyAlias: 'seat-a', metadata: { seat: 'a' } };
|
|
65
|
+
const deploy = (models: readonly string[]) =>
|
|
66
|
+
withEnv({ [VAR]: FAKE_KEY }, () => stack.deploy(declare({ ...props, models })));
|
|
67
|
+
expect(await deploy([])).toEqual({ Seat: 'create' });
|
|
68
|
+
await dashboardEdit(fake, 'seat-a', { metadata: { seat: 'a', ...folded } });
|
|
69
|
+
// ★ THE NEXT DEPLOY'S RECONCILE READS THE ROW, SEES THE GUARDRAIL AND MUST NOT SEND `metadata`
|
|
70
|
+
// AT ALL: the declared key already matches, and a body without it leaves the column as it is.
|
|
71
|
+
expect(await deploy(['m'])).toEqual({ Seat: 'update' });
|
|
72
|
+
expect(fake.keys.writes().at(-1)?.body).toEqual({ key_alias: 'seat-a', models: ['m'] });
|
|
73
|
+
expect(fake.keys.rows()[0]?.['metadata']).toEqual({ seat: 'a', ...folded });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('a key is cleared by declaring it null, and is quiet after', async () => {
|
|
77
|
+
const fake = newFake({ keySeed: [seat(folded)] });
|
|
78
|
+
const stack = keyStack(fake);
|
|
79
|
+
const props = { keyAlias: 'seat-a', metadata: { seat: null } };
|
|
80
|
+
await stack.deploy(declare(props), { adopt: true });
|
|
81
|
+
expect(fake.keys.rows()[0]?.['metadata']).toEqual({ seat: null, ...folded });
|
|
82
|
+
expect(await stack.deploy(declare(props))).toEqual({ Seat: 'noop' });
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('form: only declared keys differ, and the body merges', () => {
|
|
86
|
+
const live = toAttributes(seat(folded));
|
|
87
|
+
expect(differing(live, { keyAlias: 'seat-a', metadata: { seat: 'a' } })).toEqual([]);
|
|
88
|
+
const props = { keyAlias: 'seat-a', metadata: { seat: 'b' } };
|
|
89
|
+
expect(differing(live, props)).toEqual(['metadata']);
|
|
90
|
+
expect(updateBody(props, live)).toEqual({
|
|
91
|
+
key_alias: 'seat-a',
|
|
92
|
+
metadata: { seat: 'b', ...folded },
|
|
93
|
+
});
|
|
94
|
+
expect(mergedMetadata({ a: 1 }, undefined)).toEqual({ a: 1 });
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('callback credentials never reach state', () => {
|
|
99
|
+
const held = seat({ ...folded, logging });
|
|
100
|
+
|
|
101
|
+
test('an adopted key with legacy plaintext logging leaves the secret out of state', async () => {
|
|
102
|
+
const fake = newFake({ keySeed: [held] });
|
|
103
|
+
const stack = keyStack(fake);
|
|
104
|
+
const props = { keyAlias: 'seat-a', metadata: { seat: 'a' } };
|
|
105
|
+
await stack.deploy(declare(props), { adopt: true });
|
|
106
|
+
expect(fake.keys.writes()).toEqual([]);
|
|
107
|
+
const state = stack.snapshot();
|
|
108
|
+
expect(state).not.toContain(SECRET);
|
|
109
|
+
expect(state).toContain('logging'); // the slot's NAME is what state keeps
|
|
110
|
+
expect(state).toContain('pii-mask'); // and the rest of the metadata, which it needs to carry it
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('a metadata write onto a key that holds callback config is refused, nothing written', async () => {
|
|
114
|
+
const fake = newFake({ keySeed: [held] });
|
|
115
|
+
const message = await failureOf(
|
|
116
|
+
keyStack(fake).deploy(declare({ keyAlias: 'seat-a', metadata: { seat: 'b' } }), {
|
|
117
|
+
adopt: true,
|
|
118
|
+
}),
|
|
119
|
+
);
|
|
120
|
+
expect(message).toContain('logging');
|
|
121
|
+
expect(message).not.toContain(SECRET);
|
|
122
|
+
expect(fake.keys.writes()).toEqual([]);
|
|
123
|
+
expect(fake.keys.rows()[0]?.['metadata']).toEqual({ seat: 'a', ...folded, logging });
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('a write that touches no metadata still goes ahead on such a key', async () => {
|
|
127
|
+
const fake = newFake({ keySeed: [held] });
|
|
128
|
+
await keyStack(fake).deploy(declare({ keyAlias: 'seat-a', models: ['m'] }), { adopt: true });
|
|
129
|
+
expect(fake.keys.writes()).toEqual([
|
|
130
|
+
{ body: { key_alias: 'seat-a', models: ['m'] }, path: '/key/update' },
|
|
131
|
+
]);
|
|
132
|
+
expect(fake.keys.rows()[0]?.['metadata']).toMatchObject({ logging });
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
for (const slot of ['logging', 'callback_settings', 'secret_manager_settings']) {
|
|
136
|
+
test(`a declaration naming ${slot} is refused before anything is written`, async () => {
|
|
137
|
+
const fake = newFake();
|
|
138
|
+
const props = {
|
|
139
|
+
keyAlias: 'seat-a',
|
|
140
|
+
metadata: { [slot]: [{ callback_vars: { k: SECRET } }] },
|
|
141
|
+
};
|
|
142
|
+
const message = await failureOf(keyStack(fake).deploy(declare(props)));
|
|
143
|
+
expect(message).toContain(slot);
|
|
144
|
+
expect(message).not.toContain(SECRET);
|
|
145
|
+
expect(fake.requests().filter((each) => each.method !== 'GET')).toEqual([]);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
test('form: the slots are split off the row, and only their names stay', () => {
|
|
150
|
+
const split = splitMetadata({ seat: 'a', logging, callback_settings: {} });
|
|
151
|
+
expect(split).toEqual({ metadata: { seat: 'a' }, withheld: ['callback_settings', 'logging'] });
|
|
152
|
+
expect(JSON.stringify(toAttributes(held))).not.toContain(SECRET);
|
|
153
|
+
expect(callbackSlotsIn(undefined)).toEqual([]);
|
|
154
|
+
expect(callbackSlotsIn({ tags: [] })).toEqual([]);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `LiteLLM.Key`'s `metadata` column: what is compared, what is sent, and what is never held.
|
|
3
|
+
* Pure, so it is testable without a server.
|
|
4
|
+
*
|
|
5
|
+
* ⛔ `metadata` IS NOT ONLY THE DECLARER'S. LiteLLM 1.103 keeps a key's guardrails, policies, tags,
|
|
6
|
+
* prompts, per-model RPM/TPM caps, `enforced_params`, `allowed_passthrough_routes`,
|
|
7
|
+
* `end_user_budget_id` and more INSIDE it: they are `LiteLLM_ManagementEndpoint_MetadataFields`
|
|
8
|
+
* and `..._Premium` (`proxy/_types.py:4821-4853`), folded in on create by
|
|
9
|
+
* `metadata_json_with_limits` (`key_management_endpoints.py:4429`) and on update by
|
|
10
|
+
* `prepare_metadata_fields` (:2312). And `/key/update` REPLACES the column with the `metadata` it is
|
|
11
|
+
* sent: only `service_account_id`, the one reserved field, survives an omission (:2323-2336). A
|
|
12
|
+
* body carrying just the declared keys therefore strips every one of those from a live credential,
|
|
13
|
+
* and the deploy reports success. Measured over the fake, which replaces the column the same way
|
|
14
|
+
* (key.test.ts, "metadata is a merge").
|
|
15
|
+
* ★ SO `metadata` IS A MERGE, AT THE TOP-LEVEL KEY. A declaration COMPARES and WRITES only the keys it
|
|
16
|
+
* names; a live key it does not name is CARRIED into the update body unchanged, never dropped. The
|
|
17
|
+
* alternative, a list of the keys LiteLLM owns to carry and clear the rest, was not taken: the list
|
|
18
|
+
* is per-version (a key LiteLLM adds next release would be stripped until it is updated here),
|
|
19
|
+
* and this way fails safe. The cost: a key cannot be removed by leaving it out. Declare it `null`.
|
|
20
|
+
* ⛔ THREE SLOTS ARE NEVER HELD, READ OR WRITTEN: `logging`, `callback_settings` and
|
|
21
|
+
* `secret_manager_settings` (`common_utils/callback_utils.py:51`, `_CALLBACK_CONFIG_SLOTS`). They
|
|
22
|
+
* carry `callback_vars`, an observability platform's secret keys. LiteLLM encrypts them at rest
|
|
23
|
+
* (`encrypt_callback_vars`, :716) but a legacy row's are plaintext ("pass through unchanged",
|
|
24
|
+
* :725-731), and Alchemy persists attributes unencrypted. So a row's slots are dropped from
|
|
25
|
+
* `KeyAttributes.metadata` and only their NAMES are kept (`withheld`); a declaration that names
|
|
26
|
+
* one is refused, since it would be stored encrypted and so never read back equal (a deploy that
|
|
27
|
+
* failed on every run); and a metadata write on a key that HAS one is refused, since this resource
|
|
28
|
+
* cannot carry what it never read. Configure key logging in LiteLLM itself.
|
|
29
|
+
* ⚠️ TOP-LEVEL KEYS ONLY, as LiteLLM reads them: `_transform_callback_vars` looks at `metadata["logging"]`
|
|
30
|
+
* and no deeper.
|
|
31
|
+
*/
|
|
32
|
+
import { deepEqual } from 'alchemy/Diff';
|
|
33
|
+
|
|
34
|
+
export type Metadata = Readonly<Record<string, unknown>>;
|
|
35
|
+
|
|
36
|
+
/** The slots that carry callback credentials: never in attributes, never declared, never written. */
|
|
37
|
+
export const CALLBACK_SLOTS = ['callback_settings', 'logging', 'secret_manager_settings'] as const;
|
|
38
|
+
|
|
39
|
+
const isObject = (value: unknown): value is Record<string, unknown> =>
|
|
40
|
+
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
41
|
+
|
|
42
|
+
/** The callback slots `metadata` names, sorted (`CALLBACK_SLOTS` is). */
|
|
43
|
+
export const callbackSlotsIn = (metadata: Metadata | undefined): readonly string[] =>
|
|
44
|
+
metadata === undefined ? [] : CALLBACK_SLOTS.filter((slot) => Object.hasOwn(metadata, slot));
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A row's `metadata` as attributes hold it: everything but the callback slots, whose NAMES are
|
|
48
|
+
* returned beside it (`withheld`) so a write knows it could not carry them.
|
|
49
|
+
*/
|
|
50
|
+
export const splitMetadata = (
|
|
51
|
+
raw: unknown,
|
|
52
|
+
): { readonly metadata: Record<string, unknown>; readonly withheld: readonly string[] } => {
|
|
53
|
+
if (!isObject(raw)) return { metadata: {}, withheld: [] };
|
|
54
|
+
const withheld = callbackSlotsIn(raw);
|
|
55
|
+
return {
|
|
56
|
+
metadata: Object.fromEntries(Object.entries(raw).filter(([name]) => !withheld.includes(name))),
|
|
57
|
+
withheld,
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Whether a key the declaration names differs from the live row. An absent live key differs from any
|
|
63
|
+
* declared value, `null` included (`deepEqual` tells `undefined` from `null`), so `{ seat: null }`
|
|
64
|
+
* writes a `null` once and is quiet after.
|
|
65
|
+
*/
|
|
66
|
+
export const metadataDiffers = (live: Metadata, declared: Metadata | undefined): boolean =>
|
|
67
|
+
declared !== undefined &&
|
|
68
|
+
Object.entries(declared).some(([name, value]) => !deepEqual(live[name], value));
|
|
69
|
+
|
|
70
|
+
/** The body's `metadata`: every live key carried, the declared ones on top. */
|
|
71
|
+
export const mergedMetadata = (live: Metadata, declared: Metadata | undefined): Metadata => ({
|
|
72
|
+
...live,
|
|
73
|
+
...declared,
|
|
74
|
+
});
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The four LiteLLM `/key/*` calls `LiteLLM.Key` uses, through the SDK's typed operations.
|
|
3
|
+
*
|
|
4
|
+
* ★ EVERY OPERATION HERE EXISTS IN `@distilled.cloud/litellm/key_management` (generated at LiteLLM
|
|
5
|
+
* 1.103.0) AND IS CALLED WITH ITS OWN REQUEST TYPE — none is guessed, none is hand-rolled:
|
|
6
|
+
* `listKeysKeyListGet` GET /key/list read, by `key_alias`, `return_full_object`
|
|
7
|
+
* `generateKeyFnKeyGeneratePost` POST /key/generate create, with the user-defined `key`
|
|
8
|
+
* `updateKeyFnKeyUpdatePost` POST /key/update merge patch, found by `key_alias`
|
|
9
|
+
* `deleteKeyFnKeyDeletePost` POST /key/delete by `key_aliases`
|
|
10
|
+
* `/key/info` and `/v2/key/info` are NOT used: they take the key or its hash, and this resource
|
|
11
|
+
* holds neither for an adopted key. Listing by alias needs no value.
|
|
12
|
+
* ⚠️ NO SEMAPHORE, unlike pass-through endpoints: every key is its own DB row (budget-operations.ts).
|
|
13
|
+
* ⚠️ THE SDK'S DEFAULT RETRY APPLIES TO `/key/generate` TOO. A create whose response was lost and is
|
|
14
|
+
* then retried surfaces the proxy's duplicate-alias 400 rather than a second key; the next deploy
|
|
15
|
+
* reads the row. UNVERIFIED against a live proxy — the retry policy is core's, not this file's.
|
|
16
|
+
* ⚠️ DELETE IS IDEMPOTENT BY THIS FUNCTION, not by the vendor. MEASURED in the litellm 1.103.0 wheel
|
|
17
|
+
* (`key_management_endpoints.py`): `/key/delete` for an alias it does not hold answers 404 "No keys
|
|
18
|
+
* found" (`delete_verification_tokens`, :4915-4918), and 403 for a caller that may not delete the
|
|
19
|
+
* key (:4933-4936). The SDK types both as resource-specific tags, `KeyNotFound` and
|
|
20
|
+
* `KeyDeleteForbidden` (a distilled patch, `patches/key_management/delete_key_fn_key_delete_post.json`,
|
|
21
|
+
* each matched on the status AND a phrase of the vendor's message), so `catchTag` sees them: the
|
|
22
|
+
* MESSAGE MATCH LIVES IN THE SDK, never here (S21). An absent alias is settled by the LIST, which
|
|
23
|
+
* answers absence unambiguously, and a `KeyNotFound` (another caller won the race, or a retry of a
|
|
24
|
+
* delete whose answer was lost) re-lists and is swallowed only if the key has gone since. Every
|
|
25
|
+
* other failure, `KeyDeleteForbidden` included, propagates typed: a 404 that is not the vendor's
|
|
26
|
+
* is core's `NotFound`, not absence. operations.ts's `deletePassThroughEndpoint` and
|
|
27
|
+
* budget-operations.ts decide the same way, on their own tag.
|
|
28
|
+
*/
|
|
29
|
+
import * as keys from '@distilled.cloud/litellm/key_management';
|
|
30
|
+
import * as Effect from 'effect/Effect';
|
|
31
|
+
import * as Predicate from 'effect/Predicate';
|
|
32
|
+
import { isHttpClientError } from 'effect/unstable/http/HttpClientError';
|
|
33
|
+
import {
|
|
34
|
+
LitellmKeyAliasEmptyError,
|
|
35
|
+
LitellmKeyAmbiguousAliasError,
|
|
36
|
+
LitellmKeyTransportError,
|
|
37
|
+
LitellmKeyUnreadableError,
|
|
38
|
+
} from './key-errors.ts';
|
|
39
|
+
import { type KeyAttributes, toAttributes } from './key-form.ts';
|
|
40
|
+
import { type LitellmOpContext, throughFetch } from './operations.ts';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The one live key with this alias, or `undefined`. Two rows are an error, never a pick: the
|
|
44
|
+
* server filters by an EXACT alias by default, and this filters again so a proxy that matched a
|
|
45
|
+
* substring cannot hand back someone else's key.
|
|
46
|
+
*/
|
|
47
|
+
export const findKey = (
|
|
48
|
+
keyAlias: string,
|
|
49
|
+
): Effect.Effect<
|
|
50
|
+
KeyAttributes | undefined,
|
|
51
|
+
| keys.ListKeysKeyListGetError
|
|
52
|
+
| LitellmKeyUnreadableError
|
|
53
|
+
| LitellmKeyAmbiguousAliasError
|
|
54
|
+
| LitellmKeyAliasEmptyError,
|
|
55
|
+
LitellmOpContext
|
|
56
|
+
> =>
|
|
57
|
+
keyAlias.trim() === '' ? Effect.fail(new LitellmKeyAliasEmptyError()) : findKeyByAlias(keyAlias);
|
|
58
|
+
|
|
59
|
+
const findKeyByAlias = (keyAlias: string) =>
|
|
60
|
+
findKeyRowByAlias(keyAlias).pipe(
|
|
61
|
+
Effect.map((row) => (row === undefined ? undefined : toAttributes(row))),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The one live key with this alias, as the raw `/key/list` row — the shape `toAttributes` reads
|
|
66
|
+
* and drops the `token` from. `findKey` maps it to `KeyAttributes`; `readKeyToken` reads the raw
|
|
67
|
+
* row because the row's `token` (the vendor's sha256 of the key) is what key-secret.ts's
|
|
68
|
+
* `verifyKeyValue` compares a declared value against, and it must not pass through `toAttributes`
|
|
69
|
+
* (which would put the hash in state).
|
|
70
|
+
*/
|
|
71
|
+
const findKeyRowByAlias = (keyAlias: string) =>
|
|
72
|
+
throughFetch(keys.listKeysKeyListGet({ key_alias: keyAlias, return_full_object: true })).pipe(
|
|
73
|
+
Effect.flatMap((response) =>
|
|
74
|
+
Effect.gen(function* () {
|
|
75
|
+
const rows: readonly unknown[] = response.keys ?? [];
|
|
76
|
+
// ⚠️ A STRING ROW IS A HASH: the proxy ignored `return_full_object`, and a hash carries none
|
|
77
|
+
// of the columns compared. Refused rather than read as "no key".
|
|
78
|
+
if (rows.some((row) => typeof row !== 'object' || row === null)) {
|
|
79
|
+
return yield* new LitellmKeyUnreadableError({ keyAlias });
|
|
80
|
+
}
|
|
81
|
+
const mine = (rows as readonly Record<string, unknown>[]).filter(
|
|
82
|
+
(row) => row['key_alias'] === keyAlias,
|
|
83
|
+
);
|
|
84
|
+
if (mine.length > 1) {
|
|
85
|
+
return yield* new LitellmKeyAmbiguousAliasError({ count: mine.length, keyAlias });
|
|
86
|
+
}
|
|
87
|
+
return mine[0];
|
|
88
|
+
}),
|
|
89
|
+
),
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The row's `token` (the vendor's sha256 of the key), `null` when the row carries none, or
|
|
94
|
+
* `undefined` when the alias is absent. Absence must plan recreation, not a value mismatch.
|
|
95
|
+
* Read only for the in-memory mismatch check (`verifyKeyValue`); the hash is never persisted.
|
|
96
|
+
*/
|
|
97
|
+
export const readKeyToken = (
|
|
98
|
+
keyAlias: string,
|
|
99
|
+
): Effect.Effect<
|
|
100
|
+
string | null | undefined,
|
|
101
|
+
| keys.ListKeysKeyListGetError
|
|
102
|
+
| LitellmKeyUnreadableError
|
|
103
|
+
| LitellmKeyAmbiguousAliasError
|
|
104
|
+
| LitellmKeyAliasEmptyError,
|
|
105
|
+
LitellmOpContext
|
|
106
|
+
> =>
|
|
107
|
+
keyAlias.trim() === ''
|
|
108
|
+
? Effect.fail(new LitellmKeyAliasEmptyError())
|
|
109
|
+
: findKeyRowByAlias(keyAlias).pipe(
|
|
110
|
+
Effect.map((row) =>
|
|
111
|
+
row === undefined ? undefined : typeof row['token'] === 'string' ? row['token'] : null,
|
|
112
|
+
),
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Whether `value` holds the request it answers. An `HttpClientError` does, through `reason.request`
|
|
117
|
+
* (its `request` getter reads it), and its body is the JSON the call sent.
|
|
118
|
+
*/
|
|
119
|
+
const carriesRequest = (value: unknown): boolean =>
|
|
120
|
+
isHttpClientError(value) || Predicate.hasProperty(value, 'request');
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* `/key/generate`. The response's `key` is `Redacted` (the SDK's `SensitiveValue`); never log the response.
|
|
124
|
+
* ⛔ THE REQUEST BODY IS `{"key":"sk-…"}`, AND A FAILED CALL CAN CARRY ITS REQUEST. Two ways, both
|
|
125
|
+
* measured 2026-09-29 with a synthetic key (key-transport.test.ts): a transport failure is an
|
|
126
|
+
* `HttpClientError` in the error channel, and a body that fails mid-read is the SAME error as a
|
|
127
|
+
* DEFECT (`Effect.orDie` in `@distilled.cloud/core`'s `protocol-rest.ts`). Either would put the
|
|
128
|
+
* plaintext in front of anything that serialises, inspects or logs the failure — while the SDK's
|
|
129
|
+
* own `HttpClientError` is not something this resource can leave typed as it is. Both become
|
|
130
|
+
* `LitellmKeyTransportError`, which keeps the alias and the reason's tag and nothing else.
|
|
131
|
+
* The SDK's status errors (`BadRequest`, `Forbidden`, …) carry only the proxy's message text and
|
|
132
|
+
* stay as they are, so a caller can still `catchTag` them.
|
|
133
|
+
*/
|
|
134
|
+
export const generateKey = (body: keys.GenerateKeyFnKeyGeneratePostRequest) => {
|
|
135
|
+
const keyAlias = body.key_alias ?? '';
|
|
136
|
+
return throughFetch(keys.generateKeyFnKeyGeneratePost(body)).pipe(
|
|
137
|
+
Effect.catchTag('HttpClientError', (error) =>
|
|
138
|
+
Effect.fail(new LitellmKeyTransportError({ keyAlias, reason: error.reason._tag })),
|
|
139
|
+
),
|
|
140
|
+
Effect.catchDefect((defect) =>
|
|
141
|
+
carriesRequest(defect)
|
|
142
|
+
? Effect.fail(
|
|
143
|
+
new LitellmKeyTransportError({
|
|
144
|
+
keyAlias,
|
|
145
|
+
reason: isHttpClientError(defect) ? defect.reason._tag : 'Defect',
|
|
146
|
+
}),
|
|
147
|
+
)
|
|
148
|
+
: Effect.die(defect),
|
|
149
|
+
),
|
|
150
|
+
);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/** `/key/update`, found by the body's `key_alias`. */
|
|
154
|
+
export const updateKey = (body: keys.UpdateKeyFnKeyUpdatePostRequest) =>
|
|
155
|
+
throughFetch(keys.updateKeyFnKeyUpdatePost(body)).pipe(Effect.asVoid);
|
|
156
|
+
|
|
157
|
+
/** `/key/delete` by alias, when the list shows the key; a no-op when it does not. */
|
|
158
|
+
export const deleteKey = (
|
|
159
|
+
keyAlias: string,
|
|
160
|
+
): Effect.Effect<
|
|
161
|
+
void,
|
|
162
|
+
| keys.DeleteKeyFnKeyDeletePostError
|
|
163
|
+
| keys.ListKeysKeyListGetError
|
|
164
|
+
| LitellmKeyUnreadableError
|
|
165
|
+
| LitellmKeyAmbiguousAliasError
|
|
166
|
+
| LitellmKeyAliasEmptyError,
|
|
167
|
+
LitellmOpContext
|
|
168
|
+
> =>
|
|
169
|
+
findKey(keyAlias).pipe(
|
|
170
|
+
Effect.flatMap((live) =>
|
|
171
|
+
live === undefined
|
|
172
|
+
? Effect.void
|
|
173
|
+
: throughFetch(keys.deleteKeyFnKeyDeletePost({ key_aliases: [keyAlias] })).pipe(
|
|
174
|
+
Effect.asVoid,
|
|
175
|
+
Effect.catchTag('KeyNotFound', (original) =>
|
|
176
|
+
findKey(keyAlias).pipe(
|
|
177
|
+
Effect.flatMap((still) =>
|
|
178
|
+
still === undefined ? Effect.void : Effect.fail(original),
|
|
179
|
+
),
|
|
180
|
+
),
|
|
181
|
+
),
|
|
182
|
+
),
|
|
183
|
+
),
|
|
184
|
+
);
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/** Real beta.79 Plan/Apply: only the environment changes; no adoption or incidental drift. */
|
|
2
|
+
import { expect, test } from 'bun:test';
|
|
3
|
+
import * as Effect from 'effect/Effect';
|
|
4
|
+
import { FAKE_BASE } from './fake-litellm.ts';
|
|
5
|
+
import { keyHandlers } from './key.ts';
|
|
6
|
+
import {
|
|
7
|
+
FAKE_KEY,
|
|
8
|
+
MASTER_KEY,
|
|
9
|
+
OTHER_FAKE_KEY,
|
|
10
|
+
VAR,
|
|
11
|
+
declare,
|
|
12
|
+
failureOf,
|
|
13
|
+
keyStack,
|
|
14
|
+
newFake,
|
|
15
|
+
runAgainst,
|
|
16
|
+
tokenFor,
|
|
17
|
+
withEnv,
|
|
18
|
+
} from './key-harness.ts';
|
|
19
|
+
|
|
20
|
+
const seat = { key: { fromEnv: VAR }, keyAlias: 'seat-a', models: ['old'] };
|
|
21
|
+
|
|
22
|
+
for (const models of [['new'], seat.models]) {
|
|
23
|
+
test(`an owned key deleted in the dashboard is recreated with models ${models.join(',')}`, async () => {
|
|
24
|
+
const fake = newFake();
|
|
25
|
+
const engine = keyStack(fake);
|
|
26
|
+
await withEnv({ [VAR]: FAKE_KEY }, async () => {
|
|
27
|
+
await engine.deploy(declare(seat));
|
|
28
|
+
const removed = await fake.fetch(`${FAKE_BASE}/key/delete`, {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
headers: { authorization: `Bearer ${MASTER_KEY}`, 'content-type': 'application/json' },
|
|
31
|
+
body: JSON.stringify({ key_aliases: [seat.keyAlias] }),
|
|
32
|
+
});
|
|
33
|
+
expect(removed.status).toBe(200);
|
|
34
|
+
expect(fake.keys.rows()).toHaveLength(0);
|
|
35
|
+
const news = { ...seat, models };
|
|
36
|
+
expect(Object.values(await engine.deploy(declare(news)))).toEqual(['update']);
|
|
37
|
+
expect(fake.keys.rows()).toHaveLength(1);
|
|
38
|
+
expect(fake.keys.rows()[0]?.models).toEqual(models);
|
|
39
|
+
expect(fake.keys.received().every((value) => value === FAKE_KEY)).toBe(true);
|
|
40
|
+
expect(Object.values(await engine.deploy(declare(news)))).toEqual(['noop']);
|
|
41
|
+
});
|
|
42
|
+
expect(fake.keys.writes().map((write) => write.path)).toEqual([
|
|
43
|
+
'/key/generate',
|
|
44
|
+
'/key/delete',
|
|
45
|
+
'/key/generate',
|
|
46
|
+
]);
|
|
47
|
+
expect(engine.snapshot().includes(FAKE_KEY)).toBe(false);
|
|
48
|
+
expect(engine.snapshot().includes(tokenFor(FAKE_KEY))).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
test('an owned key with identical props refuses a rotated variable during plan', async () => {
|
|
53
|
+
const fake = newFake();
|
|
54
|
+
const engine = keyStack(fake);
|
|
55
|
+
await withEnv({ [VAR]: FAKE_KEY }, () => engine.deploy(declare(seat)));
|
|
56
|
+
const writes = fake.keys.writes().length;
|
|
57
|
+
const state = engine.snapshot();
|
|
58
|
+
const unchanged = await withEnv({ [VAR]: FAKE_KEY }, () => engine.deploy(declare(seat)));
|
|
59
|
+
expect(Object.values(unchanged)).toEqual(['noop']);
|
|
60
|
+
for (const news of [seat, { ...seat, models: ['new'] }]) {
|
|
61
|
+
const message = await failureOf(
|
|
62
|
+
withEnv({ [VAR]: OTHER_FAKE_KEY }, () => engine.deploy(declare(news))),
|
|
63
|
+
);
|
|
64
|
+
expect(message).toContain('is not the key');
|
|
65
|
+
expect(message).toContain(VAR);
|
|
66
|
+
expect(message.includes(OTHER_FAKE_KEY)).toBe(false);
|
|
67
|
+
expect(fake.keys.writes()).toHaveLength(writes);
|
|
68
|
+
expect(engine.snapshot()).toBe(state);
|
|
69
|
+
}
|
|
70
|
+
for (const material of [FAKE_KEY, OTHER_FAKE_KEY, tokenFor(FAKE_KEY), tokenFor(OTHER_FAKE_KEY)]) {
|
|
71
|
+
expect(engine.snapshot().includes(material)).toBe(false);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('the diff refusal is typed and catchTag handles it without storing any key material', async () => {
|
|
76
|
+
const fake = newFake();
|
|
77
|
+
const attributes = await withEnv({ [VAR]: FAKE_KEY }, () =>
|
|
78
|
+
runAgainst(
|
|
79
|
+
fake,
|
|
80
|
+
keyHandlers.reconcile({ fqn: 'Seat', instanceId: 'i-1', news: seat, output: undefined }),
|
|
81
|
+
),
|
|
82
|
+
);
|
|
83
|
+
const tag = await withEnv({ [VAR]: OTHER_FAKE_KEY }, () =>
|
|
84
|
+
runAgainst(
|
|
85
|
+
fake,
|
|
86
|
+
keyHandlers
|
|
87
|
+
.diff({ news: { ...seat, models: ['new'] }, output: attributes })
|
|
88
|
+
.pipe(
|
|
89
|
+
Effect.catchTag('LitellmKeyValueMismatchError', (error) => Effect.succeed(error._tag)),
|
|
90
|
+
),
|
|
91
|
+
),
|
|
92
|
+
);
|
|
93
|
+
expect(tag).toBe('LitellmKeyValueMismatchError');
|
|
94
|
+
expect(fake.keys.writes().map((write) => write.path)).toEqual(['/key/generate']);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('an owned key can update and then noop with the declared variable unset or empty', async () => {
|
|
98
|
+
for (const missing of [undefined, '']) {
|
|
99
|
+
const fake = newFake();
|
|
100
|
+
const engine = keyStack(fake);
|
|
101
|
+
await withEnv({ [VAR]: FAKE_KEY }, () => engine.deploy(declare(seat)));
|
|
102
|
+
const news = { ...seat, models: ['new'] };
|
|
103
|
+
await withEnv({ [VAR]: missing }, async () => {
|
|
104
|
+
expect(Object.values(await engine.deploy(declare(news)))).toEqual(['update']);
|
|
105
|
+
expect(Object.values(await engine.deploy(declare(news)))).toEqual(['noop']);
|
|
106
|
+
});
|
|
107
|
+
expect(fake.keys.writes().map((write) => write.path)).toEqual(['/key/generate', '/key/update']);
|
|
108
|
+
expect(JSON.stringify(fake.keys.writes().slice(1)).includes(FAKE_KEY)).toBe(false);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `LiteLLM.Key` manages only what a declaration names. An omitted `budgetId`, `models`,
|
|
3
|
+
* `allowedRoutes`, `teamId` or `duration` must not mean "clear it", because clearing FAILS OPEN on a
|
|
4
|
+
* credential: `[]` is "all models" and "no route restriction" to LiteLLM, and an adopted key's plan
|
|
5
|
+
* says `adopted`, never what the apply would write (key-form.ts's header).
|
|
6
|
+
*/
|
|
7
|
+
import { describe, expect, test } from 'bun:test';
|
|
8
|
+
import * as Effect from 'effect/Effect';
|
|
9
|
+
import * as Redacted from 'effect/Redacted';
|
|
10
|
+
import { LiteLLMBudget } from './budget.ts';
|
|
11
|
+
import { LiteLLMKey } from './key.ts';
|
|
12
|
+
import { createBody, differing, toAttributes } from './key-form.ts';
|
|
13
|
+
import { FAKE_KEY, VAR, declare, keyStack, liveRow, newFake, withEnv } from './key-harness.ts';
|
|
14
|
+
|
|
15
|
+
const tier = (id: string) => ({ budget_id: id, created_at: 'x', updated_at: 'x' });
|
|
16
|
+
/** A key scoped as narrowly as a seat's: one model, one route, a tier, a team, an expiry. */
|
|
17
|
+
const scoped = () =>
|
|
18
|
+
liveRow({
|
|
19
|
+
allowed_routes: ['/v1/embeddings'],
|
|
20
|
+
budget_id: 'old-tier',
|
|
21
|
+
expires: '2099-01-01T00:00:00.000Z',
|
|
22
|
+
models: ['embed-only'],
|
|
23
|
+
team_id: 'team-a',
|
|
24
|
+
});
|
|
25
|
+
const stillScoped = {
|
|
26
|
+
allowed_routes: ['/v1/embeddings'],
|
|
27
|
+
budget_id: 'old-tier',
|
|
28
|
+
expires: '2099-01-01T00:00:00.000Z',
|
|
29
|
+
models: ['embed-only'],
|
|
30
|
+
team_id: 'team-a',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
describe('an omitted prop is left alone', () => {
|
|
34
|
+
test('adopting with only the alias writes nothing: the scoped key is not widened', async () => {
|
|
35
|
+
const fake = newFake({ budgetSeed: [tier('old-tier')], keySeed: [scoped()] });
|
|
36
|
+
const planned = await keyStack(fake).deploy(declare({ keyAlias: 'seat-a' }), { adopt: true });
|
|
37
|
+
expect(Object.values(planned)).toEqual(['adopted']);
|
|
38
|
+
expect(fake.keys.writes()).toEqual([]);
|
|
39
|
+
expect(fake.keys.rows()[0]).toMatchObject(stillScoped);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('a budget bound by Output writes only the budget: models and routes stay', async () => {
|
|
43
|
+
// The documented declaration. Its plan says `create` (an Output cannot be probed), and reading
|
|
44
|
+
// the omissions as "clear" would send `models: []` and `allowed_routes: []` with the budget.
|
|
45
|
+
const fake = newFake({ keySeed: [scoped()] });
|
|
46
|
+
const body = Effect.gen(function* () {
|
|
47
|
+
const tierRow = yield* LiteLLMBudget('Tier', { budgetId: 'hf-tier-new', softBudget: 5 });
|
|
48
|
+
yield* LiteLLMKey('Seat', { budgetId: tierRow.budgetId, keyAlias: 'seat-a' });
|
|
49
|
+
});
|
|
50
|
+
await keyStack(fake).deploy(body, { adopt: true });
|
|
51
|
+
expect(fake.keys.writes().filter((write) => write.path === '/key/update')).toEqual([
|
|
52
|
+
{ body: { budget_id: 'hf-tier-new', key_alias: 'seat-a' }, path: '/key/update' },
|
|
53
|
+
]);
|
|
54
|
+
expect(fake.keys.rows()[0]).toMatchObject({
|
|
55
|
+
...stillScoped,
|
|
56
|
+
budget_id: 'hf-tier-new',
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('a key the stack owns keeps what a later declaration stops naming', async () => {
|
|
61
|
+
const fake = newFake({ budgetSeed: [tier('tier-a')] });
|
|
62
|
+
const stack = keyStack(fake);
|
|
63
|
+
const seat = { key: { fromEnv: VAR }, keyAlias: 'seat-a' } as const;
|
|
64
|
+
const full = {
|
|
65
|
+
...seat,
|
|
66
|
+
allowedRoutes: ['/v1/*'],
|
|
67
|
+
budgetId: 'tier-a',
|
|
68
|
+
models: ['m'],
|
|
69
|
+
teamId: 't',
|
|
70
|
+
};
|
|
71
|
+
await withEnv({ [VAR]: FAKE_KEY }, () => stack.deploy(declare(full)));
|
|
72
|
+
const writes = fake.keys.writes().length;
|
|
73
|
+
expect(await stack.deploy(declare(seat))).toEqual({ Seat: 'noop' });
|
|
74
|
+
expect(fake.keys.writes()).toHaveLength(writes);
|
|
75
|
+
expect(fake.keys.rows()[0]).toMatchObject({
|
|
76
|
+
allowed_routes: ['/v1/*'],
|
|
77
|
+
budget_id: 'tier-a',
|
|
78
|
+
models: ['m'],
|
|
79
|
+
team_id: 't',
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('clearing is an explicit declaration', () => {
|
|
85
|
+
test('[] and null clear a live scope, and the update body says so', async () => {
|
|
86
|
+
const fake = newFake({ budgetSeed: [tier('old-tier')], keySeed: [scoped()] });
|
|
87
|
+
await keyStack(fake).deploy(
|
|
88
|
+
declare({
|
|
89
|
+
allowedRoutes: [],
|
|
90
|
+
budgetId: null,
|
|
91
|
+
duration: null,
|
|
92
|
+
keyAlias: 'seat-a',
|
|
93
|
+
models: [],
|
|
94
|
+
teamId: null,
|
|
95
|
+
}),
|
|
96
|
+
{ adopt: true },
|
|
97
|
+
);
|
|
98
|
+
expect(fake.keys.writes()).toEqual([
|
|
99
|
+
{
|
|
100
|
+
body: {
|
|
101
|
+
allowed_routes: [],
|
|
102
|
+
budget_id: null,
|
|
103
|
+
duration: null,
|
|
104
|
+
key_alias: 'seat-a',
|
|
105
|
+
models: [],
|
|
106
|
+
team_id: null,
|
|
107
|
+
},
|
|
108
|
+
path: '/key/update',
|
|
109
|
+
},
|
|
110
|
+
]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('a declared null on a key that already has nothing is quiet', () => {
|
|
114
|
+
const live = toAttributes(liveRow());
|
|
115
|
+
expect(
|
|
116
|
+
differing(live, { budgetId: null, duration: null, keyAlias: 'seat-a', teamId: null }),
|
|
117
|
+
).toEqual([]);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('form helpers', () => {
|
|
122
|
+
const live = toAttributes(scoped());
|
|
123
|
+
|
|
124
|
+
test('differing names nothing the declaration does not', () => {
|
|
125
|
+
expect(differing(live, { keyAlias: 'seat-a' })).toEqual([]);
|
|
126
|
+
expect(differing(live, { keyAlias: 'seat-a', models: ['other'] })).toEqual(['models']);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('a new key is sent only what is declared, and a declared null is not sent', () => {
|
|
130
|
+
const secret = Redacted.make(FAKE_KEY);
|
|
131
|
+
expect(
|
|
132
|
+
Object.keys(createBody({ budgetId: null, keyAlias: 'seat-a', teamId: null }, secret)),
|
|
133
|
+
).toEqual(['key', 'key_alias']);
|
|
134
|
+
});
|
|
135
|
+
});
|