ai: route reviewer through OpenRouter + Claude Haiku 4.5
The DeepSeek-V4-flash reviewer was unreliable in production: it pads its JSON verdicts with internal chain-of-thought even when the prompt forbids it, so the verdict gets truncated at any reasonable max_tokens cap and the parser drops it as malformed (a false-negative verdict that would purge clean rows). A live run on 50 rows reproduced the failure on 8 of 12 rejections, even at 800 tokens. Fix: pin the reviewer call to OpenRouter with anthropic/claude-haiku-4.5. Haiku answers structured-output classification tersely (no scratchpad preamble), which means a 300-token cap is comfortably above the ~30-token JSON verdict. Cost is roughly the same (~$0.0001-$0.0003 per review) and the latency tax is smaller. To enable the pinned-provider call without disrupting other callers, call_llm grows an optional `provider` parameter: when set, only that provider is used (no fallback chain). All existing call sites default to provider=None and keep the chain behaviour. REVIEWER_MODEL is read from settings via getattr-with-fallback so an env override can swap models without code changes — useful if we want to A/B test against e.g. gemini-2.5-flash later. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
8b9d3c9c3e
commit
788563a81f
3 changed files with 45 additions and 14 deletions
|
|
@ -199,6 +199,7 @@ async def call_llm(
|
|||
model: str | None = None,
|
||||
max_tokens: int = 4000,
|
||||
response_format: dict | None = None,
|
||||
provider: str | None = None,
|
||||
) -> LogResult:
|
||||
"""Provider-aware chat completion with fallback. Tries primary
|
||||
(LLM_PROVIDER) first; if it raises after retries, falls through to
|
||||
|
|
@ -211,8 +212,16 @@ async def call_llm(
|
|||
|
||||
Pass response_format={"type": "json_object"} to force JSON-mode
|
||||
output (the model still needs to be instructed in the system prompt
|
||||
to emit valid JSON — this flag enforces, not asks)."""
|
||||
chain = _provider_chain()
|
||||
to emit valid JSON — this flag enforces, not asks).
|
||||
|
||||
Pass `provider` (e.g. "openrouter") to skip the configured chain
|
||||
and pin the call to a specific provider. Used by the reviewer agent
|
||||
to force routing through OpenRouter so it can address a non-DeepSeek
|
||||
model that doesn't pre-think before emitting JSON."""
|
||||
if provider is not None:
|
||||
chain = [provider]
|
||||
else:
|
||||
chain = _provider_chain()
|
||||
if not chain:
|
||||
raise RuntimeError("No LLM provider configured (no API key set)")
|
||||
|
||||
|
|
|
|||
|
|
@ -20,12 +20,23 @@ from dataclasses import dataclass
|
|||
|
||||
import httpx
|
||||
|
||||
from app.config import get_settings
|
||||
from app.logging import get_logger
|
||||
from app.services.openrouter import call_llm
|
||||
|
||||
log = get_logger("output_review")
|
||||
|
||||
|
||||
# The reviewer runs through OpenRouter against a small, non-thinking
|
||||
# model. DeepSeek-V4-flash (our generator default) emits internal
|
||||
# chain-of-thought before its JSON output even when the prompt forbids
|
||||
# it, which truncates the JSON at any reasonable max_tokens cap and
|
||||
# breaks the parser. Anthropic's Haiku family answers structured-output
|
||||
# tasks tersely and deterministically — no chain-of-thought tax. Cost
|
||||
# is ~$0.0001-$0.0003 per review depending on candidate length.
|
||||
DEFAULT_REVIEWER_MODEL = "anthropic/claude-haiku-4.5"
|
||||
|
||||
|
||||
_SYSTEM_PROMPT = """\
|
||||
You are a strict editor for a financial-markets dashboard. The author
|
||||
was asked to produce a short interpretive read for human readers.
|
||||
|
|
@ -81,17 +92,21 @@ async def review_read(client: httpx.AsyncClient, candidate: str) -> Verdict:
|
|||
# contain prompt-like prose.
|
||||
{"role": "user", "content": f"Candidate read:\n```\n{candidate}\n```"},
|
||||
]
|
||||
settings = get_settings()
|
||||
reviewer_model = getattr(settings, "REVIEWER_MODEL", None) or DEFAULT_REVIEWER_MODEL
|
||||
try:
|
||||
result = await call_llm(
|
||||
client, messages,
|
||||
# 800 tokens is well above the ~30-token JSON verdict the
|
||||
# prompt asks for. The reviewer model (DeepSeek-V4-flash)
|
||||
# occasionally pads with its own thinking before the JSON
|
||||
# even though response_format is enforced; smaller caps
|
||||
# (120, 300) produced finish_reason=length cutoffs that
|
||||
# left the JSON half-written and broke the parser. 800
|
||||
# removes the artefact entirely at ~$0.00022 per call.
|
||||
max_tokens=800,
|
||||
# Pin to OpenRouter so a non-DeepSeek model like Haiku is
|
||||
# actually reachable; the default provider chain would try
|
||||
# DeepSeek native first and 404 on the Anthropic model name.
|
||||
provider="openrouter",
|
||||
model=reviewer_model,
|
||||
# 300 tokens is well above the ~30-token JSON verdict.
|
||||
# Haiku doesn't pad with hidden reasoning the way DeepSeek
|
||||
# does, so we don't need the 800-token headroom required to
|
||||
# absorb the generator's chain-of-thought.
|
||||
max_tokens=300,
|
||||
response_format={"type": "json_object"},
|
||||
)
|
||||
except Exception as e:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue