affaan-m/ECC · error · SystemExit
the vision model never returned usable JSON after 3…
Error message
the vision model never returned usable JSON after 3 attempts; detail: {json.dumps(attempts)} What it means
describe() retries the vision-model call up to 3 times, collecting per-attempt failure details in `attempts`. If every attempt failed and no best candidate exists, it raises SystemExit with the JSON-encoded attempt details, telling the operator the vision model never produced usable JSON and why each attempt failed.
Solutions
- Read the `detail` JSON in the message: it lists each attempt's failure reason; fix the root cause it reports (auth error, HTTP status, parse error).
- Verify FAL_KEY is set and valid, or confirm dry-run mode is intentional (falapi.is_dry_run()).
- Test the vision endpoint/model name directly with a single request to confirm the model is available.
- Improve the prompt to require a bare JSON object, and increase max_tokens to avoid truncation.
- If failures are transient (rate limits, network), add backoff between the 3 attempts or raise the retry count.
Example fix
null
Defensive patterns
Strategy: fallback
Validate before calling
def can_call_vision() -> bool:
return bool(os.environ.get("FAL_KEY")) or falapi.is_dry_run() Type guard
null
Try / catch
try:
result = describe(...)
except SystemExit as exc:
log.error("vision pipeline failed after retries: %s", exc)
result = default_description # cached or placeholder Prevention
- Validate FAL_KEY presence before starting a long distill run
- Check the attempts detail JSON on failure to fix the root cause, not just retry
- Add exponential backoff between attempts for rate-limit errors
- Monitor the vision endpoint's availability before batch runs
- Keep prompt instructions strict about emitting bare JSON
When it happens
Trigger: describe(genre/paths) calls the vision LLM 3 times; all attempts raise (e.g. extract_json ValueError, empty response, API errors) or return non-dict output, leaving best=None, so the final raise fires after the retry loop.
Common situations: Invalid or missing FAL API key causing every call to fail; persistent network outage; model endpoint renamed/unavailable; prompt so malformed the model never emits JSON; rate limiting across all retries.
Understand the failure class
Background: "API request failed": what wrapped HTTP errors from external APIs mean and how to find the real cause — this error's family across 29 libraries.
Related errors
- Codex review failed ` : ''}
- completion contradicts the existing receipt
- ContextLengthError(msg, provider=ProviderType.CLAUDE) from e
- ContextLengthError(msg, provider=ProviderType.OLLAMA) from e
- distill failed
AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16).
Data as JSON: /api/errors/625b8ccb54b0f2eb.
Report an issue: GitHub.
Appendix: source
Thrown at skills/taste-distillation/scripts/distill.py:381
if not hits:
return spec, prov
# Keep the best answer seen so far, so three hedged attempts still
# yield the least-hedged one rather than an exception.
if best is None or len(hits) < len(banned_hits(best[0])):
best = (spec, prov)
if attempt < 3:
log.warning("attempt %d hedged on %s; asking it to commit",
attempt, ", ".join(sorted(hits)))
prompt = _rewrite_prompt(base, hits, spec)
continue
log.warning("still hedging on %s after 3 attempts; keeping best",
", ".join(sorted(banned_hits(best[0]))))
return best
if best is not None:
return best
raise SystemExit(
"the vision model never returned usable JSON after 3 attempts; "
f"detail: {json.dumps(attempts)}"
)
def mint_prop(sp: pack_mod.StylePack, stills: list[Path]) -> dict | None:
"""Turn the highest-detail still into a GLB and store it in the pack."""
scored = sorted(((detail_score(p), p) for p in stills), key=lambda t: -t[0])
if not scored:
return None
score, hero = scored[0]
print(f" prop source : {hero.name} (detail {score:.1f})")
url = falapi.upload(hero)
mesh_url = falapi.image_to_3d(url)
dest = sp.props_dir / f"{hero.stem}.glb"
falapi.download(mesh_url, dest)
return {View on GitHub (pinned to 8321021c54)