OpenAI-compatible endpoints that don't strictly honor response_format crash all LLM analyzers (fenced/prose JSON)
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 17.9k
- Forks
- 1.5k
- Avg merge
- 5d 10h
- Merged PRs (30d)
- 66
Description
Summary
When SKILLSPECTOR_PROVIDER=openai points at an OpenAI-compatible endpoint (via OPENAI_BASE_URL) that does not strictly honor response_format: {type: json_object}, every LLM semantic analyzer fails and the scan aborts with a JSON decode error. Static/pattern analysis still runs, but all LLM-backed findings are lost.
The OpenAI-compatible surface is documented as supported ("any OpenAI-compatible URL", Ollama/vLLM/llama.cpp/"managed inference gateways"), but many such endpoints either ignore response_format or proxy a model that emits Markdown-fenced JSON or a prose report instead of a bare JSON object. langchain's with_structured_output(...) (json-mode) then receives non-JSON and raises.
Impact
- Hard failure, not graceful degradation: the process exits non-zero (
Error: 1 validation error for LLMAnalysisResult / Invalid JSONorError: Expecting value: line 1 column 1 (char 0)). - Affects every analyzer that inherits
LLMAnalyzerBase(per-file analyzers + the meta-analyzer), since they all rely on_structured_llm = self._llm.with_structured_output(self.response_schema). - Silent quality loss in the best case: users on a compatible-but-non-strict endpoint get pattern-only results while believing LLM analysis ran.
Reproduction
- Configure an OpenAI-compatible endpoint that does not enforce strict JSON mode:
export SKILLSPECTOR_PROVIDER=openai export OPENAI_BASE_URL=https://<openai-compatible-endpoint>/v1 export OPENAI_API_KEY=<key> export SKILLSPECTOR_MODEL=<model-served-there> skillspector scan ./some-skill/ - Observe the run abort during the semantic stage.
Probing such an endpoint directly shows the root cause — response_format: json_object is ignored and the body is fenced:
{"choices":[{"message":{"content":"```json\n{\"status\": \"ok\"}\n```"}}]}
Some models go further and return a full Markdown report ("# Security Analysis Report ... No findings") instead of JSON, so even fence-stripping a single block is not always enough.
Root cause
src/skillspector/llm_analyzer_base.py:
self._structured_llm = (
self._llm.with_structured_output(self.response_schema) if self.response_schema else None
)
This delegates parsing to the provider/langchain json-mode path, which assumes the endpoint returns a clean JSON object. OpenAI-compatible endpoints that don't enforce that contract break it.
Suggested fix
Make structured parsing tolerant of OpenAI-compatible endpoints that don't strictly honor response_format:
- Inject an explicit, schema-derived "respond with a single JSON object only, no prose, no code fences" instruction into the prompt (so behavior is driven by the prompt, not solely by
response_format). - Before validating, strip a wrapping
```json ... ```fence and, as a fallback, slice the outermost{...}/[...]when prose surrounds the payload. - Then
model_validatethe recovered JSON against the existing Pydantic schema.
This keeps strict behavior for endpoints that already return clean JSON while recovering the large class of compatible-but-lenient endpoints. A working local patch wrapping with_structured_output with a fence-tolerant validator (plus a prompt-level JSON directive) resolved the failure across all analyzers in my testing; happy to open a PR.
Environment
- SkillSpector v2.1.4
- Provider:
openaiagainst a non-OpenAI, OpenAI-compatible endpoint (OPENAI_BASE_URL) - Failure reproduced on both per-file analyzers and the meta-analyzer
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in src/skillspector/llm_analyzer_base.py at the _structured_llm construction and trace how analyzer responses are validated. Reproduce the scan with a non-strict OpenAI-compatible endpoint, then verify that prompt guidance and tolerant JSON recovery handle fenced or prose-wrapped responses while preserving Pydantic validation. Done means per-file and meta-analyzers no longer abort on these responses and their findings are retained.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- ai
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 68/100