---
tags: [di, protocols, providers, llm, embeddings, structural-subtyping]
modules: [src/arandu/providers/, src/arandu/]
applies_to: [providers, services, clients]
confidence: inferred
---
# Pattern: Dependency Injection via Protocols

<!-- vibeflow:auto:start -->
## What
LLM and embedding backends are injected via `typing.Protocol` interfaces. No concrete provider is hard-coded anywhere in the core SDK. Consumers implement the protocols or use the shipped OpenAI provider.

## Where
- `src/arandu/protocols.py` — Protocol definitions
- `src/arandu/providers/openai.py` — Concrete OpenAI implementation
- `src/arandu/client.py` — MemoryClient receives providers via constructor
- All pipeline/background modules accept `llm: LLMProvider` and `embeddings: EmbeddingProvider` params

## The Pattern
1. Define `@runtime_checkable` `Protocol` classes with async methods.
2. Concrete providers implement protocols via structural subtyping (no inheritance).
3. `MemoryClient.__init__` accepts `llm: LLMProvider` and `embeddings: EmbeddingProvider`.
4. All internal functions receive providers as explicit parameters — never via globals or singletons.
5. Optional dependencies are imported lazily inside the provider class (e.g., `from openai import AsyncOpenAI` inside `__init__`).

## Rules
- Never import a concrete provider inside core SDK modules.
- Protocol methods are async.
- `LLMProvider.complete()` returns `str` (raw response text).
- `EmbeddingProvider.embed()` returns `list[list[float]]`, `embed_one()` returns `list[float] | None`.
- Providers can implement both protocols in a single class (composition, not multiple inheritance).

## Examples from this codebase
File: src/arandu/protocols.py
```python
@runtime_checkable
class LLMProvider(Protocol):
    async def complete(
        self,
        messages: list[dict],
        temperature: float = 0,
        response_format: dict | None = None,
        max_tokens: int | None = None,
    ) -> str: ...

@runtime_checkable
class EmbeddingProvider(Protocol):
    async def embed(self, texts: list[str]) -> list[list[float]]: ...
    async def embed_one(self, text: str) -> list[float] | None: ...
```

File: src/arandu/providers/openai.py
```python
class OpenAIProvider:
    """Implements both protocols with a single class via composition."""
    def __init__(self, api_key: str, model: str = "gpt-4o", ...):
        try:
            from openai import AsyncOpenAI  # lazy import
        except ImportError as err:
            raise ImportError("openai package is required...") from err
        self._client = AsyncOpenAI(api_key=api_key)

    async def complete(self, messages, temperature=0, ...) -> str: ...
    async def embed(self, texts: list[str]) -> list[list[float]]: ...
    async def embed_one(self, text: str) -> list[float] | None: ...
```

File: src/arandu/client.py
```python
class MemoryClient:
    def __init__(self, database_url: str, llm: LLMProvider,
                 embeddings: EmbeddingProvider, config: MemoryConfig | None = None):
        self._llm = llm
        self._embeddings = embeddings
```
<!-- vibeflow:auto:end -->
