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.
@@ -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 }}
@@ -0,0 +1,18 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .eggs/
8
+ *.egg
9
+ .mypy_cache/
10
+ .pytest_cache/
11
+ .ruff_cache/
12
+ .venv/
13
+ venv/
14
+ .env
15
+ *.so
16
+ .coverage
17
+ htmlcov/
18
+ site/
@@ -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
+ [![CI](https://github.com/alexmar07/llmwire/actions/workflows/ci.yml/badge.svg)](https://github.com/alexmar07/llmwire/actions/workflows/ci.yml)
40
+ [![PyPI](https://img.shields.io/pypi/v/llmwire)](https://pypi.org/project/llmwire/)
41
+ [![Python](https://img.shields.io/pypi/pyversions/llmwire)](https://pypi.org/project/llmwire/)
42
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](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).
@@ -0,0 +1,136 @@
1
+ # LLMWire
2
+
3
+ [![CI](https://github.com/alexmar07/llmwire/actions/workflows/ci.yml/badge.svg)](https://github.com/alexmar07/llmwire/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/llmwire)](https://pypi.org/project/llmwire/)
5
+ [![Python](https://img.shields.io/pypi/pyversions/llmwire)](https://pypi.org/project/llmwire/)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](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