llm-seam 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.
llm_seam-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 m0j0d
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.
@@ -0,0 +1,146 @@
1
+ Metadata-Version: 2.4
2
+ Name: llm-seam
3
+ Version: 0.1.0
4
+ Summary: Unified LLM abstraction with provider pattern for portfolio projects
5
+ Author-email: Mike Donnelly <82827803+m0j0d@users.noreply.github.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/m0j0d/libs/tree/main/llm-seam
8
+ Project-URL: Repository, https://github.com/m0j0d/libs
9
+ Project-URL: Issues, https://github.com/m0j0d/libs/issues
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
+ Classifier: Development Status :: 5 - Production/Stable
17
+ Requires-Python: >=3.11
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: anthropic>=0.122.0
21
+ Requires-Dist: openai>=3.1.0
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest>=9.1.1; extra == "dev"
24
+ Requires-Dist: pytest-asyncio>=1.4.0; extra == "dev"
25
+ Provides-Extra: sdk
26
+ Requires-Dist: claude-agent-sdk>=0.2.139; extra == "sdk"
27
+ Provides-Extra: ollama
28
+ Requires-Dist: litellm>=1.96.2; extra == "ollama"
29
+ Dynamic: license-file
30
+
31
+ # llm-seam
32
+
33
+ Unified LLM abstraction with a provider pattern — one interface, five backends,
34
+ automatic fallback and retry.
35
+
36
+ **Note on the name:** this package was `llm-base` until 2026-08-21. It was
37
+ renamed to `llm-seam` because the PyPI name `llm-base` belongs to an
38
+ unrelated, older project — no relation to this package.
39
+
40
+ ## What's in the box
41
+
42
+ | Module | What it gives you |
43
+ |---|---|
44
+ | `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 |
45
+ | `llm_seam.providers.base` | `LLMProvider` — the `Protocol` every provider implements (`complete`, `get_name`, `supports_streaming`) |
46
+ | `llm_seam.providers.anthropic` | `AnthropicProvider` — wraps the `anthropic` SDK |
47
+ | `llm_seam.providers.openai` | `OpenAIProvider` — wraps the `openai` SDK |
48
+ | `llm_seam.providers.claude_cli` | `ClaudeCLIProvider` — shells out to the Claude Code CLI |
49
+ | `llm_seam.providers.agent_sdk` | `AgentSDKProvider` — wraps `claude-agent-sdk` (requires the `[sdk]` extra); supports an optional `can_use_tool` security callback (see below) |
50
+ | `llm_seam.providers.ollama` | `OllamaProvider` — local models via `litellm` (requires the `[ollama]` extra) |
51
+ | `llm_seam.providers.mock` | `MockProvider` — fixed-response provider for tests and offline use |
52
+ | `llm_seam.fallback` | `FallbackChain` — tries a list of providers in order, raises `AllProvidersFailed` only if every one of them fails |
53
+ | `llm_seam.retry` | `with_retry` — decorator with exponential backoff, retries on `RateLimitError` by default (configurable) |
54
+ | `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 |
55
+ | `llm_seam.exceptions` | `LLMError` (base), `RateLimitError`, `TokenLimitError`, `TimeoutError`, `AllProvidersFailed` |
56
+
57
+ ## Design
58
+
59
+ **One interface, provider-agnostic callers.** `LLMClient` and every provider
60
+ implement the same three-method `LLMProvider` protocol (`complete`,
61
+ `get_name`, `supports_streaming`), so a caller can swap Anthropic for a local
62
+ Ollama model, the Claude CLI, or a mock in tests without touching call sites.
63
+
64
+ **Auto-selection is a convenience, not a requirement.** `LLMClient()` with no
65
+ arguments picks Anthropic if `ANTHROPIC_API_KEY` is set, else OpenAI if
66
+ `OPENAI_API_KEY` is set, else falls back to `MockProvider`. Pass an explicit
67
+ `provider=` name or instance to bypass auto-selection entirely.
68
+
69
+ **Provider-name convention.** When constructing by name string, this
70
+ portfolio distinguishes two Claude transports: `"claude-sdk"` selects
71
+ `AgentSDKProvider` (the `claude-agent-sdk` package, preferred for new
72
+ scheduled work — see `libs/CLAUDE.md`), and `"claude-cli"` selects
73
+ `ClaudeCLIProvider` (shells out to the installed Claude Code CLI). `"claude-sdk"`
74
+ is only registered as a valid name if `claude-agent-sdk` is installed (the
75
+ `[sdk]` extra); otherwise constructing by that name raises `ValueError`.
76
+
77
+ **`AgentSDKProvider`'s `can_use_tool` is a security boundary, not a hygiene
78
+ knob.** It threads straight to `ClaudeAgentOptions.can_use_tool`. If a caller
79
+ passes a callback and the installed `claude-agent-sdk` can't accept the
80
+ field, the call raises `LLMError` rather than silently proceeding ungated —
81
+ a caller that believes tool calls are being permission-checked must never
82
+ find out otherwise the hard way. Use
83
+ `tool_guards.path_scoped_tool_guard(allowed_dirs)` rather than hand-rolling
84
+ path checks.
85
+
86
+ **Fallback and retry compose, they don't replace each other.** `FallbackChain`
87
+ moves between *providers*; `with_retry` retries a single call on a
88
+ *transient* error (rate limits by default). Wrap a `FallbackChain`'s
89
+ `.complete` in `with_retry`, or use either alone.
90
+
91
+ ## Install
92
+
93
+ ```bash
94
+ pip install llm-seam
95
+ ```
96
+
97
+ Optional extras:
98
+
99
+ ```bash
100
+ pip install "llm-seam[sdk]" # AgentSDKProvider (claude-agent-sdk)
101
+ pip install "llm-seam[ollama]" # OllamaProvider (litellm)
102
+ ```
103
+
104
+ ## Usage
105
+
106
+ ```python
107
+ from llm_seam import LLMClient, FallbackChain, with_retry
108
+
109
+ # Auto-select a provider from the environment
110
+ client = LLMClient()
111
+ response = client.complete("Hello, world!")
112
+
113
+ # Explicit provider by name
114
+ client = LLMClient(provider="anthropic")
115
+
116
+ # Fall back through providers in order
117
+ chain = FallbackChain(providers=[
118
+ LLMClient(provider="claude-cli").provider,
119
+ LLMClient(provider="anthropic").provider,
120
+ ])
121
+ response = chain.complete("Hello")
122
+
123
+ # Retry a call with exponential backoff on rate limits
124
+ @with_retry(max_retries=3, initial_delay=1.0)
125
+ def call():
126
+ return client.complete("Hello")
127
+ ```
128
+
129
+ ## Testing
130
+
131
+ ```bash
132
+ cd llm-seam
133
+ pytest tests/
134
+ ```
135
+
136
+ ## Dependencies
137
+
138
+ - `anthropic>=0.122.0`
139
+ - `openai>=3.1.0`
140
+ - `claude-agent-sdk>=0.2.139` (optional, `[sdk]` extra)
141
+ - `litellm>=1.96.2` (optional, `[ollama]` extra — pinned floor to avoid a
142
+ supply-chain attack affecting 1.82.7–1.82.8)
143
+
144
+ ## License
145
+
146
+ MIT
@@ -0,0 +1,116 @@
1
+ # llm-seam
2
+
3
+ Unified LLM abstraction with a provider pattern — one interface, five backends,
4
+ automatic fallback and retry.
5
+
6
+ **Note on the name:** this package was `llm-base` until 2026-08-21. It was
7
+ renamed to `llm-seam` because the PyPI name `llm-base` belongs to an
8
+ unrelated, older project — no relation to this package.
9
+
10
+ ## What's in the box
11
+
12
+ | Module | What it gives you |
13
+ |---|---|
14
+ | `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 |
15
+ | `llm_seam.providers.base` | `LLMProvider` — the `Protocol` every provider implements (`complete`, `get_name`, `supports_streaming`) |
16
+ | `llm_seam.providers.anthropic` | `AnthropicProvider` — wraps the `anthropic` SDK |
17
+ | `llm_seam.providers.openai` | `OpenAIProvider` — wraps the `openai` SDK |
18
+ | `llm_seam.providers.claude_cli` | `ClaudeCLIProvider` — shells out to the Claude Code CLI |
19
+ | `llm_seam.providers.agent_sdk` | `AgentSDKProvider` — wraps `claude-agent-sdk` (requires the `[sdk]` extra); supports an optional `can_use_tool` security callback (see below) |
20
+ | `llm_seam.providers.ollama` | `OllamaProvider` — local models via `litellm` (requires the `[ollama]` extra) |
21
+ | `llm_seam.providers.mock` | `MockProvider` — fixed-response provider for tests and offline use |
22
+ | `llm_seam.fallback` | `FallbackChain` — tries a list of providers in order, raises `AllProvidersFailed` only if every one of them fails |
23
+ | `llm_seam.retry` | `with_retry` — decorator with exponential backoff, retries on `RateLimitError` by default (configurable) |
24
+ | `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 |
25
+ | `llm_seam.exceptions` | `LLMError` (base), `RateLimitError`, `TokenLimitError`, `TimeoutError`, `AllProvidersFailed` |
26
+
27
+ ## Design
28
+
29
+ **One interface, provider-agnostic callers.** `LLMClient` and every provider
30
+ implement the same three-method `LLMProvider` protocol (`complete`,
31
+ `get_name`, `supports_streaming`), so a caller can swap Anthropic for a local
32
+ Ollama model, the Claude CLI, or a mock in tests without touching call sites.
33
+
34
+ **Auto-selection is a convenience, not a requirement.** `LLMClient()` with no
35
+ arguments picks Anthropic if `ANTHROPIC_API_KEY` is set, else OpenAI if
36
+ `OPENAI_API_KEY` is set, else falls back to `MockProvider`. Pass an explicit
37
+ `provider=` name or instance to bypass auto-selection entirely.
38
+
39
+ **Provider-name convention.** When constructing by name string, this
40
+ portfolio distinguishes two Claude transports: `"claude-sdk"` selects
41
+ `AgentSDKProvider` (the `claude-agent-sdk` package, preferred for new
42
+ scheduled work — see `libs/CLAUDE.md`), and `"claude-cli"` selects
43
+ `ClaudeCLIProvider` (shells out to the installed Claude Code CLI). `"claude-sdk"`
44
+ is only registered as a valid name if `claude-agent-sdk` is installed (the
45
+ `[sdk]` extra); otherwise constructing by that name raises `ValueError`.
46
+
47
+ **`AgentSDKProvider`'s `can_use_tool` is a security boundary, not a hygiene
48
+ knob.** It threads straight to `ClaudeAgentOptions.can_use_tool`. If a caller
49
+ passes a callback and the installed `claude-agent-sdk` can't accept the
50
+ field, the call raises `LLMError` rather than silently proceeding ungated —
51
+ a caller that believes tool calls are being permission-checked must never
52
+ find out otherwise the hard way. Use
53
+ `tool_guards.path_scoped_tool_guard(allowed_dirs)` rather than hand-rolling
54
+ path checks.
55
+
56
+ **Fallback and retry compose, they don't replace each other.** `FallbackChain`
57
+ moves between *providers*; `with_retry` retries a single call on a
58
+ *transient* error (rate limits by default). Wrap a `FallbackChain`'s
59
+ `.complete` in `with_retry`, or use either alone.
60
+
61
+ ## Install
62
+
63
+ ```bash
64
+ pip install llm-seam
65
+ ```
66
+
67
+ Optional extras:
68
+
69
+ ```bash
70
+ pip install "llm-seam[sdk]" # AgentSDKProvider (claude-agent-sdk)
71
+ pip install "llm-seam[ollama]" # OllamaProvider (litellm)
72
+ ```
73
+
74
+ ## Usage
75
+
76
+ ```python
77
+ from llm_seam import LLMClient, FallbackChain, with_retry
78
+
79
+ # Auto-select a provider from the environment
80
+ client = LLMClient()
81
+ response = client.complete("Hello, world!")
82
+
83
+ # Explicit provider by name
84
+ client = LLMClient(provider="anthropic")
85
+
86
+ # Fall back through providers in order
87
+ chain = FallbackChain(providers=[
88
+ LLMClient(provider="claude-cli").provider,
89
+ LLMClient(provider="anthropic").provider,
90
+ ])
91
+ response = chain.complete("Hello")
92
+
93
+ # Retry a call with exponential backoff on rate limits
94
+ @with_retry(max_retries=3, initial_delay=1.0)
95
+ def call():
96
+ return client.complete("Hello")
97
+ ```
98
+
99
+ ## Testing
100
+
101
+ ```bash
102
+ cd llm-seam
103
+ pytest tests/
104
+ ```
105
+
106
+ ## Dependencies
107
+
108
+ - `anthropic>=0.122.0`
109
+ - `openai>=3.1.0`
110
+ - `claude-agent-sdk>=0.2.139` (optional, `[sdk]` extra)
111
+ - `litellm>=1.96.2` (optional, `[ollama]` extra — pinned floor to avoid a
112
+ supply-chain attack affecting 1.82.7–1.82.8)
113
+
114
+ ## License
115
+
116
+ MIT
@@ -0,0 +1,37 @@
1
+ # llm-seam: Unified LLM abstraction with provider pattern
2
+
3
+ from .client import LLMClient
4
+ from .exceptions import (
5
+ LLMError,
6
+ RateLimitError,
7
+ TokenLimitError,
8
+ TimeoutError,
9
+ AllProvidersFailed,
10
+ )
11
+ from .providers.base import LLMProvider
12
+ from .providers.mock import MockProvider
13
+ from .providers.anthropic import AnthropicProvider
14
+ from .providers.openai import OpenAIProvider
15
+ from .providers.claude_cli import ClaudeCLIProvider
16
+ from .providers.ollama import OllamaProvider
17
+ from .fallback import FallbackChain
18
+ from .retry import with_retry
19
+ from .tool_guards import path_scoped_tool_guard
20
+
21
+ __all__ = [
22
+ "LLMClient",
23
+ "LLMProvider",
24
+ "MockProvider",
25
+ "AnthropicProvider",
26
+ "OpenAIProvider",
27
+ "ClaudeCLIProvider",
28
+ "OllamaProvider",
29
+ "FallbackChain",
30
+ "with_retry",
31
+ "path_scoped_tool_guard",
32
+ "LLMError",
33
+ "RateLimitError",
34
+ "TokenLimitError",
35
+ "TimeoutError",
36
+ "AllProvidersFailed",
37
+ ]
@@ -0,0 +1,126 @@
1
+ """Main LLMClient entry point with auto-provider selection."""
2
+
3
+ import os
4
+ from typing import Any
5
+ from llm_seam.providers.base import LLMProvider
6
+ from llm_seam.providers.mock import MockProvider
7
+ from llm_seam.providers.anthropic import AnthropicProvider
8
+ from llm_seam.providers.openai import OpenAIProvider
9
+ from llm_seam.providers.claude_cli import ClaudeCLIProvider
10
+ from llm_seam.providers.ollama import OllamaProvider
11
+
12
+ try:
13
+ from llm_seam.providers.agent_sdk import AgentSDKProvider
14
+ HAS_AGENT_SDK = True
15
+ except RuntimeError:
16
+ HAS_AGENT_SDK = False
17
+
18
+
19
+ class LLMClient:
20
+ """Main LLM client with automatic provider selection.
21
+
22
+ Auto-selection priority:
23
+ 1. Anthropic (if ANTHROPIC_API_KEY is set)
24
+ 2. OpenAI (if OPENAI_API_KEY is set)
25
+ 3. MockProvider (fallback when no API keys available)
26
+
27
+ Args:
28
+ provider: Provider instance or name string ('anthropic', 'openai', 'claude-cli', 'ollama', 'mock')
29
+ If None, auto-selects based on available API keys
30
+
31
+ Example:
32
+ # Auto-select based on environment
33
+ client = LLMClient()
34
+
35
+ # Explicit provider by name
36
+ client = LLMClient(provider="anthropic")
37
+
38
+ # Explicit provider instance
39
+ client = LLMClient(provider=AnthropicProvider(api_key="..."))
40
+ """
41
+
42
+ def __init__(self, provider: LLMProvider | str | None = None):
43
+ if provider is None:
44
+ self.provider = self._auto_select_provider()
45
+ elif isinstance(provider, str):
46
+ self.provider = self._create_provider_from_name(provider)
47
+ else:
48
+ self.provider = provider
49
+
50
+ def _auto_select_provider(self) -> LLMProvider:
51
+ """Auto-select provider based on available API keys."""
52
+ # Check Anthropic first
53
+ if os.getenv("ANTHROPIC_API_KEY"):
54
+ try:
55
+ return AnthropicProvider()
56
+ except Exception:
57
+ pass # Fall through to next option
58
+
59
+ # Check OpenAI
60
+ if os.getenv("OPENAI_API_KEY"):
61
+ try:
62
+ return OpenAIProvider()
63
+ except Exception:
64
+ pass # Fall through to next option
65
+
66
+ # Default to mock provider
67
+ return MockProvider(response="Mock LLM response (no API keys configured)")
68
+
69
+ def _create_provider_from_name(self, name: str) -> LLMProvider:
70
+ """Create provider instance from name string."""
71
+ providers: dict[str, type] = {
72
+ "anthropic": AnthropicProvider,
73
+ "openai": OpenAIProvider,
74
+ "claude-cli": ClaudeCLIProvider,
75
+ "ollama": OllamaProvider,
76
+ "mock": MockProvider,
77
+ }
78
+
79
+ if HAS_AGENT_SDK:
80
+ providers["claude-sdk"] = AgentSDKProvider
81
+
82
+ if name not in providers:
83
+ raise ValueError(
84
+ f"Unknown provider: {name}. "
85
+ f"Valid options: {', '.join(providers.keys())}"
86
+ )
87
+
88
+ provider_class = providers[name]
89
+
90
+ # MockProvider doesn't need any args
91
+ if name == "mock":
92
+ return provider_class()
93
+
94
+ # Other providers will use env vars or fail if not available
95
+ return provider_class()
96
+
97
+ def complete(self, prompt: str, **kwargs: Any) -> str:
98
+ """Generate completion using the configured provider.
99
+
100
+ Args:
101
+ prompt: The input prompt
102
+ **kwargs: Provider-specific options
103
+
104
+ Returns:
105
+ The completion text
106
+ """
107
+ return self.provider.complete(prompt, **kwargs)
108
+
109
+ def get_provider_name(self) -> str:
110
+ """Return the name of the current provider."""
111
+ return self.provider.get_name()
112
+
113
+ def supports_streaming(self) -> bool:
114
+ """Return whether the current provider supports streaming."""
115
+ return self.provider.supports_streaming()
116
+
117
+ def set_provider(self, provider: LLMProvider | str) -> None:
118
+ """Switch to a different provider.
119
+
120
+ Args:
121
+ provider: Provider instance or name string
122
+ """
123
+ if isinstance(provider, str):
124
+ self.provider = self._create_provider_from_name(provider)
125
+ else:
126
+ self.provider = provider
@@ -0,0 +1,33 @@
1
+ """Custom exceptions for llm-seam."""
2
+
3
+
4
+ class LLMError(Exception):
5
+ """Base exception for all LLM errors."""
6
+ pass
7
+
8
+
9
+ class RateLimitError(LLMError):
10
+ """Raised when API rate limit is exceeded."""
11
+
12
+ def __init__(self, message: str = "Rate limit exceeded", retry_after: float | None = None):
13
+ super().__init__(message)
14
+ self.retry_after = retry_after
15
+
16
+
17
+ class TokenLimitError(LLMError):
18
+ """Raised when token limit is exceeded."""
19
+ pass
20
+
21
+
22
+ class TimeoutError(LLMError):
23
+ """Raised when request times out."""
24
+ pass
25
+
26
+
27
+ class AllProvidersFailed(LLMError):
28
+ """Raised when all providers in a fallback chain fail."""
29
+
30
+ def __init__(self, errors: list[Exception] | None = None):
31
+ self.errors = errors or []
32
+ message = f"All providers failed: {[str(e) for e in self.errors]}"
33
+ super().__init__(message)
@@ -0,0 +1,70 @@
1
+ """Fallback chain for trying multiple LLM providers in order.
2
+
3
+ Based on pepai's llm_gateway.py local-first fallback pattern.
4
+ """
5
+
6
+ from typing import Any
7
+ from llm_seam.providers.base import LLMProvider
8
+ from llm_seam.exceptions import AllProvidersFailed
9
+
10
+
11
+ class FallbackChain:
12
+ """Chain of LLM providers tried in order until one succeeds.
13
+
14
+ Args:
15
+ providers: List of LLMProvider instances to try in order
16
+
17
+ Example:
18
+ # Try local Ollama first, fallback to Anthropic
19
+ chain = FallbackChain(providers=[
20
+ OllamaProvider(),
21
+ AnthropicProvider()
22
+ ])
23
+ response = chain.complete("Hello")
24
+ """
25
+
26
+ def __init__(self, providers: list[LLMProvider]):
27
+ if not providers:
28
+ raise ValueError("At least one provider must be specified")
29
+
30
+ self.providers = providers
31
+ self.last_provider_used: LLMProvider | None = None
32
+
33
+ def complete(self, prompt: str, **kwargs: Any) -> str:
34
+ """Try each provider in order until one succeeds.
35
+
36
+ Args:
37
+ prompt: The input prompt
38
+ **kwargs: Additional parameters passed to each provider
39
+
40
+ Returns:
41
+ The completion text from the first successful provider
42
+
43
+ Raises:
44
+ AllProvidersFailed: When all providers fail
45
+ """
46
+ errors: list[Exception] = []
47
+
48
+ for provider in self.providers:
49
+ try:
50
+ response = provider.complete(prompt, **kwargs)
51
+ self.last_provider_used = provider
52
+ return response
53
+
54
+ except Exception as e:
55
+ errors.append(e)
56
+ # Continue to next provider
57
+ continue
58
+
59
+ # All providers failed
60
+ self.last_provider_used = None
61
+ raise AllProvidersFailed(errors=errors)
62
+
63
+ def get_name(self) -> str:
64
+ """Return fallback chain identifier."""
65
+ provider_names = [p.get_name() for p in self.providers]
66
+ return f"fallback({', '.join(provider_names)})"
67
+
68
+ def supports_streaming(self) -> bool:
69
+ """Return whether streaming is supported (always False for chains)."""
70
+ return False
@@ -0,0 +1,24 @@
1
+ """LLM provider implementations."""
2
+
3
+ from .base import LLMProvider
4
+ from .mock import MockProvider
5
+ from .anthropic import AnthropicProvider
6
+ from .openai import OpenAIProvider
7
+ from .claude_cli import ClaudeCLIProvider
8
+ from .ollama import OllamaProvider
9
+
10
+ # Optional — requires pip install llm-seam[sdk]
11
+ try:
12
+ from .agent_sdk import AgentSDKProvider
13
+ except RuntimeError:
14
+ AgentSDKProvider = None # type: ignore[assignment,misc]
15
+
16
+ __all__ = [
17
+ "LLMProvider",
18
+ "MockProvider",
19
+ "AnthropicProvider",
20
+ "OpenAIProvider",
21
+ "ClaudeCLIProvider",
22
+ "OllamaProvider",
23
+ "AgentSDKProvider",
24
+ ]