From a02148aacd384ec4cca73e01932d7da31047ff2e Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Fri, 3 Apr 2026 11:17:27 -0700 Subject: [PATCH] Rename fabro-api-types to fabro-api with progenitor client generation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace typify-only type generation with progenitor, which generates both Rust types (in a `types` module) and a reqwest-based HTTP client from the OpenAPI spec. Also upgrades reqwest 0.12→0.13 and rmcp 0.15→1.3 to align dependency versions. Co-Authored-By: Claude Opus 4.6 (1M context) --- AGENTS.md | 6 +- Cargo.lock | 386 ++++++++++++++++-- Cargo.toml | 4 +- docs/api-reference/client-sdks.mdx | 21 +- lib/crates/fabro-api-types/build.rs | 50 --- lib/crates/fabro-api-types/src/lib.rs | 5 - .../{fabro-api-types => fabro-api}/Cargo.toml | 10 +- lib/crates/fabro-api/build.rs | 105 +++++ lib/crates/fabro-api/src/lib.rs | 13 + lib/crates/fabro-mcp/src/client.rs | 10 +- lib/crates/fabro-mcp/src/client_handler.rs | 18 +- .../fabro-mcp/src/connection_manager.rs | 34 +- lib/crates/fabro-server/Cargo.toml | 2 +- lib/crates/fabro-server/src/demo/mod.rs | 35 +- lib/crates/fabro-server/src/server.rs | 97 +++-- lib/crates/fabro-server/src/sessions.rs | 88 ++-- 16 files changed, 617 insertions(+), 267 deletions(-) delete mode 100644 lib/crates/fabro-api-types/build.rs delete mode 100644 lib/crates/fabro-api-types/src/lib.rs rename lib/crates/{fabro-api-types => fabro-api}/Cargo.toml (71%) create mode 100644 lib/crates/fabro-api/build.rs create mode 100644 lib/crates/fabro-api/src/lib.rs diff --git a/AGENTS.md b/AGENTS.md index 8b19e8fe4..a714d7710 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -40,7 +40,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co The OpenAPI spec at `docs/api-reference/fabro-api.yaml` is the source of truth for the fabro-api HTTP interface. 1. Edit `docs/api-reference/fabro-api.yaml` -2. `cargo build -p fabro-api-types` — build.rs regenerates Rust types via typify +2. `cargo build -p fabro-api` — build.rs regenerates Rust types and client via progenitor 3. Write/update handler in `lib/crates/fabro-server/src/server.rs`, add route to `build_router()` 4. `cargo nextest run -p fabro-server` — conformance test catches spec/router drift 5. `cd lib/packages/fabro-api-client && bun run generate` — regenerates TypeScript Axios client @@ -55,7 +55,7 @@ Fabro is an AI-powered workflow orchestration platform. Workflows are defined as - **fabro-agent** — AI coding agent with tool use (Bash, Read, Write, Edit, Glob, Grep, WebFetch). `Sandbox` trait abstracts execution environments - **fabro-server** — Axum HTTP server. Routes for runs, sessions, models, completions, usage. SSE event streaming. Demo mode via header - **fabro-llm** — Unified LLM client with providers: Anthropic, OpenAI, Gemini, OpenAI-compatible, plus retry/middleware/streaming -- **fabro-api-types** — Auto-generated Rust types from OpenAPI spec (build.rs + typify) +- **fabro-api** — Auto-generated Rust types and reqwest HTTP client from OpenAPI spec (build.rs + progenitor) - **fabro-github** — GitHub App auth (JWT signing, installation tokens, PR creation) - **fabro-db** — SQLite with WAL mode, schema migrations - **fabro-mcp** — Model Context Protocol client/server @@ -72,7 +72,7 @@ Fabro is an AI-powered workflow orchestration platform. Workflows are defined as ### Key design patterns - **Sandbox trait** — Uniform interface for local, Docker, and Daytona execution environments - **Graphviz graph workflows** — Stages and transitions defined as Graphviz graph attributes -- **OpenAPI-first** — `fabro-api.yaml` drives both Rust type generation (typify) and TypeScript client generation (openapi-generator) +- **OpenAPI-first** — `fabro-api.yaml` drives Rust type + client generation (progenitor) and TypeScript client generation (openapi-generator) - **Checkpoint/resume** — Workflows can be paused, checkpointed, and resumed ## Strategy docs diff --git a/Cargo.lock b/Cargo.lock index bb20082c2..095c67621 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -650,6 +650,12 @@ dependencies = [ "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cfg-if" version = "1.0.4" @@ -766,6 +772,16 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -1156,7 +1172,7 @@ name = "daytona-api-client" version = "0.1.0" source = "git+https://github.com/brynary/daytona-sdk-rust?rev=06033ca#06033caaf5d9e12918ae68396048c5c0a98822c6" dependencies = [ - "reqwest", + "reqwest 0.12.28", "reqwest-middleware", "serde", "serde_json", @@ -1173,7 +1189,7 @@ dependencies = [ "daytona-api-client", "daytona-toolbox-client", "futures-util", - "reqwest", + "reqwest 0.12.28", "reqwest-middleware", "serde", "serde_json", @@ -1189,7 +1205,7 @@ name = "daytona-toolbox-client" version = "0.1.0" source = "git+https://github.com/brynary/daytona-sdk-rust?rev=06033ca#06033caaf5d9e12918ae68396048c5c0a98822c6" dependencies = [ - "reqwest", + "reqwest 0.12.28", "reqwest-middleware", "serde", "serde_json", @@ -1528,7 +1544,7 @@ dependencies = [ "jsonschema", "libc", "paste", - "reqwest", + "reqwest 0.13.2", "serde", "serde_json", "shell-escape", @@ -1541,17 +1557,19 @@ dependencies = [ ] [[package]] -name = "fabro-api-types" +name = "fabro-api" version = "0.176.2" dependencies = [ "chrono", + "openapiv3", "prettyplease", - "schemars 0.8.22", + "progenitor", + "progenitor-client", + "reqwest 0.13.2", "serde", "serde_json", "serde_yaml", "syn 2.0.117", - "typify", "uuid", ] @@ -1624,7 +1642,7 @@ dependencies = [ "predicates", "rand 0.8.5", "regex", - "reqwest", + "reqwest 0.13.2", "rustls", "rustls-pemfile", "scopeguard", @@ -1694,7 +1712,7 @@ version = "0.176.2" dependencies = [ "fabro-util", "insta", - "reqwest", + "reqwest 0.13.2", "serde", "serde_json", "serde_yaml", @@ -1713,7 +1731,7 @@ dependencies = [ "fabro-macros", "fabro-test", "jsonwebtoken", - "reqwest", + "reqwest 0.13.2", "serde", "serde_json", "tokio", @@ -1745,7 +1763,7 @@ dependencies = [ "fabro-util", "httpmock", "regex", - "reqwest", + "reqwest 0.13.2", "serde", "serde_json", "tokio", @@ -1789,7 +1807,7 @@ dependencies = [ "indicatif", "insta", "rand 0.8.5", - "reqwest", + "reqwest 0.13.2", "serde", "serde_json", "thiserror 2.0.18", @@ -1817,7 +1835,7 @@ dependencies = [ "fabro-config", "fabro-types", "futures", - "reqwest", + "reqwest 0.13.2", "rmcp", "serde", "serde_json", @@ -1844,7 +1862,7 @@ dependencies = [ "httpmock", "open", "rand 0.8.5", - "reqwest", + "reqwest 0.13.2", "serde", "serde_json", "sha2", @@ -1924,7 +1942,7 @@ dependencies = [ "cookie", "dirs", "fabro-agent", - "fabro-api-types", + "fabro-api", "fabro-config", "fabro-db", "fabro-github", @@ -1950,7 +1968,7 @@ dependencies = [ "object_store", "openapiv3", "rand 0.8.5", - "reqwest", + "reqwest 0.13.2", "rust-embed", "rustls", "rustls-pemfile", @@ -1980,7 +1998,7 @@ dependencies = [ "fabro-interview", "fabro-workflow", "futures-util", - "reqwest", + "reqwest 0.13.2", "rustls", "serde", "serde_json", @@ -2027,7 +2045,7 @@ dependencies = [ "mac_address", "md5", "regex", - "reqwest", + "reqwest 0.13.2", "sentry", "serde", "serde_json", @@ -2044,7 +2062,7 @@ dependencies = [ "axum", "insta", "regex", - "reqwest", + "reqwest 0.13.2", "serde_json", "tempfile", "tokio", @@ -2059,7 +2077,7 @@ dependencies = [ "async-trait", "fabro-github", "httpmock", - "reqwest", + "reqwest 0.13.2", "serde_json", "tokio", "tracing", @@ -2148,7 +2166,7 @@ dependencies = [ "predicates", "rand 0.8.5", "regex", - "reqwest", + "reqwest 0.13.2", "scopeguard", "serde", "serde_json", @@ -3285,6 +3303,50 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.117", +] + [[package]] name = "jobserver" version = "0.1.34" @@ -4127,7 +4189,7 @@ dependencies = [ "percent-encoding", "quick-xml", "rand 0.9.2", - "reqwest", + "reqwest 0.12.28", "ring", "serde", "serde_json", @@ -4669,6 +4731,72 @@ dependencies = [ "windows 0.62.2", ] +[[package]] +name = "progenitor" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d36315275b213c64c68dff684477ea7118a0f630832f737b550796a368f9962c" +dependencies = [ + "progenitor-client", + "progenitor-impl", + "progenitor-macro", +] + +[[package]] +name = "progenitor-client" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3999c302f5f2a42b7ca1cc39ad9e612c74cf2910ef6e58f869e45f3068b9659f" +dependencies = [ + "bytes", + "futures-core", + "percent-encoding", + "reqwest 0.13.2", + "serde", + "serde_json", + "serde_urlencoded", +] + +[[package]] +name = "progenitor-impl" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de362a0477182f45accdbad4d43cd89a95a1db0a518a7c1ddf3e525e6896f0f0" +dependencies = [ + "heck 0.5.0", + "http", + "indexmap 2.13.0", + "openapiv3", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "serde", + "serde_json", + "syn 2.0.117", + "thiserror 2.0.18", + "typify", + "unicode-ident", +] + +[[package]] +name = "progenitor-macro" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c98aeaaab266bf848a602c78e039e7d62c80ba36303ae4092ec65f17e7fd0eaa" +dependencies = [ + "openapiv3", + "proc-macro2", + "progenitor-impl", + "quote", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_tokenstream", + "serde_yaml", + "syn 2.0.117", +] + [[package]] name = "quick-xml" version = "0.38.4" @@ -4705,6 +4833,7 @@ version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ + "aws-lc-rs", "bytes", "getrandom 0.3.4", "lru-slab", @@ -4974,11 +5103,53 @@ dependencies = [ "url", "wasm-bindgen", "wasm-bindgen-futures", - "wasm-streams", + "wasm-streams 0.4.2", "web-sys", "webpki-roots 1.0.6", ] +[[package]] +name = "reqwest" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams 0.5.0", + "web-sys", +] + [[package]] name = "reqwest-middleware" version = "0.4.2" @@ -4988,7 +5159,7 @@ dependencies = [ "anyhow", "async-trait", "http", - "reqwest", + "reqwest 0.12.28", "serde", "thiserror 1.0.69", "tower-service", @@ -5010,9 +5181,9 @@ dependencies = [ [[package]] name = "rmcp" -version = "0.15.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bef41ebc9ebed2c1b1d90203e9d1756091e8a00bbc3107676151f39868ca0ee" +checksum = "2231b2c085b371c01bc90c0e6c1cab8834711b6394533375bdbf870b0166d419" dependencies = [ "async-trait", "chrono", @@ -5021,7 +5192,7 @@ dependencies = [ "pastey", "pin-project-lite", "process-wrap", - "reqwest", + "reqwest 0.13.2", "rmcp-macros", "schemars 1.2.1", "serde", @@ -5036,9 +5207,9 @@ dependencies = [ [[package]] name = "rmcp-macros" -version = "0.15.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e88ad84b8b6237a934534a62b379a5be6388915663c0cc598ceb9b3292bbbfe" +checksum = "36ea0e100fadf81be85d7ff70f86cd805c7572601d4ab2946207f36540854b43" dependencies = [ "darling 0.23.0", "proc-macro2", @@ -5201,6 +5372,33 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + [[package]] name = "rustls-webpki" version = "0.103.9" @@ -5249,10 +5447,12 @@ version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" dependencies = [ + "chrono", "dyn-clone", "schemars_derive 0.8.22", "serde", "serde_json", + "uuid", ] [[package]] @@ -5339,6 +5539,10 @@ name = "semver" version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] [[package]] name = "sentry" @@ -5347,7 +5551,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "016958f51b96861dead7c1e02290f138411d05e94fad175c8636a835dee6e51e" dependencies = [ "httpdate", - "reqwest", + "reqwest 0.12.28", "rustls", "sentry-backtrace", "sentry-contexts", @@ -5530,6 +5734,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_tokenstream" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c49585c52c01f13c5c2ebb333f14f6885d76daa768d8a037d28017ec538c69" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.117", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -6681,7 +6897,7 @@ dependencies = [ "base64", "chrono", "jsonwebtoken", - "reqwest", + "reqwest 0.13.2", "serde", "serde_json", "tempfile", @@ -6700,7 +6916,7 @@ dependencies = [ "axum", "futures-util", "http", - "reqwest", + "reqwest 0.13.2", "serde", "serde_json", "tokio", @@ -6731,6 +6947,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b715573a376585888b742ead9be5f4826105e622169180662e2c81bed4a149c3" dependencies = [ "typify-impl", + "typify-macro", ] [[package]] @@ -6753,6 +6970,23 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "typify-macro" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd04bb1207cd4e250941cc1641f4c4815f7eaa2145f45c09dd49cb0a3691710a" +dependencies = [ + "proc-macro2", + "quote", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "serde_tokenstream", + "syn 2.0.117", + "typify-impl", +] + [[package]] name = "ulid" version = "1.2.1" @@ -7123,6 +7357,19 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wasmparser" version = "0.244.0" @@ -7167,6 +7414,15 @@ dependencies = [ "string_cache_codegen", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webpki-roots" version = "0.26.11" @@ -7418,6 +7674,15 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -7463,6 +7728,21 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -7529,6 +7809,12 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -7547,6 +7833,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -7565,6 +7857,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -7595,6 +7893,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -7613,6 +7917,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -7631,6 +7941,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -7649,6 +7965,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" diff --git a/Cargo.toml b/Cargo.toml index eb0eac9d8..0403b3ee0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ thiserror = "2" serde = { version = "1", features = ["derive"] } serde_json = { version = "1", features = ["preserve_order"] } tokio = { version = "1", features = ["full"] } -reqwest = { version = "0.12", default-features = false, features = ["json", "stream", "rustls-tls"] } +reqwest = { version = "0.13", default-features = false, features = ["json", "stream", "rustls", "query", "form"] } ulid = "1" uuid = { version = "1", features = ["v4", "v7"] } rand = "0.8" @@ -41,7 +41,7 @@ git2 = { version = "0.20", default-features = false } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] } tracing-appender = "0.2" -rmcp = { version = "0.15.0", default-features = false } +rmcp = { version = "1.3", default-features = false } walkdir = "2" regex = "1" semver = "1" diff --git a/docs/api-reference/client-sdks.mdx b/docs/api-reference/client-sdks.mdx index 3e852fbf9..e0170abf4 100644 --- a/docs/api-reference/client-sdks.mdx +++ b/docs/api-reference/client-sdks.mdx @@ -34,31 +34,34 @@ console.log(data); The generated client includes a typed API class for each endpoint group: `RunsApi`, `WorkflowsApi`, `SessionsApi`, `VerificationsApi`, `InsightsApi`, and others. -## Rust (Types Only) +## Rust (Types + Client) -The `fabro-api-types` crate generates Rust structs and enums from the OpenAPI component schemas at compile time using [typify](https://github.com/oxidecomputer/typify). This provides type-safe representations of all API models but does not include an HTTP client. +The `fabro-api` crate generates Rust structs, enums, and a `reqwest`-based HTTP client from the full OpenAPI spec at compile time using [progenitor](https://github.com/oxidecomputer/progenitor). This provides type-safe representations of all API models and a builder-style client for every endpoint. ### How It Works -A `build.rs` script reads `docs/api-reference/fabro-api.yaml`, extracts `components/schemas`, and feeds them to typify. The generated code is written to `OUT_DIR` and included via: +A `build.rs` script reads `docs/api-reference/fabro-api.yaml`, patches it from OpenAPI 3.1 to 3.0 for progenitor compatibility, and generates both types and a client. The generated code is written to `OUT_DIR` and included via: ```rust -// lib/crates/fabro-api-types/src/lib.rs -include!(concat!(env!("OUT_DIR"), "/openapi_types.rs")); +// lib/crates/fabro-api/src/lib.rs +include!(concat!(env!("OUT_DIR"), "/codegen.rs")); ``` ### Regenerating -The types are regenerated automatically on every `cargo build` when the OpenAPI spec changes: +The types and client are regenerated automatically on every `cargo build` when the OpenAPI spec changes: ```bash -cargo build -p fabro-api-types +cargo build -p fabro-api ``` ### Usage ```rust -use fabro_api_types::RunListItem; +use fabro_api::types::RunListItem; +use fabro_api::Client; + +let client = Client::new("http://localhost:3000"); ``` -All generated types derive `serde::Deserialize` and `serde::Serialize`, so they work directly with any Rust HTTP client for request and response parsing. +All generated types derive `serde::Deserialize` and `serde::Serialize`. The client uses builder-style methods for each endpoint. diff --git a/lib/crates/fabro-api-types/build.rs b/lib/crates/fabro-api-types/build.rs deleted file mode 100644 index 5be485f4b..000000000 --- a/lib/crates/fabro-api-types/build.rs +++ /dev/null @@ -1,50 +0,0 @@ -use std::{env, fs, path::Path}; - -use schemars::schema::Schema; -use typify::{TypeSpace, TypeSpaceSettings}; - -fn main() { - let spec_path = Path::new(env!("CARGO_MANIFEST_DIR")) - .parent() - .unwrap() - .parent() - .unwrap() - .parent() - .unwrap() - .join("docs/api-reference/fabro-api.yaml"); - - println!("cargo::rerun-if-changed={}", spec_path.display()); - - let spec_text = fs::read_to_string(&spec_path) - .unwrap_or_else(|e| panic!("failed to read {}: {e}", spec_path.display())); - let spec: serde_json::Value = - serde_yaml::from_str(&spec_text).unwrap_or_else(|e| panic!("failed to parse YAML: {e}")); - - let schemas = spec["components"]["schemas"] - .as_object() - .expect("no components/schemas in spec"); - - let named_schemas: Vec<(String, Schema)> = schemas - .iter() - .map(|(name, value)| { - let schema: Schema = serde_json::from_value(value.clone()) - .unwrap_or_else(|e| panic!("failed to parse schema {name}: {e}")); - (name.clone(), schema) - }) - .collect(); - - let settings = TypeSpaceSettings::default(); - let mut type_space = TypeSpace::new(&settings); - type_space - .add_ref_types(named_schemas) - .expect("failed to add schemas to type space"); - - let token_stream = type_space.to_stream(); - let syntax_tree = - syn::parse2::(token_stream).expect("failed to parse generated tokens"); - let formatted = prettyplease::unparse(&syntax_tree); - - let out_dir = env::var("OUT_DIR").unwrap(); - let out_path = Path::new(&out_dir).join("openapi_types.rs"); - fs::write(&out_path, formatted).expect("failed to write generated types"); -} diff --git a/lib/crates/fabro-api-types/src/lib.rs b/lib/crates/fabro-api-types/src/lib.rs deleted file mode 100644 index ecb7d0772..000000000 --- a/lib/crates/fabro-api-types/src/lib.rs +++ /dev/null @@ -1,5 +0,0 @@ -#[allow(clippy::absolute_paths, clippy::derivable_impls)] -mod generated { - include!(concat!(env!("OUT_DIR"), "/openapi_types.rs")); -} -pub use generated::*; diff --git a/lib/crates/fabro-api-types/Cargo.toml b/lib/crates/fabro-api/Cargo.toml similarity index 71% rename from lib/crates/fabro-api-types/Cargo.toml rename to lib/crates/fabro-api/Cargo.toml index 3542aed9f..0e711afed 100644 --- a/lib/crates/fabro-api-types/Cargo.toml +++ b/lib/crates/fabro-api/Cargo.toml @@ -1,10 +1,10 @@ [package] -name = "fabro-api-types" +name = "fabro-api" edition.workspace = true version.workspace = true publish = false license.workspace = true -description = "Generated Rust types from the Fabro API OpenAPI spec" +description = "Generated Rust types and HTTP client from the Fabro API OpenAPI spec" [lib] doctest = false @@ -15,13 +15,15 @@ wildcard_imports = "warn" [dependencies] chrono = { workspace = true, features = ["serde"] } +progenitor-client = "0.13" +reqwest.workspace = true serde.workspace = true serde_json.workspace = true uuid = { workspace = true, features = ["serde"] } [build-dependencies] -typify = { version = "0.6", default-features = false } -schemars = "0.8" +openapiv3 = "2" +progenitor = "0.13" serde_json = "1" serde_yaml = "0.9" prettyplease = "0.2" diff --git a/lib/crates/fabro-api/build.rs b/lib/crates/fabro-api/build.rs new file mode 100644 index 000000000..9c802dbaa --- /dev/null +++ b/lib/crates/fabro-api/build.rs @@ -0,0 +1,105 @@ +use std::{env, fs, path::Path}; + +use progenitor::{GenerationSettings, Generator, InterfaceStyle}; + +/// Recursively convert OpenAPI 3.1 `type: "null"` patterns to 3.0 `nullable: true`. +/// +/// Handles two patterns: +/// - `oneOf: [{...}, {type: "null"}]` → the non-null schema with `nullable: true` +/// - `type: [T1, ..., "null"]` → the remaining types with `nullable: true` +fn patch_nullable(value: &mut serde_json::Value) { + match value { + serde_json::Value::Object(map) => { + // Pattern: oneOf with a {type: "null"} variant + if let Some(one_of) = map.get_mut("oneOf") { + if let Some(variants) = one_of.as_array_mut() { + let null_idx = variants.iter().position(|v| { + v.get("type").and_then(serde_json::Value::as_str) == Some("null") + }); + if let Some(idx) = null_idx { + variants.remove(idx); + if variants.len() == 1 { + // Collapse single-variant oneOf into the schema itself + let mut inner = variants.remove(0); + inner + .as_object_mut() + .unwrap() + .insert("nullable".to_string(), serde_json::Value::Bool(true)); + patch_nullable(&mut inner); + *value = inner; + return; + } + map.insert("nullable".to_string(), serde_json::Value::Bool(true)); + } + } + } + + // Pattern: type array containing "null" + let needs_nullable_from_type = map + .get("type") + .and_then(|v| v.as_array()) + .is_some_and(|arr| arr.iter().any(|v| v.as_str() == Some("null"))); + if needs_nullable_from_type { + if let Some(type_val) = map.get_mut("type") { + if let Some(arr) = type_val.as_array_mut() { + arr.retain(|v| v.as_str() != Some("null")); + if arr.len() == 1 { + *type_val = arr.remove(0); + } + } + } + map.insert("nullable".to_string(), serde_json::Value::Bool(true)); + } + + for v in map.values_mut() { + patch_nullable(v); + } + } + serde_json::Value::Array(arr) => { + for v in arr { + patch_nullable(v); + } + } + _ => {} + } +} + +fn main() { + let spec_path = Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap() + .parent() + .unwrap() + .parent() + .unwrap() + .join("docs/api-reference/fabro-api.yaml"); + + println!("cargo::rerun-if-changed={}", spec_path.display()); + + let spec_text = fs::read_to_string(&spec_path) + .unwrap_or_else(|e| panic!("failed to read {}: {e}", spec_path.display())); + let mut spec_value: serde_json::Value = + serde_yaml::from_str(&spec_text).unwrap_or_else(|e| panic!("failed to parse YAML: {e}")); + + // Progenitor only supports OpenAPI 3.0.x; our spec uses 3.1.0 but doesn't + // rely on any 3.1-only features that affect codegen. + spec_value["openapi"] = serde_json::Value::String("3.0.3".to_string()); + patch_nullable(&mut spec_value); + + let spec: openapiv3::OpenAPI = + serde_json::from_value(spec_value).expect("failed to deserialize OpenAPI spec"); + + let mut settings = GenerationSettings::default(); + settings.with_interface(InterfaceStyle::Builder); + + let mut generator = Generator::new(&settings); + let tokens = generator + .generate_tokens(&spec) + .expect("failed to generate tokens from OpenAPI spec"); + let syntax_tree = syn::parse2::(tokens).expect("failed to parse generated tokens"); + let formatted = prettyplease::unparse(&syntax_tree); + + let out_dir = env::var("OUT_DIR").unwrap(); + let out_path = Path::new(&out_dir).join("codegen.rs"); + fs::write(&out_path, formatted).expect("failed to write generated code"); +} diff --git a/lib/crates/fabro-api/src/lib.rs b/lib/crates/fabro-api/src/lib.rs new file mode 100644 index 000000000..11dfe6268 --- /dev/null +++ b/lib/crates/fabro-api/src/lib.rs @@ -0,0 +1,13 @@ +#[allow( + clippy::absolute_paths, + clippy::all, + clippy::derivable_impls, + clippy::needless_lifetimes, + unreachable_pub, + unused_imports +)] +mod generated { + include!(concat!(env!("OUT_DIR"), "/codegen.rs")); +} +pub use generated::Client; +pub use generated::types; diff --git a/lib/crates/fabro-mcp/src/client.rs b/lib/crates/fabro-mcp/src/client.rs index a89c9329f..74238fc04 100644 --- a/lib/crates/fabro-mcp/src/client.rs +++ b/lib/crates/fabro-mcp/src/client.rs @@ -212,12 +212,10 @@ impl McpClient { } }; - let params = CallToolRequestParams { - meta: None, - name: name.to_string().into(), - arguments: args, - task: None, - }; + let mut params = CallToolRequestParams::new(name.to_string()); + if let Some(arguments) = args { + params = params.with_arguments(arguments); + } debug!(server = %self.server_name, tool = %name, "Calling MCP tool"); diff --git a/lib/crates/fabro-mcp/src/client_handler.rs b/lib/crates/fabro-mcp/src/client_handler.rs index 36500bab9..16af69ab6 100644 --- a/lib/crates/fabro-mcp/src/client_handler.rs +++ b/lib/crates/fabro-mcp/src/client_handler.rs @@ -13,19 +13,11 @@ pub(crate) struct LoggingClientHandler; impl ClientHandler for LoggingClientHandler { fn get_info(&self) -> ClientInfo { - ClientInfo { - protocol_version: ProtocolVersion::V_2025_03_26, - capabilities: ClientCapabilities::default(), - client_info: Implementation { - name: "fabro-mcp".into(), - version: env!("CARGO_PKG_VERSION").into(), - title: None, - description: None, - icons: None, - website_url: None, - }, - meta: None, - } + ClientInfo::new( + ClientCapabilities::default(), + Implementation::new("fabro-mcp", env!("CARGO_PKG_VERSION")), + ) + .with_protocol_version(ProtocolVersion::V_2025_03_26) } async fn on_cancelled( diff --git a/lib/crates/fabro-mcp/src/connection_manager.rs b/lib/crates/fabro-mcp/src/connection_manager.rs index 3d696da94..8fefda9cf 100644 --- a/lib/crates/fabro-mcp/src/connection_manager.rs +++ b/lib/crates/fabro-mcp/src/connection_manager.rs @@ -184,7 +184,7 @@ impl Default for McpConnectionManager { #[cfg(test)] mod tests { use super::*; - use rmcp::model::{Content, RawTextContent}; + use rmcp::model::Content; #[test] fn qualified_tool_name_basic() { @@ -242,21 +242,14 @@ mod tests { } fn make_text_content(text: &str) -> Content { - Content { - raw: RawContent::Text(RawTextContent { - text: text.to_string(), - meta: None, - }), - annotations: None, - } + Content::text(text) } fn make_call_result(content: Vec, is_error: Option) -> CallToolResult { - CallToolResult { - content, - structured_content: None, - is_error, - meta: None, + if is_error == Some(true) { + CallToolResult::error(content) + } else { + CallToolResult::success(content) } } @@ -292,20 +285,7 @@ mod tests { #[test] fn call_result_to_string_image_placeholder() { - use rmcp::model::RawImageContent; - let result = CallToolResult { - content: vec![Content { - raw: RawContent::Image(RawImageContent { - data: "base64data".to_string(), - mime_type: "image/png".to_string(), - meta: None, - }), - annotations: None, - }], - structured_content: None, - is_error: None, - meta: None, - }; + let result = CallToolResult::success(vec![Content::image("base64data", "image/png")]); assert_eq!( call_result_to_string(&result), Ok("[image content]".to_string()) diff --git a/lib/crates/fabro-server/Cargo.toml b/lib/crates/fabro-server/Cargo.toml index 9ef154309..ab82689f1 100644 --- a/lib/crates/fabro-server/Cargo.toml +++ b/lib/crates/fabro-server/Cargo.toml @@ -27,7 +27,7 @@ fabro-retro = { path = "../fabro-retro" } fabro-types = { path = "../fabro-types" } fabro-util = { path = "../fabro-util" } fabro-db = { path = "../fabro-db" } -fabro-api-types = { path = "../fabro-api-types" } +fabro-api = { path = "../fabro-api" } fabro-store = { path = "../fabro-store" } chrono.workspace = true futures-util.workspace = true diff --git a/lib/crates/fabro-server/src/demo/mod.rs b/lib/crates/fabro-server/src/demo/mod.rs index 2864ded99..06e20c6da 100644 --- a/lib/crates/fabro-server/src/demo/mod.rs +++ b/lib/crates/fabro-server/src/demo/mod.rs @@ -8,6 +8,7 @@ use axum::Json; use axum::extract::{Path, Query, State}; use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; +use fabro_api::types::{RunStatus, RunStatusResponse, SessionTurn, SmoothnessRating}; use serde_json::json; use crate::error::ApiError; @@ -21,7 +22,7 @@ pub(crate) struct RetroListParams { #[serde(rename = "page[offset]", default)] offset: u32, workflow: Option, - smoothness: Option, + smoothness: Option, } fn paginated_response( @@ -141,9 +142,9 @@ pub(crate) async fn get_run_status( match runs::list_items().into_iter().find(|r| r.id == id) { Some(item) => ( StatusCode::OK, - Json(fabro_api_types::RunStatusResponse { + Json(RunStatusResponse { id: id.clone(), - status: fabro_api_types::RunStatus::Running, + status: RunStatus::Running, error: None, queue_position: None, created_at: item.created_at, @@ -470,13 +471,9 @@ pub(crate) async fn session_events_stub( for turn in &session.turns { let (event_type, data) = match turn { - fabro_api_types::SessionTurn::UserTurn(_) => continue, - fabro_api_types::SessionTurn::AssistantTurn(t) => { - ("assistant_turn", serde_json::to_string(t).unwrap()) - } - fabro_api_types::SessionTurn::ToolTurn(t) => { - ("tool_turn", serde_json::to_string(t).unwrap()) - } + SessionTurn::UserTurn(_) => continue, + SessionTurn::AssistantTurn(t) => ("assistant_turn", serde_json::to_string(t).unwrap()), + SessionTurn::ToolTurn(t) => ("tool_turn", serde_json::to_string(t).unwrap()), }; if last_event_id.is_none() || seq > last_event_id.unwrap() { @@ -620,7 +617,7 @@ fn ts(s: &str) -> DateTime { mod runs { use super::ts; - use fabro_api_types::*; + use fabro_api::types::*; pub(super) fn list_items() -> Vec { vec![ @@ -1241,7 +1238,7 @@ mod runs { } } - pub(super) fn verifications() -> Vec { + pub(super) fn verifications() -> Vec { super::verifications::run_verifications() } @@ -1340,7 +1337,7 @@ mod runs { } mod usage { - use fabro_api_types::*; + use fabro_api::types::*; pub(super) fn aggregate() -> AggregateUsage { AggregateUsage { @@ -1392,7 +1389,7 @@ mod usage { mod workflows { use super::ts; - use fabro_api_types::*; + use fabro_api::types::*; pub(super) fn list_items() -> Vec { vec![ @@ -1764,7 +1761,7 @@ mod workflows { mod verifications { use super::ts; - use fabro_api_types::*; + use fabro_api::types::*; // ── Category definitions (name, question, controls) ───────────────── @@ -2823,7 +2820,7 @@ mod verifications { mod signoffs { use super::ts; - use fabro_api_types::*; + use fabro_api::types::*; struct SignoffDef { id: &'static str, @@ -2938,7 +2935,7 @@ mod signoffs { mod retros { use super::ts; - use fabro_api_types::*; + use fabro_api::types::*; #[allow(clippy::too_many_arguments)] fn stage( @@ -3272,7 +3269,7 @@ mod retros { mod sessions { use super::ts; - use fabro_api_types::*; + use fabro_api::types::*; use uuid::Uuid; fn uid(n: u128) -> Uuid { @@ -3414,7 +3411,7 @@ mod sessions { mod insights { use super::ts; - use fabro_api_types::*; + use fabro_api::types::*; pub(super) fn saved_queries() -> Vec { vec![ diff --git a/lib/crates/fabro-server/src/server.rs b/lib/crates/fabro-server/src/server.rs index d0609c3e7..6d992be05 100644 --- a/lib/crates/fabro-server/src/server.rs +++ b/lib/crates/fabro-server/src/server.rs @@ -52,10 +52,13 @@ use fabro_workflow::operations::{self, CreateRunInput, WorkflowInput}; use fabro_workflow::pipeline::Persisted; use fabro_workflow::records::Checkpoint; -pub use fabro_api_types::{ - ApiQuestion, ApiQuestionOption, PaginatedRunList, PaginationMeta, - QuestionType as ApiQuestionType, RunStatus, RunStatusResponse, StartRunRequest, - SubmitAnswerRequest, +use fabro_api::types::AggregateUsageTotals as ApiAggregateUsageTotals; +pub use fabro_api::types::{ + AggregateUsage, ApiQuestion, ApiQuestionOption, CompletionContentPart, CompletionMessage, + CompletionMessageRole, CompletionResponse, CompletionToolChoiceMode, CompletionUsage, + CreateCompletionRequest, ModelReference, PaginatedRunList, PaginationMeta, + QuestionType as ApiQuestionType, RunError, RunStatus, RunStatusResponse, StartRunRequest, + SubmitAnswerRequest, TokenUsage, UsageByModel, }; pub fn default_page_limit() -> u32 { @@ -377,21 +380,21 @@ async fn get_aggregate_usage( .aggregate_usage .lock() .expect("aggregate_usage lock poisoned"); - let by_model: Vec = agg + let by_model: Vec = agg .by_model .iter() - .map(|(model, totals)| fabro_api_types::UsageByModel { - model: fabro_api_types::ModelReference { id: model.clone() }, + .map(|(model, totals)| UsageByModel { + model: ModelReference { id: model.clone() }, stages: totals.stages, - usage: fabro_api_types::TokenUsage { + usage: TokenUsage { input_tokens: totals.input_tokens, output_tokens: totals.output_tokens, cost: totals.cost, }, }) .collect(); - let response = fabro_api_types::AggregateUsage { - totals: fabro_api_types::AggregateUsageTotals { + let response = AggregateUsage { + totals: ApiAggregateUsageTotals { runs: agg.total_runs, input_tokens: by_model.iter().map(|m| m.usage.input_tokens).sum(), output_tokens: by_model.iter().map(|m| m.usage.output_tokens).sum(), @@ -489,12 +492,9 @@ async fn list_runs( .map(|(id, managed_run)| RunStatusResponse { id: id.to_string(), status: managed_run.status, - error: managed_run - .error - .as_ref() - .map(|msg| fabro_api_types::RunError { - message: msg.clone(), - }), + error: managed_run.error.as_ref().map(|msg| RunError { + message: msg.clone(), + }), queue_position: queue_positions.get(id).copied(), created_at: managed_run.created_at, }) @@ -891,12 +891,9 @@ async fn get_run_status( Json(RunStatusResponse { id: id.to_string(), status: managed_run.status, - error: managed_run - .error - .as_ref() - .map(|msg| fabro_api_types::RunError { - message: msg.clone(), - }), + error: managed_run.error.as_ref().map(|msg| RunError { + message: msg.clone(), + }), created_at: managed_run.created_at, queue_position, }), @@ -1276,13 +1273,13 @@ fn finish_reason_to_api_stop_reason(reason: &FinishReason) -> String { } } -fn convert_api_message(msg: &fabro_api_types::CompletionMessage) -> LlmMessage { +fn convert_api_message(msg: &CompletionMessage) -> LlmMessage { let role = match msg.role { - fabro_api_types::CompletionMessageRole::System => Role::System, - fabro_api_types::CompletionMessageRole::User => Role::User, - fabro_api_types::CompletionMessageRole::Assistant => Role::Assistant, - fabro_api_types::CompletionMessageRole::Tool => Role::Tool, - fabro_api_types::CompletionMessageRole::Developer => Role::Developer, + CompletionMessageRole::System => Role::System, + CompletionMessageRole::User => Role::User, + CompletionMessageRole::Assistant => Role::Assistant, + CompletionMessageRole::Tool => Role::Tool, + CompletionMessageRole::Developer => Role::Developer, }; let content: Vec = msg .content @@ -1300,15 +1297,15 @@ fn convert_api_message(msg: &fabro_api_types::CompletionMessage) -> LlmMessage { } } -fn convert_llm_message(msg: &LlmMessage) -> fabro_api_types::CompletionMessage { +fn convert_llm_message(msg: &LlmMessage) -> CompletionMessage { let role = match msg.role { - Role::System => fabro_api_types::CompletionMessageRole::System, - Role::User => fabro_api_types::CompletionMessageRole::User, - Role::Assistant => fabro_api_types::CompletionMessageRole::Assistant, - Role::Tool => fabro_api_types::CompletionMessageRole::Tool, - Role::Developer => fabro_api_types::CompletionMessageRole::Developer, + Role::System => CompletionMessageRole::System, + Role::User => CompletionMessageRole::User, + Role::Assistant => CompletionMessageRole::Assistant, + Role::Tool => CompletionMessageRole::Tool, + Role::Developer => CompletionMessageRole::Developer, }; - let content: Vec = msg + let content: Vec = msg .content .iter() .filter_map(|part| { @@ -1316,7 +1313,7 @@ fn convert_llm_message(msg: &LlmMessage) -> fabro_api_types::CompletionMessage { serde_json::from_value(json).ok() }) .collect(); - fabro_api_types::CompletionMessage { + CompletionMessage { role, content, name: msg.name.clone(), @@ -1327,7 +1324,7 @@ fn convert_llm_message(msg: &LlmMessage) -> fabro_api_types::CompletionMessage { async fn create_completion( _auth: AuthenticatedService, State(state): State>, - Json(req): Json, + Json(req): Json, ) -> Response { // Resolve model let model_id = req.model.unwrap_or_else(|| { @@ -1373,12 +1370,10 @@ async fn create_completion( // Convert tool_choice let tool_choice: Option = req.tool_choice.map(|tc| match tc.mode { - fabro_api_types::CompletionToolChoiceMode::Auto => ToolChoice::Auto, - fabro_api_types::CompletionToolChoiceMode::None => ToolChoice::None, - fabro_api_types::CompletionToolChoiceMode::Required => ToolChoice::Required, - fabro_api_types::CompletionToolChoiceMode::Named => { - ToolChoice::named(tc.tool_name.unwrap_or_default()) - } + CompletionToolChoiceMode::Auto => ToolChoice::Auto, + CompletionToolChoiceMode::None => ToolChoice::None, + CompletionToolChoiceMode::Required => ToolChoice::Required, + CompletionToolChoiceMode::Named => ToolChoice::named(tc.tool_name.unwrap_or_default()), }); // Build the LLM request @@ -1431,18 +1426,18 @@ async fn create_completion( )]); return Sse::new(sse_stream).into_response(); } - let empty_msg = fabro_api_types::CompletionMessage { - role: fabro_api_types::CompletionMessageRole::Assistant, + let empty_msg = CompletionMessage { + role: CompletionMessageRole::Assistant, content: vec![], name: None, tool_call_id: None, }; - return Json(fabro_api_types::CompletionResponse { + return Json(CompletionResponse { id: msg_id, model: model_id, message: empty_msg, stop_reason: "end_turn".to_string(), - usage: fabro_api_types::CompletionUsage { + usage: CompletionUsage { input_tokens: 0, output_tokens: 0, }, @@ -1528,12 +1523,12 @@ async fn create_completion( params = params.top_p(top_p); } match generate_object(params, schema).await { - Ok(result) => Json(fabro_api_types::CompletionResponse { + Ok(result) => Json(CompletionResponse { id: msg_id, model: model_id, message: convert_llm_message(&result.response.message), stop_reason: finish_reason_to_api_stop_reason(&result.finish_reason), - usage: fabro_api_types::CompletionUsage { + usage: CompletionUsage { input_tokens: result.usage.input_tokens, output_tokens: result.usage.output_tokens, }, @@ -1545,12 +1540,12 @@ async fn create_completion( } } else { match client.complete(&request).await { - Ok(response) => Json(fabro_api_types::CompletionResponse { + Ok(response) => Json(CompletionResponse { id: response.id, model: response.model, message: convert_llm_message(&response.message), stop_reason: finish_reason_to_api_stop_reason(&response.finish_reason), - usage: fabro_api_types::CompletionUsage { + usage: CompletionUsage { input_tokens: response.usage.input_tokens, output_tokens: response.usage.output_tokens, }, diff --git a/lib/crates/fabro-server/src/sessions.rs b/lib/crates/fabro-server/src/sessions.rs index e9368cf99..beb3bba62 100644 --- a/lib/crates/fabro-server/src/sessions.rs +++ b/lib/crates/fabro-server/src/sessions.rs @@ -14,6 +14,12 @@ use fabro_llm::types::{Message as LlmMessage, StreamEvent}; use tokio::sync::broadcast; use tokio_stream::wrappers::BroadcastStream; +use fabro_api::types::{ + AssistantTurn, AssistantTurnKind, CreateSessionRequest, CreateSessionResponse, ModelReference, + PaginatedSessionList, PaginationMeta, SendMessageRequest, SendMessageResponse, SessionDetail, + SessionListItem, SessionTurn, UserTurn, UserTurnKind, +}; + use crate::error::ApiError; use crate::jwt_auth::AuthenticatedService; use crate::server::{AppState, PaginationParams}; @@ -30,7 +36,7 @@ pub struct SessionState { pub model_id: String, pub model_provider: Option, pub system_prompt: Option, - pub turns: Vec, + pub turns: Vec, pub created_at: chrono::DateTime, pub updated_at: chrono::DateTime, pub event_tx: broadcast::Sender, @@ -77,15 +83,13 @@ fn resolve_model(model_arg: Option) -> (String, Option) { } } -fn turns_to_messages(turns: &[fabro_api_types::SessionTurn]) -> Vec { +fn turns_to_messages(turns: &[SessionTurn]) -> Vec { turns .iter() .filter_map(|turn| match turn { - fabro_api_types::SessionTurn::UserTurn(t) => Some(LlmMessage::user(&t.content)), - fabro_api_types::SessionTurn::AssistantTurn(t) => { - Some(LlmMessage::assistant(&t.content)) - } - fabro_api_types::SessionTurn::ToolTurn(_) => None, + SessionTurn::UserTurn(t) => Some(LlmMessage::user(&t.content)), + SessionTurn::AssistantTurn(t) => Some(LlmMessage::assistant(&t.content)), + SessionTurn::ToolTurn(_) => None, }) .collect() } @@ -125,13 +129,11 @@ fn spawn_generation(store: SessionStore, session_id: uuid::Uuid, dry_run: bool, if let Some(session) = store.get_mut(&session_id) { session .turns - .push(fabro_api_types::SessionTurn::AssistantTurn( - fabro_api_types::AssistantTurn { - kind: fabro_api_types::AssistantTurnKind::Assistant, - content, - created_at: now, - }, - )); + .push(SessionTurn::AssistantTurn(AssistantTurn { + kind: AssistantTurnKind::Assistant, + content, + created_at: now, + })); session.updated_at = now; } } @@ -193,13 +195,11 @@ fn spawn_generation(store: SessionStore, session_id: uuid::Uuid, dry_run: bool, if let Some(session) = store.get_mut(&session_id) { session .turns - .push(fabro_api_types::SessionTurn::AssistantTurn( - fabro_api_types::AssistantTurn { - kind: fabro_api_types::AssistantTurnKind::Assistant, - content: full_text, - created_at: now, - }, - )); + .push(SessionTurn::AssistantTurn(AssistantTurn { + kind: AssistantTurnKind::Assistant, + content: full_text, + created_at: now, + })); session.updated_at = now; } } @@ -210,7 +210,7 @@ fn spawn_generation(store: SessionStore, session_id: uuid::Uuid, dry_run: bool, pub async fn create_session( _auth: AuthenticatedService, State(state): State>, - Json(req): Json, + Json(req): Json, ) -> Response { let (model_id, model_provider) = resolve_model(req.model); let now = chrono::Utc::now(); @@ -220,8 +220,8 @@ pub async fn create_session( let (event_tx, _) = broadcast::channel(256); let generation_seq = Arc::new(AtomicU64::new(1)); - let user_turn = fabro_api_types::SessionTurn::UserTurn(fabro_api_types::UserTurn { - kind: fabro_api_types::UserTurnKind::User, + let user_turn = SessionTurn::UserTurn(UserTurn { + kind: UserTurnKind::User, content: req.content, created_at: now, }); @@ -248,10 +248,10 @@ pub async fn create_session( ( StatusCode::CREATED, - Json(fabro_api_types::CreateSessionResponse { + Json(CreateSessionResponse { id: session_id, title, - model: fabro_api_types::ModelReference { id: model_id }, + model: ModelReference { id: model_id }, created_at: now, updated_at: now, }), @@ -268,10 +268,10 @@ pub async fn retrieve_session( match store.get(&id) { Some(session) => ( StatusCode::OK, - Json(fabro_api_types::SessionDetail { + Json(SessionDetail { id: session.id, title: session.title.clone(), - model: fabro_api_types::ModelReference { + model: ModelReference { id: session.model_id.clone(), }, created_at: session.created_at, @@ -288,20 +288,18 @@ pub async fn send_message( _auth: AuthenticatedService, State(state): State>, Path(id): Path, - Json(req): Json, + Json(req): Json, ) -> Response { let seq = { let mut store = state.sessions.write().expect("session store lock poisoned"); match store.get_mut(&id) { Some(session) => { let now = chrono::Utc::now(); - session.turns.push(fabro_api_types::SessionTurn::UserTurn( - fabro_api_types::UserTurn { - kind: fabro_api_types::UserTurnKind::User, - content: req.content, - created_at: now, - }, - )); + session.turns.push(SessionTurn::UserTurn(UserTurn { + kind: UserTurnKind::User, + content: req.content, + created_at: now, + })); session.updated_at = now; session.generation_seq.fetch_add(1, Ordering::Relaxed) + 1 } @@ -313,7 +311,7 @@ pub async fn send_message( ( StatusCode::ACCEPTED, - Json(fabro_api_types::SendMessageResponse { accepted: true }), + Json(SendMessageResponse { accepted: true }), ) .into_response() } @@ -377,16 +375,16 @@ pub async fn list_sessions( let limit = pagination.limit.clamp(1, 100) as usize; let offset = pagination.offset as usize; - let mut items: Vec = store + let mut items: Vec = store .values() .map(|session| { let last_message_preview = session .turns .last() .map(|t| match t { - fabro_api_types::SessionTurn::UserTurn(u) => u.content.clone(), - fabro_api_types::SessionTurn::AssistantTurn(a) => a.content.clone(), - fabro_api_types::SessionTurn::ToolTurn(_) => String::new(), + SessionTurn::UserTurn(u) => u.content.clone(), + SessionTurn::AssistantTurn(a) => a.content.clone(), + SessionTurn::ToolTurn(_) => String::new(), }) .unwrap_or_default(); let preview = if last_message_preview.len() > 100 { @@ -394,10 +392,10 @@ pub async fn list_sessions( } else { last_message_preview }; - fabro_api_types::SessionListItem { + SessionListItem { id: session.id, title: session.title.clone(), - model: fabro_api_types::ModelReference { + model: ModelReference { id: session.model_id.clone(), }, last_message_preview: preview, @@ -416,9 +414,9 @@ pub async fn list_sessions( ( StatusCode::OK, - Json(fabro_api_types::PaginatedSessionList { + Json(PaginatedSessionList { data, - meta: fabro_api_types::PaginationMeta { has_more }, + meta: PaginationMeta { has_more }, }), ) .into_response()