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:

  1. An explicit api_key argument overrides everything.
  2. 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.py ensure consistency across LiteLLM, Claude Code, and direct API implementations.
  • Result wrapper – Every complete call returns Result[CompletionResponse, ProviderError], enabling callers to handle success and failure uniformly without try-catch blocks.
  • Provider detection – The _extract_provider utility 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 LLMAdapter protocol in src/ouroboros/providers/base.py defines the complete() coroutine that all adapters must implement.
  • LiteLLMAdapter leverages litellm.acompletion for broad model support and uses the stamina library 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_provider ensures 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →