Source code for ooai_llm.types

"""Typed model-string helpers.

Purpose:
    Provide a reusable Pydantic root model for provider/model strings that can
    be used across chat, embeddings, rerankers, and related model families.

Design:
    - Store the canonical raw model string as a ``RootModel[str]``.
    - Reuse provider normalization and inference logic from
      :mod:`ooai_llm.providers`.
    - Offer ergonomic class methods for parsing, construction, conversion to
      LangChain and LiteLLM naming styles, and provider enrichment.

Examples:
    >>> model = ModelString.parse("gpt-5.4-mini")
    >>> model.provider
    <Provider.OPENAI: 'openai'>
    >>> model.as_litellm()
    'openai/gpt-5.4-mini'
"""

from __future__ import annotations

from typing import Self

from pydantic import ConfigDict, RootModel, computed_field, field_validator

from .providers import (
    Provider,
    get_litellm_provider_prefix,
    infer_provider_from_model_name,
    normalize_provider_name,
    split_model_string,
)


[docs] class ModelString(RootModel[str]): """Root model for provider/model strings. Args: root: Raw model string, optionally provider-prefixed. Examples: >>> ModelString("openai:gpt-5.4-mini").model_name 'gpt-5.4-mini' >>> ModelString.parse("anthropic/claude-sonnet-4").provider <Provider.ANTHROPIC: 'anthropic'> """
[docs] model_config = ConfigDict(frozen=True)
@field_validator("root") @classmethod def _validate_root(cls, value: str) -> str: """Normalize and validate the root string. Args: value: Incoming raw value. Returns: Normalized model string. Raises: TypeError: If the value is not a string. ValueError: If the value is empty or malformed. """ if not isinstance(value, str): raise TypeError("ModelString must wrap a string.") text = value.strip() if not text: raise ValueError("Model string cannot be empty.") if text.endswith(":") or text.endswith("/"): raise ValueError("Model string cannot end with a provider separator.") return text @computed_field # type: ignore[prop-decorator] @property
[docs] def provider(self) -> Provider | None: """Return the canonical provider when present or inferable.""" explicit_provider, model_name = split_model_string(self.root) return explicit_provider or infer_provider_from_model_name(model_name)
@computed_field # type: ignore[prop-decorator] @property
[docs] def provider_prefix(self) -> str | None: """Return the canonical provider prefix string when available.""" provider = self.provider return None if provider is None else str(provider)
@computed_field # type: ignore[prop-decorator] @property
[docs] def litellm_provider_prefix(self) -> str | None: """Return the canonical LiteLLM provider prefix when available.""" return get_litellm_provider_prefix(self.provider)
@computed_field # type: ignore[prop-decorator] @property
[docs] def model_name(self) -> str: """Return the unprefixed model name.""" _, model_name = split_model_string(self.root) return model_name
@computed_field # type: ignore[prop-decorator] @property
[docs] def is_prefixed(self) -> bool: """Whether the model string includes an explicit provider prefix.""" explicit_provider, _ = split_model_string(self.root) return explicit_provider is not None
@computed_field # type: ignore[prop-decorator] @property
[docs] def is_litellm_style(self) -> bool: """Whether the raw model string uses LiteLLM's slash separator.""" return "/" in self.root and ":" not in self.root
@computed_field # type: ignore[prop-decorator] @property
[docs] def is_langchain_style(self) -> bool: """Whether the raw model string uses LangChain's colon separator.""" return ":" in self.root
@computed_field # type: ignore[prop-decorator] @property
[docs] def is_bare(self) -> bool: """Whether the model string omits an explicit provider prefix.""" return not self.is_prefixed
@classmethod
[docs] def parse(cls, value: str | Self) -> Self: """Parse a raw string or return the existing model-string instance. Args: value: Model string or model-string instance. Returns: Parsed model-string instance. """ if isinstance(value, cls): return value return cls(value)
@classmethod
[docs] def from_parts(cls, model_name: str, *, provider: Provider | str | None = None) -> Self: """Build a model string from parts. Args: model_name: Bare model name. provider: Optional provider enum or alias. Returns: Constructed model-string instance. """ normalized_provider = normalize_provider_name(provider) if normalized_provider is None: return cls(model_name) return cls(f"{normalized_provider}:{model_name}")
@classmethod
[docs] def from_litellm(cls, model_name: str) -> Self: """Parse a LiteLLM-style model string. Args: model_name: Provider-prefixed or bare LiteLLM model string. Returns: Canonical model-string instance using LangChain-style provider separators. """ parsed = cls.parse(model_name) return parsed.as_langchain_model_string()
@classmethod
[docs] def infer(cls, model_name: str) -> Self: """Create a provider-prefixed model string when inference succeeds. Args: model_name: Bare or prefixed model string. Returns: Canonicalized model-string instance. """ parsed = cls.parse(model_name) if parsed.is_prefixed: return parsed.as_langchain_model_string() inferred = parsed.provider if inferred is None: return parsed return cls.from_parts(parsed.model_name, provider=inferred)
[docs] def split(self) -> tuple[Provider | None, str]: """Return the provider and model-name components. Returns: Tuple of ``(provider, model_name)``. """ return self.provider, self.model_name
[docs] def with_provider(self, provider: Provider | str) -> Self: """Return a provider-prefixed model string. Args: provider: Provider enum or alias. Returns: New provider-prefixed model-string instance. """ normalized_provider = normalize_provider_name(provider) if normalized_provider is None: raise ValueError("Provider cannot be None.") return self.from_parts(self.model_name, provider=normalized_provider)
[docs] def ensure_provider(self, provider: Provider | str | None = None) -> Self: """Return a provider-prefixed model string when possible. Args: provider: Explicit provider to apply. If omitted, provider inference is attempted. Returns: Provider-prefixed model-string instance when possible, otherwise the original value. """ if provider is not None: return self.with_provider(provider) return self.canonical()
[docs] def without_provider(self) -> str: """Return only the bare model name. Returns: Unprefixed model name. """ return self.model_name
[docs] def canonical(self) -> Self: """Return the canonical model string. Returns: Provider-prefixed model string when the provider can be inferred, otherwise the original string. """ return self.infer(self.root)
[docs] def as_langchain(self) -> str: """Return the string in LangChain-style ``provider:model`` form. Returns: LangChain-style model string when possible. """ return str(self.as_langchain_model_string())
[docs] def as_langchain_model_string(self) -> Self: """Return a LangChain-style typed model string. Returns: Canonical model string using ``provider:model`` when possible. """ if self.provider is None: return self return self.from_parts(self.model_name, provider=self.provider)
[docs] def as_litellm(self) -> str: """Return the string in LiteLLM-style ``provider/model`` form. Returns: LiteLLM-style model string when the provider is known, otherwise the bare model name. """ if self.provider is None: return self.model_name prefix = self.litellm_provider_prefix or str(self.provider) return f"{prefix}/{self.model_name}"
[docs] def __str__(self) -> str: """Return the raw model string.""" return self.root