Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
EVEROS_LLM__MODEL=openai/gpt-4.1-mini
EVEROS_LLM__API_KEY=
EVEROS_LLM__BASE_URL=https://openrouter.ai/api/v1
# Per-request timeout in seconds (default 60):
# EVEROS_LLM__TIMEOUT_SECONDS=60


# ─── Multimodal LLM (independent from [llm]; vision/audio capable) ────
Expand Down
1 change: 1 addition & 0 deletions config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
model = "gpt-4.1-mini"
api_key = "sk-..."
base_url = "https://api.openai.com/v1"
timeout_seconds = 60.0

# ── Embedding ─────────────────────────────────────────
[embedding]
Expand Down
1 change: 1 addition & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ everos init --root /data/everos
| `model` | string | `"gpt-4.1-mini"` | No | LLM model identifier. |
| `api_key` | string | — | **Yes** | API key for the LLM provider. |
| `base_url` | string | — | No | Custom endpoint URL (OpenAI-compatible). |
| `timeout_seconds` | float | `60.0` | No | Per-request timeout in seconds. Must be greater than zero. |

### `[multimodal]`

Expand Down
1 change: 1 addition & 0 deletions src/everos/component/llm/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def get_llm_client() -> LLMClient:
model=llm_cfg.model,
api_key=api_key,
base_url=llm_cfg.base_url,
timeout=llm_cfg.timeout_seconds,
)
)
logger.info("llm_client_built", model=llm_cfg.model)
Expand Down
1 change: 1 addition & 0 deletions src/everos/component/llm/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,5 @@ def build_llm_provider(settings: LLMSettings) -> LLMClient:
model=settings.model,
api_key=settings.api_key.get_secret_value(),
base_url=settings.base_url,
timeout=settings.timeout_seconds,
)
4 changes: 3 additions & 1 deletion src/everos/config/default.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,13 @@ cache_size_kb = 2048

[llm]
# Provider-agnostic OpenAI-protocol client config. Override via env:
# EVEROS_LLM__MODEL, EVEROS_LLM__API_KEY, EVEROS_LLM__BASE_URL
# EVEROS_LLM__MODEL, EVEROS_LLM__API_KEY, EVEROS_LLM__BASE_URL,
# EVEROS_LLM__TIMEOUT_SECONDS
# Or set the field directly in this file (<root>/everos.toml).
model = "openai/gpt-4.1-mini"
api_key = ""
base_url = "https://openrouter.ai/api/v1"
timeout_seconds = 60.0

[multimodal]
# Independent LLM for multimodal parsing (everalgo-parser); must accept
Expand Down
2 changes: 2 additions & 0 deletions src/everos/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,13 @@ class LLMSettings(BaseModel):
EVEROS_LLM__MODEL
EVEROS_LLM__API_KEY
EVEROS_LLM__BASE_URL
EVEROS_LLM__TIMEOUT_SECONDS
"""

model: str = "gpt-4.1-mini"
api_key: SecretStr | None = None
base_url: str | None = None
timeout_seconds: float = Field(default=60.0, gt=0)


class MultimodalSettings(BaseModel):
Expand Down
2 changes: 2 additions & 0 deletions src/everos/templates/env.template
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
EVEROS_LLM__MODEL=openai/gpt-4.1-mini
EVEROS_LLM__API_KEY=
EVEROS_LLM__BASE_URL=https://openrouter.ai/api/v1
# Per-request timeout in seconds (default 60):
# EVEROS_LLM__TIMEOUT_SECONDS=60


# ─── Multimodal LLM (independent from [llm]; vision/audio capable) ────
Expand Down
28 changes: 28 additions & 0 deletions tests/unit/test_component/test_llm/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import importlib

import pytest
from everalgo.llm.config import LLMConfig
from pydantic import SecretStr

from everos.component.llm import LLMNotConfiguredError
Expand All @@ -23,13 +24,15 @@ def _patch_settings(
*,
api_key: str | None,
base_url: str | None,
timeout_seconds: float = 60.0,
) -> None:
"""Stub the ``load_settings`` reference bound inside the client module."""
cfg = Settings(
llm=LLMSettings(
model="gpt-4.1-mini",
api_key=SecretStr(api_key) if api_key is not None else None,
base_url=base_url,
timeout_seconds=timeout_seconds,
)
)
monkeypatch.setattr(_client_mod, "load_settings", lambda: cfg)
Expand Down Expand Up @@ -62,3 +65,28 @@ def test_returns_singleton_when_configured(monkeypatch: pytest.MonkeyPatch) -> N

assert first is sentinel
assert first is second


def test_passes_configured_timeout_to_everalgo_client(
monkeypatch: pytest.MonkeyPatch,
) -> None:
_reset_singleton(monkeypatch)
_patch_settings(
monkeypatch,
api_key="sk-test",
base_url="https://example.test",
timeout_seconds=135.0,
)
sentinel = object()
received_timeouts: list[float] = []

def fake_build_client(cfg: LLMConfig) -> object:
received_timeouts.append(cfg.timeout)
return sentinel

monkeypatch.setattr(_client_mod, "build_client", fake_build_client)

client = _client_mod.get_llm_client()

assert client is sentinel
assert received_timeouts == [135.0]
28 changes: 28 additions & 0 deletions tests/unit/test_component/test_llm/test_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@

from __future__ import annotations

import importlib

import pytest
from pydantic import SecretStr

from everos.component.llm import build_llm_provider
from everos.component.llm.openai_provider import OpenAIProvider
from everos.config.settings import LLMSettings

_factory_mod = importlib.import_module("everos.component.llm.factory")


def test_raises_when_api_key_missing() -> None:
s = LLMSettings(model="m", api_key=None, base_url="https://x")
Expand All @@ -26,3 +30,27 @@ def test_builds_openai_provider() -> None:
s = LLMSettings(model="m", api_key=SecretStr("k"), base_url="https://x")
p = build_llm_provider(s)
assert isinstance(p, OpenAIProvider)


def test_passes_configured_timeout_to_provider(
monkeypatch: pytest.MonkeyPatch,
) -> None:
captured: dict[str, object] = {}
sentinel = object()

def fake_provider(**kwargs: object) -> object:
captured.update(kwargs)
return sentinel

monkeypatch.setattr(_factory_mod, "OpenAIProvider", fake_provider)
settings = LLMSettings(
model="m",
api_key=SecretStr("k"),
base_url="https://x",
timeout_seconds=135.0,
)

provider = _factory_mod.build_llm_provider(settings)

assert provider is sentinel
assert captured["timeout"] == 135.0
23 changes: 23 additions & 0 deletions tests/unit/test_config/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,21 @@ def test_env_overrides_toml(monkeypatch: pytest.MonkeyPatch) -> None:
assert s.sqlite.synchronous == "NORMAL"


def test_llm_timeout_env_overrides_everos_toml(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
root = tmp_path / "myroot"
root.mkdir()
(root / "everos.toml").write_text(
"[llm]\ntimeout_seconds = 90.0\n", encoding="utf-8"
)
monkeypatch.setenv("EVEROS_LLM__TIMEOUT_SECONDS", "135.0")

settings = Settings(_everos_root=root)

assert settings.llm.timeout_seconds == 135.0


def test_init_args_override_env(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("EVEROS_SQLITE__BUSY_TIMEOUT_MS", "10000")
from everos.config.settings import SqliteSettings
Expand All @@ -98,6 +113,13 @@ def test_negative_busy_timeout_rejected() -> None:
Settings.model_validate({"sqlite": {"busy_timeout_ms": -1}})


def test_non_positive_llm_timeout_rejected() -> None:
from pydantic import ValidationError

with pytest.raises(ValidationError):
Settings.model_validate({"llm": {"timeout_seconds": 0}})


def test_load_settings_is_cached() -> None:
a = load_settings()
b = load_settings()
Expand All @@ -118,6 +140,7 @@ def test_embedding_rerank_defaults() -> None:
assert s.rerank.api_key.get_secret_value() == ""
assert s.rerank.timeout_seconds == 30.0
assert s.llm.api_key.get_secret_value() == ""
assert s.llm.timeout_seconds == 60.0


def test_resolve_root_default(monkeypatch: pytest.MonkeyPatch) -> None:
Expand Down