Fix doctor provider checks: custom endpoints and processing models
This commit is contained in:
parent
eb855f827a
commit
4fae733a50
2 changed files with 106 additions and 23 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ---
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue