Model Provider Error Handling#
Model provider errors in Dify flow through a typed InvokeError hierarchy. Provider plugins must wrap all failure modes into the correct subclass so the API layer maps them to the right HTTP status codes. Unwrapped raw exceptions surface as a generic 500 Internal Server Error at the API boundary β the exact bug fixed for Agent V2 in PR #38584.
InvokeError Subclasses#
These are defined in dify_plugin.errors.model :
| Subclass | Typical cause | HTTP status |
|---|---|---|
InvokeAuthorizationError | Invalid API key / credentials | 401 |
InvokeBadRequestError | Malformed request, unsupported parameter | 400 |
InvokeConnectionError | Network / TCP failure reaching provider | 503 |
InvokeRateLimitError | Provider rate limit hit | 429 |
InvokeServerUnavailableError | Provider 5xx response | 503 |
Error Mapping in Plugins#
Each plugin exposes _invoke_error_mapping, which maps OpenAI SDK exception types to the canonical InvokeError subclasses. For the openai_api_compatible plugin this looks like:
openai.APIConnectionError/openai.APITimeoutErrorβInvokeConnectionErroropenai.InternalServerErrorβInvokeServerUnavailableErroropenai.RateLimitErrorβInvokeRateLimitErroropenai.AuthenticationError/openai.PermissionDeniedErrorβInvokeAuthorizationErroropenai.BadRequestError/openai.NotFoundError/openai.APIErrorβInvokeBadRequestError
HTTP / Error Code Mapping at the API Layer#
Two paths apply the final exception-to-HTTP translation :
- Synchronous responses β
try/exceptblocks in controllers such ascompletion.py - Streaming responses β
_error_to_stream_response()inbase_app_generate_response_converter.py
Exception ordering matters:
services.errors.llm.InvokeRateLimitError(service-layer quota) is a subclass ofservices.errors.llm.InvokeError. If the baseInvokeErrorhandler is placed first in theexceptchain, rate-limit errors are silently downgraded to HTTP 400. Controllers correctly placeInvokeRateLimitErrorbeforeInvokeError.
Three distinct
InvokeRateLimitErrorclasses exist βservices.errors.llm.InvokeRateLimitError(Cloud quota, HTTP 429),graphon.model_runtime.errors.invoke.InvokeRateLimitError(provider throttling, HTTP 400 in sync /status: 429in streaming), andcontrollers.web.error.InvokeRateLimitError(HTTP response class). Controllers import all three with aliases to avoid collisions.
Agent V2 Error Propagation Path#
Agent V2 runs as a separate dify-agent microservice. Errors from model providers reach the API caller through a two-step translation :
dify-agentruntime serializes the structured error reason (e.g.,"InvokeRateLimitError") into therun_failedSSE event'sreasonfield.AgentAppRunnerreadsAgentBackendRunFailedInternalEventand calls_agent_backend_failure_to_exception(), which looks up the reason string in_AGENT_BACKEND_INVOKE_ERROR_BY_REASONand raises the matchingInvokeErrorsubclass.
Unmapped reason strings fall through to a generic AgentBackendRunFailedError.
Agent V2 / vLLM Compatibility Bugs Fixed in v1.16.x#
Several bugs caused Agent V2 to malfunction with vLLM and OpenAI-compatible providers. All were fixed in the 1.16.x timeframe :
| Issue | Fix | PR |
|---|---|---|
| Provider errors returned as generic 500 | Structured error reasons now propagated with correct HTTP codes | #38584 |
| Text-only message history serialized as part-object lists | _normalize_prompt_content() collapses text-only parts to a plain string | #38464 |
vLLM omits or sends placeholder tool call IDs ("none", "null", empty) | _normalize_tool_call_id() converts to None; stable fallback IDs from chunk position | #38592 |
| Qwen/vLLM requires system message at index 0 | _map_messages_to_prompt_messages() merges and hoists system messages | #39136 |
vLLM Function Calling Requirements#
Agent V2 uses pydantic-ai and requires native OpenAI-style function calling β no CoT/ReAct fallback . For vLLM, enable function calling at startup:
--enable-auto-tool-choice
--tool-call-parser <parser> # e.g., "mistral", "llama3_json", "hermes"
If the model doesn't support function calling, fall back to classic agent mode (Agent V1 / ReAct).
SSRF Proxy Error Wrapping#
All outbound model provider requests route through api/core/helper/ssrf_proxy.py. Network and SSL failures that occur at the SSRF proxy layer are not automatically wrapped into InvokeError subclasses β the proxy's make_request() raises httpx.RequestError (on first failure when max_retries=0) or MaxRetriesExceededError (after exhausting retries). Providers or plugin daemon wrappers must re-wrap these into InvokeConnectionError or InvokeServerUnavailableError as appropriate.
Retried HTTP status codes include 429, 500, 502, 503, 504 with exponential backoff (0.5 Γ 2^(retry-1)); default max retries = 3, controlled by SSRF_DEFAULT_MAX_RETRIES.
Key Source Files#
| File | Purpose |
|---|---|
dify-official-plugins: openai_api_compatible/models/common_openai.py | _invoke_error_mapping β maps OpenAI SDK exceptions to InvokeError subclasses |
api/core/app/apps/agent_app/app_runner.py | _agent_backend_failure_to_exception() and _AGENT_BACKEND_INVOKE_ERROR_BY_REASON |
api/core/app/apps/base_app_generate_response_converter.py | _error_to_stream_response() β streaming InvokeError β HTTP code mapping |
api/controllers/service_api/app/completion.py | Sync error handler; InvokeRateLimitError ordering |
api/core/helper/ssrf_proxy.py | SSRF client: make_request(), retry loop, MaxRetriesExceededError, ToolSSRFError |
dify-agent/src/dify_agent/adapters/llm/model.py | _normalize_prompt_content(), _normalize_tool_call_id(), _map_messages_to_prompt_messages() |
dify-official-plugins: deepseek/models/llm/llm.py | DeepSeek LLM β extends OAICompatLargeLanguageModel (inherits error mapping) |