Environment
- modlens 3.12.1 (also reproduced on 3.11.0), run via the skill launcher (
npx --yes --package @liustack/modlens@3.12.1 modlens)
- provider:
openai, model: qwen3-vl-plus, baseUrl: DashScope (Alibaba Cloud Model Studio) OpenAI-compatible endpoint
- image: matplotlib Spearman correlation heatmap, 14×14 matrix, panel label
a, color bar on the right
- Windows 11, node 25
Symptom
Intermittent failures on the openai route (roughly 1 in 2–3 runs), always with finish_reason=stop:
OpenAI-compatible API returned non-JSON output: {"summary":"..."}
OpenAI-compatible API returned JSON that does not match the vision schema (missing: layout.regions[3].type). Got: {"summary":...,"layout":{"regions":[...{"type":"legend",...}]...
Root cause 1 — the JSON object closes itself twice
I captured the raw response body through a local forward proxy. A failing generation looks like this (abbreviated):
{"summary":"...","ocr":{...},"layout":{...},"semantics":{"scene":"...","entities":[...],"relations":[...]}},"visual":{...},"uncertainty":[]}"
Note the extra } right after relations: the model closes the top-level object early, then keeps emitting ,"visual":{...},"uncertainty":[]} plus a stray trailing ". JSON.parse fails with "Unexpected non-whitespace character after JSON", and the old first-{-to-last-} brace slice fails too because of the early close. The error surfaces as the opaque non-JSON output, indistinguishable from a truncation (finish_reason=length).
The only parseable prefix is the partial object (keys: summary, ocr, layout, semantics), which then fails the schema check with missing: visual, uncertainty.
Root cause 2 — the schema enum rejects legend
The model labels the color-bar region "type":"legend"; the layout.regions[].type enum is title|subtitle|paragraph|list|table|chart|form|code|image|icon|other and rejects it, failing the whole read. legend is a normal region type for charts with color bars / map keys, and qwen3-vl-plus uses it consistently.
Suggested fixes
- String-aware tolerant extraction: track brace depth outside string literals (escapes included) and keep the longest well-formed object. A stray close then yields the recoverable prefix and the schema check names the missing fields instead of "non-JSON output".
- Report
finish_reason and the content tail in parse-failure errors; give a dedicated message for finish_reason=length suggesting max_tokens in openai.extraBody.
- Add
legend to the layout.regions[].type enum (schema.ts, the JSON_TEMPLATE_INSTRUCTION in prompt.ts, dsh/vision-schema.json, docs/output-schema.md).
- Document the gateway-side mitigation that removes the whole failure class at the source:
modlens config set openai.extraBody '{"response_format":{"type":"json_object"}}'
Verified 3/3 clean against DashScope qwen3-vl-plus: constrained decoding cannot emit the stray close or the trailing fragment.
Repro
modlens -i <heatmap.png> — run 2–4 times to hit the failure (generation is non-deterministic). Happy to attach a redacted failing raw response body if useful.
Note
A local patch implementing fixes 1–3 (with tests) exists; happy to turn it into a PR if that helps.
Environment
npx --yes --package @liustack/modlens@3.12.1 modlens)openai, model:qwen3-vl-plus, baseUrl: DashScope (Alibaba Cloud Model Studio) OpenAI-compatible endpointa, color bar on the rightSymptom
Intermittent failures on the openai route (roughly 1 in 2–3 runs), always with
finish_reason=stop:OpenAI-compatible API returned non-JSON output: {"summary":"..."}OpenAI-compatible API returned JSON that does not match the vision schema (missing: layout.regions[3].type). Got: {"summary":...,"layout":{"regions":[...{"type":"legend",...}]...Root cause 1 — the JSON object closes itself twice
I captured the raw response body through a local forward proxy. A failing generation looks like this (abbreviated):
{"summary":"...","ocr":{...},"layout":{...},"semantics":{"scene":"...","entities":[...],"relations":[...]}},"visual":{...},"uncertainty":[]}"Note the extra
}right afterrelations: the model closes the top-level object early, then keeps emitting,"visual":{...},"uncertainty":[]}plus a stray trailing".JSON.parsefails with "Unexpected non-whitespace character after JSON", and the old first-{-to-last-}brace slice fails too because of the early close. The error surfaces as the opaquenon-JSON output, indistinguishable from a truncation (finish_reason=length).The only parseable prefix is the partial object (keys: summary, ocr, layout, semantics), which then fails the schema check with
missing: visual, uncertainty.Root cause 2 — the schema enum rejects
legendThe model labels the color-bar region
"type":"legend"; thelayout.regions[].typeenum istitle|subtitle|paragraph|list|table|chart|form|code|image|icon|otherand rejects it, failing the whole read.legendis a normal region type for charts with color bars / map keys, and qwen3-vl-plus uses it consistently.Suggested fixes
finish_reasonand the content tail in parse-failure errors; give a dedicated message forfinish_reason=lengthsuggestingmax_tokensinopenai.extraBody.legendto thelayout.regions[].typeenum (schema.ts, the JSON_TEMPLATE_INSTRUCTION in prompt.ts, dsh/vision-schema.json, docs/output-schema.md).modlens config set openai.extraBody '{"response_format":{"type":"json_object"}}'Verified 3/3 clean against DashScope qwen3-vl-plus: constrained decoding cannot emit the stray close or the trailing fragment.
Repro
modlens -i <heatmap.png>— run 2–4 times to hit the failure (generation is non-deterministic). Happy to attach a redacted failing raw response body if useful.Note
A local patch implementing fixes 1–3 (with tests) exists; happy to turn it into a PR if that helps.