- llm.py: set_model_overrides(vision, text) replaces the single job override;
UI picks still beat per-call agent model args, but never name the hybrid
local model (avoids main's hybrid footgun); local->cloud fallback uses the
text pick.
- jobs.py: timestamped line-split tee (job_log.py), in-memory log + log_tail
polls, full log on terminal states (done/error/needs_review/finalization_error),
log-only disk recovery, error email links to the run log, and failed runs now
append the full traceback to job.log. Keeps pipeline_mode, job.json, and the
review gate.
- models.py: vision/text split via architecture modalities, pricing kept;
/models returns {vision, text, defaults}; /check takes vision_model/text_model
(replacing model); /health adds text_model. models_catalog.py dropped.
- UI: two priced dropdowns (OpenRouter compute only) + live run-log panel.
- Tests updated for dual overrides and the /models shape; new coverage for
traceback capture and local-model immunity.
89 lines
3.1 KiB
Python
89 lines
3.1 KiB
Python
from fastapi.testclient import TestClient
|
|
|
|
import backend.models as models
|
|
from backend import config
|
|
from backend.main import app
|
|
|
|
_PAYLOAD = {
|
|
"data": [
|
|
{
|
|
"id": "openai/gpt-4o",
|
|
"name": "GPT-4o",
|
|
"pricing": {"prompt": "0.0000025", "completion": "0.00001"},
|
|
"context_length": 128000,
|
|
"architecture": {"input_modalities": ["text", "image"],
|
|
"output_modalities": ["text"]},
|
|
},
|
|
{
|
|
"id": "google/gemini-2.5-pro",
|
|
"name": "Gemini 2.5 Pro",
|
|
"pricing": {"prompt": "0.00000125", "completion": "0.00001"},
|
|
"context_length": 1000000,
|
|
"architecture": {"modality": "text+image->text"},
|
|
},
|
|
{
|
|
"id": "meta-llama/llama-3.1-70b-instruct",
|
|
"name": "Llama 3.1 70B Instruct",
|
|
"pricing": {"prompt": "0.0000005", "completion": "0.0000008"},
|
|
"context_length": 131072,
|
|
"architecture": {"input_modalities": ["text"],
|
|
"output_modalities": ["text"]},
|
|
},
|
|
]
|
|
}
|
|
|
|
|
|
def _reset_cache():
|
|
models._cache["models"] = None
|
|
models._cache["at"] = 0.0
|
|
|
|
|
|
def test_models_endpoint_normalizes_pricing(monkeypatch):
|
|
_reset_cache()
|
|
monkeypatch.setattr(models, "_fetch_openrouter_models", lambda: _PAYLOAD["data"])
|
|
client = TestClient(app)
|
|
response = client.get("/models")
|
|
assert response.status_code == 200
|
|
body = response.json()
|
|
assert body["defaults"] == {"vision": config.MODEL, "text": config.TEXT_MODEL}
|
|
by_id = {m["id"]: m for m in body["text"]}
|
|
assert by_id["openai/gpt-4o"]["prompt_usd_per_mtok"] == 2.5
|
|
assert by_id["openai/gpt-4o"]["completion_usd_per_mtok"] == 10.0
|
|
assert by_id["openai/gpt-4o"]["context_length"] == 128000
|
|
|
|
|
|
def test_models_endpoint_splits_vision_and_text(monkeypatch):
|
|
_reset_cache()
|
|
monkeypatch.setattr(models, "_fetch_openrouter_models", lambda: _PAYLOAD["data"])
|
|
client = TestClient(app)
|
|
body = client.get("/models").json()
|
|
vision_ids = {m["id"] for m in body["vision"]}
|
|
text_ids = {m["id"] for m in body["text"]}
|
|
# Both modality shapes (structured and legacy string) are recognized.
|
|
assert vision_ids == {"openai/gpt-4o", "google/gemini-2.5-pro"}
|
|
# Text list is the full catalog; vision models appear in both.
|
|
assert text_ids == {"openai/gpt-4o", "google/gemini-2.5-pro",
|
|
"meta-llama/llama-3.1-70b-instruct"}
|
|
|
|
|
|
def test_models_endpoint_caches(monkeypatch):
|
|
_reset_cache()
|
|
calls = []
|
|
|
|
def fake_fetch():
|
|
calls.append(1)
|
|
return _PAYLOAD["data"]
|
|
|
|
monkeypatch.setattr(models, "_fetch_openrouter_models", fake_fetch)
|
|
client = TestClient(app)
|
|
assert client.get("/models").status_code == 200
|
|
assert client.get("/models").status_code == 200
|
|
assert len(calls) == 1
|
|
|
|
|
|
def test_models_endpoint_502_on_fetch_failure(monkeypatch):
|
|
_reset_cache()
|
|
monkeypatch.setattr(models, "_fetch_openrouter_models", lambda: None)
|
|
client = TestClient(app)
|
|
assert client.get("/models").status_code == 502
|