Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .beads/issues.jsonl

Large diffs are not rendered by default.

43 changes: 43 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,17 @@ jobs:

openai-sdk-compat:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
workspaces: |
. -> target
examples/server-openai-responses -> target
- uses: actions/setup-python@v5
with:
python-version: "3.12"
Expand All @@ -50,6 +55,34 @@ jobs:
official_openai_python_sdk_matches_generated_axum_server --
--ignored --exact --nocapture

anthropic-sdk-compat:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
workspaces: |
. -> target
examples/server-anthropic-messages -> target
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
cache-dependency-path: tests/python/anthropic_sdk_compat/requirements.txt
- name: Install pinned official Anthropic Python SDK
run: python -m pip install --requirement tests/python/anthropic_sdk_compat/requirements.txt
- name: Test official SDK against generated Axum server
env:
ANTHROPIC_COMPAT_PYTHON: python
run: >-
cargo test --test anthropic_sdk_compat_test
official_anthropic_python_sdk_matches_generated_axum_server --
--ignored --exact --nocapture

clippy:
runs-on: ubuntu-latest
steps:
Expand Down Expand Up @@ -86,6 +119,10 @@ jobs:
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: scripts/install-smoke.sh
env:
# Keep cargo-install artifacts inside the target tree restored by
# rust-cache; the script still isolates and removes its package root.
CARGO_TARGET_DIR: ${{ github.workspace }}/target/install-smoke

msrv:
runs-on: ubuntu-latest
Expand All @@ -103,6 +140,10 @@ jobs:
spec-compile:
if: github.event_name != 'schedule' && github.event_name != 'workflow_dispatch'
runs-on: ubuntu-latest
env:
# Share scratch-crate dependencies across runs through rust-cache's
# root-workspace target directory.
SPEC_COMPILE_TARGET_DIR: ${{ github.workspace }}/target/spec-compile
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
Expand All @@ -114,6 +155,8 @@ jobs:
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
timeout-minutes: 240
env:
SPEC_COMPILE_TARGET_DIR: ${{ github.workspace }}/target/spec-compile
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
Expand Down
33 changes: 32 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,36 @@ All notable changes are recorded here. This project follows semantic versioning,
with one pre-1.0 qualification: a minor release may change generated Rust APIs
when correcting output that was wrong or incomplete on the wire.

## [0.10.0] - 2026-07-26

### Added

- Every-PR compatibility coverage for the generated Anthropic Messages server
through the pinned official Python SDK, including unary and SSE responses.

### Changed

- Regenerated server response enums now use the declared status in bodyless and
SSE variant names and require a runtime status for wildcard/default variants.
This is a source-breaking correction for existing server trait implementations.

### Fixed

- Config-driven `server list` and `server add` now apply
`generator.schema_extensions`, so overlay-provided operations and SSE media
types match generation.
- Schema extensions accept the documented JSON, YAML, and YML formats with
path-rich parse errors.
- Generated server response enums retain reusable Response Object references,
including structurally compatible local refs stored outside
`components.responses`, plus bodyless status codes, vendor/problem JSON media
types, SSE status codes, and runtime status values for wildcard/default
responses.
- Server generation rejects response sets that contain only unsupported media
types and reports normalized Rust identifier collisions between distinct tags.
- Server example tests use Cargo's current integration-test binary instead of
a potentially stale hard-coded `target/debug` executable.

## [0.9.1] - 2026-07-26

### Fixed
Expand Down Expand Up @@ -152,7 +182,8 @@ when correcting output that was wrong or incomplete on the wire.
signed enum values, recursive unions, parameter collisions, optional request
bodies, range response codes, and path-segment encoding.

[Unreleased]: https://github.com/gpu-cli/openapi-to-rust/compare/v0.9.1...HEAD
[Unreleased]: https://github.com/gpu-cli/openapi-to-rust/compare/v0.10.0...HEAD
[0.10.0]: https://github.com/gpu-cli/openapi-to-rust/compare/v0.9.1...v0.10.0
[0.9.1]: https://github.com/gpu-cli/openapi-to-rust/compare/v0.9.0...v0.9.1
[0.9.0]: https://github.com/gpu-cli/openapi-to-rust/compare/v0.8.0...v0.9.0
[0.8.0]: https://github.com/gpu-cli/openapi-to-rust/compare/v0.7.0...v0.8.0
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ exclude = ["tmp"]

[package]
name = "openapi-to-rust"
version = "0.9.1"
version = "0.10.0"
edition = "2024"
rust-version = "1.88.0"
autobins = false
Expand Down
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ Two complete examples are in the repo:
| `streaming.rs` | SSE streaming **client** with event parsing (when configured) |
| `server/mod.rs` | Module re-exports for the server (when `[server]` is set) |
| `server/api.rs` | `trait <Tag>Api { async fn <op>(&self, …) -> <Op>Response; }` per tag |
| `server/errors.rs` | `enum <Op>Response { Ok(T), BadRequest(E), …, OkStream(Sse<…>) }` with `IntoResponse` |
| `server/errors.rs` | Status-typed response enums with JSON, bodyless, wildcard/default, and status-correct SSE variants plus `IntoResponse` |
| `server/router.rs` | Per-tag `Router` factory; combined `build_router<…>(…)` for multi-tag selections |
| `server/validation.rs` | Offline JSON Schema validators and sanitized Problem Details rejections |
| `mod.rs` | Module declarations + re-exports |
Expand Down Expand Up @@ -665,10 +665,11 @@ The compile tiers are intentionally different:
OpenAPI specs. The bundled Gitea Swagger 2.0 document is reported as skipped.
- Local `scripts/spec-compile.sh` runs the same full tier; pass spec names as
arguments for a smaller targeted run.
- Every pull request also starts the generated OpenAI Responses Axum example
on loopback and exercises it through the pinned official OpenAI Python SDK.
The compatibility gate covers unary and streaming Responses, input-item
path/query parameters, and the organization costs query.
- Every pull request also starts the generated OpenAI Responses and Anthropic
Messages Axum examples on loopback and exercises them through pinned official
Python SDKs. The compatibility gates cover unary and streaming responses,
with the OpenAI row also covering input-item path/query parameters and the
organization costs query.

## Examples

Expand Down
2 changes: 2 additions & 0 deletions examples/server-anthropic-messages/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ version = "0.0.0"
edition = "2021"
publish = false

[workspace]

[dependencies]
async-trait = "0.1"
axum = "0.8"
Expand Down
20 changes: 12 additions & 8 deletions examples/server-anthropic-messages/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,18 @@ cargo run --manifest-path examples/server-anthropic-messages/Cargo.toml
curl -s http://127.0.0.1:3001/v1/messages \
-H 'content-type: application/json' \
-d '{"model":"claude-x","max_tokens":50,"messages":[{"role":"user","content":"hi"}]}'

# Stream the response as server-sent events:
curl -N -s http://127.0.0.1:3001/v1/messages \
-H 'content-type: application/json' \
-d '{"model":"claude-x","max_tokens":50,"stream":true,"messages":[{"role":"user","content":"hi"}]}'
```

**Streaming caveat.** Anthropic's published OpenAPI spec doesn't
declare `text/event-stream` on the 200 response, so the generator
can't emit an `OkStream` variant. Tracked as `openapi-generator-in6`
— the planned fix is a schema-extension overlay that adds the
streaming content type, after which this example will gain a
streaming branch with no manual changes.
Anthropic's published OpenAPI spec does not declare `text/event-stream` on the
200 response. The example config applies `sse-overlay.json` through
`generator.schema_extensions`, adding that response media type before analysis
so generation emits both `Ok(Message)` and `OkStream` response variants.

The integration test in `tests/server_examples_test.rs` guarantees
the example keeps building.
The integration test in `tests/server_examples_test.rs` regenerates and tests
the example. PR CI additionally calls the generated server with the pinned
official Anthropic Python SDK, covering both unary and streaming responses.
24 changes: 19 additions & 5 deletions examples/server-anthropic-messages/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ use futures_util::stream;
use gen::CreateMessageParams;
use gen::server::{MessagesPostResponse, ServerApi, server_api_router, sse_response};
use std::convert::Infallible;
use std::io::Write;

#[derive(Clone)]
struct AppState;
Expand Down Expand Up @@ -62,7 +63,7 @@ fn messages_unary() -> MessagesPostResponse {
model: gen::Model::Custom("claude-demo".into()),
role: gen::MessageRole::Assistant,
stop_details: None,
stop_reason: None,
stop_reason: Some(gen::StopReason::EndTurn),
stop_sequence: None,
r#type: gen::MessageType::Message,
usage: gen::Usage {
Expand All @@ -71,7 +72,7 @@ fn messages_unary() -> MessagesPostResponse {
cache_read_input_tokens: None,
inference_geo: None,
input_tokens: 0,
output_tokens: 0,
output_tokens: 2,
server_tool_use: None,
service_tier: None,
},
Expand All @@ -86,7 +87,10 @@ fn messages_streaming() -> MessagesPostResponse {
// subset; production code mirrors the full sequence from the
// upstream model.
let events = stream::iter(vec![
sse_event("message_start", r#"{"type":"message_start","message":{"id":"msg_demo"}}"#),
sse_event(
"message_start",
r#"{"type":"message_start","message":{"id":"msg_demo","type":"message","role":"assistant","model":"claude-demo","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}}"#,
),
sse_event(
"content_block_start",
r#"{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}"#,
Expand All @@ -100,6 +104,10 @@ fn messages_streaming() -> MessagesPostResponse {
r#"{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"world"}}"#,
),
sse_event("content_block_stop", r#"{"type":"content_block_stop","index":0}"#),
sse_event(
"message_delta",
r#"{"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":2}}"#,
),
sse_event("message_stop", r#"{"type":"message_stop"}"#),
]);
MessagesPostResponse::OkStream(sse_response(events))
Expand All @@ -112,8 +120,14 @@ fn sse_event(name: &str, data: &str) -> Result<Event, Infallible> {
#[tokio::main]
async fn main() {
let app = server_api_router(AppState);
let listener = tokio::net::TcpListener::bind("127.0.0.1:3001").await.unwrap();
println!("listening on http://{}", listener.local_addr().unwrap());
let bind_address =
std::env::var("OPENAPI_SERVER_ADDR").unwrap_or_else(|_| "127.0.0.1:3001".to_string());
let listener = tokio::net::TcpListener::bind(&bind_address).await.unwrap();
println!(
"OPENAPI_SERVER_URL=http://{}",
listener.local_addr().unwrap()
);
std::io::stdout().flush().unwrap();
axum::serve(listener, app).await.unwrap();
}

Expand Down
Loading
Loading