Skip to content

openai route: intermittent non-JSON output / schema failures on qwen3-vl-plus (double-closed JSON, legend region type) #45

Description

@abyss-stars

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:

  1. OpenAI-compatible API returned non-JSON output: {"summary":"..."}
  2. 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

  1. 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".
  2. 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.
  3. 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).
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions