Make AI model configurable per feature, and split news grading/formatting models

Every AI-calling feature (content correction, the @ai modifier, quiz grading,
SEO metadata generation, the AI Usage Analyzer, and DeepSearch) can now name
its own model via admin-editable configuration, defaulting to the gateway's
default model when left blank. The gateway a feature talks to stays fixed to
the internal endpoint either way, only the model name is a knob, so the best
model can be picked per task.

Also splits the news service's shared AI grading/formatting config into two
independent endpoint/model/key pairs: reformatting no longer requires
repointing (and thereby breaking) the free, scoring-only grading model.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FZ5x6KZTxZjbJqsEkxGbxG
This commit is contained in:
2026-09-03 08:47:57 +02:00
co-authored by Claude Sonnet 5
parent 81d6aa68b6
commit 8ae3f628c7
22 changed files with 211 additions and 52 deletions
@@ -14,7 +14,7 @@ from devplacepy.services.deepsearch.store import VectorStore
def _patch_pipeline(monkeypatch, pages):
async def fake_plan(query, api_key, emit=lambda frame: None):
async def fake_plan(query, api_key, emit=lambda frame: None, model=None):
return [query, f"{query} overview"]
async def fake_search(queries, emit=lambda frame: None):
@@ -29,7 +29,7 @@ def _patch_pipeline(monkeypatch, pages):
outcome.pages.append(page)
return outcome
async def fake_plan_followups(query, covered_titles, api_key, emit=lambda frame: None):
async def fake_plan_followups(query, covered_titles, api_key, emit=lambda frame: None, model=None):
return []
def fake_embed(texts, api_key):
@@ -38,7 +38,7 @@ def _patch_pipeline(monkeypatch, pages):
async def fake_embed_async(texts, api_key, **kwargs):
return local_embed(texts)
async def fake_orchestrate(question, crawled, api_key, emit, store=None, queries=None):
async def fake_orchestrate(question, crawled, api_key, emit, store=None, queries=None, model=None):
from devplacepy.services.jobs.deepsearch.orchestrate import Orchestration
return Orchestration(
@@ -166,7 +166,7 @@ def test_worker_run_performs_refinement_round_when_budget_remains(monkeypatch):
followup_calls = []
async def fake_plan_followups_once(query, covered_titles, api_key, emit=lambda frame: None):
async def fake_plan_followups_once(query, covered_titles, api_key, emit=lambda frame: None, model=None):
if followup_calls:
return []
followup_calls.append(covered_titles)
+1 -1
View File
@@ -134,7 +134,7 @@ def test_grade_free_text_uses_a_valid_verdict(monkeypatch):
def test_grade_free_text_passes_the_answering_key_through(monkeypatch):
seen = {}
def capture(api_key, system, text, timeout):
def capture(api_key, system, text, timeout, model=None):
seen["api_key"] = api_key
return json.dumps({"score": 1.0}), None