Metadata-Version: 2.4
Name: llm-seam
Version: 0.1.0
Summary: Unified LLM abstraction with provider pattern for portfolio projects
Author-email: Mike Donnelly <82827803+m0j0d@users.noreply.github.com>
License: MIT
Project-URL: Homepage, https://github.com/m0j0d/libs/tree/main/llm-seam
Project-URL: Repository, https://github.com/m0j0d/libs
Project-URL: Issues, https://github.com/m0j0d/libs/issues
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Development Status :: 5 - Production/Stable
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: anthropic>=0.122.0
Requires-Dist: openai>=3.1.0
Provides-Extra: dev
Requires-Dist: pytest>=9.1.1; extra == "dev"
Requires-Dist: pytest-asyncio>=1.4.0; extra == "dev"
Provides-Extra: sdk
Requires-Dist: claude-agent-sdk>=0.2.139; extra == "sdk"
Provides-Extra: ollama
Requires-Dist: litellm>=1.96.2; extra == "ollama"
Dynamic: license-file

# llm-seam

Unified LLM abstraction with a provider pattern — one interface, five backends,
automatic fallback and retry.

**Note on the name:** this package was `llm-base` until 2026-08-21. It was
renamed to `llm-seam` because the PyPI name `llm-base` belongs to an
unrelated, older project — no relation to this package.

## What's in the box

| Module | What it gives you |
|---|---|
| `llm_seam.client` | `LLMClient` — auto-selects a provider from the environment (Anthropic, then OpenAI, then a mock fallback), or accepts an explicit provider name/instance |
| `llm_seam.providers.base` | `LLMProvider` — the `Protocol` every provider implements (`complete`, `get_name`, `supports_streaming`) |
| `llm_seam.providers.anthropic` | `AnthropicProvider` — wraps the `anthropic` SDK |
| `llm_seam.providers.openai` | `OpenAIProvider` — wraps the `openai` SDK |
| `llm_seam.providers.claude_cli` | `ClaudeCLIProvider` — shells out to the Claude Code CLI |
| `llm_seam.providers.agent_sdk` | `AgentSDKProvider` — wraps `claude-agent-sdk` (requires the `[sdk]` extra); supports an optional `can_use_tool` security callback (see below) |
| `llm_seam.providers.ollama` | `OllamaProvider` — local models via `litellm` (requires the `[ollama]` extra) |
| `llm_seam.providers.mock` | `MockProvider` — fixed-response provider for tests and offline use |
| `llm_seam.fallback` | `FallbackChain` — tries a list of providers in order, raises `AllProvidersFailed` only if every one of them fails |
| `llm_seam.retry` | `with_retry` — decorator with exponential backoff, retries on `RateLimitError` by default (configurable) |
| `llm_seam.tool_guards` | `path_scoped_tool_guard` — ready-made `can_use_tool` callback for `AgentSDKProvider`; confines a tool's path arguments to an allowlist of directories, resists `..` traversal, symlink escapes, and absolute paths outside the allowlist |
| `llm_seam.exceptions` | `LLMError` (base), `RateLimitError`, `TokenLimitError`, `TimeoutError`, `AllProvidersFailed` |

## Design

**One interface, provider-agnostic callers.** `LLMClient` and every provider
implement the same three-method `LLMProvider` protocol (`complete`,
`get_name`, `supports_streaming`), so a caller can swap Anthropic for a local
Ollama model, the Claude CLI, or a mock in tests without touching call sites.

**Auto-selection is a convenience, not a requirement.** `LLMClient()` with no
arguments picks Anthropic if `ANTHROPIC_API_KEY` is set, else OpenAI if
`OPENAI_API_KEY` is set, else falls back to `MockProvider`. Pass an explicit
`provider=` name or instance to bypass auto-selection entirely.

**Provider-name convention.** When constructing by name string, this
portfolio distinguishes two Claude transports: `"claude-sdk"` selects
`AgentSDKProvider` (the `claude-agent-sdk` package, preferred for new
scheduled work — see `libs/CLAUDE.md`), and `"claude-cli"` selects
`ClaudeCLIProvider` (shells out to the installed Claude Code CLI). `"claude-sdk"`
is only registered as a valid name if `claude-agent-sdk` is installed (the
`[sdk]` extra); otherwise constructing by that name raises `ValueError`.

**`AgentSDKProvider`'s `can_use_tool` is a security boundary, not a hygiene
knob.** It threads straight to `ClaudeAgentOptions.can_use_tool`. If a caller
passes a callback and the installed `claude-agent-sdk` can't accept the
field, the call raises `LLMError` rather than silently proceeding ungated —
a caller that believes tool calls are being permission-checked must never
find out otherwise the hard way. Use
`tool_guards.path_scoped_tool_guard(allowed_dirs)` rather than hand-rolling
path checks.

**Fallback and retry compose, they don't replace each other.** `FallbackChain`
moves between *providers*; `with_retry` retries a single call on a
*transient* error (rate limits by default). Wrap a `FallbackChain`'s
`.complete` in `with_retry`, or use either alone.

## Install

```bash
pip install llm-seam
```

Optional extras:

```bash
pip install "llm-seam[sdk]"      # AgentSDKProvider (claude-agent-sdk)
pip install "llm-seam[ollama]"   # OllamaProvider (litellm)
```

## Usage

```python
from llm_seam import LLMClient, FallbackChain, with_retry

# Auto-select a provider from the environment
client = LLMClient()
response = client.complete("Hello, world!")

# Explicit provider by name
client = LLMClient(provider="anthropic")

# Fall back through providers in order
chain = FallbackChain(providers=[
    LLMClient(provider="claude-cli").provider,
    LLMClient(provider="anthropic").provider,
])
response = chain.complete("Hello")

# Retry a call with exponential backoff on rate limits
@with_retry(max_retries=3, initial_delay=1.0)
def call():
    return client.complete("Hello")
```

## Testing

```bash
cd llm-seam
pytest tests/
```

## Dependencies

- `anthropic>=0.122.0`
- `openai>=3.1.0`
- `claude-agent-sdk>=0.2.139` (optional, `[sdk]` extra)
- `litellm>=1.96.2` (optional, `[ollama]` extra — pinned floor to avoid a
  supply-chain attack affecting 1.82.7–1.82.8)

## License

MIT
