Rename fabro-types to fabro-api-types

Better reflects that this crate contains auto-generated types scoped
to the API layer. Pure rename with no behavior change.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-03-27 18:35:44 -04:00
parent 75b5aa35b9
commit d5152ed7a3
10 changed files with 130 additions and 120 deletions

View file

@ -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-types` — build.rs regenerates Rust types via typify
2. `cargo build -p fabro-api-types` — build.rs regenerates Rust types via typify
3. Write/update handler in `lib/crates/fabro-api/src/server.rs`, add route to `build_router()`
4. `cargo nextest run -p fabro-api` — conformance test catches spec/router drift
5. `cd lib/packages/fabro-api-client && bun run generate` — regenerates TypeScript Axios client
@ -57,7 +57,7 @@ Fabro is an AI-powered workflow orchestration platform. Workflows are defined as
- **fabro-exe** — SSH-based sandbox implementation (`ExeSandbox`)
- **fabro-sprites** — Sprites VM sandbox implementation via `sprite` CLI
- **fabro-llm** — Unified LLM client with providers: Anthropic, OpenAI, Gemini, OpenAI-compatible, plus retry/middleware/streaming
- **fabro-types** — Auto-generated Rust types from OpenAPI spec (build.rs + typify)
- **fabro-api-types** — Auto-generated Rust types from OpenAPI spec (build.rs + typify)
- **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

32
Cargo.lock generated
View file

@ -1265,6 +1265,7 @@ dependencies = [
"clap",
"dirs",
"fabro-agent",
"fabro-api-types",
"fabro-config",
"fabro-db",
"fabro-github",
@ -1275,7 +1276,6 @@ dependencies = [
"fabro-model",
"fabro-retro",
"fabro-sandbox",
"fabro-types",
"fabro-util",
"fabro-workflows",
"futures-util",
@ -1308,6 +1308,21 @@ dependencies = [
"x509-parser",
]
[[package]]
name = "fabro-api-types"
version = "0.176.2"
dependencies = [
"chrono",
"prettyplease",
"schemars 0.8.22",
"serde",
"serde_json",
"serde_yaml",
"syn",
"typify",
"uuid",
]
[[package]]
name = "fabro-cli"
version = "0.176.2"
@ -1693,21 +1708,6 @@ dependencies = [
"tracing",
]
[[package]]
name = "fabro-types"
version = "0.176.2"
dependencies = [
"chrono",
"prettyplease",
"schemars 0.8.22",
"serde",
"serde_json",
"serde_yaml",
"syn",
"typify",
"uuid",
]
[[package]]
name = "fabro-util"
version = "0.176.2"

View file

@ -36,14 +36,14 @@ The generated client includes a typed API class for each endpoint group: `RunsAp
## Rust (Types Only)
The `fabro-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-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.
### 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:
```rust
// lib/crates/fabro-types/src/lib.rs
// lib/crates/fabro-api-types/src/lib.rs
include!(concat!(env!("OUT_DIR"), "/openapi_types.rs"));
```
@ -52,13 +52,13 @@ include!(concat!(env!("OUT_DIR"), "/openapi_types.rs"));
The types are regenerated automatically on every `cargo build` when the OpenAPI spec changes:
```bash
cargo build -p fabro-types
cargo build -p fabro-api-types
```
### Usage
```rust
use fabro_types::RunListItem;
use fabro_api_types::RunListItem;
```
All generated types derive `serde::Deserialize` and `serde::Serialize`, so they work directly with any Rust HTTP client for request and response parsing.

View file

@ -1,5 +1,5 @@
[package]
name = "fabro-types"
name = "fabro-api-types"
edition.workspace = true
version.workspace = true
license.workspace = true

View file

@ -22,7 +22,7 @@ fabro-model = { path = "../fabro-model" }
fabro-retro = { path = "../fabro-retro" }
fabro-util = { path = "../fabro-util" }
fabro-db = { path = "../fabro-db" }
fabro-types = { path = "../fabro-types" }
fabro-api-types = { path = "../fabro-api-types" }
chrono.workspace = true
futures-util.workspace = true
axum = "0.8"

View file

@ -20,7 +20,7 @@ pub struct RetroListParams {
#[serde(rename = "page[offset]", default)]
offset: u32,
workflow: Option<String>,
smoothness: Option<fabro_types::SmoothnessRating>,
smoothness: Option<fabro_api_types::SmoothnessRating>,
}
fn paginated_response<T: serde::Serialize>(
@ -140,9 +140,9 @@ pub async fn get_run_status(
match runs::list_items().into_iter().find(|r| r.id == id) {
Some(item) => (
StatusCode::OK,
Json(fabro_types::RunStatusResponse {
Json(fabro_api_types::RunStatusResponse {
id: id.clone(),
status: fabro_types::RunStatus::Running,
status: fabro_api_types::RunStatus::Running,
error: None,
queue_position: None,
created_at: item.created_at,
@ -464,11 +464,11 @@ pub async fn session_events_stub(
for turn in &session.turns {
let (event_type, data) = match turn {
fabro_types::SessionTurn::UserTurn(_) => continue,
fabro_types::SessionTurn::AssistantTurn(t) => {
fabro_api_types::SessionTurn::UserTurn(_) => continue,
fabro_api_types::SessionTurn::AssistantTurn(t) => {
("assistant_turn", serde_json::to_string(t).unwrap())
}
fabro_types::SessionTurn::ToolTurn(t) => {
fabro_api_types::SessionTurn::ToolTurn(t) => {
("tool_turn", serde_json::to_string(t).unwrap())
}
};
@ -614,7 +614,7 @@ fn ts(s: &str) -> DateTime<Utc> {
mod runs {
use super::ts;
use fabro_types::*;
use fabro_api_types::*;
pub fn list_items() -> Vec<RunListItem> {
vec![
@ -1235,7 +1235,7 @@ mod runs {
}
}
pub fn verifications() -> Vec<fabro_types::RunVerification> {
pub fn verifications() -> Vec<fabro_api_types::RunVerification> {
super::verifications::run_verifications()
}
@ -1336,7 +1336,7 @@ mod runs {
}
mod usage {
use fabro_types::*;
use fabro_api_types::*;
pub fn aggregate() -> AggregateUsage {
AggregateUsage {
@ -1388,7 +1388,7 @@ mod usage {
mod workflows {
use super::ts;
use fabro_types::*;
use fabro_api_types::*;
pub fn list_items() -> Vec<WorkflowListItem> {
vec![
@ -1768,7 +1768,7 @@ mod workflows {
mod verifications {
use super::ts;
use fabro_types::*;
use fabro_api_types::*;
// ── Category definitions (name, question, controls) ─────────────────
@ -2611,7 +2611,7 @@ mod verifications {
mod signoffs {
use super::ts;
use fabro_types::*;
use fabro_api_types::*;
struct SignoffDef {
id: &'static str,
@ -2726,7 +2726,7 @@ mod signoffs {
mod retros {
use super::ts;
use fabro_types::*;
use fabro_api_types::*;
#[allow(clippy::too_many_arguments)]
fn stage(
@ -3060,7 +3060,7 @@ mod retros {
mod sessions {
use super::ts;
use fabro_types::*;
use fabro_api_types::*;
use uuid::Uuid;
fn uid(n: u128) -> Uuid {
@ -3202,7 +3202,7 @@ mod sessions {
mod insights {
use super::ts;
use fabro_types::*;
use fabro_api_types::*;
pub fn saved_queries() -> Vec<SavedQuery> {
vec![

View file

@ -29,7 +29,7 @@ use fabro_workflows::records::Checkpoint;
use fabro_workflows::run_options::LifecycleOptions;
use fabro_workflows::run_options::RunOptions;
pub use fabro_types::{
pub use fabro_api_types::{
ApiQuestion, ApiQuestionOption, PaginatedRunList, PaginationMeta,
QuestionType as ApiQuestionType, RunStatus, RunStatusResponse, StartRunRequest,
SubmitAnswerRequest,
@ -356,21 +356,21 @@ async fn get_aggregate_usage(
.aggregate_usage
.lock()
.expect("aggregate_usage lock poisoned");
let by_model: Vec<fabro_types::UsageByModel> = agg
let by_model: Vec<fabro_api_types::UsageByModel> = agg
.by_model
.iter()
.map(|(model, totals)| fabro_types::UsageByModel {
model: fabro_types::ModelReference { id: model.clone() },
.map(|(model, totals)| fabro_api_types::UsageByModel {
model: fabro_api_types::ModelReference { id: model.clone() },
stages: totals.stages,
usage: fabro_types::TokenUsage {
usage: fabro_api_types::TokenUsage {
input_tokens: totals.input_tokens,
output_tokens: totals.output_tokens,
cost: totals.cost,
},
})
.collect();
let response = fabro_types::AggregateUsage {
totals: fabro_types::AggregateUsageTotals {
let response = fabro_api_types::AggregateUsage {
totals: fabro_api_types::AggregateUsageTotals {
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(),
@ -476,9 +476,12 @@ async fn list_runs(
.map(|(id, managed_run)| RunStatusResponse {
id: id.clone(),
status: managed_run.status,
error: managed_run.error.as_ref().map(|msg| fabro_types::RunError {
message: msg.clone(),
}),
error: managed_run
.error
.as_ref()
.map(|msg| fabro_api_types::RunError {
message: msg.clone(),
}),
queue_position: queue_positions.get(id).copied(),
created_at: managed_run.created_at,
})
@ -891,9 +894,12 @@ async fn get_run_status(
Json(RunStatusResponse {
id: id.clone(),
status: managed_run.status,
error: managed_run.error.as_ref().map(|msg| fabro_types::RunError {
message: msg.clone(),
}),
error: managed_run
.error
.as_ref()
.map(|msg| fabro_api_types::RunError {
message: msg.clone(),
}),
created_at: managed_run.created_at,
queue_position,
}),
@ -1232,13 +1238,13 @@ fn finish_reason_to_api_stop_reason(reason: &fabro_llm::types::FinishReason) ->
}
}
fn convert_api_message(msg: &fabro_types::CompletionMessage) -> fabro_llm::types::Message {
fn convert_api_message(msg: &fabro_api_types::CompletionMessage) -> fabro_llm::types::Message {
let role = match msg.role {
fabro_types::CompletionMessageRole::System => fabro_llm::types::Role::System,
fabro_types::CompletionMessageRole::User => fabro_llm::types::Role::User,
fabro_types::CompletionMessageRole::Assistant => fabro_llm::types::Role::Assistant,
fabro_types::CompletionMessageRole::Tool => fabro_llm::types::Role::Tool,
fabro_types::CompletionMessageRole::Developer => fabro_llm::types::Role::Developer,
fabro_api_types::CompletionMessageRole::System => fabro_llm::types::Role::System,
fabro_api_types::CompletionMessageRole::User => fabro_llm::types::Role::User,
fabro_api_types::CompletionMessageRole::Assistant => fabro_llm::types::Role::Assistant,
fabro_api_types::CompletionMessageRole::Tool => fabro_llm::types::Role::Tool,
fabro_api_types::CompletionMessageRole::Developer => fabro_llm::types::Role::Developer,
};
let content: Vec<fabro_llm::types::ContentPart> = msg
.content
@ -1256,15 +1262,15 @@ fn convert_api_message(msg: &fabro_types::CompletionMessage) -> fabro_llm::types
}
}
fn convert_llm_message(msg: &fabro_llm::types::Message) -> fabro_types::CompletionMessage {
fn convert_llm_message(msg: &fabro_llm::types::Message) -> fabro_api_types::CompletionMessage {
let role = match msg.role {
fabro_llm::types::Role::System => fabro_types::CompletionMessageRole::System,
fabro_llm::types::Role::User => fabro_types::CompletionMessageRole::User,
fabro_llm::types::Role::Assistant => fabro_types::CompletionMessageRole::Assistant,
fabro_llm::types::Role::Tool => fabro_types::CompletionMessageRole::Tool,
fabro_llm::types::Role::Developer => fabro_types::CompletionMessageRole::Developer,
fabro_llm::types::Role::System => fabro_api_types::CompletionMessageRole::System,
fabro_llm::types::Role::User => fabro_api_types::CompletionMessageRole::User,
fabro_llm::types::Role::Assistant => fabro_api_types::CompletionMessageRole::Assistant,
fabro_llm::types::Role::Tool => fabro_api_types::CompletionMessageRole::Tool,
fabro_llm::types::Role::Developer => fabro_api_types::CompletionMessageRole::Developer,
};
let content: Vec<fabro_types::CompletionContentPart> = msg
let content: Vec<fabro_api_types::CompletionContentPart> = msg
.content
.iter()
.filter_map(|part| {
@ -1272,7 +1278,7 @@ fn convert_llm_message(msg: &fabro_llm::types::Message) -> fabro_types::Completi
serde_json::from_value(json).ok()
})
.collect();
fabro_types::CompletionMessage {
fabro_api_types::CompletionMessage {
role,
content,
name: msg.name.clone(),
@ -1283,7 +1289,7 @@ fn convert_llm_message(msg: &fabro_llm::types::Message) -> fabro_types::Completi
async fn create_completion(
_auth: AuthenticatedService,
State(state): State<Arc<AppState>>,
Json(req): Json<fabro_types::CreateCompletionRequest>,
Json(req): Json<fabro_api_types::CreateCompletionRequest>,
) -> Response {
// Resolve model
let model_id = req.model.unwrap_or_else(|| {
@ -1330,12 +1336,12 @@ async fn create_completion(
// Convert tool_choice
let tool_choice: Option<fabro_llm::types::ToolChoice> =
req.tool_choice.map(|tc| match tc.mode {
fabro_types::CompletionToolChoiceMode::Auto => fabro_llm::types::ToolChoice::Auto,
fabro_types::CompletionToolChoiceMode::None => fabro_llm::types::ToolChoice::None,
fabro_types::CompletionToolChoiceMode::Required => {
fabro_api_types::CompletionToolChoiceMode::Auto => fabro_llm::types::ToolChoice::Auto,
fabro_api_types::CompletionToolChoiceMode::None => fabro_llm::types::ToolChoice::None,
fabro_api_types::CompletionToolChoiceMode::Required => {
fabro_llm::types::ToolChoice::Required
}
fabro_types::CompletionToolChoiceMode::Named => {
fabro_api_types::CompletionToolChoiceMode::Named => {
fabro_llm::types::ToolChoice::named(tc.tool_name.unwrap_or_default())
}
});
@ -1390,18 +1396,18 @@ async fn create_completion(
)]);
return Sse::new(sse_stream).into_response();
}
let empty_msg = fabro_types::CompletionMessage {
role: fabro_types::CompletionMessageRole::Assistant,
let empty_msg = fabro_api_types::CompletionMessage {
role: fabro_api_types::CompletionMessageRole::Assistant,
content: vec![],
name: None,
tool_call_id: None,
};
return Json(fabro_types::CompletionResponse {
return Json(fabro_api_types::CompletionResponse {
id: msg_id,
model: model_id,
message: empty_msg,
stop_reason: "end_turn".to_string(),
usage: fabro_types::CompletionUsage {
usage: fabro_api_types::CompletionUsage {
input_tokens: 0,
output_tokens: 0,
},
@ -1493,12 +1499,12 @@ async fn create_completion(
params = params.top_p(top_p);
}
match fabro_llm::generate::generate_object(params, schema).await {
Ok(result) => Json(fabro_types::CompletionResponse {
Ok(result) => Json(fabro_api_types::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_types::CompletionUsage {
usage: fabro_api_types::CompletionUsage {
input_tokens: result.usage.input_tokens,
output_tokens: result.usage.output_tokens,
},
@ -1510,12 +1516,12 @@ async fn create_completion(
}
} else {
match client.complete(&request).await {
Ok(response) => Json(fabro_types::CompletionResponse {
Ok(response) => Json(fabro_api_types::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_types::CompletionUsage {
usage: fabro_api_types::CompletionUsage {
input_tokens: response.usage.input_tokens,
output_tokens: response.usage.output_tokens,
},

View file

@ -25,7 +25,7 @@ pub struct SessionState {
pub model_id: String,
pub model_provider: Option<String>,
pub system_prompt: Option<String>,
pub turns: Vec<fabro_types::SessionTurn>,
pub turns: Vec<fabro_api_types::SessionTurn>,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
pub event_tx: broadcast::Sender<SessionEvent>,
@ -72,17 +72,17 @@ fn resolve_model(model_arg: Option<String>) -> (String, Option<String>) {
}
}
fn turns_to_messages(turns: &[fabro_types::SessionTurn]) -> Vec<fabro_llm::types::Message> {
fn turns_to_messages(turns: &[fabro_api_types::SessionTurn]) -> Vec<fabro_llm::types::Message> {
turns
.iter()
.filter_map(|turn| match turn {
fabro_types::SessionTurn::UserTurn(t) => {
fabro_api_types::SessionTurn::UserTurn(t) => {
Some(fabro_llm::types::Message::user(&t.content))
}
fabro_types::SessionTurn::AssistantTurn(t) => {
fabro_api_types::SessionTurn::AssistantTurn(t) => {
Some(fabro_llm::types::Message::assistant(&t.content))
}
fabro_types::SessionTurn::ToolTurn(_) => None,
fabro_api_types::SessionTurn::ToolTurn(_) => None,
})
.collect()
}
@ -120,13 +120,15 @@ fn spawn_generation(store: SessionStore, session_id: uuid::Uuid, dry_run: bool,
{
let mut store = store.write().expect("session store lock poisoned");
if let Some(session) = store.get_mut(&session_id) {
session.turns.push(fabro_types::SessionTurn::AssistantTurn(
fabro_types::AssistantTurn {
kind: fabro_types::AssistantTurnKind::Assistant,
content,
created_at: now,
},
));
session
.turns
.push(fabro_api_types::SessionTurn::AssistantTurn(
fabro_api_types::AssistantTurn {
kind: fabro_api_types::AssistantTurnKind::Assistant,
content,
created_at: now,
},
));
session.updated_at = now;
}
}
@ -187,13 +189,15 @@ fn spawn_generation(store: SessionStore, session_id: uuid::Uuid, dry_run: bool,
{
let mut store = store.write().expect("session store lock poisoned");
if let Some(session) = store.get_mut(&session_id) {
session.turns.push(fabro_types::SessionTurn::AssistantTurn(
fabro_types::AssistantTurn {
kind: fabro_types::AssistantTurnKind::Assistant,
content: full_text,
created_at: now,
},
));
session
.turns
.push(fabro_api_types::SessionTurn::AssistantTurn(
fabro_api_types::AssistantTurn {
kind: fabro_api_types::AssistantTurnKind::Assistant,
content: full_text,
created_at: now,
},
));
session.updated_at = now;
}
}
@ -204,7 +208,7 @@ fn spawn_generation(store: SessionStore, session_id: uuid::Uuid, dry_run: bool,
pub async fn create_session(
_auth: AuthenticatedService,
State(state): State<Arc<crate::server::AppState>>,
Json(req): Json<fabro_types::CreateSessionRequest>,
Json(req): Json<fabro_api_types::CreateSessionRequest>,
) -> Response {
let (model_id, model_provider) = resolve_model(req.model);
let now = chrono::Utc::now();
@ -214,8 +218,8 @@ pub async fn create_session(
let (event_tx, _) = broadcast::channel(256);
let generation_seq = Arc::new(AtomicU64::new(1));
let user_turn = fabro_types::SessionTurn::UserTurn(fabro_types::UserTurn {
kind: fabro_types::UserTurnKind::User,
let user_turn = fabro_api_types::SessionTurn::UserTurn(fabro_api_types::UserTurn {
kind: fabro_api_types::UserTurnKind::User,
content: req.content,
created_at: now,
});
@ -242,10 +246,10 @@ pub async fn create_session(
(
StatusCode::CREATED,
Json(fabro_types::CreateSessionResponse {
Json(fabro_api_types::CreateSessionResponse {
id: session_id,
title,
model: fabro_types::ModelReference { id: model_id },
model: fabro_api_types::ModelReference { id: model_id },
created_at: now,
updated_at: now,
}),
@ -262,10 +266,10 @@ pub async fn retrieve_session(
match store.get(&id) {
Some(session) => (
StatusCode::OK,
Json(fabro_types::SessionDetail {
Json(fabro_api_types::SessionDetail {
id: session.id,
title: session.title.clone(),
model: fabro_types::ModelReference {
model: fabro_api_types::ModelReference {
id: session.model_id.clone(),
},
created_at: session.created_at,
@ -282,20 +286,20 @@ pub async fn send_message(
_auth: AuthenticatedService,
State(state): State<Arc<crate::server::AppState>>,
Path(id): Path<uuid::Uuid>,
Json(req): Json<fabro_types::SendMessageRequest>,
Json(req): Json<fabro_api_types::SendMessageRequest>,
) -> 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_types::SessionTurn::UserTurn(fabro_types::UserTurn {
kind: fabro_types::UserTurnKind::User,
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.updated_at = now;
session.generation_seq.fetch_add(1, Ordering::Relaxed) + 1
}
@ -307,7 +311,7 @@ pub async fn send_message(
(
StatusCode::ACCEPTED,
Json(fabro_types::SendMessageResponse { accepted: true }),
Json(fabro_api_types::SendMessageResponse { accepted: true }),
)
.into_response()
}
@ -373,16 +377,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<fabro_types::SessionListItem> = store
let mut items: Vec<fabro_api_types::SessionListItem> = store
.values()
.map(|session| {
let last_message_preview = session
.turns
.last()
.map(|t| match t {
fabro_types::SessionTurn::UserTurn(u) => u.content.clone(),
fabro_types::SessionTurn::AssistantTurn(a) => a.content.clone(),
fabro_types::SessionTurn::ToolTurn(_) => String::new(),
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(),
})
.unwrap_or_default();
let preview = if last_message_preview.len() > 100 {
@ -390,10 +394,10 @@ pub async fn list_sessions(
} else {
last_message_preview
};
fabro_types::SessionListItem {
fabro_api_types::SessionListItem {
id: session.id,
title: session.title.clone(),
model: fabro_types::ModelReference {
model: fabro_api_types::ModelReference {
id: session.model_id.clone(),
},
last_message_preview: preview,
@ -412,9 +416,9 @@ pub async fn list_sessions(
(
StatusCode::OK,
Json(fabro_types::PaginatedSessionList {
Json(fabro_api_types::PaginatedSessionList {
data,
meta: fabro_types::PaginationMeta { has_more },
meta: fabro_api_types::PaginationMeta { has_more },
}),
)
.into_response()