fix(admin): LLM provider/model switching now reads DB overrides correctly

- get_llm_provider() now accepts provider + model args so DB overrides
  propagate through to all provider constructors (was always reading
  env vars, ignoring the admin UI selection)
- /test-llm replaced with lightweight ping (max_tokens=20) instead of
  running a full bill analysis; shows model name + reply, no truncation
- /api/settings/llm-models endpoint fetches available models live from
  each provider's API (OpenAI, Anthropic REST, Gemini, Ollama)
- Admin UI model picker dynamically populated from provider API;
  falls back to manual text input on error; Custom model name option kept
- Default Gemini model updated: gemini-1.5-pro → gemini-2.0-flash

Co-Authored-By: Jack Levy
This commit is contained in:
Jack Levy
2026-03-01 04:03:51 -05:00
parent 12a3eac48f
commit defc2c116d
6 changed files with 466 additions and 50 deletions

View File

@@ -185,10 +185,10 @@ class LLMProvider(ABC):
class OpenAIProvider(LLMProvider):
def __init__(self):
def __init__(self, model: str | None = None):
from openai import OpenAI
self.client = OpenAI(api_key=settings.OPENAI_API_KEY)
self.model = settings.OPENAI_MODEL
self.model = model or settings.OPENAI_MODEL
def generate_brief(self, doc_text: str, bill_metadata: dict) -> ReverseBrief:
prompt = build_prompt(doc_text, bill_metadata, MAX_TOKENS_DEFAULT)
@@ -220,10 +220,10 @@ class OpenAIProvider(LLMProvider):
class AnthropicProvider(LLMProvider):
def __init__(self):
def __init__(self, model: str | None = None):
import anthropic
self.client = anthropic.Anthropic(api_key=settings.ANTHROPIC_API_KEY)
self.model = settings.ANTHROPIC_MODEL
self.model = model or settings.ANTHROPIC_MODEL
def generate_brief(self, doc_text: str, bill_metadata: dict) -> ReverseBrief:
prompt = build_prompt(doc_text, bill_metadata, MAX_TOKENS_DEFAULT)
@@ -249,11 +249,11 @@ class AnthropicProvider(LLMProvider):
class GeminiProvider(LLMProvider):
def __init__(self):
def __init__(self, model: str | None = None):
import google.generativeai as genai
genai.configure(api_key=settings.GEMINI_API_KEY)
self._genai = genai
self.model_name = settings.GEMINI_MODEL
self.model_name = model or settings.GEMINI_MODEL
def _make_model(self, system_prompt: str):
return self._genai.GenerativeModel(
@@ -274,9 +274,9 @@ class GeminiProvider(LLMProvider):
class OllamaProvider(LLMProvider):
def __init__(self):
def __init__(self, model: str | None = None):
self.base_url = settings.OLLAMA_BASE_URL.rstrip("/")
self.model = settings.OLLAMA_MODEL
self.model = model or settings.OLLAMA_MODEL
def _generate(self, system_prompt: str, user_prompt: str) -> str:
import requests as req
@@ -327,15 +327,20 @@ class OllamaProvider(LLMProvider):
return parse_brief_json(raw2, "ollama", self.model)
def get_llm_provider() -> LLMProvider:
"""Factory — returns the configured LLM provider."""
provider = settings.LLM_PROVIDER.lower()
def get_llm_provider(provider: str | None = None, model: str | None = None) -> LLMProvider:
"""Factory — returns the configured LLM provider.
Pass ``provider`` and/or ``model`` explicitly (e.g. from DB overrides) to bypass env defaults.
"""
if provider is None:
provider = settings.LLM_PROVIDER
provider = provider.lower()
if provider == "openai":
return OpenAIProvider()
return OpenAIProvider(model=model)
elif provider == "anthropic":
return AnthropicProvider()
return AnthropicProvider(model=model)
elif provider == "gemini":
return GeminiProvider()
return GeminiProvider(model=model)
elif provider == "ollama":
return OllamaProvider()
return OllamaProvider(model=model)
raise ValueError(f"Unknown LLM_PROVIDER: '{provider}'. Must be one of: openai, anthropic, gemini, ollama")