Coverage for src / lilbee / providers / model_ref.py: 100%
50 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-04-29 19:16 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-04-29 19:16 +0000
1"""Model reference parsing and option translation.
3Single source of truth for classifying model strings and translating
4generation options per provider type. This module must NOT import from
5lilbee.config or lilbee.models to avoid circular imports.
6"""
8from __future__ import annotations
10from dataclasses import dataclass
11from typing import Any
13from lilbee.providers.base import filter_options
15_API_PROVIDERS = {"openai", "anthropic", "gemini"}
17# All provider prefixes that route a ref away from the local registry.
18# Includes API providers and ollama (which keeps its own name:tag shape).
19PROVIDER_PREFIXES: frozenset[str] = frozenset(_API_PROVIDERS | {"ollama"})
21OLLAMA_PREFIX = "ollama/"
24@dataclass(frozen=True)
25class ProviderModelRef:
26 """Parsed model reference with provider routing information."""
28 raw: str
29 provider: str # "local", "ollama", "openai", "anthropic", "gemini"
30 name: str # provider-specific name with tag normalization applied
32 @property
33 def is_api(self) -> bool:
34 return self.provider in _API_PROVIDERS
36 @property
37 def is_local(self) -> bool:
38 return self.provider == "local"
40 @property
41 def is_remote(self) -> bool:
42 """True if this model must route through a remote SDK (API or Ollama).
44 Remote means "not a locally-loaded GGUF". Both Ollama (HTTP
45 localhost server) and hosted API providers share the same
46 dispatch path; they go through whichever SDK backend is wired
47 up.
48 """
49 return self.provider != "local"
51 def for_openai_prefix(self) -> str:
52 """Name formatted with canonical ``provider/model`` prefix.
54 The prefix convention is the same one used by OpenAI-compatible
55 SDKs: ``openai/gpt-4o``, ``ollama/llama3.2:1b``, etc. Every
56 dispatching SDK accepts this shape.
57 """
58 if self.provider == "ollama":
59 return f"{OLLAMA_PREFIX}{self.name}"
60 if self.is_api:
61 return f"{self.provider}/{self.name}"
62 return self.name
64 def for_display(self) -> str:
65 """Human-readable name for UI."""
66 return self.raw
68 @property
69 def needs_api_base(self) -> bool:
70 """True if the SDK needs an explicit api_base (Ollama/local)."""
71 return not self.is_api
74def parse_model_ref(raw: str) -> ProviderModelRef:
75 """Classify a model string by its prefix and return the routing ref.
77 Native HuggingFace refs are ``<org>/<repo>/<file>.gguf``. Remote
78 providers use prefixes: ``openai/``, ``anthropic/``, ``gemini/``,
79 ``ollama/``.
80 """
81 if "/" not in raw:
82 raise ValueError(
83 f"Model ref {raw!r} must be a HuggingFace ref "
84 "('<org>/<repo>/<filename>.gguf') or carry a provider prefix "
85 "('ollama/', 'openai/', 'anthropic/', 'gemini/')."
86 )
87 prefix, rest = raw.split("/", 1)
88 if prefix in _API_PROVIDERS:
89 return ProviderModelRef(raw=raw, provider=prefix, name=rest)
90 if prefix == "ollama":
91 name = rest if ":" in rest else f"{rest}:latest"
92 return ProviderModelRef(raw=raw, provider="ollama", name=name)
93 return ProviderModelRef(raw=raw, provider="local", name=raw)
96def translate_options(options: dict[str, Any], ref: ProviderModelRef) -> dict[str, Any]:
97 """Translate generation options for the target provider."""
98 filtered = filter_options(options)
99 if ref.is_api:
100 # API providers use max_tokens, not num_predict
101 if "num_predict" in filtered:
102 filtered["max_tokens"] = filtered.pop("num_predict")
103 # num_ctx is a model-load param, not per-call
104 filtered.pop("num_ctx", None)
105 # top_k not supported by most API providers
106 filtered.pop("top_k", None)
107 return filtered