How Ouroboros Provider Adapters Interface with LiteLLM, Anthropic, and Claude Code
Ouroboros unifies LiteLLM, Anthropic, and Claude Code behind a single LLMAdapter protocol that standardizes request handling, retry logic, and error normalization across every supported model.
The Q00/ouroboros repository defines a clean abstraction layer that allows developers to switch between language models without changing application code. By implementing the LLMAdapter protocol, Ouroboros Provider Adapters translate unified configuration objects into provider-specific API calls while normalizing responses and failures. This architecture separates model orchestration from provider implementation details.
The LLMAdapter Protocol Foundation
Every adapter conforms to the LLMAdapter protocol defined in src/ouroboros/providers/base.py. This contract requires a single complete(messages, config) coroutine that accepts a list of Message objects and a CompletionConfig, returning a Result[CompletionResponse, ProviderError].
The protocol enforces a unified data model across all providers. Message, CompletionConfig, CompletionResponse, and UsageInfo are defined once in base.py and reused by every adapter. This ensures that tools and agents built on Ouroboros remain agnostic to whether they are calling GPT-4 via OpenRouter or Claude via the Agent SDK.
Error handling is similarly normalized. All adapters convert native exceptions into the ProviderError class from src/ouroboros/core/errors.py, using the _extract_provider helper to label the error source (e.g., "openrouter" or "anthropic").
LiteLLMAdapter: Unified Multi-Provider Gateway
The LiteLLMAdapter in src/ouroboros/providers/litellm_adapter.py acts as a universal gateway, leveraging the litellm library to support hundreds of models through a single interface.
Model Selection and API Resolution
Model selection is driven by the model field of CompletionConfig. Values like openrouter/openai/gpt-4 are passed directly to litellm.acompletion. API key resolution follows a precedence hierarchy:
- An explicit
api_keyargument overrides everything. - If omitted, the adapter inspects environment variables (
OPENROUTER_API_KEY,OPENAI_API_KEY,ANTHROPIC_API_KEY) based on the prefix of the model string.
Request Construction
The _build_completion_kwargs method constructs the payload dictionary with model, messages, temperature, max_tokens, and top_p (omitted for Anthropic models). It optionally injects stop sequences, response_format, and a custom api_base.
Resilience with Stamina
Transient failures are handled automatically via the @stamina.retry decorator. The adapter retries litellm exceptions including RateLimitError, ServiceUnavailableError, Timeout, and APIConnectionError up to max_retries (default 3). All failures are converted to ProviderError before returning to the caller.
ClaudeCodeAdapter: Agent SDK Integration
The ClaudeCodeAdapter in src/ouroboros/providers/claude_code_adapter.py interfaces with the Claude Agent SDK, enabling access to Claude Code's capabilities through the same LLMAdapter surface.
CLI Path and Authentication
Unlike other adapters, this implementation does not require per-provider API keys. It relies on the user's Claude Code Max-Plan subscription. A custom CLI binary can be supplied via the cli_path parameter, which resolves from the OUROBOROS_CLI_PATH environment variable or config.yaml.
Prompt Engineering and Options
The adapter transforms the message list into a single string via _build_prompt. It constructs a ClaudeAgentOptions object containing allowed/disallowed tools, max_turns, permission_mode, cwd, and optional system_prompt. When a JSON schema is requested, it sets the output_format accordingly.
Manual Retry Logic
Rather than using stamina, this adapter implements manual exponential backoff with _MAX_RETRIES = 5. It detects retryable conditions by matching error messages against _RETRYABLE_ERROR_PATTERNS or catching unknown SDK message types. After the generator finishes, the adapter walks through SDK messages (AssistantMessage, ToolUseBlock, ResultMessage) to assemble the final CompletionResponse.
AnthropicAdapter Direct Integration
While not detailed in the primary excerpts, the AnthropicAdapter follows the same architectural pattern. It targets the anthropic/* model namespace, pulling ANTHROPIC_API_KEY from the environment unless overridden. It builds request payloads according to Anthropic's native specification and uses the stamina decorator for retry logic on rate-limit and service-unavailable errors.
Shared Architectural Patterns
All Ouroboros Provider Adapters share three critical design traits:
- Unified data model – Types defined in
base.pyensure consistency across LiteLLM, Claude Code, and direct API implementations. - Result wrapper – Every
completecall returnsResult[CompletionResponse, ProviderError], enabling callers to handle success and failure uniformly without try-catch blocks. - Provider detection – The
_extract_providerutility parses model identifiers to ensure accurate error attribution and logging.
Implementation Examples
The following examples demonstrate instantiating adapters with environment-driven or explicit configuration.
# LiteLLMAdapter with OpenRouter
from ouroboros.providers.litellm_adapter import LiteLLMAdapter
from ouroboros.providers.base import Message, MessageRole, CompletionConfig
adapter = LiteLLMAdapter() # Resolves API key from env vars
cfg = CompletionConfig(
model="openrouter/openai/gpt-4",
temperature=0.5,
max_tokens=512,
)
messages = [
Message(role=MessageRole.USER, content="Explain Ouroboros architecture in 2 sentences.")
]
result = await adapter.complete(messages, cfg)
if result.is_ok:
print(result.value.content)
else:
raise result.error
# ClaudeCodeAdapter with custom CLI path
from ouroboros.providers.claude_code_adapter import ClaudeCodeAdapter
from ouroboros.providers.base import Message, MessageRole, CompletionConfig
adapter = ClaudeCodeAdapter(cli_path="/usr/local/bin/claude")
cfg = CompletionConfig(
model="claude-sonnet-4-6",
temperature=0.0,
max_tokens=256,
)
messages = [
Message(role=MessageRole.USER, content="Generate a Mermaid diagram for a binary tree.")
]
result = await adapter.complete(messages, cfg)
if result.is_ok:
print(result.value.content)
else:
raise result.error
Summary
- The
LLMAdapterprotocol insrc/ouroboros/providers/base.pydefines thecomplete()coroutine that all adapters must implement. - LiteLLMAdapter leverages
litellm.acompletionfor broad model support and uses thestaminalibrary for automatic retries on transient errors. - ClaudeCodeAdapter integrates the Claude Agent SDK, folds message lists into strings via
_build_prompt, and implements custom exponential backoff with_MAX_RETRIES = 5. - All adapters return
Result[CompletionResponse, ProviderError]for uniform error handling across the Ouroboros codebase. - Provider detection via
_extract_providerensures errors are accurately attributed to their source (e.g.,"openrouter"or"anthronic").
Frequently Asked Questions
What is the primary interface for Ouroboros Provider Adapters?
The primary interface is the LLMAdapter protocol defined in src/ouroboros/providers/base.py. It requires a single async method complete(messages, config) that accepts a list of Message objects and a CompletionConfig, returning a Result type containing either a CompletionResponse or a ProviderError.
How does LiteLLMAdapter handle API authentication?
LiteLLMAdapter resolves API keys through a cascading strategy. An explicit api_key argument takes highest precedence. If omitted, the adapter examines environment variables such as OPENROUTER_API_KEY, OPENAI_API_KEY, or ANTHROPIC_API_KEY based on the provider prefix in the model identifier (e.g., openrouter/ or anthropic/).
Why does ClaudeCodeAdapter use manual retry logic instead of stamina?
The ClaudeCodeAdapter integrates with the Claude Agent SDK, which streams messages via a generator pattern rather than simple HTTP requests. This requires custom logic to detect retryable conditions by matching error strings against _RETRYABLE_ERROR_PATTERNS and implementing exponential backoff manually across up to five attempts, rather than using the stamina decorator designed for standard async function calls.
Can I add a custom provider adapter to Ouroboros?
Yes. Create a new class that implements the LLMAdapter protocol from src/ouroboros/providers/base.py. Your implementation must accept List[Message] and CompletionConfig in its complete method and return Result[CompletionResponse, ProviderError]. Follow the patterns in litellm_adapter.py for HTTP-based providers or claude_code_adapter.py for SDK-based integrations to ensure proper error normalization and retry behavior.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →