Fix doctor provider checks: custom endpoints and processing models

This commit is contained in:
Yiorgis Gozadinos 2026-06-23 11:48:21 +03:00
parent eb855f827a
commit 4fae733a50
No known key found for this signature in database
2 changed files with 106 additions and 23 deletions

View file

@ -83,25 +83,48 @@ def _sample(ids: list[str]) -> list[str]:
return [*ids[:_SAMPLE_LIMIT], f"... (+{extra} more)"]
def _configured_providers(config: AppConfig) -> set[str]:
"""Providers referenced by the current config across every model role."""
providers = {config.embeddings.model.provider}
for model in (
config.reranking.model,
config.qa.model,
config.analysis.model,
):
def _active_models(config: AppConfig) -> list[tuple[str, str, str | None]]:
"""(provider, name, base_url) for every model role the config activates.
Picture-description and title models are only included when their feature
is enabled (``processing.pictures == "description"`` / ``auto_title``), so
doctor checks exactly the providers the next ingest will use.
"""
models = [
(
config.embeddings.model.provider,
config.embeddings.model.name,
config.embeddings.model.base_url,
)
]
for model in (config.reranking.model, config.qa.model, config.analysis.model):
if model is not None:
providers.add(model.provider)
return providers
models.append((model.provider, model.name, model.base_url))
proc = config.processing
if proc.pictures == "description":
pd = proc.conversion_options.picture_description.model
models.append((pd.provider, pd.name, pd.base_url))
if proc.auto_title:
tm = proc.title_model
models.append((tm.provider, tm.name, tm.base_url))
return models
def _check_api_keys(config: AppConfig, environ: dict[str, str]) -> CheckResult:
missing: list[str] = []
for provider in sorted(_configured_providers(config)):
env_var = _PROVIDER_ENV_VARS.get(provider)
if env_var and not environ.get(env_var):
missing.append(f"{provider} ({env_var})")
# A custom base_url points at a self-hosted OpenAI-compatible endpoint that
# uses a placeholder key, so the SaaS key is only required when a provider
# is used without one. Reachability of custom endpoints is the probe's job.
need_key = {
provider
for provider, _name, base_url in _active_models(config)
if not base_url and provider in _PROVIDER_ENV_VARS
}
missing = [
f"{provider} ({_PROVIDER_ENV_VARS[provider]})"
for provider in sorted(need_key)
if not environ.get(_PROVIDER_ENV_VARS[provider])
]
if missing:
return CheckResult(
name="api_keys",
@ -607,14 +630,8 @@ def _provider_targets(
{"kind": "docling-serve", "display": base, "models": set()},
)
add_model(
config.embeddings.model.provider,
config.embeddings.model.name,
config.embeddings.model.base_url,
)
for model in (config.reranking.model, config.qa.model, config.analysis.model):
if model is not None:
add_model(model.provider, model.name, model.base_url)
for provider, name, base_url in _active_models(config):
add_model(provider, name, base_url)
return targets, local

View file

@ -9,9 +9,12 @@ from typer.testing import CliRunner
from haiku.rag.cli import _cli as cli
from haiku.rag.config.models import (
AppConfig,
ConversionOptions,
DoclingServeConfig,
EmbeddingModelConfig,
EmbeddingsConfig,
ModelConfig,
PictureDescriptionConfig,
ProcessingConfig,
ProvidersConfig,
)
@ -19,6 +22,8 @@ from haiku.rag.doctor import (
CheckResult,
DoctorReport,
Severity,
_active_models,
_check_api_keys,
_check_embedding_drift,
_check_vector_index,
_model_present,
@ -628,6 +633,67 @@ def test_cli_doctor_exits_1_on_failure(monkeypatch):
assert result.exit_code == 1
# --- Active models / API keys ---
def test_api_key_not_required_for_custom_openai_base_url():
config = AppConfig(
embeddings=EmbeddingsConfig(
model=EmbeddingModelConfig(
provider="openai",
name="x",
vector_dim=4,
base_url="http://localhost:1234/v1",
)
)
)
assert _check_api_keys(config, {}).severity is Severity.OK
def test_api_key_required_for_openai_without_base_url():
config = AppConfig(
embeddings=EmbeddingsConfig(
model=EmbeddingModelConfig(provider="openai", name="x", vector_dim=4)
)
)
result = _check_api_keys(config, {})
assert result.severity is Severity.FAIL
assert any("OPENAI_API_KEY" in d for d in result.details)
def test_active_models_includes_picture_description_when_enabled():
config = AppConfig(processing=ProcessingConfig(pictures="description"))
names = [name for _p, name, _b in _active_models(config)]
assert "ministral-3" in names
def test_active_models_excludes_picture_description_by_default():
names = [name for _p, name, _b in _active_models(AppConfig())]
assert "ministral-3" not in names
def test_active_models_includes_title_model_when_auto_title():
base = _active_models(AppConfig())
with_title = _active_models(AppConfig(processing=ProcessingConfig(auto_title=True)))
assert len(with_title) == len(base) + 1
def test_picture_description_model_checked_for_api_key():
config = AppConfig(
processing=ProcessingConfig(
pictures="description",
conversion_options=ConversionOptions(
picture_description=PictureDescriptionConfig(
model=ModelConfig(provider="openai", name="gpt-4o")
)
),
)
)
result = _check_api_keys(config, {})
assert result.severity is Severity.FAIL
assert any("OPENAI_API_KEY" in d for d in result.details)
# --- Provider connectivity ---