feat: add compile-time OpenAPI spec generation via utoipa - #229
feat: add compile-time OpenAPI spec generation via utoipa#229avinashsingh77 wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
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
openapifeature (default-on) inagentic-serverand propagates anopenapifeature intoagentic-server-corefor schema derives. - Adds
utoipa::pathannotations across all HTTP handlers and a newopenapimodule that builds the OpenAPI doc + security scheme + Swagger UI router. - Adds integration-style tests that fetch
/openapi.jsonand/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
CodexNamespaceMemberis also#[serde(tag = "type")], but this schema only referencesFunctionToolParamand does not include the requiredtypediscriminator property (nor theunknownvariant). This makes the generated OpenAPI schema inaccurate for clients consumingCodexNamespaceToolParam.tools. Recommend emitting an object schema that includestypeand the variant payload (e.g.,allOfwith{type: "function"}+FunctionToolParam, plus anunknownfallback 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.
4b1e085 to
bd6c928
Compare
|
@maralbahari Thanks for the valuable review. Addressed all of them. |
thanks. kindly fix the CI for approval |
Done. Fixed remaining rustfmt issues due to rustup version mismatch. |
maralbahari
left a comment
There was a problem hiding this comment.
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 buildon 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.
|
@avinashsingh77 could you please resolve merge conflicts. Thanks |
ffdfe08 to
5518c9a
Compare
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>
d3859ad to
3b7a7c0
Compare
|
@maralbahari Please review and approve. |
franciscojavierarceo
left a comment
There was a problem hiding this comment.
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 = [] |
There was a problem hiding this comment.
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"), |
There was a problem hiding this comment.
/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()) |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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")) |
There was a problem hiding this comment.
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()) |
There was a problem hiding this comment.
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.
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 enabledcargo check --no-default-features— compiles with OpenAPI disabled (zero-cost gate)cargo clippy --features openapi --all-targets— zero warningscargo test --features openapi— all tests passcargo test --no-default-features— all tests pass without the featurespec_contains_expected_paths— all 8 endpoint paths presentspec_contains_core_schemas— core schemas registeredspec_has_security_scheme—bearer_authscheme presentspec_serializes_to_valid_json— valid OpenAPI 3.x JSONspec_schema_refs_resolve— all$refpointers resolvespec_validates_against_openapi_3_1_meta_schema— formal validation against OpenAPI 3.1 meta-schemaGET /openapi.jsonreturns valid spec with all paths/schemasGET /swagger-ui/returns HTMRelated #218