Source code for jeevesagent.model.litellm

"""LiteLLM-backed model adapter — one adapter, every provider.

`LiteLLM <https://github.com/BerriAI/litellm>`_ normalises 100+
provider APIs to OpenAI's chat-completion shape, including:

* Anthropic (``claude-*``) — though :class:`AnthropicModel` is a
  faster direct path
* OpenAI (``gpt-*``) — same; :class:`OpenAIModel` is the direct path
* Cohere (``command-r``, ``command-r-plus``)
* Mistral (``mistral-large``, ``mistral-small``, ...)
* AWS Bedrock (``bedrock/anthropic.claude-3-...``)
* Google Vertex AI (``vertex_ai/gemini-pro``)
* Together AI (``together_ai/...``)
* Groq, Replicate, Ollama, …

Because LiteLLM produces OpenAI-shaped streaming chunks, this adapter
can subclass :class:`OpenAIModel` and reuse its entire chunk
aggregation / tool-call delta accumulation logic. The only
difference: where :class:`OpenAIModel` calls
``self._client.chat.completions.create``, this one routes through
``litellm.acompletion``.

Usage::

    from jeevesagent import Agent
    from jeevesagent.model.litellm import LiteLLMModel

    agent = Agent(
        "...",
        model=LiteLLMModel("mistral-large", api_key="..."),
    )

The string-based resolver in :mod:`jeevesagent.agent.api` recognises
several common LiteLLM prefixes (``mistral-``, ``command-``,
``bedrock/``, ``vertex_ai/``, ``together_ai/``, ``ollama/``,
``gemini/``) so passing the bare model spec works too.
"""

from __future__ import annotations

from typing import Any

from .openai import OpenAIModel


class _LiteLLMCompletions:
    """OpenAI-shaped ``completions`` surface backed by ``litellm.acompletion``."""

    def __init__(self, defaults: dict[str, Any]) -> None:
        self._defaults = defaults

    async def create(self, **kwargs: Any) -> Any:
        from litellm import acompletion  # type: ignore[import-not-found, import-untyped]

        merged = {**self._defaults, **kwargs}
        return await acompletion(**merged)


class _LiteLLMChat:
    def __init__(self, completions: _LiteLLMCompletions) -> None:
        self.completions = completions


class _LiteLLMClient:
    """A duck-typed ``openai.AsyncOpenAI`` that routes through LiteLLM.

    Exposes ``client.chat.completions.create`` so :class:`OpenAIModel`'s
    inherited :meth:`stream` works without modification.
    """

    def __init__(self, **defaults: Any) -> None:
        self.chat = _LiteLLMChat(_LiteLLMCompletions(defaults))


[docs] class LiteLLMModel(OpenAIModel): """Talks to any LiteLLM-supported provider. Inherits chunk normalisation, tool-call delta aggregation, and message-conversion from :class:`OpenAIModel` because LiteLLM produces OpenAI-shaped outputs. """ def __init__( self, model: str, *, api_key: str | None = None, client: Any | None = None, **litellm_kwargs: Any, ) -> None: if client is None: try: import litellm # type: ignore[import-not-found, import-untyped] # noqa: F401 except ImportError as exc: # pragma: no cover — depends on user env raise ImportError( "LiteLLM is not installed. " "Install with: pip install 'jeevesagent[litellm]'" ) from exc defaults: dict[str, Any] = dict(litellm_kwargs) if api_key is not None: defaults["api_key"] = api_key client = _LiteLLMClient(**defaults) # Skip OpenAIModel's openai-SDK import path by passing the # client we just built. ``api_key`` is included as a kwarg # purely to satisfy OpenAIModel's signature; LiteLLM itself # picks up provider-specific keys from environment variables # or the ``api_key=`` we shoved into the defaults above. super().__init__(model, client=client, api_key=api_key)