Skip to content

feat: add compile-time OpenAPI spec generation via utoipa - #229

Open
avinashsingh77 wants to merge 1 commit into
vllm-project:mainfrom
avinashsingh77:218_openapi_docs
Open

feat: add compile-time OpenAPI spec generation via utoipa#229
avinashsingh77 wants to merge 1 commit into
vllm-project:mainfrom
avinashsingh77:218_openapi_docs

Conversation

@avinashsingh77

@avinashsingh77 avinashsingh77 commented Sep 1, 2026

Copy link
Copy Markdown

Summary

Adds a feature-gated (openapi, default-on) OpenAPI 3.1 spec served at GET /openapi.json, covering all 8 HTTP endpoints with request/response schemas, Bearer auth, and error envelopes.

~40 types auto-derive ToSchema; ~15 complex types (untagged enums, custom serde) get manual PartialSchema + ToSchema impls.

Zero-cost when the feature is disabled.

Test Plan

  • cargo check --features openapi — compiles with OpenAPI enabled
  • cargo check --no-default-features — compiles with OpenAPI disabled (zero-cost gate)
  • cargo clippy --features openapi --all-targets — zero warnings
  • cargo test --features openapi — all tests pass
  • cargo test --no-default-features — all tests pass without the feature
  • spec_contains_expected_paths — all 8 endpoint paths present
  • spec_contains_core_schemas — core schemas registered
  • spec_has_security_schemebearer_auth scheme present
  • spec_serializes_to_valid_json — valid OpenAPI 3.x JSON
  • spec_schema_refs_resolve — all $ref pointers resolve
  • spec_validates_against_openapi_3_1_meta_schema — formal validation against OpenAPI 3.1 meta-schema
  • Integration test: GET /openapi.json returns valid spec with all paths/schemas
  • Swagger UI: GET /swagger-ui/ returns HTM

Related #218

Comment thread crates/agentic-server/src/handler/http/responses.rs Outdated
Comment thread crates/agentic-server/src/handler/http/messages.rs Outdated
Comment thread crates/agentic-server/src/handler/http/messages.rs Outdated
Comment thread crates/agentic-server-core/src/types/io/input.rs Outdated
Comment thread crates/agentic-server-core/src/types/io/input.rs
Comment thread crates/agentic-server-core/src/types/tools/params.rs
Comment thread crates/agentic-server/src/handler/http/conversations.rs Outdated
Comment thread crates/agentic-server/src/openapi.rs Outdated
Comment thread crates/agentic-server/src/app.rs Outdated
Comment thread crates/agentic-server/tests/openapi_test.rs

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds feature-gated OpenAPI 3.1 generation (via utoipa) to agentic-server, exposing the generated spec at GET /openapi.json and wiring a Swagger UI under /swagger-ui/, while annotating all HTTP handlers and a large set of core wire types with schema metadata.

Changes:

  • Introduces openapi feature (default-on) in agentic-server and propagates an openapi feature into agentic-server-core for schema derives.
  • Adds utoipa::path annotations across all HTTP handlers and a new openapi module that builds the OpenAPI doc + security scheme + Swagger UI router.
  • Adds integration-style tests that fetch /openapi.json and /swagger-ui/ and validate basic spec structure.

Reviewed changes

Copilot reviewed 23 out of 24 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
crates/agentic-server/tests/openapi_test.rs Adds runtime tests for /openapi.json and /swagger-ui/.
crates/agentic-server/src/openapi.rs Defines ApiDoc OpenAPI doc, security modifier, error schemas, Swagger UI router, and spec-shape unit tests.
crates/agentic-server/src/lib.rs Conditionally exports the new openapi module behind the feature gate.
crates/agentic-server/src/handler/http/responses.rs Adds utoipa::path metadata for /v1/responses and /v1/responses/compact.
crates/agentic-server/src/handler/http/models.rs Adds utoipa::path metadata for /health, /ready, and /v1/models.
crates/agentic-server/src/handler/http/mod.rs Makes HTTP submodules pub(crate) so OpenAPI path references can resolve.
crates/agentic-server/src/handler/http/messages.rs Adds utoipa::path metadata for /v1/messages and /v1/messages/count_tokens.
crates/agentic-server/src/handler/http/conversations.rs Adds utoipa::path metadata for /v1/conversations.
crates/agentic-server/src/app.rs Merges Swagger UI/OpenAPI router into public routes when openapi is enabled.
crates/agentic-server/Cargo.toml Adds openapi feature (default-on) and optional utoipa* dependencies.
crates/agentic-server-core/src/types/tools/params.rs Adds OpenAPI schema support for tool parameter types (including manual schemas).
crates/agentic-server-core/src/types/request_response.rs Adds ToSchema derives for Responses request/response payload types.
crates/agentic-server-core/src/types/messages/request.rs Adds ToSchema derives and manual schemas for untagged / complex message request shapes.
crates/agentic-server-core/src/types/messages/mod.rs Re-exports additional message types needed by schema registration.
crates/agentic-server-core/src/types/io/usage.rs Adds ToSchema derives for usage structs.
crates/agentic-server-core/src/types/io/tools.rs Adds manual OpenAPI schema for ToolChoice and derives for related tool-choice types.
crates/agentic-server-core/src/types/io/output.rs Adds ToSchema derives and manual schemas for output event item types.
crates/agentic-server-core/src/types/io/mod.rs Re-exports additional IO types needed by schema registration.
crates/agentic-server-core/src/types/io/input.rs Adds ToSchema derives and manual schemas for input item/content unions.
crates/agentic-server-core/src/types/event.rs Adds ToSchema derives for status enums used in schemas.
crates/agentic-server-core/Cargo.toml Adds openapi feature and optional utoipa dependency.
Cargo.toml Adds workspace deps for utoipa, utoipa-axum, and utoipa-swagger-ui.
Cargo.lock Locks transitive deps introduced by the new OpenAPI/Swagger UI crates.
Suppressed comments (1)

crates/agentic-server-core/src/types/tools/params.rs:313

  • CodexNamespaceMember is also #[serde(tag = "type")], but this schema only references FunctionToolParam and does not include the required type discriminator property (nor the unknown variant). This makes the generated OpenAPI schema inaccurate for clients consuming CodexNamespaceToolParam.tools. Recommend emitting an object schema that includes type and the variant payload (e.g., allOf with {type: "function"} + FunctionToolParam, plus an unknown fallback if desired).
impl utoipa::PartialSchema for CodexNamespaceMember {
    fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
        utoipa::openapi::schema::OneOfBuilder::new()
            .discriminator(Some(utoipa::openapi::schema::Discriminator::new("type")))
            .item(utoipa::openapi::Ref::from_schema_name("FunctionToolParam"))
            .into()

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread crates/agentic-server-core/src/types/tools/params.rs Outdated
Comment thread crates/agentic-server-core/src/types/io/tools.rs
Comment thread crates/agentic-server-core/src/types/io/input.rs
Comment thread crates/agentic-server-core/src/types/io/output.rs
@avinashsingh77
avinashsingh77 force-pushed the 218_openapi_docs branch 2 times, most recently from 4b1e085 to bd6c928 Compare September 2, 2026 10:09
@avinashsingh77

Copy link
Copy Markdown
Author

@maralbahari Thanks for the valuable review. Addressed all of them.
PTAL again

@maralbahari

Copy link
Copy Markdown
Collaborator

@avinashsingh77

@maralbahari Thanks for the valuable review. Addressed all of them. PTAL again

thanks. kindly fix the CI for approval

@avinashsingh77

Copy link
Copy Markdown
Author

@avinashsingh77

@maralbahari Thanks for the valuable review. Addressed all of them. PTAL again

thanks. kindly fix the CI for approval

Done. Fixed remaining rustfmt issues due to rustup version mismatch.

@maralbahari maralbahari left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the work on this.
Left four more inline comments. Two bits of context on where they came from:

  • I ran a local cargo build on the branch and hit a build-time network fetch pulled in by the Swagger UI dependency. That is the one I would want sorted before merge.
  • #214 landed after you opened this, and the rebase left a couple of the hand-written schemas slightly behind the types they describe.

The rest are small.

Comment thread Cargo.toml Outdated
Comment thread crates/agentic-server-core/src/types/io/input.rs
Comment thread crates/agentic-server-core/src/types/io/input.rs
Comment thread crates/agentic-server-core/src/types/tools/params.rs Outdated
@maralbahari

Copy link
Copy Markdown
Collaborator

@avinashsingh77 could you please resolve merge conflicts. Thanks

Add feature-gated (`openapi`) OpenAPI 3.1 spec served at /openapi.json
with Swagger UI at /swagger-ui/. Covers all HTTP endpoints (Responses,
Messages, Conversations, health/ready, models) with full request/response
schemas including tagged enums, serde aliases, and untagged unions.

Includes 9 unit tests validating path coverage, schema ref resolution,
OpenAPI 3.1 meta-schema conformance, and per-variant fixture validation
for InputItem, OutputItem, and ResponsesTool.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Avinash Singh <avinashsingh.rcoem@gmail.com>
@avinashsingh77

Copy link
Copy Markdown
Author

@maralbahari Please review and approve.
Sorry, it took a while to rebase and fix conflicts.
Squashed commits into one.

@franciscojavierarceo franciscojavierarceo left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the feature currently isn't exercised by CI, and several published schemas do not match request and response shapes that the runtime and existing tests accept. requesting changes so the generated contract is usable by clients and tested whenever openapi is enabled.

repository.workspace = true

[features]
default = []

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the server's default feature set is empty, so the normal Docker build and Rust CI compile this feature out; openapi_test.rs is also entirely gated on openapi. that leaves the new implementation absent from the standard image and uncompiled by required checks. we should enable openapi explicitly in the release build and CI feature matrix, or make it a default feature, and update the PR description to match.

post,
path = "/v1/messages/count_tokens",
responses(
(status = 200, description = "Token count result"),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/v1/messages/count_tokens consumes and proxies a request body, but this operation declares no request_body and no schema for the 200 JSON body. generated clients therefore cannot model a valid call or decode its result. we should add typed request and response schemas, or at minimum explicit free-form JSON schemas for this transparent proxy surface; the same applies to the unit JSON response on /v1/messages and the untyped /v1/models response.

.required("model")
.property("input", Ref::from_schema_name("ResponsesInput"))
.required("input")
.property("instructions", str_type())

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RequestPayload's Option fields are documented as non-null strings, numbers, and arrays. serde accepts explicit null, and several fields also serialize None as null, so the published schema rejects valid wire values. we should make every optional field's schema nullable and add a serde-output-to-schema fixture.

#[cfg(feature = "openapi")]
impl utoipa::PartialSchema for McpCallError {
fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
utoipa::openapi::schema::OneOfBuilder::new()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

McpCallError uses oneOf for both McpToolExecutionError and a generic object. a structured tool-execution error matches both branches, so it fails oneOf; Unknown(Value) also accepts arrays, scalars, and null that match neither object branch. we should use non-overlapping branches or anyOf and add fixtures for the typed and arbitrary forms.

pub message: String,
#[serde(rename = "type")]
pub error_type: String,
pub code: String,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ApiError.code is a required string here, but ToolError::MissingOutput intentionally emits "code": null and has a unit test locking that behavior. the generated error schema therefore rejects a real 400 response. we should make code nullable and validate the existing error fixture against ApiErrorResponse.

/// Request body for POST /v1/conversations.
#[derive(serde::Serialize, serde::Deserialize, utoipa::ToSchema)]
pub struct CreateConversationRequest {
pub store: bool,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

store: bool makes this property required, while the handler and existing integration tests accept {} or no body and default store to true. generated clients become stricter than the endpoint. we should model the body and property as optional with a default of true, or tighten the handler and tests to this contract.

fn schema() -> RefOr<Schema> {
OneOfBuilder::new()
.discriminator(Some(utoipa::openapi::schema::Discriminator::new("type")))
.item(tagged_ref("message", "InputMessage"))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tagged_ref makes type: "message" required, but the custom deserializer treats a missing type as a message and existing tests exercise {"role":"user","content":"history"}. the schema currently rejects that supported shorthand. we should make the message discriminator optional and add the shorthand to the schema-validation fixtures.

ObjectBuilder::new()
.property("type", str_type().enum_values(Some(["function"])))
.required("type")
.property("name", str_type())

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the schema permits empty name values even though NonEmptyToolName rejects them, and it omits the legacy {"function":{"name":"..."}} form that Deserialize still accepts. validators diverge from the runtime in both directions. we should reuse the NonEmptyToolName schema and add the legacy object branch, with positive and negative fixtures.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants