llmwarden 0.1.0__tar.gz
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.
- llmwarden-0.1.0/.github/workflows/ci.yml +22 -0
- llmwarden-0.1.0/.github/workflows/publish.yml +48 -0
- llmwarden-0.1.0/.gitignore +22 -0
- llmwarden-0.1.0/.python-version +1 -0
- llmwarden-0.1.0/CHANGELOG.md +17 -0
- llmwarden-0.1.0/LICENSE +21 -0
- llmwarden-0.1.0/PKG-INFO +198 -0
- llmwarden-0.1.0/README.md +146 -0
- llmwarden-0.1.0/docs/architecture.md +464 -0
- llmwarden-0.1.0/docs/contributing.md +32 -0
- llmwarden-0.1.0/docs/releasing.md +41 -0
- llmwarden-0.1.0/examples/01_quickstart.py +17 -0
- llmwarden-0.1.0/examples/02_structured_output.py +30 -0
- llmwarden-0.1.0/examples/03_secret_stores.py +39 -0
- llmwarden-0.1.0/examples/04_hosted_app_custom_endpoint.py +41 -0
- llmwarden-0.1.0/examples/05_cost_report.py +63 -0
- llmwarden-0.1.0/examples/06_testing_your_app.py +53 -0
- llmwarden-0.1.0/pyproject.toml +81 -0
- llmwarden-0.1.0/src/llmwarden/__init__.py +80 -0
- llmwarden-0.1.0/src/llmwarden/backends/__init__.py +65 -0
- llmwarden-0.1.0/src/llmwarden/backends/anthropic.py +123 -0
- llmwarden-0.1.0/src/llmwarden/backends/base.py +59 -0
- llmwarden-0.1.0/src/llmwarden/backends/fake.py +108 -0
- llmwarden-0.1.0/src/llmwarden/backends/litellm.py +137 -0
- llmwarden-0.1.0/src/llmwarden/backends/openai.py +118 -0
- llmwarden-0.1.0/src/llmwarden/guard.py +148 -0
- llmwarden-0.1.0/src/llmwarden/hygiene.py +87 -0
- llmwarden-0.1.0/src/llmwarden/model.py +407 -0
- llmwarden-0.1.0/src/llmwarden/observe/__init__.py +30 -0
- llmwarden-0.1.0/src/llmwarden/observe/memory.py +38 -0
- llmwarden-0.1.0/src/llmwarden/observe/otel.py +68 -0
- llmwarden-0.1.0/src/llmwarden/observe/records.py +83 -0
- llmwarden-0.1.0/src/llmwarden/observe/report.py +67 -0
- llmwarden-0.1.0/src/llmwarden/observe/tags.py +37 -0
- llmwarden-0.1.0/src/llmwarden/prices.py +48 -0
- llmwarden-0.1.0/src/llmwarden/retry.py +71 -0
- llmwarden-0.1.0/src/llmwarden/schema.py +48 -0
- llmwarden-0.1.0/src/llmwarden/secrets/__init__.py +31 -0
- llmwarden-0.1.0/src/llmwarden/secrets/aws.py +52 -0
- llmwarden-0.1.0/src/llmwarden/secrets/azure.py +54 -0
- llmwarden-0.1.0/src/llmwarden/secrets/base.py +130 -0
- llmwarden-0.1.0/src/llmwarden/secrets/env.py +35 -0
- llmwarden-0.1.0/src/llmwarden/secrets/gcp.py +47 -0
- llmwarden-0.1.0/src/llmwarden/secrets/vault.py +58 -0
- llmwarden-0.1.0/src/llmwarden/stores.py +33 -0
- llmwarden-0.1.0/src/llmwarden/types.py +149 -0
- llmwarden-0.1.0/tests/__init__.py +0 -0
- llmwarden-0.1.0/tests/conftest.py +13 -0
- llmwarden-0.1.0/tests/contract/__init__.py +25 -0
- llmwarden-0.1.0/tests/contract/scenarios.py +189 -0
- llmwarden-0.1.0/tests/contract/test_anthropic.py +94 -0
- llmwarden-0.1.0/tests/contract/test_fake.py +42 -0
- llmwarden-0.1.0/tests/contract/test_litellm.py +81 -0
- llmwarden-0.1.0/tests/contract/test_openai.py +94 -0
- llmwarden-0.1.0/tests/unit/__init__.py +1 -0
- llmwarden-0.1.0/tests/unit/test_backend_anthropic.py +204 -0
- llmwarden-0.1.0/tests/unit/test_backend_litellm.py +116 -0
- llmwarden-0.1.0/tests/unit/test_backend_openai.py +210 -0
- llmwarden-0.1.0/tests/unit/test_examples.py +34 -0
- llmwarden-0.1.0/tests/unit/test_fake_backend.py +54 -0
- llmwarden-0.1.0/tests/unit/test_guard.py +170 -0
- llmwarden-0.1.0/tests/unit/test_hygiene.py +76 -0
- llmwarden-0.1.0/tests/unit/test_model.py +365 -0
- llmwarden-0.1.0/tests/unit/test_observe.py +138 -0
- llmwarden-0.1.0/tests/unit/test_otel.py +70 -0
- llmwarden-0.1.0/tests/unit/test_prices.py +20 -0
- llmwarden-0.1.0/tests/unit/test_public_api.py +45 -0
- llmwarden-0.1.0/tests/unit/test_retry.py +85 -0
- llmwarden-0.1.0/tests/unit/test_schema.py +29 -0
- llmwarden-0.1.0/tests/unit/test_secrets_base.py +107 -0
- llmwarden-0.1.0/tests/unit/test_secrets_cloud.py +122 -0
- llmwarden-0.1.0/tests/unit/test_secrets_env.py +27 -0
- llmwarden-0.1.0/tests/unit/test_types.py +67 -0
- llmwarden-0.1.0/uv.lock +2950 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
matrix:
|
|
13
|
+
python: ["3.12", "3.13"]
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: astral-sh/setup-uv@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: ${{ matrix.python }}
|
|
19
|
+
- run: uv sync --group dev
|
|
20
|
+
- run: uv run ruff check . && uv run ruff format --check .
|
|
21
|
+
- run: uv run mypy
|
|
22
|
+
- run: uv run pytest
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
name: publish
|
|
2
|
+
|
|
3
|
+
# Publishes to PyPI when a version tag is pushed, for example `git tag v0.1.0 && git push --tags`.
|
|
4
|
+
# Uses PyPI trusted publishing (OIDC): no API token is stored anywhere. Set it up once on
|
|
5
|
+
# PyPI as described in docs/releasing.md.
|
|
6
|
+
|
|
7
|
+
on:
|
|
8
|
+
push:
|
|
9
|
+
tags: ["v*"]
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
build:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: astral-sh/setup-uv@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: "3.12"
|
|
19
|
+
- run: uv sync --group dev
|
|
20
|
+
- run: uv run ruff check . && uv run ruff format --check .
|
|
21
|
+
- run: uv run mypy
|
|
22
|
+
- run: uv run pytest
|
|
23
|
+
- name: Check the tag matches the package version
|
|
24
|
+
run: |
|
|
25
|
+
tag="${GITHUB_REF_NAME#v}"
|
|
26
|
+
version="$(uv version --short)"
|
|
27
|
+
test "$tag" = "$version" || { echo "tag v$tag != pyproject version $version"; exit 1; }
|
|
28
|
+
- run: uv build
|
|
29
|
+
- uses: actions/upload-artifact@v4
|
|
30
|
+
with:
|
|
31
|
+
name: dist
|
|
32
|
+
path: dist/
|
|
33
|
+
|
|
34
|
+
publish:
|
|
35
|
+
needs: build
|
|
36
|
+
runs-on: ubuntu-latest
|
|
37
|
+
environment:
|
|
38
|
+
name: pypi
|
|
39
|
+
url: https://pypi.org/project/llmwarden/
|
|
40
|
+
permissions:
|
|
41
|
+
id-token: write # required for trusted publishing
|
|
42
|
+
contents: read
|
|
43
|
+
steps:
|
|
44
|
+
- uses: actions/download-artifact@v4
|
|
45
|
+
with:
|
|
46
|
+
name: dist
|
|
47
|
+
path: dist/
|
|
48
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Planning artifacts stay local
|
|
2
|
+
docs/superpowers/
|
|
3
|
+
.remember/
|
|
4
|
+
.claude/
|
|
5
|
+
.superpowers/
|
|
6
|
+
|
|
7
|
+
# Python
|
|
8
|
+
__pycache__/
|
|
9
|
+
*.py[cod]
|
|
10
|
+
*.egg-info/
|
|
11
|
+
dist/
|
|
12
|
+
build/
|
|
13
|
+
.venv/
|
|
14
|
+
.pytest_cache/
|
|
15
|
+
.mypy_cache/
|
|
16
|
+
.ruff_cache/
|
|
17
|
+
.coverage
|
|
18
|
+
htmlcov/
|
|
19
|
+
|
|
20
|
+
# Secrets never live in the repo
|
|
21
|
+
.env
|
|
22
|
+
.env.*
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.13
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 (2026-10-15)
|
|
4
|
+
|
|
5
|
+
First version. Everything here is described in `docs/architecture.md`.
|
|
6
|
+
|
|
7
|
+
- `lw.model(name, api_key=...)` creates a model with its key from a string, a function or a secret reference, and `Model.complete` makes one call with messages or `system` and `user`.
|
|
8
|
+
- Three backends behind one contract: `OpenAIBackend` (OpenAI, OpenRouter, Ollama, vLLM, any OpenAI-compatible server), `AnthropicBackend` (beta features pass through), `LiteLLMBackend`. The provider in the model name picks the backend; `backend=` overrides it. `FakeBackend` is public for applications' own tests.
|
|
9
|
+
- Secret stores: `env` built in; AWS Secrets Manager, Azure Key Vault, Google Secret Manager and HashiCorp Vault as extras. Values are cached by reference, fetched once under concurrency, re-fetched once on an auth error, and never written or logged.
|
|
10
|
+
- Endpoint guard for `public_only` models: https only, public addresses only, the checked address pinned in the transport, no redirects, built from the HTTP library the backend's SDK uses.
|
|
11
|
+
- Key hygiene: every error reaches the caller scrubbed with the original dropped; masked `repr`; a logging filter.
|
|
12
|
+
- Retries run in llmwarden with every library retry turned off, so every attempt is recorded. Optional fallback model with a shared call id.
|
|
13
|
+
- Structured output into a Pydantic class on every backend, natively where the provider supports it and through the prompt where it does not, always validated by llmwarden.
|
|
14
|
+
- Observability: tags through a context variable, one `AttemptRecord` per attempt, an in-memory sink and `lw.report`, a function sink, and an OpenTelemetry sink using the GenAI attribute names.
|
|
15
|
+
- Prices: the model's own, then the backend's, then a small dated table, else unknown. Unknown is never zero.
|
|
16
|
+
|
|
17
|
+
Not in this version: streaming, async, embeddings, tool calling.
|
llmwarden-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nadeem Khan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
llmwarden-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: llmwarden
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Call any model with any key, fetched safely from wherever you keep secrets, and see exactly where your money goes
|
|
5
|
+
Project-URL: Homepage, https://github.com/nadeem4/llmwarden
|
|
6
|
+
Project-URL: Repository, https://github.com/nadeem4/llmwarden
|
|
7
|
+
Project-URL: Issues, https://github.com/nadeem4/llmwarden/issues
|
|
8
|
+
Author: Nadeem Khan
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: anthropic,cost,litellm,llm,observability,openai,opentelemetry,secrets
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
17
|
+
Classifier: Topic :: Security
|
|
18
|
+
Requires-Python: >=3.12
|
|
19
|
+
Requires-Dist: httpx>=0.27
|
|
20
|
+
Requires-Dist: pydantic>=2.9
|
|
21
|
+
Provides-Extra: all
|
|
22
|
+
Requires-Dist: anthropic>=1.7.0; extra == 'all'
|
|
23
|
+
Requires-Dist: azure-identity>=1.17; extra == 'all'
|
|
24
|
+
Requires-Dist: azure-keyvault-secrets>=4.8; extra == 'all'
|
|
25
|
+
Requires-Dist: boto3>=1.35; extra == 'all'
|
|
26
|
+
Requires-Dist: google-cloud-secret-manager>=2.20; extra == 'all'
|
|
27
|
+
Requires-Dist: hvac>=2.3; extra == 'all'
|
|
28
|
+
Requires-Dist: litellm>=1.105.0; extra == 'all'
|
|
29
|
+
Requires-Dist: openai>=3.17.0; extra == 'all'
|
|
30
|
+
Requires-Dist: opentelemetry-api>=1.27; extra == 'all'
|
|
31
|
+
Requires-Dist: python-dotenv>=1.0; extra == 'all'
|
|
32
|
+
Provides-Extra: anthropic
|
|
33
|
+
Requires-Dist: anthropic>=1.7.0; extra == 'anthropic'
|
|
34
|
+
Provides-Extra: aws
|
|
35
|
+
Requires-Dist: boto3>=1.35; extra == 'aws'
|
|
36
|
+
Provides-Extra: azure
|
|
37
|
+
Requires-Dist: azure-identity>=1.17; extra == 'azure'
|
|
38
|
+
Requires-Dist: azure-keyvault-secrets>=4.8; extra == 'azure'
|
|
39
|
+
Provides-Extra: dotenv
|
|
40
|
+
Requires-Dist: python-dotenv>=1.0; extra == 'dotenv'
|
|
41
|
+
Provides-Extra: gcp
|
|
42
|
+
Requires-Dist: google-cloud-secret-manager>=2.20; extra == 'gcp'
|
|
43
|
+
Provides-Extra: litellm
|
|
44
|
+
Requires-Dist: litellm>=1.105.0; extra == 'litellm'
|
|
45
|
+
Provides-Extra: openai
|
|
46
|
+
Requires-Dist: openai>=3.17.0; extra == 'openai'
|
|
47
|
+
Provides-Extra: otel
|
|
48
|
+
Requires-Dist: opentelemetry-api>=1.27; extra == 'otel'
|
|
49
|
+
Provides-Extra: vault
|
|
50
|
+
Requires-Dist: hvac>=2.3; extra == 'vault'
|
|
51
|
+
Description-Content-Type: text/markdown
|
|
52
|
+
|
|
53
|
+
# llmwarden
|
|
54
|
+
|
|
55
|
+
Call any model with any key, fetched safely from wherever you keep secrets, and see exactly where your money goes and which requests fail.
|
|
56
|
+
|
|
57
|
+
> **Status: 0.1.0 released.** The core, the three backends, the five secret stores, the endpoint guard, key hygiene, recorded retries, tags, reports and the OpenTelemetry sink are implemented and covered by a contract test suite that runs the same scenarios against every backend. Install it with `pip install "llmwarden[openai]"`. The design is in [docs/architecture.md](docs/architecture.md), the working rules in [docs/contributing.md](docs/contributing.md), and what shipped in [CHANGELOG.md](CHANGELOG.md).
|
|
58
|
+
|
|
59
|
+
## The problem
|
|
60
|
+
|
|
61
|
+
An AI application is rarely one model call. A retrieval pipeline rewrites the question, reranks results and writes the answer. A multi-agent system plans, writes, checks and summarizes. Each part may use a different provider, a different model and a different key.
|
|
62
|
+
|
|
63
|
+
Calling many providers is a solved problem. What is not solved is everything around the call:
|
|
64
|
+
|
|
65
|
+
- **Secrets live in many places.** One team keeps keys in environment variables, another in AWS Secrets Manager, Azure Key Vault or HashiCorp Vault. Keys get rotated. Every application writes its own code to fetch them.
|
|
66
|
+
- **Custom endpoints are a hole.** If users can type a base URL, a hosted app can be made to send requests, with a key attached, to an internal address or a cloud metadata service.
|
|
67
|
+
- **Keys leak.** Provider SDK errors keep the request, the request holds the key, and the traceback ends up in a log.
|
|
68
|
+
- **Retries hide cost.** Libraries retry quietly, and the failed attempts never show up in any report.
|
|
69
|
+
- **Nobody knows where the money went.** Usage comes back per call, in a different shape for each provider, with nothing linking it to the user, request or feature that caused it.
|
|
70
|
+
|
|
71
|
+
Every project ends up rewriting the same layer. llmwarden is that layer, written once.
|
|
72
|
+
|
|
73
|
+
## What it provides
|
|
74
|
+
|
|
75
|
+
### 1. A model with its key, in one line
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
import llmwarden as lw
|
|
79
|
+
|
|
80
|
+
fast = lw.model(
|
|
81
|
+
"groq/llama-3.3-70b-versatile", api_key=lw.secret("aws", "prod/llm", field="groq_key")
|
|
82
|
+
)
|
|
83
|
+
strong = lw.model("anthropic/claude-sonnet-5-5", api_key=lw.secret("env", "ANTHROPIC_API_KEY"))
|
|
84
|
+
|
|
85
|
+
reply = fast.complete(system="Rewrite the question.", user=question)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
No config file. Create a model in code, give it a key, call it. A model is cheap to create, so a hosted app can build one per request with the end user's own key.
|
|
89
|
+
|
|
90
|
+
### 2. Secrets from any store, and rotation that just works
|
|
91
|
+
|
|
92
|
+
- The key can be a plain string, a function that returns one, or a reference: `lw.secret(store, name, field=None)`.
|
|
93
|
+
- `env` is built in. AWS Secrets Manager, Azure Key Vault, HashiCorp Vault and Google Secret Manager are optional extras. Register a store once at startup, with its region or address, then refer to secrets by name.
|
|
94
|
+
- Write your own store with one method.
|
|
95
|
+
- When a key is rotated, `model.refresh()` fetches it again. If a call fails because the key was refused and the key came from a store, llmwarden fetches it again and retries once, so a rotated key works without a restart.
|
|
96
|
+
- Fetched keys stay in memory only. They are never written to disk, logs or errors.
|
|
97
|
+
|
|
98
|
+
### 3. A guard for custom endpoints
|
|
99
|
+
|
|
100
|
+
With `public_only=True`, meant for hosted apps that strangers use:
|
|
101
|
+
|
|
102
|
+
- A custom base URL must be `https`.
|
|
103
|
+
- Its host must resolve to public addresses only. Private, loopback, link-local and cloud metadata addresses are refused.
|
|
104
|
+
- The request goes to the exact address that was checked, so a DNS change between check and call cannot reach a private address.
|
|
105
|
+
- Redirects are not followed.
|
|
106
|
+
- URLs with credentials, fragments or query tricks are refused.
|
|
107
|
+
|
|
108
|
+
### 4. Key hygiene
|
|
109
|
+
|
|
110
|
+
- A key is handed to the client that needs it and to nothing else.
|
|
111
|
+
- Every provider error is re-raised as a fresh error with the key scrubbed and the original dropped, so the key cannot appear in a traceback.
|
|
112
|
+
- A model prints with its key masked.
|
|
113
|
+
|
|
114
|
+
### 5. Observability and cost attribution, out of the box
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
with lw.tags(request="q-1842", customer="acme"):
|
|
118
|
+
plan = planner.complete(..., tags={"agent": "planner"})
|
|
119
|
+
draft = writer.complete(..., tags={"agent": "writer"})
|
|
120
|
+
|
|
121
|
+
lw.report(group_by=["customer", "agent"])
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
- **Tags** set once where a request comes in apply to every call inside it, however deep. Add more on any call.
|
|
125
|
+
- **Every attempt is recorded,** retries included: model, outcome, error kind, tokens, cost, time and tags. Retries happen inside llmwarden, never hidden inside a provider library, so none go uncounted.
|
|
126
|
+
- **Reports** add up cost, tokens, failures and time by any tag: per customer, per feature, per agent, per request.
|
|
127
|
+
- **OpenTelemetry** spans are emitted for every attempt, using the standard GenAI attribute names, so Grafana, Jaeger, Langfuse, Phoenix, Datadog or any other OpenTelemetry tool can show them with no extra code.
|
|
128
|
+
- Unknown prices and token counts are reported as unknown, never as zero.
|
|
129
|
+
- Prompts and replies are never recorded. Keys never are.
|
|
130
|
+
|
|
131
|
+
### 6. Structured output, the same everywhere
|
|
132
|
+
|
|
133
|
+
Pass a Pydantic class and get back a validated object, on every provider, because llmwarden validates the reply itself. A reply that fails validation is retried like any other failure, and recorded.
|
|
134
|
+
|
|
135
|
+
### 7. Swappable backends
|
|
136
|
+
|
|
137
|
+
Three backends ship in the first version, and the same contract test suite runs against each:
|
|
138
|
+
|
|
139
|
+
| Backend | Reaches | Install |
|
|
140
|
+
|---|---|---|
|
|
141
|
+
| `OpenAIBackend` | OpenAI, OpenRouter, Ollama, vLLM, any OpenAI-compatible server | `llmwarden[openai]` |
|
|
142
|
+
| `AnthropicBackend` | Anthropic, with beta features passed through | `llmwarden[anthropic]` |
|
|
143
|
+
| `LiteLLMBackend` | everything LiteLLM lists | `llmwarden[litellm]` |
|
|
144
|
+
|
|
145
|
+
The provider in the model name picks the backend. Pass `backend=` to override it, or write your own adapter in one file. Retries, records, scrubbing, schema validation and pricing live in llmwarden, so every backend behaves the same.
|
|
146
|
+
|
|
147
|
+
## What it is not
|
|
148
|
+
|
|
149
|
+
- **Not another provider client.** Calls go through an existing library behind one small interface. Wherever that library already does something well, llmwarden uses it, and fills in only what is missing.
|
|
150
|
+
- **Not a framework.** It does not know about your stages, agents or pipelines. Your code decides which model each part uses.
|
|
151
|
+
- **Not a proxy or gateway server.** It is a library inside your application.
|
|
152
|
+
- **Not a dashboard.** It emits OpenTelemetry, which the tools you already use can show.
|
|
153
|
+
|
|
154
|
+
## Not yet
|
|
155
|
+
|
|
156
|
+
Streaming, async calls, embeddings and tool calling come after the first release, as additions to the same interface.
|
|
157
|
+
|
|
158
|
+
## Install
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
pip install "llmwarden[openai]" # one backend
|
|
162
|
+
pip install "llmwarden[anthropic,aws]" # a backend and a secret store
|
|
163
|
+
pip install "llmwarden[all]" # everything
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Python 3.12 or newer.
|
|
167
|
+
|
|
168
|
+
## Examples
|
|
169
|
+
|
|
170
|
+
Runnable scripts in [examples/](examples/), each a few lines with the setup it needs at the top:
|
|
171
|
+
|
|
172
|
+
| File | Shows |
|
|
173
|
+
|---|---|
|
|
174
|
+
| [01_quickstart.py](examples/01_quickstart.py) | one model, one call, usage and cost on the reply |
|
|
175
|
+
| [02_structured_output.py](examples/02_structured_output.py) | a Pydantic schema validated on every backend, retried on bad output |
|
|
176
|
+
| [03_secret_stores.py](examples/03_secret_stores.py) | a cloud store, a field inside a JSON secret, rotation, a store of your own |
|
|
177
|
+
| [04_hosted_app_custom_endpoint.py](examples/04_hosted_app_custom_endpoint.py) | one model per request with the user's key, a guarded user-typed base URL, scrubbed errors |
|
|
178
|
+
| [05_cost_report.py](examples/05_cost_report.py) | tags, every attempt recorded, a report by customer and agent, your own sink, OpenTelemetry |
|
|
179
|
+
| [06_testing_your_app.py](examples/06_testing_your_app.py) | testing your code offline with `FakeBackend` |
|
|
180
|
+
|
|
181
|
+
The last three run without a key or a network; the test suite runs them.
|
|
182
|
+
|
|
183
|
+
## Who it is for
|
|
184
|
+
|
|
185
|
+
Anyone shipping an application with more than one model call, especially one that is hosted for other people: retrieval pipelines, multi-agent systems, and tools where users bring their own key or point at their own server.
|
|
186
|
+
|
|
187
|
+
## Developing
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
uv sync --group dev
|
|
191
|
+
uv run pytest
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Every test runs without a network connection. See [docs/contributing.md](docs/contributing.md) for the working rules and [docs/releasing.md](docs/releasing.md) for how a version reaches PyPI.
|
|
195
|
+
|
|
196
|
+
## Next steps
|
|
197
|
+
|
|
198
|
+
Streaming, async and tool calling, as additions to the same interface.
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# llmwarden
|
|
2
|
+
|
|
3
|
+
Call any model with any key, fetched safely from wherever you keep secrets, and see exactly where your money goes and which requests fail.
|
|
4
|
+
|
|
5
|
+
> **Status: 0.1.0 released.** The core, the three backends, the five secret stores, the endpoint guard, key hygiene, recorded retries, tags, reports and the OpenTelemetry sink are implemented and covered by a contract test suite that runs the same scenarios against every backend. Install it with `pip install "llmwarden[openai]"`. The design is in [docs/architecture.md](docs/architecture.md), the working rules in [docs/contributing.md](docs/contributing.md), and what shipped in [CHANGELOG.md](CHANGELOG.md).
|
|
6
|
+
|
|
7
|
+
## The problem
|
|
8
|
+
|
|
9
|
+
An AI application is rarely one model call. A retrieval pipeline rewrites the question, reranks results and writes the answer. A multi-agent system plans, writes, checks and summarizes. Each part may use a different provider, a different model and a different key.
|
|
10
|
+
|
|
11
|
+
Calling many providers is a solved problem. What is not solved is everything around the call:
|
|
12
|
+
|
|
13
|
+
- **Secrets live in many places.** One team keeps keys in environment variables, another in AWS Secrets Manager, Azure Key Vault or HashiCorp Vault. Keys get rotated. Every application writes its own code to fetch them.
|
|
14
|
+
- **Custom endpoints are a hole.** If users can type a base URL, a hosted app can be made to send requests, with a key attached, to an internal address or a cloud metadata service.
|
|
15
|
+
- **Keys leak.** Provider SDK errors keep the request, the request holds the key, and the traceback ends up in a log.
|
|
16
|
+
- **Retries hide cost.** Libraries retry quietly, and the failed attempts never show up in any report.
|
|
17
|
+
- **Nobody knows where the money went.** Usage comes back per call, in a different shape for each provider, with nothing linking it to the user, request or feature that caused it.
|
|
18
|
+
|
|
19
|
+
Every project ends up rewriting the same layer. llmwarden is that layer, written once.
|
|
20
|
+
|
|
21
|
+
## What it provides
|
|
22
|
+
|
|
23
|
+
### 1. A model with its key, in one line
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
import llmwarden as lw
|
|
27
|
+
|
|
28
|
+
fast = lw.model(
|
|
29
|
+
"groq/llama-3.3-70b-versatile", api_key=lw.secret("aws", "prod/llm", field="groq_key")
|
|
30
|
+
)
|
|
31
|
+
strong = lw.model("anthropic/claude-sonnet-5-5", api_key=lw.secret("env", "ANTHROPIC_API_KEY"))
|
|
32
|
+
|
|
33
|
+
reply = fast.complete(system="Rewrite the question.", user=question)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
No config file. Create a model in code, give it a key, call it. A model is cheap to create, so a hosted app can build one per request with the end user's own key.
|
|
37
|
+
|
|
38
|
+
### 2. Secrets from any store, and rotation that just works
|
|
39
|
+
|
|
40
|
+
- The key can be a plain string, a function that returns one, or a reference: `lw.secret(store, name, field=None)`.
|
|
41
|
+
- `env` is built in. AWS Secrets Manager, Azure Key Vault, HashiCorp Vault and Google Secret Manager are optional extras. Register a store once at startup, with its region or address, then refer to secrets by name.
|
|
42
|
+
- Write your own store with one method.
|
|
43
|
+
- When a key is rotated, `model.refresh()` fetches it again. If a call fails because the key was refused and the key came from a store, llmwarden fetches it again and retries once, so a rotated key works without a restart.
|
|
44
|
+
- Fetched keys stay in memory only. They are never written to disk, logs or errors.
|
|
45
|
+
|
|
46
|
+
### 3. A guard for custom endpoints
|
|
47
|
+
|
|
48
|
+
With `public_only=True`, meant for hosted apps that strangers use:
|
|
49
|
+
|
|
50
|
+
- A custom base URL must be `https`.
|
|
51
|
+
- Its host must resolve to public addresses only. Private, loopback, link-local and cloud metadata addresses are refused.
|
|
52
|
+
- The request goes to the exact address that was checked, so a DNS change between check and call cannot reach a private address.
|
|
53
|
+
- Redirects are not followed.
|
|
54
|
+
- URLs with credentials, fragments or query tricks are refused.
|
|
55
|
+
|
|
56
|
+
### 4. Key hygiene
|
|
57
|
+
|
|
58
|
+
- A key is handed to the client that needs it and to nothing else.
|
|
59
|
+
- Every provider error is re-raised as a fresh error with the key scrubbed and the original dropped, so the key cannot appear in a traceback.
|
|
60
|
+
- A model prints with its key masked.
|
|
61
|
+
|
|
62
|
+
### 5. Observability and cost attribution, out of the box
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
with lw.tags(request="q-1842", customer="acme"):
|
|
66
|
+
plan = planner.complete(..., tags={"agent": "planner"})
|
|
67
|
+
draft = writer.complete(..., tags={"agent": "writer"})
|
|
68
|
+
|
|
69
|
+
lw.report(group_by=["customer", "agent"])
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
- **Tags** set once where a request comes in apply to every call inside it, however deep. Add more on any call.
|
|
73
|
+
- **Every attempt is recorded,** retries included: model, outcome, error kind, tokens, cost, time and tags. Retries happen inside llmwarden, never hidden inside a provider library, so none go uncounted.
|
|
74
|
+
- **Reports** add up cost, tokens, failures and time by any tag: per customer, per feature, per agent, per request.
|
|
75
|
+
- **OpenTelemetry** spans are emitted for every attempt, using the standard GenAI attribute names, so Grafana, Jaeger, Langfuse, Phoenix, Datadog or any other OpenTelemetry tool can show them with no extra code.
|
|
76
|
+
- Unknown prices and token counts are reported as unknown, never as zero.
|
|
77
|
+
- Prompts and replies are never recorded. Keys never are.
|
|
78
|
+
|
|
79
|
+
### 6. Structured output, the same everywhere
|
|
80
|
+
|
|
81
|
+
Pass a Pydantic class and get back a validated object, on every provider, because llmwarden validates the reply itself. A reply that fails validation is retried like any other failure, and recorded.
|
|
82
|
+
|
|
83
|
+
### 7. Swappable backends
|
|
84
|
+
|
|
85
|
+
Three backends ship in the first version, and the same contract test suite runs against each:
|
|
86
|
+
|
|
87
|
+
| Backend | Reaches | Install |
|
|
88
|
+
|---|---|---|
|
|
89
|
+
| `OpenAIBackend` | OpenAI, OpenRouter, Ollama, vLLM, any OpenAI-compatible server | `llmwarden[openai]` |
|
|
90
|
+
| `AnthropicBackend` | Anthropic, with beta features passed through | `llmwarden[anthropic]` |
|
|
91
|
+
| `LiteLLMBackend` | everything LiteLLM lists | `llmwarden[litellm]` |
|
|
92
|
+
|
|
93
|
+
The provider in the model name picks the backend. Pass `backend=` to override it, or write your own adapter in one file. Retries, records, scrubbing, schema validation and pricing live in llmwarden, so every backend behaves the same.
|
|
94
|
+
|
|
95
|
+
## What it is not
|
|
96
|
+
|
|
97
|
+
- **Not another provider client.** Calls go through an existing library behind one small interface. Wherever that library already does something well, llmwarden uses it, and fills in only what is missing.
|
|
98
|
+
- **Not a framework.** It does not know about your stages, agents or pipelines. Your code decides which model each part uses.
|
|
99
|
+
- **Not a proxy or gateway server.** It is a library inside your application.
|
|
100
|
+
- **Not a dashboard.** It emits OpenTelemetry, which the tools you already use can show.
|
|
101
|
+
|
|
102
|
+
## Not yet
|
|
103
|
+
|
|
104
|
+
Streaming, async calls, embeddings and tool calling come after the first release, as additions to the same interface.
|
|
105
|
+
|
|
106
|
+
## Install
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
pip install "llmwarden[openai]" # one backend
|
|
110
|
+
pip install "llmwarden[anthropic,aws]" # a backend and a secret store
|
|
111
|
+
pip install "llmwarden[all]" # everything
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Python 3.12 or newer.
|
|
115
|
+
|
|
116
|
+
## Examples
|
|
117
|
+
|
|
118
|
+
Runnable scripts in [examples/](examples/), each a few lines with the setup it needs at the top:
|
|
119
|
+
|
|
120
|
+
| File | Shows |
|
|
121
|
+
|---|---|
|
|
122
|
+
| [01_quickstart.py](examples/01_quickstart.py) | one model, one call, usage and cost on the reply |
|
|
123
|
+
| [02_structured_output.py](examples/02_structured_output.py) | a Pydantic schema validated on every backend, retried on bad output |
|
|
124
|
+
| [03_secret_stores.py](examples/03_secret_stores.py) | a cloud store, a field inside a JSON secret, rotation, a store of your own |
|
|
125
|
+
| [04_hosted_app_custom_endpoint.py](examples/04_hosted_app_custom_endpoint.py) | one model per request with the user's key, a guarded user-typed base URL, scrubbed errors |
|
|
126
|
+
| [05_cost_report.py](examples/05_cost_report.py) | tags, every attempt recorded, a report by customer and agent, your own sink, OpenTelemetry |
|
|
127
|
+
| [06_testing_your_app.py](examples/06_testing_your_app.py) | testing your code offline with `FakeBackend` |
|
|
128
|
+
|
|
129
|
+
The last three run without a key or a network; the test suite runs them.
|
|
130
|
+
|
|
131
|
+
## Who it is for
|
|
132
|
+
|
|
133
|
+
Anyone shipping an application with more than one model call, especially one that is hosted for other people: retrieval pipelines, multi-agent systems, and tools where users bring their own key or point at their own server.
|
|
134
|
+
|
|
135
|
+
## Developing
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
uv sync --group dev
|
|
139
|
+
uv run pytest
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Every test runs without a network connection. See [docs/contributing.md](docs/contributing.md) for the working rules and [docs/releasing.md](docs/releasing.md) for how a version reaches PyPI.
|
|
143
|
+
|
|
144
|
+
## Next steps
|
|
145
|
+
|
|
146
|
+
Streaming, async and tool calling, as additions to the same interface.
|