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

1"""Model reference parsing and option translation. 

2 

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""" 

7 

8from __future__ import annotations 

9 

10from dataclasses import dataclass 

11from typing import Any 

12 

13from lilbee.providers.base import filter_options 

14 

15_API_PROVIDERS = {"openai", "anthropic", "gemini"} 

16 

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"}) 

20 

21OLLAMA_PREFIX = "ollama/" 

22 

23 

24@dataclass(frozen=True) 

25class ProviderModelRef: 

26 """Parsed model reference with provider routing information.""" 

27 

28 raw: str 

29 provider: str # "local", "ollama", "openai", "anthropic", "gemini" 

30 name: str # provider-specific name with tag normalization applied 

31 

32 @property 

33 def is_api(self) -> bool: 

34 return self.provider in _API_PROVIDERS 

35 

36 @property 

37 def is_local(self) -> bool: 

38 return self.provider == "local" 

39 

40 @property 

41 def is_remote(self) -> bool: 

42 """True if this model must route through a remote SDK (API or Ollama). 

43 

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" 

50 

51 def for_openai_prefix(self) -> str: 

52 """Name formatted with canonical ``provider/model`` prefix. 

53 

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 

63 

64 def for_display(self) -> str: 

65 """Human-readable name for UI.""" 

66 return self.raw 

67 

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 

72 

73 

74def parse_model_ref(raw: str) -> ProviderModelRef: 

75 """Classify a model string by its prefix and return the routing ref. 

76 

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) 

94 

95 

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