From 4bd975321773a5ede5cf834b4dcecf678da038f3 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Fri, 24 Jul 2026 07:44:40 -0400 Subject: [PATCH 1/3] Expose model reasoning effort controls --- docs/public/api-reference/fabro-api.yaml | 17 +++++ lib/apps/fabro-cli/src/commands/model.rs | 6 +- lib/apps/fabro-server/src/server/tests.rs | 29 +++++++++ lib/components/fabro-llm/src/model_test.rs | 5 +- .../fabro-api/tests/model_round_trip.rs | 14 ++++- .../fabro-api/tests/provider_id_round_trip.rs | 4 +- lib/foundation/fabro-model/src/catalog.rs | 63 ++++++++++++++++++- lib/foundation/fabro-model/src/lib.rs | 4 +- lib/foundation/fabro-model/src/types.rs | 12 ++++ .../src/.openapi-generator/FILES | 1 + .../fabro-api-client/src/models/index.ts | 1 + .../src/models/model-controls.ts | 28 +++++++++ .../fabro-api-client/src/models/model.ts | 4 ++ 13 files changed, 182 insertions(+), 6 deletions(-) create mode 100644 lib/packages/fabro-api-client/src/models/model-controls.ts diff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml index 519ee4188..1260db2b4 100644 --- a/docs/public/api-reference/fabro-api.yaml +++ b/docs/public/api-reference/fabro-api.yaml @@ -8289,6 +8289,20 @@ components: description: Cost per million cached input tokens in USD. example: 1.50 + ModelControls: + description: Request-control values accepted by a provider/model offering. + type: object + required: + - reasoning_effort + properties: + reasoning_effort: + type: array + description: >- + Exact reasoning-effort values accepted by this offering. An empty + array means the request control is unsupported. + items: + $ref: "#/components/schemas/ReasoningEffort" + Model: description: | One provider's offering of an LLM model. The `id` is unique within @@ -8303,6 +8317,7 @@ components: - training - knowledge_cutoff - features + - controls - costs - estimated_output_tps - aliases @@ -8336,6 +8351,8 @@ components: example: "May 2025" features: $ref: "#/components/schemas/ModelFeatures" + controls: + $ref: "#/components/schemas/ModelControls" costs: $ref: "#/components/schemas/ModelCosts" estimated_output_tps: diff --git a/lib/apps/fabro-cli/src/commands/model.rs b/lib/apps/fabro-cli/src/commands/model.rs index f84bd44e2..774feae42 100644 --- a/lib/apps/fabro-cli/src/commands/model.rs +++ b/lib/apps/fabro-cli/src/commands/model.rs @@ -517,7 +517,9 @@ impl Default for ModelsCommand { #[cfg(test)] mod tests { - use fabro_model::{ModelCosts, ModelFeatures, ModelLimits, ReasoningEffortFeature}; + use fabro_model::{ + ModelControls, ModelCosts, ModelFeatures, ModelLimits, ReasoningEffortFeature, + }; use super::*; @@ -546,6 +548,7 @@ mod tests { cache_control_breakpoints: false, sampling_params: true, }, + controls: ModelControls::default(), costs: ModelCosts { input_cost_per_mtok: Some(1.0), output_cost_per_mtok: Some(2.0), @@ -581,6 +584,7 @@ mod tests { cache_control_breakpoints: false, sampling_params: true, }, + controls: ModelControls::default(), costs: ModelCosts { input_cost_per_mtok: Some(1.0), output_cost_per_mtok: Some(2.0), diff --git a/lib/apps/fabro-server/src/server/tests.rs b/lib/apps/fabro-server/src/server/tests.rs index 454455be0..cf59270ce 100644 --- a/lib/apps/fabro-server/src/server/tests.rs +++ b/lib/apps/fabro-server/src/server/tests.rs @@ -6768,6 +6768,35 @@ async fn list_models_filters_by_provider() { ); } +#[tokio::test] +async fn list_models_exposes_reasoning_effort_controls() { + let app = test_app_with(); + + let req = Request::builder() + .method("GET") + .uri(api("/models?provider=kimi")) + .body(Body::empty()) + .unwrap(); + + let response = app.oneshot(req).await.unwrap(); + let body = response_json!(response, StatusCode::OK).await; + let models = body["data"].as_array().unwrap(); + let kimi_k3 = models + .iter() + .find(|model| model["id"] == "kimi-k3") + .expect("Kimi K3 should be listed"); + let kimi_k2_5 = models + .iter() + .find(|model| model["id"] == "kimi-k2.5") + .expect("Kimi K2.5 should be listed"); + + assert_eq!( + kimi_k3["controls"]["reasoning_effort"], + json!(["low", "high", "max"]) + ); + assert_eq!(kimi_k2_5["controls"]["reasoning_effort"], json!([])); +} + #[tokio::test] async fn list_models_marks_configured_true_when_provider_has_credential_material() { let state = test_app_state_with_env_lookup( diff --git a/lib/components/fabro-llm/src/model_test.rs b/lib/components/fabro-llm/src/model_test.rs index aec0a83a7..6a985fb1b 100644 --- a/lib/components/fabro-llm/src/model_test.rs +++ b/lib/components/fabro-llm/src/model_test.rs @@ -169,7 +169,9 @@ fn validate_deep_result(result: &GenerateResult) -> Result<(), String> { mod tests { use std::collections::HashMap; - use fabro_model::{ModelCosts, ModelFeatures, ModelLimits, ProviderId, ReasoningEffortFeature}; + use fabro_model::{ + ModelControls, ModelCosts, ModelFeatures, ModelLimits, ProviderId, ReasoningEffortFeature, + }; use super::*; use crate::types::{FinishReason, Message, Response, StepResult, TokenCounts, ToolResult}; @@ -187,6 +189,7 @@ mod tests { training: None, knowledge_cutoff: None, features, + controls: ModelControls::default(), costs: ModelCosts { input_cost_per_mtok: None, output_cost_per_mtok: None, diff --git a/lib/foundation/fabro-api/tests/model_round_trip.rs b/lib/foundation/fabro-api/tests/model_round_trip.rs index 57d3db557..5f39eb6ab 100644 --- a/lib/foundation/fabro-api/tests/model_round_trip.rs +++ b/lib/foundation/fabro-api/tests/model_round_trip.rs @@ -2,7 +2,8 @@ use std::any::{TypeId, type_name}; use fabro_api::types::Model as ApiModel; use fabro_model::{ - Model, ModelCosts, ModelFeatures, ModelLimits, ProviderId, ReasoningEffortFeature, + Model, ModelControls, ModelCosts, ModelFeatures, ModelLimits, ProviderId, ReasoningEffort, + ReasoningEffortFeature, }; #[test] @@ -32,6 +33,13 @@ fn model_json_matches_openapi_shape() { cache_control_breakpoints: false, sampling_params: true, }, + controls: ModelControls { + reasoning_effort: vec![ + ReasoningEffort::Low, + ReasoningEffort::High, + ReasoningEffort::Max, + ], + }, costs: ModelCosts { input_cost_per_mtok: Some(5.0), output_cost_per_mtok: Some(25.0), @@ -50,6 +58,10 @@ fn model_json_matches_openapi_shape() { assert_eq!(json["knowledge_cutoff"], "May 2025"); assert_eq!(json["features"]["reasoning_effort"], "levels"); assert_eq!(json["features"]["prompt_cache"], true); + assert_eq!( + json["controls"]["reasoning_effort"], + serde_json::json!(["low", "high", "max"]) + ); assert_eq!(json["estimated_output_tps"], 25.0); assert_eq!(json["small_default"], true); assert_eq!(json["configured"], true); diff --git a/lib/foundation/fabro-api/tests/provider_id_round_trip.rs b/lib/foundation/fabro-api/tests/provider_id_round_trip.rs index 970c7158f..2c834ac3f 100644 --- a/lib/foundation/fabro-api/tests/provider_id_round_trip.rs +++ b/lib/foundation/fabro-api/tests/provider_id_round_trip.rs @@ -2,7 +2,8 @@ use std::any::{TypeId, type_name}; use fabro_api::types::Model as ApiModel; use fabro_model::{ - Model, ModelCosts, ModelFeatures, ModelLimits, ProviderId, ReasoningEffortFeature, + Model, ModelControls, ModelCosts, ModelFeatures, ModelLimits, ProviderId, + ReasoningEffortFeature, }; use serde_json::json; @@ -42,6 +43,7 @@ fn provider_id_json_matches_openapi_shape_through_model() { cache_control_breakpoints: false, sampling_params: true, }, + controls: ModelControls::default(), costs: ModelCosts { input_cost_per_mtok: None, output_cost_per_mtok: None, diff --git a/lib/foundation/fabro-model/src/catalog.rs b/lib/foundation/fabro-model/src/catalog.rs index cac9f878f..b0f57712f 100644 --- a/lib/foundation/fabro-model/src/catalog.rs +++ b/lib/foundation/fabro-model/src/catalog.rs @@ -15,7 +15,9 @@ use crate::codec::CodecKind; use crate::ids::{ModelId, ProviderId}; use crate::provider::Provider; use crate::reasoning::ReasoningEffort; -use crate::types::{Model, ModelCosts, ModelFeatures, ModelLimits, ReasoningEffortFeature}; +use crate::types::{ + Model, ModelControls, ModelCosts, ModelFeatures, ModelLimits, ReasoningEffortFeature, +}; #[derive(RustEmbed)] #[folder = "src/catalog/providers"] @@ -2203,6 +2205,9 @@ fn build_model( training: settings.training.clone(), knowledge_cutoff: settings.knowledge_cutoff.clone(), features: model_features, + controls: ModelControls { + reasoning_effort: controls.reasoning_effort.clone(), + }, costs, estimated_output_tps: settings.estimated_output_tps, aliases: settings.aliases.clone().unwrap_or_default(), @@ -3245,6 +3250,12 @@ enabled = true cache_control_breakpoints: false, sampling_params: true, }, + controls: ModelControls { + reasoning_effort: [ + High, + XHigh, + ], + }, costs: ModelCosts { input_cost_per_mtok: Some( 0.784, @@ -3310,6 +3321,13 @@ enabled = true cache_control_breakpoints: false, sampling_params: false, }, + controls: ModelControls { + reasoning_effort: [ + Low, + High, + Max, + ], + }, costs: ModelCosts { input_cost_per_mtok: Some( 3.0, @@ -5910,6 +5928,15 @@ sampling_params = false cache_control_breakpoints: false, sampling_params: true, }, + controls: ModelControls { + reasoning_effort: [ + Low, + Medium, + High, + XHigh, + Max, + ], + }, costs: ModelCosts { input_cost_per_mtok: Some( 5.0, @@ -5967,6 +5994,9 @@ sampling_params = false cache_control_breakpoints: false, sampling_params: false, }, + controls: ModelControls { + reasoning_effort: [], + }, costs: ModelCosts { input_cost_per_mtok: Some( 0.6, @@ -6016,6 +6046,13 @@ sampling_params = false cache_control_breakpoints: false, sampling_params: false, }, + controls: ModelControls { + reasoning_effort: [ + Low, + High, + Max, + ], + }, costs: ModelCosts { input_cost_per_mtok: Some( 3.0, @@ -6089,6 +6126,12 @@ sampling_params = false cache_control_breakpoints: false, sampling_params: true, }, + controls: ModelControls { + reasoning_effort: [ + High, + Max, + ], + }, costs: ModelCosts { input_cost_per_mtok: Some( 1.4, @@ -6155,6 +6198,15 @@ sampling_params = false cache_control_breakpoints: false, sampling_params: true, }, + controls: ModelControls { + reasoning_effort: [ + Low, + Medium, + High, + XHigh, + Max, + ], + }, costs: ModelCosts { input_cost_per_mtok: Some( 0.25, @@ -6212,6 +6264,15 @@ sampling_params = false cache_control_breakpoints: false, sampling_params: true, }, + controls: ModelControls { + reasoning_effort: [ + Low, + Medium, + High, + XHigh, + Max, + ], + }, costs: ModelCosts { input_cost_per_mtok: Some( 30.0, diff --git a/lib/foundation/fabro-model/src/lib.rs b/lib/foundation/fabro-model/src/lib.rs index 5f3511096..fc43c95a7 100644 --- a/lib/foundation/fabro-model/src/lib.rs +++ b/lib/foundation/fabro-model/src/lib.rs @@ -27,4 +27,6 @@ pub use model_ref::ModelHandle; pub use model_test::ModelTestMode; pub use provider::Provider; pub use reasoning::ReasoningEffort; -pub use types::{Model, ModelCosts, ModelFeatures, ModelLimits, ReasoningEffortFeature}; +pub use types::{ + Model, ModelControls, ModelCosts, ModelFeatures, ModelLimits, ReasoningEffortFeature, +}; diff --git a/lib/foundation/fabro-model/src/types.rs b/lib/foundation/fabro-model/src/types.rs index ae3804784..9a0d27148 100644 --- a/lib/foundation/fabro-model/src/types.rs +++ b/lib/foundation/fabro-model/src/types.rs @@ -1,6 +1,7 @@ use serde::{Deserialize, Serialize}; use crate::ids::{ModelId, ProviderId}; +use crate::reasoning::ReasoningEffort; // --- 2.9 Model --- @@ -83,6 +84,14 @@ pub struct ModelCosts { pub cache_input_cost_per_mtok: Option, } +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct ModelControls { + /// Exact reasoning-effort values accepted by this provider/model offering. + /// An empty list means the request control is unsupported. + #[serde(default)] + pub reasoning_effort: Vec, +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Model { pub id: ModelId, @@ -93,6 +102,8 @@ pub struct Model { pub training: Option, pub knowledge_cutoff: Option, pub features: ModelFeatures, + #[serde(default)] + pub controls: ModelControls, pub costs: ModelCosts, pub estimated_output_tps: Option, pub aliases: Vec, @@ -236,6 +247,7 @@ mod tests { cache_control_breakpoints: false, sampling_params: true, }, + controls: ModelControls::default(), costs: ModelCosts { input_cost_per_mtok: Some(1.0), output_cost_per_mtok: Some(2.0), diff --git a/lib/packages/fabro-api-client/src/.openapi-generator/FILES b/lib/packages/fabro-api-client/src/.openapi-generator/FILES index 52393c07a..bc64eab3f 100644 --- a/lib/packages/fabro-api-client/src/.openapi-generator/FILES +++ b/lib/packages/fabro-api-client/src/.openapi-generator/FILES @@ -226,6 +226,7 @@ models/mcp-transport.ts models/merge-method.ts models/merge-run-pull-request-request.ts models/merge-run-pull-request-response.ts +models/model-controls.ts models/model-costs.ts models/model-features.ts models/model-limits.ts diff --git a/lib/packages/fabro-api-client/src/models/index.ts b/lib/packages/fabro-api-client/src/models/index.ts index 88f4a75d2..8a3bfd568 100644 --- a/lib/packages/fabro-api-client/src/models/index.ts +++ b/lib/packages/fabro-api-client/src/models/index.ts @@ -197,6 +197,7 @@ export * from './merge-method'; export * from './merge-run-pull-request-request'; export * from './merge-run-pull-request-response'; export * from './model'; +export * from './model-controls'; export * from './model-costs'; export * from './model-features'; export * from './model-limits'; diff --git a/lib/packages/fabro-api-client/src/models/model-controls.ts b/lib/packages/fabro-api-client/src/models/model-controls.ts new file mode 100644 index 000000000..59f22ce2a --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/model-controls.ts @@ -0,0 +1,28 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { ReasoningEffort } from './reasoning-effort'; + +/** + * Request-control values accepted by a provider/model offering. + */ +export interface ModelControls { + /** + * Exact reasoning-effort values accepted by this offering. An empty array means the request control is unsupported. + */ + 'reasoning_effort': Array; +} diff --git a/lib/packages/fabro-api-client/src/models/model.ts b/lib/packages/fabro-api-client/src/models/model.ts index d3326e2db..0bf86bee5 100644 --- a/lib/packages/fabro-api-client/src/models/model.ts +++ b/lib/packages/fabro-api-client/src/models/model.ts @@ -13,6 +13,9 @@ */ +// May contain unused imports in some cases +// @ts-ignore +import type { ModelControls } from './model-controls'; // May contain unused imports in some cases // @ts-ignore import type { ModelCosts } from './model-costs'; @@ -53,6 +56,7 @@ export interface Model { */ 'knowledge_cutoff': string | null; 'features': ModelFeatures; + 'controls': ModelControls; 'costs': ModelCosts; /** * Estimated output tokens per second. From 5d8befa6acd443372b4e80c293d912d756cea426 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Fri, 24 Jul 2026 09:15:18 -0400 Subject: [PATCH 2/3] Reuse canonical ModelControls in fabro-api and tighten serde contract Add the missing with_replacement for ModelControls so progenitor reuses fabro_model::ModelControls instead of generating a dead parallel DTO, re-export it from fabro_api::types, and assert type identity in the round-trip test. Drop #[serde(default)] from Model.controls and ModelControls.reasoning_effort: the OpenAPI spec marks both required, matching the strict deserialization of the sibling features/costs fields. Update CLI stub payloads to include the now-required field. Co-Authored-By: Claude Fable 5 --- lib/apps/fabro-cli/tests/it/cmd/model.rs | 12 ++++++++++++ lib/apps/fabro-cli/tests/it/cmd/model_test.rs | 3 +++ lib/foundation/fabro-api/build.rs | 1 + lib/foundation/fabro-api/src/lib.rs | 5 +++-- lib/foundation/fabro-api/tests/model_round_trip.rs | 3 ++- lib/foundation/fabro-model/src/types.rs | 4 +--- 6 files changed, 22 insertions(+), 6 deletions(-) diff --git a/lib/apps/fabro-cli/tests/it/cmd/model.rs b/lib/apps/fabro-cli/tests/it/cmd/model.rs index 18d3bce01..28fe1fa01 100644 --- a/lib/apps/fabro-cli/tests/it/cmd/model.rs +++ b/lib/apps/fabro-cli/tests/it/cmd/model.rs @@ -108,6 +108,9 @@ fn list_with_filters_renders_server_models_table() { "vision": false, "reasoning": false }, + "controls": { + "reasoning_effort": [] + }, "costs": { "input_cost_per_mtok": 1.2, "output_cost_per_mtok": 3.4, @@ -134,6 +137,9 @@ fn list_with_filters_renders_server_models_table() { "vision": true, "reasoning": true }, + "controls": { + "reasoning_effort": [] + }, "costs": { "input_cost_per_mtok": null, "output_cost_per_mtok": null, @@ -206,6 +212,9 @@ fn list_uses_configured_server_target_without_server_flag() { "vision": false, "reasoning": false }, + "controls": { + "reasoning_effort": [] + }, "costs": { "input_cost_per_mtok": 1.0, "output_cost_per_mtok": 2.0, @@ -260,6 +269,9 @@ fn list_uses_fabro_config_for_machine_settings() { "vision": false, "reasoning": false }, + "controls": { + "reasoning_effort": [] + }, "costs": { "input_cost_per_mtok": 1.0, "output_cost_per_mtok": 2.0, diff --git a/lib/apps/fabro-cli/tests/it/cmd/model_test.rs b/lib/apps/fabro-cli/tests/it/cmd/model_test.rs index 70e64ac77..acaae9851 100644 --- a/lib/apps/fabro-cli/tests/it/cmd/model_test.rs +++ b/lib/apps/fabro-cli/tests/it/cmd/model_test.rs @@ -42,6 +42,9 @@ fn model_json(id: &str, provider: &str, configured: bool) -> serde_json::Value { "vision": false, "reasoning": false }, + "controls": { + "reasoning_effort": [] + }, "costs": { "input_cost_per_mtok": 1.0, "output_cost_per_mtok": 2.0, diff --git a/lib/foundation/fabro-api/build.rs b/lib/foundation/fabro-api/build.rs index cdec7f07a..e560335ae 100644 --- a/lib/foundation/fabro-api/build.rs +++ b/lib/foundation/fabro-api/build.rs @@ -478,6 +478,7 @@ fn main() { ), ("ReasoningEffort", "fabro_model::ReasoningEffort", &[]), ("ModelFeatures", "fabro_model::ModelFeatures", &[]), + ("ModelControls", "fabro_model::ModelControls", &[]), ("ModelCosts", "fabro_model::ModelCosts", &[]), ("ModelTestMode", "fabro_model::ModelTestMode", &[]), ("RunProjection", "fabro_types::RunProjection", &[]), diff --git a/lib/foundation/fabro-api/src/lib.rs b/lib/foundation/fabro-api/src/lib.rs index 6d68d57b4..17622a6ca 100644 --- a/lib/foundation/fabro-api/src/lib.rs +++ b/lib/foundation/fabro-api/src/lib.rs @@ -20,8 +20,9 @@ pub mod types { }; pub use fabro_environment::Environment; pub use fabro_model::{ - CostSource, Model, ModelCosts, ModelFeatures, ModelLimits, ModelRef as BillingModelRef, - ModelTestMode, Provider, ReasoningEffort, ReasoningEffortFeature, Speed as BillingSpeed, + CostSource, Model, ModelControls, ModelCosts, ModelFeatures, ModelLimits, + ModelRef as BillingModelRef, ModelTestMode, Provider, ReasoningEffort, + ReasoningEffortFeature, Speed as BillingSpeed, }; pub use fabro_types::run_event::AgentSessionActivatedProps; pub use fabro_types::settings::run::McpHttpProtocol; diff --git a/lib/foundation/fabro-api/tests/model_round_trip.rs b/lib/foundation/fabro-api/tests/model_round_trip.rs index 5f39eb6ab..0decc177f 100644 --- a/lib/foundation/fabro-api/tests/model_round_trip.rs +++ b/lib/foundation/fabro-api/tests/model_round_trip.rs @@ -1,6 +1,6 @@ use std::any::{TypeId, type_name}; -use fabro_api::types::Model as ApiModel; +use fabro_api::types::{Model as ApiModel, ModelControls as ApiModelControls}; use fabro_model::{ Model, ModelControls, ModelCosts, ModelFeatures, ModelLimits, ProviderId, ReasoningEffort, ReasoningEffortFeature, @@ -9,6 +9,7 @@ use fabro_model::{ #[test] fn model_reuses_canonical_type() { assert_same_type::(); + assert_same_type::(); } #[test] diff --git a/lib/foundation/fabro-model/src/types.rs b/lib/foundation/fabro-model/src/types.rs index 9a0d27148..9e4a9e96d 100644 --- a/lib/foundation/fabro-model/src/types.rs +++ b/lib/foundation/fabro-model/src/types.rs @@ -84,11 +84,10 @@ pub struct ModelCosts { pub cache_input_cost_per_mtok: Option, } -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] pub struct ModelControls { /// Exact reasoning-effort values accepted by this provider/model offering. /// An empty list means the request control is unsupported. - #[serde(default)] pub reasoning_effort: Vec, } @@ -102,7 +101,6 @@ pub struct Model { pub training: Option, pub knowledge_cutoff: Option, pub features: ModelFeatures, - #[serde(default)] pub controls: ModelControls, pub costs: ModelCosts, pub estimated_output_tps: Option, From 3970c9f545d9fc91bd2c486ade5eb0b5750e2dac Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Fri, 24 Jul 2026 09:23:18 -0400 Subject: [PATCH 3/3] Restore serde defaults on Model.controls for older-server compatibility Copilot review flagged that dropping #[serde(default)] makes newer clients hard-fail against servers that predate the controls field. The late-added Model fields (default, small_default, configured) set the precedent: required in the OpenAPI spec, defaulted on deserialization. An empty controls list already means "unsupported", so the degraded value is semantically correct. Co-Authored-By: Claude Fable 5 --- lib/foundation/fabro-model/src/types.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/foundation/fabro-model/src/types.rs b/lib/foundation/fabro-model/src/types.rs index 9e4a9e96d..50ae020dc 100644 --- a/lib/foundation/fabro-model/src/types.rs +++ b/lib/foundation/fabro-model/src/types.rs @@ -88,6 +88,7 @@ pub struct ModelCosts { pub struct ModelControls { /// Exact reasoning-effort values accepted by this provider/model offering. /// An empty list means the request control is unsupported. + #[serde(default)] pub reasoning_effort: Vec, } @@ -101,6 +102,9 @@ pub struct Model { pub training: Option, pub knowledge_cutoff: Option, pub features: ModelFeatures, + /// Required in API responses; defaulted on deserialization so newer + /// clients tolerate older servers that predate this field. + #[serde(default)] pub controls: ModelControls, pub costs: ModelCosts, pub estimated_output_tps: Option,