Skip to content

Commit c5e83c1

Browse files
committed
feat(ai): improve models.dev reasoning preset configuration
- refresh reasoning projections while editing explicit models.dev bindings - warn when the active API format cannot reliably apply catalog presets - add combined provider and model search with ranked multi-keyword matching - streamline custom preset naming and hide internal preset IDs
1 parent 47c033b commit c5e83c1

30 files changed

Lines changed: 939 additions & 125 deletions

File tree

‎src/apps/cli/src/dispatch/runner.rs‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,20 @@ pub(crate) fn process_alive(pid: u32) -> bool {
221221
};
222222
// SAFETY: signal 0 performs liveness/permission checking only.
223223
if unsafe { libc::kill(pid, 0) } == 0 {
224+
#[cfg(target_os = "linux")]
225+
{
226+
// A zombie still answers to kill(0), but it has already exited and
227+
// must not be treated as an authenticated leader for escalation.
228+
if let Ok(stat) = std::fs::read_to_string(format!("/proc/{pid}/stat")) {
229+
if stat
230+
.rsplit_once(") ")
231+
.and_then(|(_, fields)| fields.split_whitespace().next())
232+
== Some("Z")
233+
{
234+
return false;
235+
}
236+
}
237+
}
224238
return true;
225239
}
226240
matches!(

‎src/apps/desktop/src/api/commands.rs‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5172,6 +5172,13 @@ pub async fn get_ai_model_catalog() -> Result<bitfun_core::AIModelCatalog, Strin
51725172
bitfun_core::get_ai_model_catalog().await
51735173
}
51745174

5175+
#[tauri::command]
5176+
pub async fn project_ai_model_reasoning_catalog(
5177+
request: bitfun_core_types::ReasoningCatalogProjectionRequest,
5178+
) -> bitfun_core_types::ReasoningCatalogProjection {
5179+
bitfun_core::project_ai_model_reasoning_catalog(request).await
5180+
}
5181+
51755182
#[tauri::command]
51765183
pub async fn get_models_dev_catalog_status() -> bitfun_core_types::ModelsDevCatalogStatus {
51775184
bitfun_core::get_models_dev_catalog_status().await

‎src/apps/desktop/src/api/remote_workspace_policy.rs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,10 @@ pub const REMOTE_WORKSPACE_COMMAND_POLICIES: &[(&str, RemoteWorkspacePolicy)] =
525525
"get_ai_model_catalog",
526526
RemoteWorkspacePolicy::WorkspaceAgnostic,
527527
),
528+
(
529+
"project_ai_model_reasoning_catalog",
530+
RemoteWorkspacePolicy::WorkspaceAgnostic,
531+
),
528532
(
529533
"get_models_dev_catalog_status",
530534
RemoteWorkspacePolicy::LocalOnly,

‎src/apps/desktop/src/lib.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1604,6 +1604,7 @@ pub async fn run() {
16041604
subscribe_config_updates,
16051605
get_model_configs,
16061606
get_ai_model_catalog,
1607+
project_ai_model_reasoning_catalog,
16071608
get_models_dev_catalog_status,
16081609
refresh_models_dev_catalog_now,
16091610
reveal_models_dev_cache_directory,

‎src/crates/adapters/ai-adapters/src/models_dev.rs‎

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,7 @@ pub fn project_reasoning_catalog_with_limit_and_auto_binding(
567567
};
568568

569569
let mut descriptors = BTreeMap::<String, ReasoningPresetDescriptor>::new();
570+
let mut unavailable_descriptors = BTreeMap::<String, ReasoningPresetDescriptor>::new();
570571
let mut has_unmapped_reasoning = false;
571572
if let Some((source_provider, source_model)) = source_match {
572573
if source_model.reasoning {
@@ -606,6 +607,37 @@ pub fn project_reasoning_catalog_with_limit_and_auto_binding(
606607
| ModelsDevReasoningOption::Toggle
607608
| ModelsDevReasoningOption::BudgetTokens { .. } => {
608609
has_unmapped_reasoning = true;
610+
if matches!(binding, ReasoningCatalogBinding::ModelsDev { .. }) {
611+
let unavailable = match option {
612+
ModelsDevReasoningOption::Effort { values } => effort_descriptors(
613+
values,
614+
support.nullable_effort,
615+
ReasoningPresetSource::ModelsDev,
616+
source_provider,
617+
&source_model.id,
618+
),
619+
ModelsDevReasoningOption::Toggle => toggle_descriptors(
620+
ReasoningPresetSource::ModelsDev,
621+
source_provider,
622+
&source_model.id,
623+
),
624+
ModelsDevReasoningOption::BudgetTokens { min, max } => {
625+
budget_descriptors(
626+
*min,
627+
*max,
628+
source_model.output_limit,
629+
effective_max_output_tokens,
630+
provider,
631+
ReasoningPresetSource::ModelsDev,
632+
source_provider,
633+
&source_model.id,
634+
)
635+
}
636+
};
637+
for descriptor in unavailable {
638+
unavailable_descriptors.insert(descriptor.id.clone(), descriptor);
639+
}
640+
}
609641
Vec::new()
610642
}
611643
};
@@ -673,12 +705,15 @@ pub fn project_reasoning_catalog_with_limit_and_auto_binding(
673705
}
674706
if preset.disabled {
675707
descriptors.remove(preset_id);
708+
unavailable_descriptors.remove(preset_id);
676709
continue;
677710
}
678711
if preset.actions.is_empty() {
679712
descriptors.remove(preset_id);
713+
unavailable_descriptors.remove(preset_id);
680714
continue;
681715
}
716+
unavailable_descriptors.remove(preset_id);
682717
descriptors.insert(
683718
preset_id.to_string(),
684719
ReasoningPresetDescriptor {
@@ -704,6 +739,12 @@ pub fn project_reasoning_catalog_with_limit_and_auto_binding(
704739
.cmp(&right.order)
705740
.then_with(|| left.id.cmp(&right.id))
706741
});
742+
let mut unavailable_presets = unavailable_descriptors.into_values().collect::<Vec<_>>();
743+
unavailable_presets.sort_by(|left, right| {
744+
left.order
745+
.cmp(&right.order)
746+
.then_with(|| left.id.cmp(&right.id))
747+
});
707748
let status = if !presets.is_empty() {
708749
ReasoningCapabilityStatus::Known
709750
} else if matches!(binding, ReasoningCatalogBinding::Disabled) {
@@ -731,6 +772,7 @@ pub fn project_reasoning_catalog_with_limit_and_auto_binding(
731772
status,
732773
default_preset: default_preset.map(ToOwned::to_owned),
733774
presets,
775+
unavailable_presets,
734776
}
735777
}
736778

@@ -1193,7 +1235,9 @@ mod tests {
11931235
}},
11941236
"anthropic": {"models": {
11951237
"claude-sonnet-4-6": {"id":"claude-sonnet-4-6","reasoning":true,
1196-
"reasoning_options":[{"type":"effort","values":["low","high"]},{"type":"budget_tokens","min":1024}]}
1238+
"reasoning_options":[{"type":"effort","values":["low","high"]},{"type":"budget_tokens","min":1024}]},
1239+
"claude-fable-5": {"id":"claude-fable-5","reasoning":true,
1240+
"reasoning_options":{"type":"effort","values":["low","medium","high","xhigh","max"]}}
11971241
}},
11981242
"deepseek": {"models": {
11991243
"deepseek-v4-flash": {"id":"deepseek-v4-flash","reasoning":true,
@@ -1916,6 +1960,35 @@ mod tests {
19161960
}));
19171961
}
19181962

1963+
#[test]
1964+
fn explicit_anthropic_binding_reports_efforts_unavailable_to_openai_chat() {
1965+
let configured = ReasoningConfig {
1966+
catalog: ReasoningCatalogBinding::ModelsDev {
1967+
provider: "anthropic".to_string(),
1968+
model: "claude-fable-5".to_string(),
1969+
},
1970+
..Default::default()
1971+
};
1972+
let projection = project_reasoning_catalog(
1973+
"openai",
1974+
"dummy-model",
1975+
"http://localhost:8000/v1/chat/completions",
1976+
Some(&configured),
1977+
Some(&catalog()),
1978+
);
1979+
1980+
assert_eq!(projection.status, ReasoningCapabilityStatus::Unknown);
1981+
assert!(projection.presets.is_empty());
1982+
assert_eq!(
1983+
projection
1984+
.unavailable_presets
1985+
.iter()
1986+
.map(|preset| preset.id.as_str())
1987+
.collect::<Vec<_>>(),
1988+
["low", "medium", "high", "xhigh", "max"]
1989+
);
1990+
}
1991+
19191992
#[test]
19201993
fn custom_presets_keep_the_explicit_catalog_identity_for_adapter_compilation() {
19211994
let configured = ReasoningConfig {

‎src/crates/assembly/core/src/infrastructure/ai/reasoning_catalog.rs‎

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ use bitfun_core_types::{
1515
ReasoningCatalogBinding, ReasoningCatalogProjection, ReasoningPresetDescriptor,
1616
};
1717
#[cfg(feature = "model-catalog")]
18+
use bitfun_core_types::ReasoningCatalogProjectionRequest;
19+
#[cfg(feature = "model-catalog")]
1820
use bitfun_events::{AIModelCatalogUpdatedEvent, AI_MODEL_CATALOG_UPDATED_EVENT};
1921
#[cfg(feature = "model-catalog")]
2022
use bitfun_services_integrations::models_dev::{
@@ -317,6 +319,25 @@ pub(crate) fn project_model_reasoning_catalog(
317319
)
318320
}
319321

322+
#[cfg(feature = "model-catalog")]
323+
pub(crate) async fn project_reasoning_catalog_request(
324+
request: ReasoningCatalogProjectionRequest,
325+
) -> ReasoningCatalogProjection {
326+
let models_dev = load_models_dev_reasoning_catalog().await;
327+
project_model_reasoning_catalog(
328+
&AIModelConfig {
329+
provider: request.provider,
330+
model_name: request.model_name,
331+
base_url: request.base_url,
332+
context_window: request.context_window,
333+
max_tokens: request.max_tokens,
334+
reasoning: Some(request.reasoning),
335+
..Default::default()
336+
},
337+
models_dev.catalog.as_deref(),
338+
)
339+
}
340+
320341
pub(crate) fn resolve_reasoning_preset<'a>(
321342
projection: &'a ReasoningCatalogProjection,
322343
preset_id: &str,
@@ -406,8 +427,8 @@ pub(crate) fn apply_selected_reasoning_preset(
406427
#[cfg(test)]
407428
mod tests {
408429
use bitfun_core_types::{
409-
ReasoningCatalogBinding, ReasoningConfig, ReasoningPreset, ReasoningPresetAction,
410-
ReasoningPresetSource,
430+
ReasoningCatalogBinding, ReasoningCatalogProjectionRequest, ReasoningConfig,
431+
ReasoningPreset, ReasoningPresetAction, ReasoningPresetSource,
411432
};
412433

413434
use super::{
@@ -494,6 +515,45 @@ mod tests {
494515
assert_eq!(resolve_default_reasoning_preset(&projection), Some(high));
495516
}
496517

518+
#[test]
519+
fn projection_request_shape_projects_explicit_models_dev_presets() {
520+
let request = ReasoningCatalogProjectionRequest {
521+
provider: "responses".to_string(),
522+
model_name: "gateway-alias".to_string(),
523+
base_url: "https://gateway.example.com/v1/responses".to_string(),
524+
context_window: Some(128_000),
525+
max_tokens: Some(8_192),
526+
reasoning: ReasoningConfig {
527+
catalog: ReasoningCatalogBinding::ModelsDev {
528+
provider: "openai".to_string(),
529+
model: "gpt-test".to_string(),
530+
},
531+
..Default::default()
532+
},
533+
};
534+
let projection = project_model_reasoning_catalog(
535+
&AIModelConfig {
536+
provider: request.provider,
537+
model_name: request.model_name,
538+
base_url: request.base_url,
539+
context_window: request.context_window,
540+
max_tokens: request.max_tokens,
541+
reasoning: Some(request.reasoning),
542+
..Default::default()
543+
},
544+
Some(&catalog()),
545+
);
546+
547+
assert_eq!(
548+
projection
549+
.presets
550+
.iter()
551+
.map(|preset| preset.id.as_str())
552+
.collect::<Vec<_>>(),
553+
["low", "high"]
554+
);
555+
}
556+
497557
#[test]
498558
fn openbitfun_models_use_their_exact_upstream_reasoning_catalogs() {
499559
for (provider, base_url) in [

‎src/crates/assembly/core/src/lib.rs‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,13 @@ pub async fn get_ai_model_catalog() -> Result<AIModelCatalog, String> {
7878
service_agent_runtime::CoreServiceAgentRuntime::load_remote_model_catalog(None).await
7979
}
8080

81+
#[cfg(feature = "model-catalog")]
82+
pub async fn project_ai_model_reasoning_catalog(
83+
request: bitfun_core_types::ReasoningCatalogProjectionRequest,
84+
) -> bitfun_core_types::ReasoningCatalogProjection {
85+
infrastructure::ai::reasoning_catalog::project_reasoning_catalog_request(request).await
86+
}
87+
8188
#[cfg(feature = "model-catalog")]
8289
pub async fn get_models_dev_catalog_status() -> bitfun_core_types::ModelsDevCatalogStatus {
8390
infrastructure::ai::reasoning_catalog::get_models_dev_catalog_status().await

‎src/crates/contracts/core-types/src/ai.rs‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,26 @@ pub struct ReasoningCatalogProjection {
198198
pub default_preset: Option<String>,
199199
#[serde(default, skip_serializing_if = "Vec::is_empty")]
200200
pub presets: Vec<ReasoningPresetDescriptor>,
201+
/// Presets declared by the selected models.dev model that the active
202+
/// request adapter cannot compile reliably. These are informational only
203+
/// and must not be offered as selectable presets.
204+
#[serde(default, skip_serializing_if = "Vec::is_empty")]
205+
pub unavailable_presets: Vec<ReasoningPresetDescriptor>,
206+
}
207+
208+
/// Secret-free model facts used to preview the effective reasoning presets
209+
/// while a model configuration is still being edited.
210+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
211+
#[serde(rename_all = "camelCase")]
212+
pub struct ReasoningCatalogProjectionRequest {
213+
pub provider: String,
214+
pub model_name: String,
215+
pub base_url: String,
216+
#[serde(default, skip_serializing_if = "Option::is_none")]
217+
pub context_window: Option<u32>,
218+
#[serde(default, skip_serializing_if = "Option::is_none")]
219+
pub max_tokens: Option<u32>,
220+
pub reasoning: ReasoningConfig,
201221
}
202222

203223
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]

‎src/crates/contracts/core-types/src/lib.rs‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ pub use ai::{
2121
ProviderCatalogModelLimits, ProviderCatalogModelPricing, ProviderCatalogModelSource,
2222
ProviderCatalogProvider, ProviderCatalogSource, ProviderCatalogUpstreamProvider, ProxyConfig,
2323
ReasoningCapabilityStatus, ReasoningCatalogBinding, ReasoningCatalogProjection,
24-
ReasoningConfig, ReasoningPreset, ReasoningPresetAction, ReasoningPresetDescriptor,
25-
ReasoningPresetSource, RemoteModelInfo, ToolCall, ToolCallConfirmationDetails,
26-
ToolCallRequestInfo, ToolCallResponseInfo, ToolDefinition,
24+
ReasoningCatalogProjectionRequest, ReasoningConfig, ReasoningPreset, ReasoningPresetAction,
25+
ReasoningPresetDescriptor, ReasoningPresetSource, RemoteModelInfo, ToolCall,
26+
ToolCallConfirmationDetails, ToolCallRequestInfo, ToolCallResponseInfo, ToolDefinition,
2727
};
2828
pub use errors::{AiErrorDetail, ErrorCategory};
2929
pub use session::{

‎src/crates/interfaces/app-server-client/src/lib.rs‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,13 @@ impl AppServerClient {
174174
.await
175175
}
176176

177+
pub async fn project_reasoning_catalog(
178+
&self,
179+
request: ProjectReasoningCatalogRequest,
180+
) -> agent_client_protocol::Result<ProjectReasoningCatalogResponse> {
181+
self.rpc(|cx| Ok(cx.send_request(request))).await
182+
}
183+
177184
pub async fn worktree_repository_status(
178185
&self,
179186
request: WorktreeRepositoryStatusRequest,

0 commit comments

Comments
 (0)