Metadata-Version: 2.4
Name: dr-providers
Version: 0.2.0
Summary: Typed LLM provider-call kernel for OpenAI, Anthropic, OpenRouter, and Gemini: identity-hashed configs, one request/response/failure vocabulary, no-throw transport.
Project-URL: Repository, https://github.com/danielle-rothermel/dr-providers
Project-URL: Issues, https://github.com/danielle-rothermel/dr-providers/issues
Project-URL: Documentation, https://danielle-rothermel.github.io/dr-providers/
Project-URL: Changelog, https://github.com/danielle-rothermel/dr-providers/blob/main/CHANGELOG.md
Author-email: Danielle Rothermel <danielle.rothermel@gmail.com>
License-Expression: MIT
License-File: LICENSE
Keywords: anthropic,gemini,llm,openai,openrouter,provider,pydantic,transport,typed
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Python: >=3.12
Requires-Dist: dr-serialize<0.2,>=0.1.0
Requires-Dist: httpx>=0.28.1
Requires-Dist: pydantic>=2.13.4
Provides-Extra: cli
Requires-Dist: typer>=0.26.7; extra == 'cli'
Provides-Extra: serve
Requires-Dist: fastapi>=0.121; extra == 'serve'
Requires-Dist: typer>=0.26.7; extra == 'serve'
Requires-Dist: uvicorn>=0.35; extra == 'serve'
Description-Content-Type: text/markdown

# dr-providers

Typed LLM provider-call kernel for OpenRouter, OpenAI, Gemini, and
Anthropic: one Provider Call Config, Request, Transport Policy, and
no-throw Transport Outcome vocabulary across providers. Requires
Python 3.12+.

## Ecosystem

dr-providers is the typed LLM-provider HTTP transport kernel, with an
optional `[serve]` FastAPI facade for localhost HTTP callers. It builds
Provider Call Definition, Config, and Request Identity Documents — each
carrying its own full 64-char SHA-256 Identity Hash — through
`dr-serialize`. Its neighboring repos are dr-serialize, dr-graph,
dr-platform, dr-code, whetstone-ai, and unitbench. Whetstone-ai /
dr-platform, dr-graph's graph runner, and unitbench playgrounds are
consumers.

The [vocabulary sheet](https://danielle-rothermel.github.io/dr-providers/)
(source: `.defs/vocab.html`) is the authoritative statement of the
provider-call transport contract this repo implements: the terms, the
guarantees, what is in and out of scope, and the mapping from each term
to the exported names.

## Install

```bash
pip install dr-providers
```

Or with [uv](https://docs.astral.sh/uv/):

```bash
uv add dr-providers
```

## Authentication

Set the API key env var for whichever provider(s) you call:

```bash
export OPENROUTER_API_KEY="sk-or-..."
export OPENAI_API_KEY="sk-..."
export GEMINI_API_KEY="..."
export ANTHROPIC_API_KEY="sk-ant-..."
```

## Quickstart

```python
from dr_providers import (
    ApiKeyEnv,
    GenerationControls,
    HttpProvider,
    MessageRole,
    ProviderBaseUrl,
    ProviderCallRequest,
    ProviderTransportPolicy,
    ProviderTransportResponse,
    PromptMessage,
    ReasoningEffort,
    Transcript,
    openrouter_chat_config,
)

# A Provider Call Config is a complete validated assignment of one
# Provider Call Definition; it carries a full SHA-256 Identity Hash.
config = openrouter_chat_config(
    model="openai/gpt-4o-mini",
    controls=GenerationControls(reasoning=ReasoningEffort.LOW),
)

# A Provider Call Request is one Config reference + one Transcript.
request = ProviderCallRequest(
    config=config,
    transcript=Transcript(
        messages=(
            PromptMessage(
                role=MessageRole.USER, content="Say hello in one word."
            ),
        )
    ),
)

# Transport policy (credentials, base URL, timeout, native retry) is
# separate and excluded from identity. Native retry defaults to zero.
policy = ProviderTransportPolicy(
    api_key_env=str(ApiKeyEnv.OPENROUTER),
    base_url=str(ProviderBaseUrl.OPENROUTER),
)

with HttpProvider(policy=policy) as provider:
    outcome = provider.complete(request)  # no-throw typed outcome
    if isinstance(outcome, ProviderTransportResponse):
        print(outcome.text)
```

`complete` returns a closed no-throw Provider Transport Outcome
(`ProviderTransportResponse | ProviderTransportFailure`); expected
outcomes never raise. `invoke` instead returns a stable
`ProviderInvocationEvidence` artifact binding the request + policy
identities to the outcome and the complete least-processed raw request
and success/failure bodies (authorization headers and credentials are
never persisted).

`HttpProvider` is a context manager. In the default (owned) mode each
wire call runs on its own short-lived `httpx.Client` and daemon thread
under a per-invocation wall-clock deadline, so one call's deadline breach
tears down only that call's connection pool and never disturbs another.
If you inject your own client it is shared and left open for you to
manage; the transport cannot forcibly cancel a wedged call on a
caller-owned sync client (see the `HttpProvider` docstring).

## Provider matrix

Presets in `dr_providers.config` build a Provider Call Definition and
materialize its Config, fixing each provider's Model Route
`(provider, protocol, model)`, the token-limit parameter, and the
reasoning wire shape. Base URL and API key env var live on the separate
`ProviderTransportPolicy`; `policy_for(kind, ...)` derives them from the
`DEFAULT_BASE_URLS` / `DEFAULT_API_KEY_ENVS` per-provider maps (both in
`dr_providers.policy`), each overridable:

| Preset                       | Provider    | Protocol            | Reasoning wire shape                        |
| ---------------------------- | ----------- | ------------------- | ------------------------------------------- |
| `openrouter_chat_config`     | `openrouter`| `chat_completions`  | `reasoning: {"effort": ...}` object         |
| `openai_chat_config`         | `openai`    | `chat_completions`  | `reasoning_effort: ...` field               |
| `openai_responses_config`    | `openai`    | `responses`         | `reasoning: {"effort": ...}` object         |
| `gemini_chat_config`         | `gemini`    | `chat_completions`  | `reasoning_effort: ...` field (OpenAI-compat endpoint) |
| `anthropic_messages_config`  | `anthropic` | `anthropic_messages`| `output_config: {"effort": ...}` object     |

Both the OpenAI-compatible / OpenRouter `chat_completions` path and the
Anthropic `anthropic_messages` path are first-class, each usable with a
custom base URL via the transport policy.

`ReasoningEffort` is a shared enum (`NONE`, `MINIMAL`, `LOW`, `MEDIUM`,
`HIGH`, `XHIGH`); each Definition's `reasoning_shape` constraint
determines how `build_payload()` serializes it on the wire. Anthropic
Messages accepts only `low`/`medium`/`high`, so `NONE`, `MINIMAL`, and
`XHIGH` are rejected with a `ControlValidationError` rather than silently
coerced. Anthropic also requires `max_tokens`, so `anthropic_messages_config`
marks `TOKEN_LIMIT` a required control (the CLI and serve facade default it
to 4096 when unset).

OpenAI Responses bodies are normalized from wire `output[]` parts into
text, typed no-text failures, and content-free diagnostics
(`ResponsesDiagnostics`). See the [vocabulary sheet](https://danielle-rothermel.github.io/dr-providers/)
for the authoritative per-name mapping of the parse/diagnostic surface.

## Testing with ScriptedProvider

`ScriptedProvider` implements the same `Provider` interface as
`HttpProvider` but scripts outcomes with no network:

```python
from dr_providers import ScriptedOutcome, ScriptedProvider

provider = ScriptedProvider([ScriptedOutcome(text="scripted reply")])
outcome = provider.complete(request)
assert outcome.text == "scripted reply"
```

## Public API

Import stable symbols from the top-level package:

```python
from dr_providers import (
    ProviderCallConfig,
    ProviderCallRequest,
    ProviderTransportPolicy,
    HttpProvider,
    ReasoningEffort,
)
```

`dr_providers.__all__` is the authoritative export list; the
[vocabulary sheet](https://danielle-rothermel.github.io/dr-providers/)
maps every one of those names to the contract term it implements, so
this README does not repeat the per-name detail. `HttpProvider` loads
lazily so importing the pure modules (route, controls, config, request,
response, outcome, policy, evidence) never pulls in httpx.

## CLI

The `[cli]` extra installs a typer CLI, exposed as the `dr-providers`
console script, for one-shot provider calls:

```bash
pip install 'dr-providers[cli]'
dr-providers --provider openrouter --model openai/gpt-4o-mini -m "Say hello."
```

The console script is always installed, but the CLI itself requires the
`[cli]` extra; running it without that extra prints an install hint and
exits nonzero.

## Serve facade

An optional FastAPI facade (the `[serve]` extra) exposes the kernel
over HTTP for non-Python callers:

```bash
uv run python -m dr_providers.serve serve
```

Release notes live in the
[changelog](https://github.com/danielle-rothermel/dr-providers/blob/main/CHANGELOG.md);
note that 0.2.0 is a complete rewrite with no API compatibility with the
0.1.x query client.

## Development

```bash
uv sync --frozen
uv run pre-commit install
uv run pre-commit run --all-files
```

### Live verification matrix

The default `uv run pytest` run is fully offline (`addopts = "-m 'not
live'"`). A `live`-marked matrix in `tests/live/test_live_matrix.py`
exercises the five presets against real provider endpoints:

```bash
uv run pytest -m live
```

Each case skips (not fails) when its API key env var
(`OPENROUTER_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`,
`ANTHROPIC_API_KEY`) is unset, so this is safe to run without every
provider configured. Successful calls overwrite
`data/wire-corpus/<provider>_<protocol>.json` with the raw response
body; `tests/test_wire_corpus.py` re-parses those bodies offline on
every normal run.

### Audit corpus ground truth

This repo includes a small audit-output corpus and curated ground-truth
normalization artifacts under `data/audit-corpus/`. Regenerate the parsed audit
and analysis files with:

```bash
uv run python scripts/generate_audit_ground_truth.py \
  --corpus-dir data/audit-corpus \
  --output-dir data/audit-corpus/ground-truth
```
