OpenAI-Compatible Server Integration#
Dify supports OpenAI-compatible inference servers β including vLLM, SGLang, Ollama, and OpenRouter β through two integration layers:
- Model plugins in
dify-official-pluginshandle raw LLM calls (/v1/chat/completions) with SSE streaming response parsing. - Agent V2 (
dify-agentmicroservice) handles tool-calling agent loops via pydantic-ai, and imposes stricter requirements on provider behavior.
Known compatibility issues cluster around three areas: SSE format validation, function calling requirements for Agent V2, and response parsing quirks in workflow and agent execution contexts. The plain Python OpenAI SDK is more tolerant of provider quirks than Dify's parsers β providers that pass the SDK may still fail in Dify.
SSE Format Validation and "Non-JSON encountered." Error#
Where it originates#
The OpenRouter and Ollama plugin LLM implementations share nearly identical SSE streaming parsers. Each line is processed as :
decoded_chunk = chunk.strip().removeprefix("data:").lstrip()
The .lstrip() after removeprefix("data:") handles both data:{JSON} (no space) and data: {JSON} (standard space) formats. When the decoded content fails JSON validation, the plugin yields a final chunk with finish_reason="Non-JSON encountered." and terminates the stream β it does not raise an exception.
How it surfaces in workflows#
In a workflow LLM block this appears as:
{
"text": "",
"finish_reason": "Non-JSON encountered.",
"usage": { "prompt_tokens": 0, "completion_tokens": 0, ... },
"latency": 1.0
}
Zero tokens consumed and sub-second latency indicate the error occurs before the model generates any output β the LLM call itself never succeeds.
Common root causes#
| Cause | Signal | Fix |
|---|---|---|
| HTTP proxy intercepts request | Latency ~1 s, HTML or XML in stream | Set NO_PROXY=weaviate,qdrant,db,redis,web,worker,plugin_daemon in .env |
Provider wraps response in outer JSON envelope (e.g. {"code":0,...}) | First SSE line fails JSON validation | Contact provider; use a compliant endpoint |
Provider omits data: prefix or uses non-standard delimiters | All chunks fail parse | Provider-side fix required |
Wrong Content-Type on streaming response | Parser receives body as one blob | Provider must return text/event-stream |
The stream_mode_delimiter credential field (default "\n\n") can be adjusted for providers that use alternative delimiters, such as "\n".
Agent V2 Function Calling Requirements#
Agent V2 (Dify β₯ 1.16.0) uses pydantic-ai as its execution engine and requires native OpenAI-style function calling β the tools parameter in requests and tool_calls field in responses. There is no built-in CoT/ReAct fallback in Agent V2.
vLLM configuration#
For vLLM-hosted models, function calling is not enabled by default. Required flags:
--enable-auto-tool-choice
--tool-call-parser <parser> # e.g., "mistral", "llama3_json", "hermes"
Not all models served by vLLM support function calling β it depends on the specific model and chat template. Models like Llama-3, Qwen2, and Mistral generally support it.
Finish reason normalization#
The adapter normalizes "tool_calls", "function_call", and "function_calls" finish reasons to a single "tool_call" value internally.
Fallback#
If the model does not support function calling, use the classic agent mode (ReAct/CoT), which is available in all Dify versions and does not require tool_calls support.
Deployment note#
Agent V2 is experimental in 1.15.0β1.16.0-rc1 and the dify-agent microservice is not included in the default docker-compose.yaml.
vLLM + Agent V2 Compatibility Bugs (Fixed in 1.16.x)#
Several specific incompatibilities with vLLM and similar OpenAI-compatible providers were fixed in the 1.16.0 timeframe:
Error messages swallowed (PR #38584)#
Agent V2 was returning a generic "Internal Server Error" instead of the real provider error (authentication failure, rate limit, bad request, etc.). Structured errors are now passed through with correct HTTP status codes.
Text history sent as part lists (PR #38464)#
Text-only assistant message history was serialized as part objects (e.g., [{"type": "text", "text": "..."}]) instead of plain strings. Providers like vLLM that don't handle multipart structures for text-only content would reject or misparse these. The fix collapses text-only parts into a single concatenated string.
Tool call ID normalization (PR #38592)#
vLLM may omit or send placeholder tool call IDs ("none", "null", empty string, whitespace). These caused distinct tool calls to collapse into the same thought record. _normalize_tool_call_id() converts such values to None, and stable fallback vendor_id values are generated from chunk sequence position.
Qwen/vLLM system message ordering (PR #39136, open)#
Qwen served via vLLM requires the system message to be the first element in the messages array. Previously, instruction messages could be inserted mid-array. The fix in _map_messages_to_prompt_messages() merges all system messages into one and places it at index 0.
Key Source Files and References#
| File | Purpose |
|---|---|
models/openrouter/models/llm/llm.py | SSE parser; "Non-JSON encountered." origin |
models/ollama/models/llm/llm.py | Identical SSE parser pattern |
dify-agent/src/dify_agent/server/sse.py | dify-agent SSE event serializer (server side) |
dify-agent/src/dify_agent/client/_client.py | dify-agent client SSE decoder (_SSEDecoder, _SSELineDecoder) |
dify-agent/src/dify_agent/adapters/llm/model.py | Agent LLM adapter: history serialization, tool call ID normalization, system message ordering |