Source code for rath.llm.provider

"""Sampling / routing options and OpenAI HTTP identity for chat requests."""

from __future__ import annotations

from dataclasses import dataclass, field, replace
from types import MappingProxyType
from typing import TYPE_CHECKING, Any, Callable, Literal, Mapping

if TYPE_CHECKING:
    from rath.config.store import ConfigStore


[docs] @dataclass(frozen=True, kw_only=True, slots=True) class Provider: """LLM routing for ``run_session_loop`` (no ``messages`` / ``tools``). ``base_url``, ``api_key``, and ``model`` configure the HTTP client built from :attr:`provider_kind` (OpenAI-compatible or Anthropic). Other fields mirror :class:`~rath.llm.chat_request.RathLLMChatRequest` (excluding what the loop fills in). ``api_key`` may be omitted when callers supply a custom ``executor`` that never instantiates a default :class:`~rath.llm.RathOpenAIChatClient` or :class:`~rath.llm.RathAnthropicChatClient`. """ base_url: str | None = None api_key: str | None = None model: str | None = None temperature: float | None = None top_p: float | None = None max_completion_tokens: int | None = None max_tokens: int | None = None stop: str | list[str] | None = None n: int | None = None seed: int | None = None frequency_penalty: float | None = None presence_penalty: float | None = None tool_choice: Literal["auto", "none", "required"] | Mapping[str, Any] | None = None parallel_tool_calls: bool | None = None response_format: dict[str, Any] | None = None logit_bias: dict[str, int] | None = None logprobs: bool | None = None top_logprobs: int | None = None reasoning_effort: str | None = None verbosity: str | None = None metadata: dict[str, str] | None = None user: str | None = None store: bool | None = None service_tier: str | None = None extra_create_args: Mapping[str, Any] = field( default_factory=lambda: MappingProxyType({}) ) # Retry policy for transient vendor errors. ``None`` uses built-in defaults # in :mod:`rath.llm.retry`. retry_max_attempts: int | None = None retry_base_seconds: float | None = None # Token budget guardrail. When non-None, the **first** completion in a # ``run_session_loop`` that pushes ``Session.cumulative_usage`` past the # cap invokes ``on_budget_exceeded`` (or emits a single # ``logger.warning`` if no callback is set). The guard is latched per # session: subsequent completions in the same loop do not re-fire it # even if the running total stays above the cap. Callers that want to # abort the loop are expected to raise # :class:`BudgetExceededError` from the callback on that first call. budget_total_tokens: int | None = None on_budget_exceeded: Callable[..., None] | None = None # Which adapter :func:`~rath.llm.registry.chat_client_for` constructs when # no custom executor is passed. ``None`` (default) selects OpenAI-compatible; # ``"anthropic"`` selects :class:`~rath.llm.RathAnthropicChatClient`. provider_kind: Literal["openai", "anthropic"] | None = None def __str__(self) -> str: return self.model if self.model is not None else "(no model)" def __repr__(self) -> str: return self.__str__()
[docs] @classmethod def from_config( cls, name: str | None = None, *, store: "ConfigStore | None" = None, **overrides: Any, ) -> "Provider": """Build a :class:`Provider` from ``~/.openrath/config.json``. Looks up ``name`` (or ``llm.default_provider`` when ``name=None``) under ``llm.providers``, then constructs a :class:`Provider` whose fields come from the entry. Any explicit ``overrides`` win — pass e.g. ``Provider.from_config("openai-main", api_key="ad-hoc")`` to rotate one field without touching the on-disk file. Lazy-imports :mod:`rath.config` so that ``import rath.llm`` never touches the filesystem. Raises :class:`KeyError` when the named provider is missing; the message lists what is available. """ from rath.config.store import ConfigStore # local import — see docstring s = store or ConfigStore.load() entry = s.get_llm_provider(name) base = cls( provider_kind=entry.provider_kind, model=entry.model, api_key=entry.api_key, base_url=entry.base_url, temperature=entry.temperature, max_tokens=entry.max_tokens, ) if not overrides: return base return replace(base, **overrides)
__all__ = ["Provider"]