refactor: simplify model-keyed fallback internals

Consolidation pass over the fallback feature, no intended behavior
changes beyond noted validation and event-shape cleanups:

- Unify the two parallel notice types: FallbackPlanNotice is gone;
  ModelFallbackNotice now owns the runtime NoNearbyReasoningLevel case
  and the shared ChainEmpty wording. Notices emit through a new
  Emitter::notice_scoped with their own level, and each distinct notice
  is emitted once per run instead of on every LLM call.
- Move canonical_model_id onto Catalog so chain keys are written and
  read through one function; reject provider-qualified fallback keys,
  which could never match at dispatch and were silently dead config.
- Type FallbackTarget as ProviderId/ModelId, removing repeated
  ProviderId::new re-wrapping at every use site.
- Derive FallbackPlan's current route from a position index instead of
  storing current/requested_controls copies; advance() no longer has
  unreachable None branches.
- Bundle the agent invocation's live state (session, bridge, lease,
  forwarder, accounting) into LiveAgentInvocation; failover_agent_session
  drops from 21 parameters to 7 and the six copies of the
  abort/discard/classify teardown collapse into two methods.
- Share one route_request builder between one_shot and its failover
  loop; complete_one_shot_request takes the request by value instead of
  deep-cloning the message payload per call.
- Event::Failover carries FailoverProps directly; the props' original
  route and attempt fields are now required, and reasoning efforts are
  typed ReasoningEffort instead of strings.
- Reuse RunModelSettings/RunModelControls in fabro-api via
  with_replacement, add the missing controls property to the OpenAPI
  schema, regenerate the TS client, and add the type-identity/JSON
  parity test.
- Smaller cleanups: ReasoningEffort::closest_supported uses enum
  discriminants; ModelFallbackPolicy gains len(); resolve_model_fallbacks
  takes a provider slice; duplicate-target filtering lives only in the
  resolver.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-07-30 12:51:36 -04:00
parent ba82656656
commit e6eb36f852
No known key found for this signature in database
20 changed files with 715 additions and 667 deletions

View file

@ -14324,6 +14324,20 @@ components:
type: array
items:
$ref: "#/components/schemas/ModelRef"
controls:
$ref: "#/components/schemas/RunModelControls"
RunModelControls:
type: object
description: >
Run-level default values for typed model controls. Node and style
attributes still win over these defaults.
required: [reasoning_effort, speed]
properties:
reasoning_effort:
type: ["string", "null"]
speed:
type: ["string", "null"]
RunGitSettings:
type: object

View file

@ -1,4 +1,4 @@
use std::collections::{BTreeMap, HashMap, HashSet};
use std::collections::{BTreeMap, HashMap};
use std::future::Future;
use std::path::{Path, PathBuf};
use std::sync::Arc;
@ -626,8 +626,7 @@ fn run_model_fallback_check(
return true;
}
let eligible = ready_providers.iter().cloned().collect::<HashSet<_>>();
let resolved = match resolve_model_fallbacks(catalog, &eligible, configured) {
let resolved = match resolve_model_fallbacks(catalog, ready_providers, configured) {
Ok(resolved) => resolved,
Err(error) => {
checks.push(CheckResult {
@ -676,10 +675,7 @@ fn run_model_fallback_check(
} else {
CheckStatus::Pass
},
summary: format!(
"{} requested model chain(s)",
resolved.policy.iter().count()
),
summary: format!("{} requested model chain(s)", resolved.policy.len()),
details,
remediation: None,
});

View file

@ -1146,30 +1146,7 @@ fn event_body_from_event(event: &Event) -> EventBody {
ssh_command: ssh_command.clone(),
})
}
Event::Failover {
original_provider,
original_model,
attempt,
from_provider,
from_model,
to_provider,
to_model,
requested_reasoning_effort,
effective_reasoning_effort,
error,
..
} => EventBody::Failover(fabro_types::FailoverProps {
original_provider: Some(original_provider.clone()),
original_model: Some(original_model.clone()),
attempt: Some(*attempt),
from_provider: from_provider.clone(),
from_model: from_model.clone(),
to_provider: to_provider.clone(),
to_model: to_model.clone(),
requested_reasoning_effort: requested_reasoning_effort.clone(),
effective_reasoning_effort: effective_reasoning_effort.clone(),
error: error.clone(),
}),
Event::Failover { props, .. } => EventBody::Failover(props.clone()),
Event::CommandStarted {
script,
command,

View file

@ -84,6 +84,24 @@ impl Emitter {
});
}
pub fn notice_scoped(
&self,
level: RunNoticeLevel,
code: RunNoticeCode,
message: impl Into<String>,
scope: &StageScope,
) {
self.emit_scoped(
&Event::RunNotice {
level,
code: code.to_string(),
message: message.into(),
exec_output_tail: None,
},
scope,
);
}
pub fn notice_with_tail(
&self,
level: RunNoticeLevel,

View file

@ -584,16 +584,7 @@ pub enum Event {
},
Failover {
stage: String,
original_provider: String,
original_model: String,
attempt: u32,
from_provider: String,
from_model: String,
to_provider: String,
to_model: String,
requested_reasoning_effort: Option<String>,
effective_reasoning_effort: Option<String>,
error: String,
props: fabro_types::FailoverProps,
},
CommandStarted {
node_id: String,
@ -1398,31 +1389,19 @@ impl Event {
Self::SshAccessReady { ssh_command } => {
info!(ssh_command, "SSH access ready");
}
Self::Failover {
stage,
original_provider,
original_model,
attempt,
from_provider,
from_model,
to_provider,
to_model,
requested_reasoning_effort,
effective_reasoning_effort,
error,
} => {
Self::Failover { stage, props } => {
warn!(
stage,
original_provider,
original_model,
attempt,
from_provider,
from_model,
to_provider,
to_model,
requested_reasoning_effort,
effective_reasoning_effort,
error,
original_provider = %props.original_provider,
original_model = %props.original_model,
attempt = props.attempt,
from_provider = %props.from_provider,
from_model = %props.from_model,
to_provider = %props.to_provider,
to_model = %props.to_model,
requested_reasoning_effort = ?props.requested_reasoning_effort,
effective_reasoning_effort = ?props.effective_reasoning_effort,
error = %props.error,
"LLM provider failover"
);
}

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,8 @@
use std::collections::{BTreeMap, HashMap, HashSet};
use fabro_model::{Catalog, FallbackTarget, Model, ModelSelectionError, ProviderId};
use fabro_model::{
Catalog, FallbackTarget, Model, ModelSelectionError, ProviderId, ReasoningEffort,
};
use fabro_types::settings::{ModelRef, ResolvedModelRef};
use fabro_types::{RunNoticeCode, RunNoticeLevel};
@ -16,6 +18,7 @@ pub struct ModelFallbackPolicy {
}
impl ModelFallbackPolicy {
#[cfg(test)]
#[must_use]
pub fn new(chains: BTreeMap<String, Vec<FallbackTarget>>) -> Self {
Self { chains }
@ -28,8 +31,13 @@ impl ModelFallbackPolicy {
provider: &ProviderId,
model: &str,
) -> Option<&'a [FallbackTarget]> {
let canonical = canonical_model_id(catalog, provider, model);
self.chains.get(&canonical).map(Vec::as_slice)
self.chain_for_canonical(&catalog.canonical_model_id(provider, model))
}
/// Look up a chain by an already-canonicalized requested model ID.
#[must_use]
pub fn chain_for_canonical(&self, canonical_model: &str) -> Option<&[FallbackTarget]> {
self.chains.get(canonical_model).map(Vec::as_slice)
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &[FallbackTarget])> {
@ -38,23 +46,17 @@ impl ModelFallbackPolicy {
.map(|(model, chain)| (model.as_str(), chain.as_slice()))
}
#[must_use]
pub fn len(&self) -> usize {
self.chains.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.chains.is_empty()
}
}
pub(crate) fn canonical_model_id(catalog: &Catalog, provider: &ProviderId, model: &str) -> String {
catalog.get_on_provider(provider, model).map_or_else(
|| {
catalog
.select(model, None, &catalog.all_provider_ids())
.map_or_else(|_| model.to_string(), |offering| offering.id.to_string())
},
|offering| offering.id.to_string(),
)
}
/// Server-side result of canonicalizing and filtering configured fallback
/// chains.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
@ -91,6 +93,11 @@ pub enum ModelFallbackNotice {
reference: ModelRef,
target: FallbackTarget,
},
NoNearbyReasoningLevel {
requested_model: String,
target: FallbackTarget,
requested_effort: ReasoningEffort,
},
ChainEmpty {
requested_model: String,
},
@ -105,7 +112,8 @@ impl ModelFallbackNotice {
| Self::NoConfiguredOffering { .. }
| Self::PrimaryNotInCatalog { .. }
| Self::NoCompatibleModel { .. }
| Self::Duplicate { .. } => RunNoticeCode::ModelFallbackSkipped,
| Self::Duplicate { .. }
| Self::NoNearbyReasoningLevel { .. } => RunNoticeCode::ModelFallbackSkipped,
}
}
@ -117,6 +125,7 @@ impl ModelFallbackNotice {
| Self::NoConfiguredOffering { .. }
| Self::PrimaryNotInCatalog { .. }
| Self::NoCompatibleModel { .. }
| Self::NoNearbyReasoningLevel { .. }
| Self::ChainEmpty { .. } => RunNoticeLevel::Warn,
}
}
@ -166,6 +175,13 @@ impl ModelFallbackNotice {
} => format!(
"Model fallback `{reference}` for requested model `{requested_model}` was skipped because target `{target}` already appears in that chain."
),
Self::NoNearbyReasoningLevel {
requested_model,
target,
requested_effort,
} => format!(
"Model fallback `{target}` for requested model `{requested_model}` was skipped because it has no reasoning level near `{requested_effort}`."
),
Self::ChainEmpty { requested_model } => format!(
"No usable model fallbacks remain for requested model `{requested_model}` after filtering its configured candidates."
),
@ -180,15 +196,17 @@ impl ModelFallbackNotice {
/// parses the raw table and cannot canonicalize model aliases.
pub fn resolve_model_fallbacks(
catalog: &Catalog,
eligible: &HashSet<ProviderId>,
configured_providers: &[ProviderId],
configured: &BTreeMap<String, Vec<ModelRef>>,
) -> Result<ResolvedModelFallbacks, Error> {
let eligible = configured_providers.iter().cloned().collect::<HashSet<_>>();
let mut resolved = ResolvedModelFallbacks::default();
let mut raw_key_by_canonical = HashMap::<String, String>::new();
for (raw_key, references) in configured {
require_bare_model_key(catalog, raw_key)?;
let selected =
catalog.resolve_selection_with_catalog_fallback(Some(raw_key), None, eligible)?;
catalog.resolve_selection_with_catalog_fallback(Some(raw_key), None, &eligible)?;
let requested_model = selected.model;
if let Some(previous) =
@ -209,7 +227,7 @@ pub fn resolve_model_fallbacks(
&requested_model,
&primary,
primary_model,
eligible,
&eligible,
model_ref,
)? {
FallbackCandidate::Skipped(notice) => {
@ -241,6 +259,30 @@ pub fn resolve_model_fallbacks(
Ok(resolved)
}
/// Reject chain keys that name a provider. Keys are requested-model selectors;
/// a provider-qualified key can never match a dispatch-time canonical model
/// ID, so it would be silently dead configuration.
fn require_bare_model_key(catalog: &Catalog, raw_key: &str) -> Result<(), Error> {
let reference: ModelRef = raw_key
.parse()
.map_err(|error| Error::Precondition(format!("`run.model.fallbacks` key: {error}")))?;
match reference.resolve(catalog) {
Ok(ResolvedModelRef::Model { provider: None, .. }) => Ok(()),
Ok(ResolvedModelRef::Model {
provider: Some(_),
selector,
}) => Err(Error::Precondition(format!(
"`run.model.fallbacks` keys name a requested model; use `{selector}` instead of `{raw_key}`"
))),
Ok(ResolvedModelRef::Provider(provider)) => Err(Error::Precondition(format!(
"`run.model.fallbacks` key `{raw_key}` names provider `{provider}`; keys must name a requested model"
))),
Err(ambiguous) => Err(Error::Precondition(format!(
"`run.model.fallbacks` key: {ambiguous}"
))),
}
}
enum FallbackCandidate {
Target(FallbackTarget),
Skipped(ModelFallbackNotice),
@ -332,7 +374,7 @@ fn resolve_fallback_candidate(
#[cfg(test)]
mod tests {
use std::collections::{BTreeMap, HashSet};
use std::collections::BTreeMap;
use fabro_model::{Catalog, FallbackTarget, ProviderId};
@ -359,7 +401,7 @@ enabled = true
#[test]
fn canonicalizes_keys_and_keeps_each_chain_independent() {
let catalog = openrouter_catalog();
let eligible = HashSet::from([ProviderId::new("openrouter")]);
let eligible = [ProviderId::new("openrouter")];
let configured = BTreeMap::from([
("gpt-sol".to_string(), references(&["claude-opus"])),
(
@ -393,7 +435,7 @@ enabled = true
#[test]
fn rejects_aliases_that_define_the_same_requested_model_twice() {
let catalog = openrouter_catalog();
let eligible = HashSet::from([ProviderId::new("openrouter")]);
let eligible = [ProviderId::new("openrouter")];
let configured = BTreeMap::from([
("gpt-sol".to_string(), references(&["claude-opus"])),
("gpt-5.6-sol".to_string(), references(&["claude-fable"])),
@ -409,10 +451,27 @@ enabled = true
);
}
#[test]
fn rejects_provider_qualified_keys() {
let catalog = openrouter_catalog();
let eligible = [ProviderId::new("openrouter")];
let configured = BTreeMap::from([(
"openrouter:gpt-sol".to_string(),
references(&["claude-opus"]),
)]);
let error = resolve_model_fallbacks(&catalog, &eligible, &configured).unwrap_err();
assert!(
error.to_string().contains("keys name a requested model"),
"unexpected error: {error}"
);
}
#[test]
fn skips_unconfigured_candidates_per_requested_model() {
let catalog = openrouter_catalog();
let eligible = HashSet::from([ProviderId::new("openrouter")]);
let eligible = [ProviderId::new("openrouter")];
let configured = BTreeMap::from([(
"kimi-k3".to_string(),
references(&["kimi:kimi-k3", "openrouter:kimi-k3"]),
@ -451,11 +510,11 @@ enabled = true
.expect("catalog override should parse");
Catalog::from_builtin_with_overrides(&overrides).expect("catalog should build")
};
let eligible = HashSet::from([
let eligible = [
ProviderId::new("modal"),
ProviderId::new("kimi"),
ProviderId::new("openrouter"),
]);
];
let configured = BTreeMap::from([
(
"kimi-k3".to_string(),

View file

@ -617,7 +617,7 @@ fn resolve_start_llm(
settings.model.provider.as_deref(),
false,
)?;
let fallbacks = resolve_model_fallbacks(catalog, &eligible, &settings.model.fallbacks)?;
let fallbacks = resolve_model_fallbacks(catalog, configured, &settings.model.fallbacks)?;
Ok(ResolvedStartLlm {
model,

View file

@ -244,6 +244,16 @@ fn main() {
"fabro_types::settings::server::ServerAuthGithubSettings",
&[],
),
(
"RunModelSettings",
"fabro_types::settings::run::RunModelSettings",
&[],
),
(
"RunModelControls",
"fabro_types::settings::run::RunModelControls",
&[],
),
(
"ServerSandboxSettings",
"fabro_types::settings::server::ServerSandboxSettings",

View file

@ -25,7 +25,7 @@ pub mod types {
ReasoningEffortFeature, Speed as BillingSpeed, TokenCounts as CompletionUsage,
};
pub use fabro_types::run_event::AgentSessionActivatedProps;
pub use fabro_types::settings::run::McpHttpProtocol;
pub use fabro_types::settings::run::{McpHttpProtocol, RunModelControls, RunModelSettings};
pub use fabro_types::settings::server::{
GithubIntegrationSettings, GithubIntegrationStrategy, IntegrationWebhooksSettings,
LogDestination, ObjectStoreSettings, ServerApiSettings, ServerArtifactsSettings,

View file

@ -0,0 +1,67 @@
use std::any::{TypeId, type_name};
use std::collections::BTreeMap;
use fabro_api::types::{
RunModelControls as ApiRunModelControls, RunModelSettings as ApiRunModelSettings,
};
use fabro_types::settings::run::{RunModelControls, RunModelSettings};
#[test]
fn run_model_settings_reuses_domain_types() {
assert_same_type::<ApiRunModelSettings, RunModelSettings>();
assert_same_type::<ApiRunModelControls, RunModelControls>();
}
#[test]
fn run_model_settings_json_matches_openapi_shape() {
let settings = RunModelSettings {
provider: Some("openrouter".to_string()),
name: Some("claude-fable".to_string()),
fallbacks: BTreeMap::from([("claude-fable".to_string(), vec![
"gpt-sol".parse().expect("fixture reference should parse"),
"openrouter:claude-opus"
.parse()
.expect("fixture reference should parse"),
])]),
controls: RunModelControls {
reasoning_effort: Some("high".to_string()),
speed: None,
},
};
let json = serde_json::to_value(&settings).expect("run model settings should serialize");
assert_eq!(json["provider"], "openrouter");
assert_eq!(json["name"], "claude-fable");
assert_eq!(json["fallbacks"]["claude-fable"][0], "gpt-sol");
assert_eq!(
json["fallbacks"]["claude-fable"][1],
"openrouter:claude-opus"
);
assert_eq!(json["controls"]["reasoning_effort"], "high");
assert_eq!(json["controls"]["speed"], serde_json::Value::Null);
let round_trip: ApiRunModelSettings =
serde_json::from_value(json).expect("run model settings should deserialize");
assert_eq!(round_trip, settings);
}
#[test]
fn run_model_settings_tolerates_absent_controls() {
let parsed: RunModelSettings = serde_json::from_value(serde_json::json!({
"provider": null,
"name": null,
"fallbacks": {}
}))
.expect("settings without controls should deserialize");
assert_eq!(parsed.controls, RunModelControls::default());
}
fn assert_same_type<T: 'static, U: 'static>() {
assert_eq!(
TypeId::of::<T>(),
TypeId::of::<U>(),
"{} should be the same type as {}",
type_name::<T>(),
type_name::<U>()
);
}

View file

@ -401,8 +401,8 @@ static GLOBAL_CATALOG: LazyLock<Catalog> = LazyLock::new(|| {
/// A resolved fallback target: provider name + model ID.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FallbackTarget {
pub provider: String,
pub model: String,
pub provider: ProviderId,
pub model: ModelId,
}
impl FallbackTarget {
@ -411,8 +411,8 @@ impl FallbackTarget {
/// selectors all use one constructor.
pub fn new(provider: impl std::fmt::Display, model: impl std::fmt::Display) -> Self {
Self {
provider: provider.to_string(),
model: model.to_string(),
provider: ProviderId::new(provider.to_string()),
model: ModelId::new(model.to_string()),
}
}
}
@ -1051,6 +1051,23 @@ impl Catalog {
.collect()
}
/// Canonicalize a model selector to its catalog model ID, preferring the
/// given provider's offering. Unknown selectors pass through verbatim.
///
/// Model-keyed fallback chains are written and read through this one
/// function so a configured chain key and a dispatch-time lookup cannot
/// silently disagree.
#[must_use]
pub fn canonical_model_id(&self, provider: &ProviderId, selector: &str) -> String {
self.get_on_provider(provider, selector).map_or_else(
|| {
self.select(selector, None, &self.all_provider_ids())
.map_or_else(|_| selector.to_string(), |offering| offering.id.to_string())
},
|offering| offering.id.to_string(),
)
}
/// Select the highest-priority default model on an eligible provider.
pub fn select_default(
&self,

View file

@ -46,16 +46,10 @@ impl ReasoningEffort {
/// are equally distant, the higher effort wins.
#[must_use]
pub fn closest_supported(self, supported: &[Self]) -> Option<Self> {
let variants = Self::variants();
let requested_rank = variants.iter().position(|effort| *effort == self)?;
supported.iter().copied().min_by_key(|effort| {
let rank = variants
.iter()
.position(|candidate| candidate == effort)
.expect("supported reasoning effort must be an enum variant");
(requested_rank.abs_diff(rank), Reverse(rank))
})
supported
.iter()
.copied()
.min_by_key(|effort| ((self as u8).abs_diff(*effort as u8), Reverse(*effort)))
}
}

View file

@ -114,10 +114,10 @@ pub use run_blob_id::RunBlobId;
pub use run_event::{
AgentMcpToolSummary, AgentMemoryFileProps, AgentSkillActivationSource, AgentSkillSummary,
AgentToolCategory, AgentToolSource, AgentToolSummary, AgentToolsAvailableProps, EventBody,
ExecOutputTail, InterviewOption, LlmOutputKind, LlmRetryPhase, MetadataSnapshotFailureKind,
MetadataSnapshotPhase, RunEvent, RunNoticeCode, RunNoticeLevel, RunPairEndedReason,
RunPairFailedReason, RunRunnableSource, SessionCapability, TodoCreatedProps, TodoDeletedProps,
TodoUpdatedProps,
ExecOutputTail, FailoverProps, InterviewOption, LlmOutputKind, LlmRetryPhase,
MetadataSnapshotFailureKind, MetadataSnapshotPhase, RunEvent, RunNoticeCode, RunNoticeLevel,
RunPairEndedReason, RunPairFailedReason, RunRunnableSource, SessionCapability,
TodoCreatedProps, TodoDeletedProps, TodoUpdatedProps,
};
pub use run_failure::RunFailure;
pub use run_id::{RunId, fixtures};

View file

@ -1,3 +1,4 @@
use fabro_model::ReasoningEffort;
use serde::{Deserialize, Serialize};
use super::ExecOutputTail;
@ -187,20 +188,17 @@ pub struct SshAccessReadyProps {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FailoverProps {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub original_provider: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub original_model: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub attempt: Option<u32>,
pub original_provider: String,
pub original_model: String,
pub attempt: u32,
pub from_provider: String,
pub from_model: String,
pub to_provider: String,
pub to_model: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub requested_reasoning_effort: Option<String>,
pub requested_reasoning_effort: Option<ReasoningEffort>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub effective_reasoning_effort: Option<String>,
pub effective_reasoning_effort: Option<ReasoningEffort>,
pub error: String,
}

View file

@ -1013,33 +1013,6 @@ mod tests {
assert!(matches!(parsed.body, EventBody::RunCreated(_)));
}
#[test]
fn historical_failover_event_defaults_new_route_context() {
let line = json!({
"id": "evt_failover",
"ts": "2026-04-04T12:00:00.000Z",
"run_id": fixtures::RUN_1,
"event": "agent.failover",
"properties": {
"from_provider": "anthropic",
"from_model": "claude-fable-5",
"to_provider": "openai",
"to_model": "gpt-5.6-sol",
"error": "provider unavailable"
}
});
let parsed = RunEvent::from_value(line).unwrap();
let EventBody::Failover(props) = parsed.body else {
panic!("expected agent.failover");
};
assert_eq!(props.original_provider, None);
assert_eq!(props.original_model, None);
assert_eq!(props.attempt, None);
assert_eq!(props.requested_reasoning_effort, None);
assert_eq!(props.effective_reasoning_effort, None);
}
#[test]
fn run_created_round_trip_preserves_manifest_blob() {
let line = json!({

View file

@ -382,6 +382,7 @@ models/run-links.ts
models/run-manifest.ts
models/run-meta-branch-settings.ts
models/run-mode.ts
models/run-model-controls.ts
models/run-model-settings.ts
models/run-model.ts
models/run-namespace.ts

View file

@ -354,6 +354,7 @@ export * from './run-manifest';
export * from './run-meta-branch-settings';
export * from './run-mode';
export * from './run-model';
export * from './run-model-controls';
export * from './run-model-settings';
export * from './run-namespace';
export * from './run-origin';

View file

@ -0,0 +1,23 @@
/* 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.
*/
/**
* Run-level default values for typed model controls. Node and style attributes still win over these defaults.
*/
export interface RunModelControls {
'reasoning_effort': string | null;
'speed': string | null;
}

View file

@ -13,6 +13,9 @@
*/
// May contain unused imports in some cases
// @ts-ignore
import type { RunModelControls } from './run-model-controls';
export interface RunModelSettings {
'provider': string | null;
@ -21,4 +24,5 @@ export interface RunModelSettings {
* Ordered fallback targets keyed by the originally requested model. Each chain is independent; selecting a fallback target does not activate that target model\'s own chain.
*/
'fallbacks': { [key: string]: Array<string>; };
'controls'?: RunModelControls;
}