Per-provider vision blocks; parser repairs local models' almost-JSON
config.toml now carries a [vision.<provider>] block per backend — model/base_url/key_env — with vision_provider picking the active one, so the committed file documents every recipe and switching is a one-line flip. Only the active block applies; typo'd block names and keys warn like every other config mistake. First real Ollama run (qwen2.5vl:7b) surfaced what local models emit: almost-JSON with trailing commas. parse_vision_response now makes one cheap repair pass before declaring a response unusable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
This commit is contained in:
co-authored by
Claude Fable 5
parent
0358079da4
commit
118503e4e0
+25
-3
@@ -140,17 +140,39 @@ def load_config(path: Path | None = None) -> Config:
|
||||
"model": str,
|
||||
"rate_limit_seconds": float,
|
||||
"vision_provider": str,
|
||||
"vision_base_url": str,
|
||||
"vision_key_env": str,
|
||||
}
|
||||
updates = {key: caster(raw[key]) for key, caster in known.items() if key in raw}
|
||||
if unknown := sorted(raw.keys() - known.keys()):
|
||||
if unknown := sorted(raw.keys() - known.keys() - {"vision"}):
|
||||
# a typo'd key silently falling back to defaults is a debugging
|
||||
# trap ("No photos found in photos/") — say so up front
|
||||
warnings.warn(
|
||||
f"{p}: ignoring unknown key(s): {', '.join(unknown)}",
|
||||
stacklevel=2,
|
||||
)
|
||||
# [vision.<provider>] blocks: every provider's recipe can live in
|
||||
# the committed file; only the ACTIVE provider's block applies
|
||||
vision_blocks = raw.get("vision") or {}
|
||||
if stray := sorted(vision_blocks.keys() - set(VISION_PROVIDERS)):
|
||||
warnings.warn(
|
||||
f"{p}: ignoring [vision.*] block(s) for unknown provider(s): "
|
||||
f"{', '.join(stray)}",
|
||||
stacklevel=2,
|
||||
)
|
||||
provider = updates.get("vision_provider", cfg.vision_provider)
|
||||
block = vision_blocks.get(provider) or {}
|
||||
block_known = {
|
||||
"model": "model",
|
||||
"base_url": "vision_base_url",
|
||||
"key_env": "vision_key_env",
|
||||
}
|
||||
if odd := sorted(block.keys() - block_known.keys()):
|
||||
warnings.warn(
|
||||
f"{p}: [vision.{provider}]: ignoring unknown key(s): {', '.join(odd)}",
|
||||
stacklevel=2,
|
||||
)
|
||||
for key, field in block_known.items():
|
||||
if key in block:
|
||||
updates[field] = str(block[key])
|
||||
cfg = replace(cfg, **updates)
|
||||
if cfg.vision_provider not in VISION_PROVIDERS:
|
||||
# a typo here would surface as a confusing extract failure later
|
||||
|
||||
@@ -126,7 +126,12 @@ def parse_vision_response(text: str) -> tuple[list[dict], list[dict], int]:
|
||||
if end == -1:
|
||||
raise ValueError(f"unterminated JSON in vision response: {text[:200]!r}")
|
||||
cleaned = cleaned[start : end + 1]
|
||||
data = json.loads(cleaned)
|
||||
try:
|
||||
data = json.loads(cleaned)
|
||||
except json.JSONDecodeError:
|
||||
# local models emit almost-JSON (trailing commas, mostly): one
|
||||
# cheap repair pass before declaring the response unusable
|
||||
data = json.loads(re.sub(r",\s*([}\]])", r"\1", cleaned))
|
||||
if isinstance(data, list):
|
||||
titles_raw, unidentified_raw = data, []
|
||||
elif isinstance(data, dict):
|
||||
|
||||
Reference in New Issue
Block a user