New Settings → System → LLM Skills area: every LLM-powered capability
(Cinematic & Autofit translation, speech-rate slot fitting, glossary
auto-extract, direction parsing, dictation cleanup) becomes a "skill" the
user can toggle or route to a specific provider (local Ollama/LM Studio vs
a remote key) instead of everything riding the one global active provider.
Backend:
- services/llm_skills.py — skill registry + settings_store persistence
(llm_skill.<id>.enabled / .provider), resolution precedence
override > active > none, resolve_skill_client() (OpenAI-compat client
bound to the effective provider; None when disabled/unconfigured) and
skill_backend() (OffBackend when disabled — the exact no-LLM object every
caller already degrades on).
- All five consumption points wired through the registry; a disabled skill
degrades exactly like "no LLM configured" today (Fast translation
fallback, refinement pass-through, heuristic direction parse, no-llm slot
fit, 503 on glossary auto-extract). No new degradation modes; defaults
(enabled + no override) keep existing setups byte-identical.
- OpenAICompatBackend gains an optional bound provider (None = active, the
historical behavior).
- GET /api/settings/llm-skills + PUT /api/settings/llm-skills/{skill_id}
(404 unknown skill/provider); route snapshot updated.
Frontend:
- LLMSkillsPanel (Sparkles, next to LLM Providers): one row per skill —
i18n name/description, enable toggle, provider Select ("Use active
provider" + configured providers, local ones tagged), ready /
needs-setup badge linking to LLM Providers. All strings via t()
(settings.llmskills_*).
Tests: 30 backend (precedence, per-consumption-point disabled semantics,
endpoint round-trips, validation) + 4 panel render/PUT tests. Docs:
translation-engines.md gains an LLM Skills section.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
75 lines
2.6 KiB
Python
75 lines
2.6 KiB
Python
"""Glossary auto-extract — provider-error scrubbing + no-LLM guidance.
|
|
|
|
The auto-extract endpoint reuses the translator's LLM client. A provider that
|
|
echoes the API key / a user_id / a home path in its error body must not surface
|
|
that verbatim in the 502 detail, and the no-LLM 503 must point users at the
|
|
current setup surface (Settings → LLM Providers), not the legacy env vars.
|
|
"""
|
|
import os
|
|
|
|
os.environ.setdefault("OMNIVOICE_DISABLE_FILE_LOG", "1")
|
|
|
|
import pytest
|
|
from fastapi import HTTPException
|
|
|
|
|
|
def _req(**kw):
|
|
# AutoExtractRequest is defined in the glossary router module.
|
|
from api.routers.glossary import AutoExtractRequest
|
|
return AutoExtractRequest(**kw)
|
|
|
|
|
|
def test_auto_extract_no_llm_points_at_llm_providers(monkeypatch):
|
|
from api.routers import glossary
|
|
from services import llm_skills
|
|
# Auto-extract resolves its client through the LLM Skills registry
|
|
# (glossary_extract skill). None == disabled / no provider configured.
|
|
monkeypatch.setattr(llm_skills, "resolve_skill_client", lambda sid: None)
|
|
|
|
req = _req(target_lang="es", segments=[{"text": "Hello Marcus"}])
|
|
with pytest.raises(HTTPException) as ei:
|
|
glossary.auto_extract("proj1", req)
|
|
detail = ei.value.detail
|
|
assert ei.value.status_code == 503
|
|
assert "LLM Providers" in detail
|
|
# The stale env-var-only guidance must be gone.
|
|
assert "TRANSLATE_BASE_URL" not in detail
|
|
assert "TRANSLATE_API_KEY" not in detail
|
|
|
|
|
|
def test_auto_extract_scrubs_provider_error(monkeypatch):
|
|
from api.routers import glossary
|
|
from services import llm_skills
|
|
|
|
secret = "sk-LEAKLEAKLEAKLEAKLEAK12345"
|
|
home = "/Users/alice/videos"
|
|
|
|
class _Completions:
|
|
def create(self, **kw):
|
|
raise RuntimeError(f"401 bad key {secret} user_id=acct_9 at {home}")
|
|
|
|
class _Chat:
|
|
completions = _Completions()
|
|
|
|
class _Client:
|
|
chat = _Chat()
|
|
|
|
# A resolved skill client whose provider call blows up — glossary uses
|
|
# handle.client / handle.model / handle.timeout (llm_skills.SkillClient
|
|
# shape) after routing through the glossary_extract skill.
|
|
class _Handle:
|
|
client = _Client()
|
|
model = "m"
|
|
timeout = 1.0
|
|
|
|
monkeypatch.setattr(llm_skills, "resolve_skill_client", lambda sid: _Handle())
|
|
|
|
req = _req(target_lang="es", segments=[{"text": "Hello Marcus"}])
|
|
with pytest.raises(HTTPException) as ei:
|
|
glossary.auto_extract("proj1", req)
|
|
detail = ei.value.detail
|
|
assert ei.value.status_code == 502
|
|
assert secret not in detail
|
|
assert home not in detail
|
|
assert "***REDACTED***" in detail
|