Expose model reasoning effort controls

This commit is contained in:
Bryan Helmkamp 2026-07-24 07:44:40 -04:00
parent df3c834581
commit 4bd9753217
No known key found for this signature in database
13 changed files with 182 additions and 6 deletions

View file

@ -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:

View file

@ -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),

View file

@ -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(

View file

@ -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,

View file

@ -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);

View file

@ -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,

View file

@ -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,

View file

@ -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,
};

View file

@ -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<f64>,
}
#[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<ReasoningEffort>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Model {
pub id: ModelId,
@ -93,6 +102,8 @@ pub struct Model {
pub training: Option<String>,
pub knowledge_cutoff: Option<String>,
pub features: ModelFeatures,
#[serde(default)]
pub controls: ModelControls,
pub costs: ModelCosts,
pub estimated_output_tps: Option<f64>,
pub aliases: Vec<String>,
@ -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),

View file

@ -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

View file

@ -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';

View file

@ -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<ReasoningEffort>;
}

View file

@ -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.