llmwire 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.
- llmwire-0.1.0/.github/workflows/ci.yml +62 -0
- llmwire-0.1.0/.gitignore +18 -0
- llmwire-0.1.0/ARCHITECTURE.md +146 -0
- llmwire-0.1.0/CONTRIBUTING.md +89 -0
- llmwire-0.1.0/LICENSE +21 -0
- llmwire-0.1.0/PKG-INFO +172 -0
- llmwire-0.1.0/README.md +136 -0
- llmwire-0.1.0/docs/api-reference.md +21 -0
- llmwire-0.1.0/docs/getting-started.md +205 -0
- llmwire-0.1.0/docs/index.md +45 -0
- llmwire-0.1.0/mkdocs.yml +33 -0
- llmwire-0.1.0/pyproject.toml +77 -0
- llmwire-0.1.0/src/llmwire/__init__.py +22 -0
- llmwire-0.1.0/src/llmwire/client.py +270 -0
- llmwire-0.1.0/src/llmwire/config.py +41 -0
- llmwire-0.1.0/src/llmwire/exceptions.py +23 -0
- llmwire-0.1.0/src/llmwire/models.py +42 -0
- llmwire-0.1.0/src/llmwire/provider.py +36 -0
- llmwire-0.1.0/src/llmwire/providers/__init__.py +6 -0
- llmwire-0.1.0/src/llmwire/providers/anthropic.py +228 -0
- llmwire-0.1.0/src/llmwire/providers/ollama.py +202 -0
- llmwire-0.1.0/src/llmwire/providers/openai.py +200 -0
- llmwire-0.1.0/src/llmwire/retry.py +46 -0
- llmwire-0.1.0/tests/conftest.py +1 -0
- llmwire-0.1.0/tests/providers/__init__.py +0 -0
- llmwire-0.1.0/tests/providers/test_anthropic.py +97 -0
- llmwire-0.1.0/tests/providers/test_ollama.py +88 -0
- llmwire-0.1.0/tests/providers/test_openai.py +67 -0
- llmwire-0.1.0/tests/test_client.py +244 -0
- llmwire-0.1.0/tests/test_config.py +47 -0
- llmwire-0.1.0/tests/test_models.py +62 -0
- llmwire-0.1.0/tests/test_retry.py +56 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
tags: ['v*']
|
|
7
|
+
pull_request:
|
|
8
|
+
branches: [main]
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
test:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
strategy:
|
|
14
|
+
matrix:
|
|
15
|
+
python-version: ["3.12", "3.13"]
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v6
|
|
19
|
+
|
|
20
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
21
|
+
uses: actions/setup-python@v6
|
|
22
|
+
with:
|
|
23
|
+
python-version: ${{ matrix.python-version }}
|
|
24
|
+
|
|
25
|
+
- name: Install dependencies
|
|
26
|
+
run: pip install -e ".[dev]"
|
|
27
|
+
|
|
28
|
+
- name: Lint
|
|
29
|
+
run: ruff check src/ tests/
|
|
30
|
+
|
|
31
|
+
- name: Type check
|
|
32
|
+
run: mypy src/llmwire/
|
|
33
|
+
|
|
34
|
+
- name: Test
|
|
35
|
+
run: pytest tests/ -v --cov=llmwire --cov-report=xml --cov-fail-under=80
|
|
36
|
+
|
|
37
|
+
publish:
|
|
38
|
+
needs: test
|
|
39
|
+
runs-on: ubuntu-latest
|
|
40
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
41
|
+
|
|
42
|
+
permissions:
|
|
43
|
+
contents: read
|
|
44
|
+
|
|
45
|
+
steps:
|
|
46
|
+
- uses: actions/checkout@v6
|
|
47
|
+
|
|
48
|
+
- name: Set up Python
|
|
49
|
+
uses: actions/setup-python@v6
|
|
50
|
+
with:
|
|
51
|
+
python-version: "3.12"
|
|
52
|
+
|
|
53
|
+
- name: Install build tools
|
|
54
|
+
run: pip install build
|
|
55
|
+
|
|
56
|
+
- name: Build
|
|
57
|
+
run: python -m build
|
|
58
|
+
|
|
59
|
+
- name: Publish to PyPI
|
|
60
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
61
|
+
with:
|
|
62
|
+
password: ${{ secrets.PYPI_API_TOKEN }}
|
llmwire-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
## Design Philosophy
|
|
4
|
+
|
|
5
|
+
LLMWire is built around three principles:
|
|
6
|
+
|
|
7
|
+
1. **Minimal dependencies.** The runtime requires only `httpx`, `pydantic`,
|
|
8
|
+
`pydantic-settings`, and `pyyaml`. There are no provider SDKs. Each provider
|
|
9
|
+
adapter speaks the raw HTTP API directly, keeping the install small and the
|
|
10
|
+
upgrade surface narrow.
|
|
11
|
+
|
|
12
|
+
2. **Async-first.** Every I/O path is async (`httpx.AsyncClient`, `async for` streaming).
|
|
13
|
+
There is no sync wrapper; callers use `asyncio.run()` or their own event loop.
|
|
14
|
+
|
|
15
|
+
3. **Protocol-based extensibility.** Providers satisfy a structural `Protocol` rather
|
|
16
|
+
than inheriting from a base class. This keeps provider implementations independent
|
|
17
|
+
and makes it easy to add new ones without touching core logic.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Component Overview
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
┌─────────────────────────────────────┐
|
|
25
|
+
│ LLMClient │
|
|
26
|
+
│ ┌──────────────────────────────┐ │
|
|
27
|
+
│ │ chat() / stream() │ │
|
|
28
|
+
│ │ - normalize messages │ │
|
|
29
|
+
│ │ - build schema system msg │ │
|
|
30
|
+
│ │ - iterate providers_to_try │ │
|
|
31
|
+
│ └──────────────────┬───────────┘ │
|
|
32
|
+
│ │ │
|
|
33
|
+
│ retry_with_backoff() │
|
|
34
|
+
│ (exponential backoff + jitter) │
|
|
35
|
+
│ │ │
|
|
36
|
+
│ ┌────────────┴──────────┐ │
|
|
37
|
+
│ │ │ │
|
|
38
|
+
│ OpenAI Anthropic Ollama │
|
|
39
|
+
│ Provider Provider Provider │
|
|
40
|
+
│ (httpx) (httpx) (httpx) │
|
|
41
|
+
└─────────────────────────────────────┘
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**`LLMClient`** (`src/llmwire/client.py`) is the only public entry point. It holds an
|
|
45
|
+
ordered list of provider instances built from `LLMConfig` at construction time. `chat()`
|
|
46
|
+
and `stream()` iterate that list, delegating each attempt to `retry_with_backoff()`.
|
|
47
|
+
|
|
48
|
+
**`retry_with_backoff`** (`src/llmwire/retry.py`) is a standalone async function that
|
|
49
|
+
runs a callable up to `max_retries` times. Between attempts it sleeps for
|
|
50
|
+
`base_delay * 2^attempt + jitter` seconds. It only retries exceptions in the
|
|
51
|
+
`retryable_exceptions` tuple; other exceptions propagate immediately.
|
|
52
|
+
|
|
53
|
+
**Provider adapters** (`src/llmwire/providers/`) are plain classes. Each constructs a
|
|
54
|
+
single `httpx.AsyncClient` in `__init__` and uses it for all requests. `LLMClient`
|
|
55
|
+
calls `await provider._client.aclose()` in `close()` / `__aexit__`.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Provider Protocol
|
|
60
|
+
|
|
61
|
+
`Provider` (`src/llmwire/provider.py`) is a `typing.Protocol`:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
class Provider(Protocol):
|
|
65
|
+
@property
|
|
66
|
+
def name(self) -> str: ...
|
|
67
|
+
|
|
68
|
+
async def chat(
|
|
69
|
+
self, messages: list[Message], *, model: str | None, temperature: float,
|
|
70
|
+
max_tokens: int | None,
|
|
71
|
+
) -> ChatResponse: ...
|
|
72
|
+
|
|
73
|
+
async def stream(
|
|
74
|
+
self, messages: list[Message], *, model: str | None, temperature: float,
|
|
75
|
+
max_tokens: int | None,
|
|
76
|
+
) -> AsyncIterator[StreamChunk]: ...
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Any class that satisfies this interface structurally can be used as a provider.
|
|
80
|
+
`_PROVIDER_MAP` in `client.py` maps the string name from `ProviderConfig.name` to the
|
|
81
|
+
concrete class. Adding a new provider means:
|
|
82
|
+
|
|
83
|
+
1. Creating the adapter class in `src/llmwire/providers/`.
|
|
84
|
+
2. Registering it in `_PROVIDER_MAP`.
|
|
85
|
+
3. Exporting it from `src/llmwire/providers/__init__.py`.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Retry and Fallback Strategy
|
|
90
|
+
|
|
91
|
+
`LLMConfig.fallback` controls whether `LLMClient` tries more than one provider:
|
|
92
|
+
|
|
93
|
+
- `fallback=False` — only `self._providers[0]` is tried.
|
|
94
|
+
- `fallback=True` (default) — all providers are tried in order until one succeeds.
|
|
95
|
+
|
|
96
|
+
Within each provider attempt, `retry_with_backoff()` retries `ProviderError` up to
|
|
97
|
+
`max_retries` times. The delay sequence for `base_delay=1.0` is roughly:
|
|
98
|
+
`1 s → 2 s → 4 s` (plus up to 10 % random jitter each time).
|
|
99
|
+
|
|
100
|
+
If a streaming response has already started yielding chunks, a mid-stream `ProviderError`
|
|
101
|
+
is re-raised immediately — falling back mid-stream would produce a corrupt response.
|
|
102
|
+
|
|
103
|
+
If all providers exhaust their retries, `AllProvidersFailedError` is raised with the
|
|
104
|
+
list of individual `ProviderError` instances accessible as `.errors`.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Structured Output
|
|
109
|
+
|
|
110
|
+
`LLMClient.chat(..., response_model=MyModel)` works as follows:
|
|
111
|
+
|
|
112
|
+
1. The JSON schema of `MyModel` is serialised once and cached in `_schema_cache`
|
|
113
|
+
(keyed by class identity).
|
|
114
|
+
2. A system `Message` is prepended instructing the model to return only raw JSON
|
|
115
|
+
matching the schema.
|
|
116
|
+
3. The response content string is passed to `MyModel.model_validate_json()`, which
|
|
117
|
+
raises `pydantic.ValidationError` on malformed output.
|
|
118
|
+
|
|
119
|
+
This approach is provider-agnostic and requires no function-calling support from the
|
|
120
|
+
underlying API.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## File Structure
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
llmwire/
|
|
128
|
+
├── src/llmwire/
|
|
129
|
+
│ ├── __init__.py # public re-exports and __version__
|
|
130
|
+
│ ├── client.py # LLMClient — main orchestrator
|
|
131
|
+
│ ├── config.py # LLMConfig, ProviderConfig (pydantic-settings)
|
|
132
|
+
│ ├── exceptions.py # LLMWireError, ProviderError, AllProvidersFailedError
|
|
133
|
+
│ ├── models.py # Message, ChatResponse, StreamChunk, Usage
|
|
134
|
+
│ ├── provider.py # Provider protocol
|
|
135
|
+
│ ├── retry.py # retry_with_backoff()
|
|
136
|
+
│ └── providers/
|
|
137
|
+
│ ├── __init__.py
|
|
138
|
+
│ ├── anthropic.py # AnthropicProvider
|
|
139
|
+
│ ├── ollama.py # OllamaProvider
|
|
140
|
+
│ └── openai.py # OpenAIProvider
|
|
141
|
+
├── tests/ # pytest test suite (46 tests)
|
|
142
|
+
├── docs/ # MkDocs source
|
|
143
|
+
├── .github/workflows/ci.yml # GitHub Actions CI/CD
|
|
144
|
+
├── pyproject.toml # build config, dependencies, tool config
|
|
145
|
+
└── mkdocs.yml # documentation site config
|
|
146
|
+
```
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
## Dev Setup
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
git clone https://github.com/alexmar07/llmwire.git
|
|
7
|
+
cd llmwire
|
|
8
|
+
python3.12 -m venv .venv
|
|
9
|
+
source .venv/bin/activate
|
|
10
|
+
pip install -e ".[dev]"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
This installs the package in editable mode together with `pytest`, `pytest-asyncio`,
|
|
14
|
+
`pytest-cov`, `respx`, `ruff`, and `mypy`.
|
|
15
|
+
|
|
16
|
+
To also build or preview the documentation:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install -e ".[docs]"
|
|
20
|
+
mkdocs serve
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Running Checks
|
|
24
|
+
|
|
25
|
+
All three checks must pass before opening a pull request:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# Lint and format check
|
|
29
|
+
ruff check src/ tests/
|
|
30
|
+
|
|
31
|
+
# Static type checking
|
|
32
|
+
mypy src/llmwire/
|
|
33
|
+
|
|
34
|
+
# Tests with coverage report
|
|
35
|
+
pytest tests/ -v --cov=src/llmwire --cov-report=term-missing
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The CI pipeline runs these checks on Python 3.12 and 3.13.
|
|
39
|
+
|
|
40
|
+
## Code Style
|
|
41
|
+
|
|
42
|
+
- **Type hints** are required on every function and method signature, including
|
|
43
|
+
`self`-less module-level functions.
|
|
44
|
+
- **Ruff** enforces PEP 8, import ordering, and a set of quality rules (see
|
|
45
|
+
`[tool.ruff.lint]` in `pyproject.toml`). Run `ruff check --fix` to auto-fix
|
|
46
|
+
safe violations.
|
|
47
|
+
- **Mypy strict mode** is enabled. `# type: ignore` comments require a justifying
|
|
48
|
+
comment and should be rare.
|
|
49
|
+
- **Docstrings** use Google style and are required on all public classes and methods.
|
|
50
|
+
- Lines are limited to **100 characters**.
|
|
51
|
+
|
|
52
|
+
## Adding a New Provider
|
|
53
|
+
|
|
54
|
+
1. Create `src/llmwire/providers/<name>.py` with a class `<Name>Provider`.
|
|
55
|
+
The class must satisfy the `Provider` protocol (see
|
|
56
|
+
[`src/llmwire/provider.py`](src/llmwire/provider.py)):
|
|
57
|
+
- `name` property returning the lowercase provider string
|
|
58
|
+
- `async def chat(...)` returning `ChatResponse`
|
|
59
|
+
- `async def stream(...)` yielding `StreamChunk` objects
|
|
60
|
+
|
|
61
|
+
2. Export the class from `src/llmwire/providers/__init__.py`.
|
|
62
|
+
|
|
63
|
+
3. Add the provider to `_PROVIDER_MAP` in `src/llmwire/client.py`:
|
|
64
|
+
```python
|
|
65
|
+
_PROVIDER_MAP: dict[str, type[Any]] = {
|
|
66
|
+
"openai": OpenAIProvider,
|
|
67
|
+
"anthropic": AnthropicProvider,
|
|
68
|
+
"ollama": OllamaProvider,
|
|
69
|
+
"<name>": <Name>Provider, # add here
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
4. Write tests in `tests/test_<name>_provider.py`. Use `respx` to mock HTTP
|
|
74
|
+
calls — no real API credentials should appear in tests.
|
|
75
|
+
|
|
76
|
+
5. Document the provider in `docs/getting-started.md` and update the provider
|
|
77
|
+
support table in `README.md`.
|
|
78
|
+
|
|
79
|
+
## Pull Request Process
|
|
80
|
+
|
|
81
|
+
1. Fork the repository and create a branch from `main`.
|
|
82
|
+
2. Make your changes and ensure all checks pass (lint, type check, tests).
|
|
83
|
+
3. Add or update tests so coverage stays above 80 %.
|
|
84
|
+
4. Update documentation if you added or changed public API.
|
|
85
|
+
5. Open a pull request against `main` with a clear description of what changes
|
|
86
|
+
and why.
|
|
87
|
+
|
|
88
|
+
Pull requests are reviewed for correctness, type safety, test coverage, and
|
|
89
|
+
consistency with the existing code style.
|
llmwire-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alessandro Marotta
|
|
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.
|
llmwire-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: llmwire
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lightweight multi-provider LLM client for Python
|
|
5
|
+
Project-URL: Homepage, https://github.com/alexmar07/llmwire
|
|
6
|
+
Project-URL: Documentation, https://alexmar07.github.io/llmwire
|
|
7
|
+
Project-URL: Repository, https://github.com/alexmar07/llmwire
|
|
8
|
+
Author-email: Alessandro Marotta <alessand.marotta@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: ai,anthropic,async,llm,ollama,openai
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.12
|
|
20
|
+
Requires-Dist: httpx>=0.27
|
|
21
|
+
Requires-Dist: pydantic-settings>=2.0
|
|
22
|
+
Requires-Dist: pydantic>=2.0
|
|
23
|
+
Requires-Dist: pyyaml>=6.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
30
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
31
|
+
Provides-Extra: docs
|
|
32
|
+
Requires-Dist: mkdocs-material>=9.5; extra == 'docs'
|
|
33
|
+
Requires-Dist: mkdocs>=1.6; extra == 'docs'
|
|
34
|
+
Requires-Dist: mkdocstrings[python]>=0.25; extra == 'docs'
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
# LLMWire
|
|
38
|
+
|
|
39
|
+
[](https://github.com/alexmar07/llmwire/actions/workflows/ci.yml)
|
|
40
|
+
[](https://pypi.org/project/llmwire/)
|
|
41
|
+
[](https://pypi.org/project/llmwire/)
|
|
42
|
+
[](LICENSE)
|
|
43
|
+
|
|
44
|
+
Lightweight multi-provider LLM client for Python. A single async interface to
|
|
45
|
+
OpenAI, Anthropic, and Ollama — with automatic fallback, exponential-backoff retry,
|
|
46
|
+
streaming, and structured Pydantic output. No provider SDK dependencies; all requests
|
|
47
|
+
go over plain `httpx`.
|
|
48
|
+
|
|
49
|
+
## Features
|
|
50
|
+
|
|
51
|
+
- **Unified API** — one `LLMClient` for all supported providers
|
|
52
|
+
- **Async-first** — built entirely on `asyncio` and `httpx`
|
|
53
|
+
- **Automatic fallback** — on provider failure, tries the next provider in the list
|
|
54
|
+
- **Exponential backoff** — configurable retry with full jitter
|
|
55
|
+
- **Streaming** — token-by-token via `client.stream()`, async generator interface
|
|
56
|
+
- **Structured output** — pass any Pydantic `BaseModel` as `response_model`
|
|
57
|
+
- **No provider SDKs** — runtime deps are only `httpx`, `pydantic`, `pydantic-settings`, `pyyaml`
|
|
58
|
+
- **Environment variable config** — all settings readable from `LLMKIT_*` env vars
|
|
59
|
+
|
|
60
|
+
## Quick Start
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install llmwire
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Chat
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
import asyncio
|
|
70
|
+
from llmwire import LLMClient, LLMConfig, ProviderConfig
|
|
71
|
+
|
|
72
|
+
config = LLMConfig(
|
|
73
|
+
providers=[
|
|
74
|
+
ProviderConfig(name="openai", api_key="sk-...", model="gpt-4o"),
|
|
75
|
+
ProviderConfig(name="anthropic", api_key="sk-ant-...", model="claude-3-5-sonnet-20241022"),
|
|
76
|
+
]
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
async def main():
|
|
80
|
+
async with LLMClient(config) as client:
|
|
81
|
+
response = await client.chat("What is the capital of France?")
|
|
82
|
+
print(response.content)
|
|
83
|
+
# Provider: openai | Model: gpt-4o | Tokens: 42
|
|
84
|
+
|
|
85
|
+
asyncio.run(main())
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Streaming
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
async def main():
|
|
92
|
+
async with LLMClient(config) as client:
|
|
93
|
+
async for chunk in client.stream("Write a haiku about async programming."):
|
|
94
|
+
print(chunk.content, end="", flush=True)
|
|
95
|
+
print()
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Structured Output
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from pydantic import BaseModel
|
|
102
|
+
|
|
103
|
+
class Sentiment(BaseModel):
|
|
104
|
+
label: str # "positive", "negative", or "neutral"
|
|
105
|
+
confidence: float
|
|
106
|
+
|
|
107
|
+
async def main():
|
|
108
|
+
async with LLMClient(config) as client:
|
|
109
|
+
result: Sentiment = await client.chat(
|
|
110
|
+
"Classify: 'I love this library!'",
|
|
111
|
+
response_model=Sentiment,
|
|
112
|
+
)
|
|
113
|
+
print(result.label, result.confidence) # positive 0.97
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Configuration
|
|
117
|
+
|
|
118
|
+
### Direct
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
from llmwire import LLMConfig, ProviderConfig
|
|
122
|
+
|
|
123
|
+
config = LLMConfig(
|
|
124
|
+
providers=[
|
|
125
|
+
ProviderConfig(name="openai", api_key="sk-...", model="gpt-4o"),
|
|
126
|
+
ProviderConfig(name="ollama", model="llama3.2"), # no key needed
|
|
127
|
+
],
|
|
128
|
+
fallback=True, # try next provider on failure (default: True)
|
|
129
|
+
max_retries=3, # per-provider retry attempts (default: 3)
|
|
130
|
+
timeout=30.0, # request timeout in seconds (default: 30.0)
|
|
131
|
+
)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Environment Variables
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
export LLMKIT_PROVIDERS__0__NAME=openai
|
|
138
|
+
export LLMKIT_PROVIDERS__0__API_KEY=sk-...
|
|
139
|
+
export LLMKIT_PROVIDERS__0__MODEL=gpt-4o
|
|
140
|
+
|
|
141
|
+
export LLMKIT_PROVIDERS__1__NAME=anthropic
|
|
142
|
+
export LLMKIT_PROVIDERS__1__API_KEY=sk-ant-...
|
|
143
|
+
export LLMKIT_PROVIDERS__1__MODEL=claude-3-5-sonnet-20241022
|
|
144
|
+
|
|
145
|
+
export LLMKIT_FALLBACK=true
|
|
146
|
+
export LLMKIT_MAX_RETRIES=3
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
config = LLMConfig() # reads from environment
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Provider Support
|
|
154
|
+
|
|
155
|
+
| Provider | Chat | Streaming | Auth | Default endpoint |
|
|
156
|
+
|----------|------|-----------|------|-----------------|
|
|
157
|
+
| OpenAI | yes | yes | API key | `https://api.openai.com/v1` |
|
|
158
|
+
| Anthropic | yes | yes | API key | `https://api.anthropic.com/v1` |
|
|
159
|
+
| Ollama | yes | yes | none | `http://localhost:11434` |
|
|
160
|
+
|
|
161
|
+
The `base_url` field on `ProviderConfig` lets you point any provider at a compatible
|
|
162
|
+
endpoint (e.g. Azure OpenAI, local OpenAI-compatible servers).
|
|
163
|
+
|
|
164
|
+
## Further Reading
|
|
165
|
+
|
|
166
|
+
- [ARCHITECTURE.md](ARCHITECTURE.md) — design decisions, component overview, and provider protocol
|
|
167
|
+
- [CONTRIBUTING.md](CONTRIBUTING.md) — dev setup, code style, and how to add a new provider
|
|
168
|
+
- [Documentation](https://alexmar07.github.io/llmwire) — full API reference and guides
|
|
169
|
+
|
|
170
|
+
## License
|
|
171
|
+
|
|
172
|
+
MIT. See [LICENSE](LICENSE).
|
llmwire-0.1.0/README.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# LLMWire
|
|
2
|
+
|
|
3
|
+
[](https://github.com/alexmar07/llmwire/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/llmwire/)
|
|
5
|
+
[](https://pypi.org/project/llmwire/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
Lightweight multi-provider LLM client for Python. A single async interface to
|
|
9
|
+
OpenAI, Anthropic, and Ollama — with automatic fallback, exponential-backoff retry,
|
|
10
|
+
streaming, and structured Pydantic output. No provider SDK dependencies; all requests
|
|
11
|
+
go over plain `httpx`.
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- **Unified API** — one `LLMClient` for all supported providers
|
|
16
|
+
- **Async-first** — built entirely on `asyncio` and `httpx`
|
|
17
|
+
- **Automatic fallback** — on provider failure, tries the next provider in the list
|
|
18
|
+
- **Exponential backoff** — configurable retry with full jitter
|
|
19
|
+
- **Streaming** — token-by-token via `client.stream()`, async generator interface
|
|
20
|
+
- **Structured output** — pass any Pydantic `BaseModel` as `response_model`
|
|
21
|
+
- **No provider SDKs** — runtime deps are only `httpx`, `pydantic`, `pydantic-settings`, `pyyaml`
|
|
22
|
+
- **Environment variable config** — all settings readable from `LLMKIT_*` env vars
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install llmwire
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Chat
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
import asyncio
|
|
34
|
+
from llmwire import LLMClient, LLMConfig, ProviderConfig
|
|
35
|
+
|
|
36
|
+
config = LLMConfig(
|
|
37
|
+
providers=[
|
|
38
|
+
ProviderConfig(name="openai", api_key="sk-...", model="gpt-4o"),
|
|
39
|
+
ProviderConfig(name="anthropic", api_key="sk-ant-...", model="claude-3-5-sonnet-20241022"),
|
|
40
|
+
]
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
async def main():
|
|
44
|
+
async with LLMClient(config) as client:
|
|
45
|
+
response = await client.chat("What is the capital of France?")
|
|
46
|
+
print(response.content)
|
|
47
|
+
# Provider: openai | Model: gpt-4o | Tokens: 42
|
|
48
|
+
|
|
49
|
+
asyncio.run(main())
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Streaming
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
async def main():
|
|
56
|
+
async with LLMClient(config) as client:
|
|
57
|
+
async for chunk in client.stream("Write a haiku about async programming."):
|
|
58
|
+
print(chunk.content, end="", flush=True)
|
|
59
|
+
print()
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Structured Output
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from pydantic import BaseModel
|
|
66
|
+
|
|
67
|
+
class Sentiment(BaseModel):
|
|
68
|
+
label: str # "positive", "negative", or "neutral"
|
|
69
|
+
confidence: float
|
|
70
|
+
|
|
71
|
+
async def main():
|
|
72
|
+
async with LLMClient(config) as client:
|
|
73
|
+
result: Sentiment = await client.chat(
|
|
74
|
+
"Classify: 'I love this library!'",
|
|
75
|
+
response_model=Sentiment,
|
|
76
|
+
)
|
|
77
|
+
print(result.label, result.confidence) # positive 0.97
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Configuration
|
|
81
|
+
|
|
82
|
+
### Direct
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from llmwire import LLMConfig, ProviderConfig
|
|
86
|
+
|
|
87
|
+
config = LLMConfig(
|
|
88
|
+
providers=[
|
|
89
|
+
ProviderConfig(name="openai", api_key="sk-...", model="gpt-4o"),
|
|
90
|
+
ProviderConfig(name="ollama", model="llama3.2"), # no key needed
|
|
91
|
+
],
|
|
92
|
+
fallback=True, # try next provider on failure (default: True)
|
|
93
|
+
max_retries=3, # per-provider retry attempts (default: 3)
|
|
94
|
+
timeout=30.0, # request timeout in seconds (default: 30.0)
|
|
95
|
+
)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Environment Variables
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
export LLMKIT_PROVIDERS__0__NAME=openai
|
|
102
|
+
export LLMKIT_PROVIDERS__0__API_KEY=sk-...
|
|
103
|
+
export LLMKIT_PROVIDERS__0__MODEL=gpt-4o
|
|
104
|
+
|
|
105
|
+
export LLMKIT_PROVIDERS__1__NAME=anthropic
|
|
106
|
+
export LLMKIT_PROVIDERS__1__API_KEY=sk-ant-...
|
|
107
|
+
export LLMKIT_PROVIDERS__1__MODEL=claude-3-5-sonnet-20241022
|
|
108
|
+
|
|
109
|
+
export LLMKIT_FALLBACK=true
|
|
110
|
+
export LLMKIT_MAX_RETRIES=3
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
config = LLMConfig() # reads from environment
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Provider Support
|
|
118
|
+
|
|
119
|
+
| Provider | Chat | Streaming | Auth | Default endpoint |
|
|
120
|
+
|----------|------|-----------|------|-----------------|
|
|
121
|
+
| OpenAI | yes | yes | API key | `https://api.openai.com/v1` |
|
|
122
|
+
| Anthropic | yes | yes | API key | `https://api.anthropic.com/v1` |
|
|
123
|
+
| Ollama | yes | yes | none | `http://localhost:11434` |
|
|
124
|
+
|
|
125
|
+
The `base_url` field on `ProviderConfig` lets you point any provider at a compatible
|
|
126
|
+
endpoint (e.g. Azure OpenAI, local OpenAI-compatible servers).
|
|
127
|
+
|
|
128
|
+
## Further Reading
|
|
129
|
+
|
|
130
|
+
- [ARCHITECTURE.md](ARCHITECTURE.md) — design decisions, component overview, and provider protocol
|
|
131
|
+
- [CONTRIBUTING.md](CONTRIBUTING.md) — dev setup, code style, and how to add a new provider
|
|
132
|
+
- [Documentation](https://alexmar07.github.io/llmwire) — full API reference and guides
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# API Reference
|
|
2
|
+
|
|
3
|
+
::: llmwire.client.LLMClient
|
|
4
|
+
|
|
5
|
+
::: llmwire.config.LLMConfig
|
|
6
|
+
|
|
7
|
+
::: llmwire.config.ProviderConfig
|
|
8
|
+
|
|
9
|
+
::: llmwire.models.Message
|
|
10
|
+
|
|
11
|
+
::: llmwire.models.ChatResponse
|
|
12
|
+
|
|
13
|
+
::: llmwire.models.StreamChunk
|
|
14
|
+
|
|
15
|
+
::: llmwire.models.Usage
|
|
16
|
+
|
|
17
|
+
::: llmwire.exceptions.LLMWireError
|
|
18
|
+
|
|
19
|
+
::: llmwire.exceptions.ProviderError
|
|
20
|
+
|
|
21
|
+
::: llmwire.exceptions.AllProvidersFailedError
|