274 lines
12 KiB
Markdown
274 lines
12 KiB
Markdown
<!-- markdownlint-disable MD013 -->
|
|
|
|
# RFC: Align vLLM-Omni error handling with vLLM
|
|
|
|
> **Status:** Draft for discussion
|
|
> **Feedback period:** Two weeks after publication
|
|
> **vLLM-Omni snapshot:** [`1b718f7b`](https://github.com/vllm-project/vllm-omni/commit/1b718f7b1529bf7b59450de2dba0f820ef676dc8)
|
|
> **Upstream vLLM snapshot:** [`fa2a2589`](https://github.com/vllm-project/vllm/commit/fa2a2589bd2a1fce0851df7fd42ffb54b6195f04)
|
|
> **Upstream direction:** [vLLM RFC #48227](https://github.com/vllm-project/vllm/issues/48227) and [vLLM PR #49665](https://github.com/vllm-project/vllm/pull/49665)
|
|
|
|
## Motivation
|
|
|
|
The error fragmentation described in upstream vLLM also exists in vLLM-Omni
|
|
and is amplified by multi-stage execution and additional serving transports.
|
|
In the audited snapshot, `vllm_omni/` contains:
|
|
|
|
- 2,147 `raise ValueError` sites, including 171 in `entrypoints/`;
|
|
- 166 `raise TypeError` sites;
|
|
- 460 `raise RuntimeError` sites;
|
|
- no direct `raise VLLMValidationError` site; and
|
|
- one direct `raise GuardrailViolationError` site.
|
|
|
|
These counts do not imply that every raw exception should become public. They
|
|
show that raw Python types currently represent request validation, internal
|
|
invariants, configuration errors, model failures, and control flow, while
|
|
semantic classification is rare.
|
|
|
|
This causes four concrete problems:
|
|
|
|
1. **Internal failures can be reported as HTTP 400.** Several entrypoints treat
|
|
every `ValueError` as invalid user input:
|
|
|
|
```python
|
|
except OmniClientError as exc:
|
|
raise HTTPException(exc.status_code, exc.message) from exc
|
|
except ValueError as exc:
|
|
raise HTTPException(400, str(exc)) from exc
|
|
except Exception as exc:
|
|
raise HTTPException(500, str(exc)) from exc
|
|
```
|
|
|
|
A `ValueError` from parsing, model execution, output conversion, or
|
|
configuration can therefore be reported as `400 Bad Request` instead of
|
|
`500 Internal Server Error`. The safer default is that only an explicit
|
|
client-error type becomes 4xx.
|
|
|
|
2. **Ownership is guessed after crossing stage boundaries.** Current receivers
|
|
reconstruct a client error when the serialized status is in the 4xx range
|
|
and otherwise construct an engine error. This derives semantic ownership
|
|
from a status chosen earlier, so a wrong classification cannot be recovered.
|
|
Parameter information and nonfatal stage context are also not consistently
|
|
preserved.
|
|
|
|
3. **The same failure has different public contracts.** HTTP routes, SSE,
|
|
WebSocket, speech helpers, and asynchronous video jobs convert failures
|
|
independently. [vLLM-Omni PR #3969](https://github.com/vllm-project/vllm-omni/pull/3969)
|
|
already fixed one result of this fragmentation: voice endpoints returning
|
|
an error object with HTTP 200.
|
|
|
|
4. **Prometheus status metrics are fragile.**
|
|
[vLLM PR #44303](https://github.com/vllm-project/vllm/pull/44303) showed that
|
|
a client can receive 4xx while `http_requests_total` records 5xx when an
|
|
exception crosses the Prometheus middleware before reaching an outer
|
|
catch-all handler. Omni adds its own exceptions and handlers on top of the
|
|
upstream application, so relying on per-type registration can reproduce the
|
|
mismatch. Returning an error model with default HTTP 200 creates the inverse
|
|
problem: failed requests are recorded as 2xx.
|
|
|
|
## Proposed change
|
|
|
|
Adopt upstream vLLM's semantic exception hierarchy for in-process errors and
|
|
add one Omni-owned serializable record for stage/process transport:
|
|
|
|
```text
|
|
semantic VLLMError
|
|
-> normalize to OmniErrorDetails
|
|
-> stage/process transport
|
|
-> one boundary renderer
|
|
-> HTTP / SSE / WebSocket / async job / offline Python
|
|
```
|
|
|
|
The supported vLLM baseline must first contain
|
|
[vLLM PR #49665](https://github.com/vllm-project/vllm/pull/49665), including
|
|
commit `58f96593971b6287cdc8867024a6dfb9985c545c`. vLLM-Omni must import, not copy,
|
|
the upstream hierarchy.
|
|
|
|
### 1. Use semantic exception ownership
|
|
|
|
Generic failures use upstream classes directly:
|
|
|
|
| Failure | Exception | Status |
|
|
| --- | --- | ---: |
|
|
| Invalid or incompatible request parameter | `VLLMValidationError` | 400 |
|
|
| Missing model, LoRA, voice, job, or artifact | `VLLMNotFoundError` | 404 |
|
|
| Permanently inaccessible or undecodable request media | `VLLMUnprocessableEntityError` | 422 |
|
|
| Recoverable per-request engine failure | `EngineGenerateError` | 500 |
|
|
| Unrecoverable engine failure | `EngineDeadError` | 500 |
|
|
| Unknown server failure | `VLLMServerError` or the `Exception` safety net | 500 |
|
|
|
|
Omni adds only types with concrete Omni behavior:
|
|
|
|
| Omni type | Parent | Status | Use |
|
|
| --- | --- | ---: | --- |
|
|
| `GuardrailViolationError` | `VLLMClientError` | 400 | Guardrail rejects request content |
|
|
| `OmniConflictError` | `VLLMClientError` | 409 | Existing async job is in an incompatible state |
|
|
| `OmniNotImplementedError` | `VLLMServerError` | 501 | Invoked server operation is not implemented |
|
|
| `OmniServiceUnavailableError` | `VLLMServerError` | 503 | Required handler, pipeline, or stage is unavailable |
|
|
| `OmniStageTimeoutError` | `VLLMServerError` | 504 | Stage or generation deadline expires |
|
|
| `OmniEngineGenerateError` | `EngineGenerateError` | 500 | Nonfatal stage failure with `stage_id` |
|
|
| `OmniEngineDeadError` | `EngineDeadError` | 500 | Fatal stage failure with `stage_id` |
|
|
|
|
Each new subtype lands with its first production raise site. Existing 403, 413,
|
|
and 429 transport behavior remains compatible; dedicated types are added only
|
|
when a concrete use case and retry policy are defined.
|
|
|
|
`OmniClientError` remains temporarily as a compatibility and remote-error
|
|
wrapper, but is reparented from `ValueError` to `VLLMClientError`. A symmetric
|
|
`OmniServerError(VLLMServerError)` represents server failures reconstructed
|
|
from the wire.
|
|
|
|
### 2. Preserve semantics across stages
|
|
|
|
Introduce one internal serializable record:
|
|
|
|
```python
|
|
@dataclass(frozen=True, slots=True)
|
|
class OmniErrorDetails:
|
|
message: str
|
|
category: Literal["client", "server"]
|
|
status_code: int
|
|
error_type: str
|
|
parameter: str | None = None
|
|
request_id: str | None = None
|
|
stage_id: int | None = None
|
|
fatal: bool = False
|
|
```
|
|
|
|
Required invariants:
|
|
|
|
- client errors have a 4xx status and server errors have a 5xx status;
|
|
- unknown exceptions normalize to server error 500;
|
|
- `fatal=True` is valid only for server errors;
|
|
- public messages and parameter names are sanitized;
|
|
- tracebacks, exception objects, media, tensors, credentials, and rejected
|
|
values are never serialized; and
|
|
- consumers use `category`, not the numeric status range, to reconstruct
|
|
ownership.
|
|
|
|
`ErrorMessage`, `OmniRequestOutput`, `DiffusionOutput`, and stage IPC carry this
|
|
record or its complete field set. Existing flat message/status/type fields
|
|
remain readable for one compatibility window.
|
|
|
|
### 3. Centralize rendering
|
|
|
|
Register one Omni-aware `VLLMError` handler that:
|
|
|
|
- preserves upstream status, type, and parameter mapping;
|
|
- dispatches engine errors through Omni's stage-aware termination behavior;
|
|
- renders Omni-specific 409, 501, 503, and 504 errors centrally; and
|
|
- runs inside the instrumented middleware boundary so HTTP metrics observe the
|
|
actual response status.
|
|
|
|
Temporary handlers for raw `ValueError`, `TypeError`, `OverflowError`, and
|
|
`NotImplementedError` remain during migration. The permanent `Exception`
|
|
safety net always maps unknown failures to 500.
|
|
|
|
Transport rules:
|
|
|
|
- non-streaming HTTP status must equal `error.code`;
|
|
- SSE and WebSocket streams that already sent success headers emit one terminal
|
|
event with the canonical nested `error` object;
|
|
- async jobs store the canonical error and do not guess a new status when
|
|
retrieved; and
|
|
- offline APIs raise the semantic exception or a remote
|
|
`OmniClientError`/`OmniServerError` carrying `.details`.
|
|
|
|
Routes must not return a Pydantic `ErrorResponse` without an explicit HTTP
|
|
status.
|
|
|
|
### 4. Migrate incrementally
|
|
|
|
1. **Baseline:** add table-driven mapping, middleware metrics, transport, and
|
|
engine-lifecycle regression tests for current behavior.
|
|
2. **Bridge:** adopt the required vLLM baseline, reparent compatibility types,
|
|
add `OmniErrorDetails`, and register the semantic handler.
|
|
3. **Request boundaries:** migrate project-owned validation one raise site at a
|
|
time to `VLLMValidationError`, `VLLMNotFoundError`, or
|
|
`VLLMUnprocessableEntityError`. Catch `VLLMClientError`, not `ValueError`.
|
|
4. **Stage and transports:** preserve stage context, standardize HTTP/SSE/
|
|
WebSocket/job rendering, then remove raw fallbacks only when their audited
|
|
inventories reach zero or an explicit allowlist.
|
|
|
|
This is not a mechanical replacement of every `ValueError`. Each site must be
|
|
reviewed to distinguish user-correctable input from internal failure.
|
|
|
|
## Compatibility and correctness
|
|
|
|
Correctly typed errors retain their current status and OpenAI-compatible
|
|
envelope. Intentional corrections may change:
|
|
|
|
- an internal raw `ValueError` from 400 to 500;
|
|
- invalid or inaccessible media from 400/500 to 422;
|
|
- missing resources to 404;
|
|
- conflicting async-job operations to 409; and
|
|
- unavailable pipelines or stages to 503.
|
|
|
|
Every status change requires an endpoint regression test and release note.
|
|
|
|
Reparenting `OmniClientError` means external Python code that catches only
|
|
`ValueError` will no longer catch it. Retain its import and constructor for at
|
|
least one release, document the inheritance change, and recommend catching
|
|
`VLLMClientError` or a specific subtype.
|
|
|
|
Minimum test coverage:
|
|
|
|
- every upstream and Omni exception-to-status/type mapping;
|
|
- unknown `ValueError` becoming 500 after its fallback is removed;
|
|
- wire round trips preserving category, parameter, stage, and fatality;
|
|
- HTTP status matching `error.code`;
|
|
- SSE, WebSocket, and async-job terminal errors;
|
|
- `http_requests_total` distinguishing 4xx and 5xx;
|
|
- nonfatal stage failure affecting one request;
|
|
- fatal stage failure retaining existing termination behavior; and
|
|
- public errors excluding tracebacks, paths, secrets, and multimodal payloads.
|
|
|
|
## Non-goals
|
|
|
|
- Copying or forking upstream generic exception classes.
|
|
- Converting every model/kernel exception in one change.
|
|
- Making control-flow cancellation a public server error.
|
|
- Redesigning endpoint ownership, retry policy, or fatal OOM lifecycle.
|
|
- Adding speculative exception subclasses without a production raise site.
|
|
|
|
## Open questions
|
|
|
|
1. Should the generic 409, 501, 503, and 504 types eventually move upstream
|
|
after Omni validates their use cases?
|
|
2. Should `request_id` and `stage_id` become shared upstream response fields?
|
|
3. How long must mixed-version stage processes support the legacy wire fields?
|
|
4. For failed async jobs, should metadata retrieval return the stored error
|
|
status or HTTP 200 with a canonical embedded error?
|
|
|
|
## Community help wanted
|
|
|
|
We welcome community help with:
|
|
|
|
- validating the proposed status and exception mapping;
|
|
- identifying representative raw-exception sites for the first migrations;
|
|
- reviewing mixed-version and multi-node wire compatibility;
|
|
- adding mapping, middleware-metrics, stage round-trip, and transport tests; and
|
|
- coordinating the required vLLM baseline update.
|
|
|
|
If you would like to help, please comment with the endpoint, stage, or test area
|
|
you want to own. After the feedback period, implementation can be split into
|
|
small, independently reviewable PRs following the migration phases above.
|
|
|
|
## Feedback period and CC
|
|
|
|
Feedback is requested for two weeks after the RFC issue is published.
|
|
|
|
CC: maintainers for entrypoints, engine/orchestrator, diffusion, metrics, and
|
|
the upstream vLLM error hierarchy.
|
|
|
|
## References
|
|
|
|
- [vLLM PR #44303: Fix 4xx errors recorded as 5xx in `http_requests_total`](https://github.com/vllm-project/vllm/pull/44303)
|
|
- [vLLM RFC #48227: Standardize vLLM Entrypoint Error Handling](https://github.com/vllm-project/vllm/issues/48227)
|
|
- [vLLM PR #49665: Standardize request error handling with `VLLMError`](https://github.com/vllm-project/vllm/pull/49665)
|
|
- [vLLM-Omni PR #3969: Correct HTTP status for voice errors](https://github.com/vllm-project/vllm-omni/pull/3969)
|
|
- [vLLM-Omni PR #4297: Guardrail client-error propagation](https://github.com/vllm-project/vllm-omni/pull/4297)
|
|
- [vLLM-Omni RFC #1346: Exit on OOM](https://github.com/vllm-project/vllm-omni/issues/1346)
|
|
- [vLLM-Omni RFC #5198: Broad exception cleanup](https://github.com/vllm-project/vllm-omni/issues/5198)
|
|
- [vLLM-Omni RFC #5227: Entrypoint and API-server refactor](https://github.com/vllm-project/vllm-omni/issues/5227)
|