Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/apps/cli/src/dispatch/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,20 @@ pub(crate) fn process_alive(pid: u32) -> bool {
};
// SAFETY: signal 0 performs liveness/permission checking only.
if unsafe { libc::kill(pid, 0) } == 0 {
#[cfg(target_os = "linux")]
{
// A zombie still answers to kill(0), but it has already exited and
// must not be treated as an authenticated leader for escalation.
if let Ok(stat) = std::fs::read_to_string(format!("/proc/{pid}/stat")) {
if stat
.rsplit_once(") ")
.and_then(|(_, fields)| fields.split_whitespace().next())
== Some("Z")
{
return false;
}
}
}
return true;
}
matches!(
Expand Down
7 changes: 7 additions & 0 deletions src/apps/desktop/src/api/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5172,6 +5172,13 @@ pub async fn get_ai_model_catalog() -> Result<bitfun_core::AIModelCatalog, Strin
bitfun_core::get_ai_model_catalog().await
}

#[tauri::command]
pub async fn project_ai_model_reasoning_catalog(
request: bitfun_core_types::ReasoningCatalogProjectionRequest,
) -> bitfun_core_types::ReasoningCatalogProjection {
bitfun_core::project_ai_model_reasoning_catalog(request).await
}

#[tauri::command]
pub async fn get_models_dev_catalog_status() -> bitfun_core_types::ModelsDevCatalogStatus {
bitfun_core::get_models_dev_catalog_status().await
Expand Down
4 changes: 4 additions & 0 deletions src/apps/desktop/src/api/remote_workspace_policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,10 @@ pub const REMOTE_WORKSPACE_COMMAND_POLICIES: &[(&str, RemoteWorkspacePolicy)] =
"get_ai_model_catalog",
RemoteWorkspacePolicy::WorkspaceAgnostic,
),
(
"project_ai_model_reasoning_catalog",
RemoteWorkspacePolicy::WorkspaceAgnostic,
),
(
"get_models_dev_catalog_status",
RemoteWorkspacePolicy::LocalOnly,
Expand Down
1 change: 1 addition & 0 deletions src/apps/desktop/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1604,6 +1604,7 @@ pub async fn run() {
subscribe_config_updates,
get_model_configs,
get_ai_model_catalog,
project_ai_model_reasoning_catalog,
get_models_dev_catalog_status,
refresh_models_dev_catalog_now,
reveal_models_dev_cache_directory,
Expand Down
75 changes: 74 additions & 1 deletion src/crates/adapters/ai-adapters/src/models_dev.rs
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,7 @@ pub fn project_reasoning_catalog_with_limit_and_auto_binding(
};

let mut descriptors = BTreeMap::<String, ReasoningPresetDescriptor>::new();
let mut unavailable_descriptors = BTreeMap::<String, ReasoningPresetDescriptor>::new();
let mut has_unmapped_reasoning = false;
if let Some((source_provider, source_model)) = source_match {
if source_model.reasoning {
Expand Down Expand Up @@ -606,6 +607,37 @@ pub fn project_reasoning_catalog_with_limit_and_auto_binding(
| ModelsDevReasoningOption::Toggle
| ModelsDevReasoningOption::BudgetTokens { .. } => {
has_unmapped_reasoning = true;
if matches!(binding, ReasoningCatalogBinding::ModelsDev { .. }) {
let unavailable = match option {
ModelsDevReasoningOption::Effort { values } => effort_descriptors(
values,
support.nullable_effort,
ReasoningPresetSource::ModelsDev,
source_provider,
&source_model.id,
),
ModelsDevReasoningOption::Toggle => toggle_descriptors(
ReasoningPresetSource::ModelsDev,
source_provider,
&source_model.id,
),
ModelsDevReasoningOption::BudgetTokens { min, max } => {
budget_descriptors(
*min,
*max,
source_model.output_limit,
effective_max_output_tokens,
provider,
ReasoningPresetSource::ModelsDev,
source_provider,
&source_model.id,
)
}
};
for descriptor in unavailable {
unavailable_descriptors.insert(descriptor.id.clone(), descriptor);
}
}
Vec::new()
}
};
Expand Down Expand Up @@ -673,12 +705,15 @@ pub fn project_reasoning_catalog_with_limit_and_auto_binding(
}
if preset.disabled {
descriptors.remove(preset_id);
unavailable_descriptors.remove(preset_id);
continue;
}
if preset.actions.is_empty() {
descriptors.remove(preset_id);
unavailable_descriptors.remove(preset_id);
continue;
}
unavailable_descriptors.remove(preset_id);
descriptors.insert(
preset_id.to_string(),
ReasoningPresetDescriptor {
Expand All @@ -704,6 +739,12 @@ pub fn project_reasoning_catalog_with_limit_and_auto_binding(
.cmp(&right.order)
.then_with(|| left.id.cmp(&right.id))
});
let mut unavailable_presets = unavailable_descriptors.into_values().collect::<Vec<_>>();
unavailable_presets.sort_by(|left, right| {
left.order
.cmp(&right.order)
.then_with(|| left.id.cmp(&right.id))
});
let status = if !presets.is_empty() {
ReasoningCapabilityStatus::Known
} else if matches!(binding, ReasoningCatalogBinding::Disabled) {
Expand Down Expand Up @@ -731,6 +772,7 @@ pub fn project_reasoning_catalog_with_limit_and_auto_binding(
status,
default_preset: default_preset.map(ToOwned::to_owned),
presets,
unavailable_presets,
}
}

Expand Down Expand Up @@ -1193,7 +1235,9 @@ mod tests {
}},
"anthropic": {"models": {
"claude-sonnet-4-6": {"id":"claude-sonnet-4-6","reasoning":true,
"reasoning_options":[{"type":"effort","values":["low","high"]},{"type":"budget_tokens","min":1024}]}
"reasoning_options":[{"type":"effort","values":["low","high"]},{"type":"budget_tokens","min":1024}]},
"claude-fable-5": {"id":"claude-fable-5","reasoning":true,
"reasoning_options":{"type":"effort","values":["low","medium","high","xhigh","max"]}}
}},
"deepseek": {"models": {
"deepseek-v4-flash": {"id":"deepseek-v4-flash","reasoning":true,
Expand Down Expand Up @@ -1916,6 +1960,35 @@ mod tests {
}));
}

#[test]
fn explicit_anthropic_binding_reports_efforts_unavailable_to_openai_chat() {
let configured = ReasoningConfig {
catalog: ReasoningCatalogBinding::ModelsDev {
provider: "anthropic".to_string(),
model: "claude-fable-5".to_string(),
},
..Default::default()
};
let projection = project_reasoning_catalog(
"openai",
"dummy-model",
"http://localhost:8000/v1/chat/completions",
Some(&configured),
Some(&catalog()),
);

assert_eq!(projection.status, ReasoningCapabilityStatus::Unknown);
assert!(projection.presets.is_empty());
assert_eq!(
projection
.unavailable_presets
.iter()
.map(|preset| preset.id.as_str())
.collect::<Vec<_>>(),
["low", "medium", "high", "xhigh", "max"]
);
}

#[test]
fn custom_presets_keep_the_explicit_catalog_identity_for_adapter_compilation() {
let configured = ReasoningConfig {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ use bitfun_core_types::{
ReasoningCatalogBinding, ReasoningCatalogProjection, ReasoningPresetDescriptor,
};
#[cfg(feature = "model-catalog")]
use bitfun_core_types::ReasoningCatalogProjectionRequest;
#[cfg(feature = "model-catalog")]
use bitfun_events::{AIModelCatalogUpdatedEvent, AI_MODEL_CATALOG_UPDATED_EVENT};
#[cfg(feature = "model-catalog")]
use bitfun_services_integrations::models_dev::{
Expand Down Expand Up @@ -317,6 +319,25 @@ pub(crate) fn project_model_reasoning_catalog(
)
}

#[cfg(feature = "model-catalog")]
pub(crate) async fn project_reasoning_catalog_request(
request: ReasoningCatalogProjectionRequest,
) -> ReasoningCatalogProjection {
let models_dev = load_models_dev_reasoning_catalog().await;
project_model_reasoning_catalog(
&AIModelConfig {
provider: request.provider,
model_name: request.model_name,
base_url: request.base_url,
context_window: request.context_window,
max_tokens: request.max_tokens,
reasoning: Some(request.reasoning),
..Default::default()
},
models_dev.catalog.as_deref(),
)
}

pub(crate) fn resolve_reasoning_preset<'a>(
projection: &'a ReasoningCatalogProjection,
preset_id: &str,
Expand Down Expand Up @@ -406,8 +427,8 @@ pub(crate) fn apply_selected_reasoning_preset(
#[cfg(test)]
mod tests {
use bitfun_core_types::{
ReasoningCatalogBinding, ReasoningConfig, ReasoningPreset, ReasoningPresetAction,
ReasoningPresetSource,
ReasoningCatalogBinding, ReasoningCatalogProjectionRequest, ReasoningConfig,
ReasoningPreset, ReasoningPresetAction, ReasoningPresetSource,
};

use super::{
Expand Down Expand Up @@ -494,6 +515,45 @@ mod tests {
assert_eq!(resolve_default_reasoning_preset(&projection), Some(high));
}

#[test]
fn projection_request_shape_projects_explicit_models_dev_presets() {
let request = ReasoningCatalogProjectionRequest {
provider: "responses".to_string(),
model_name: "gateway-alias".to_string(),
base_url: "https://gateway.example.com/v1/responses".to_string(),
context_window: Some(128_000),
max_tokens: Some(8_192),
reasoning: ReasoningConfig {
catalog: ReasoningCatalogBinding::ModelsDev {
provider: "openai".to_string(),
model: "gpt-test".to_string(),
},
..Default::default()
},
};
let projection = project_model_reasoning_catalog(
&AIModelConfig {
provider: request.provider,
model_name: request.model_name,
base_url: request.base_url,
context_window: request.context_window,
max_tokens: request.max_tokens,
reasoning: Some(request.reasoning),
..Default::default()
},
Some(&catalog()),
);

assert_eq!(
projection
.presets
.iter()
.map(|preset| preset.id.as_str())
.collect::<Vec<_>>(),
["low", "high"]
);
}

#[test]
fn openbitfun_models_use_their_exact_upstream_reasoning_catalogs() {
for (provider, base_url) in [
Expand Down
7 changes: 7 additions & 0 deletions src/crates/assembly/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,13 @@ pub async fn get_ai_model_catalog() -> Result<AIModelCatalog, String> {
service_agent_runtime::CoreServiceAgentRuntime::load_remote_model_catalog(None).await
}

#[cfg(feature = "model-catalog")]
pub async fn project_ai_model_reasoning_catalog(
request: bitfun_core_types::ReasoningCatalogProjectionRequest,
) -> bitfun_core_types::ReasoningCatalogProjection {
infrastructure::ai::reasoning_catalog::project_reasoning_catalog_request(request).await
}

#[cfg(feature = "model-catalog")]
pub async fn get_models_dev_catalog_status() -> bitfun_core_types::ModelsDevCatalogStatus {
infrastructure::ai::reasoning_catalog::get_models_dev_catalog_status().await
Expand Down
20 changes: 20 additions & 0 deletions src/crates/contracts/core-types/src/ai.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,26 @@ pub struct ReasoningCatalogProjection {
pub default_preset: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub presets: Vec<ReasoningPresetDescriptor>,
/// Presets declared by the selected models.dev model that the active
/// request adapter cannot compile reliably. These are informational only
/// and must not be offered as selectable presets.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub unavailable_presets: Vec<ReasoningPresetDescriptor>,
}

/// Secret-free model facts used to preview the effective reasoning presets
/// while a model configuration is still being edited.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ReasoningCatalogProjectionRequest {
pub provider: String,
pub model_name: String,
pub base_url: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub context_window: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_tokens: Option<u32>,
pub reasoning: ReasoningConfig,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
Expand Down
6 changes: 3 additions & 3 deletions src/crates/contracts/core-types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ pub use ai::{
ProviderCatalogModelLimits, ProviderCatalogModelPricing, ProviderCatalogModelSource,
ProviderCatalogProvider, ProviderCatalogSource, ProviderCatalogUpstreamProvider, ProxyConfig,
ReasoningCapabilityStatus, ReasoningCatalogBinding, ReasoningCatalogProjection,
ReasoningConfig, ReasoningPreset, ReasoningPresetAction, ReasoningPresetDescriptor,
ReasoningPresetSource, RemoteModelInfo, ToolCall, ToolCallConfirmationDetails,
ToolCallRequestInfo, ToolCallResponseInfo, ToolDefinition,
ReasoningCatalogProjectionRequest, ReasoningConfig, ReasoningPreset, ReasoningPresetAction,
ReasoningPresetDescriptor, ReasoningPresetSource, RemoteModelInfo, ToolCall,
ToolCallConfirmationDetails, ToolCallRequestInfo, ToolCallResponseInfo, ToolDefinition,
};
pub use errors::{AiErrorDetail, ErrorCategory};
pub use session::{
Expand Down
7 changes: 7 additions & 0 deletions src/crates/interfaces/app-server-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,13 @@ impl AppServerClient {
.await
}

pub async fn project_reasoning_catalog(
&self,
request: ProjectReasoningCatalogRequest,
) -> agent_client_protocol::Result<ProjectReasoningCatalogResponse> {
self.rpc(|cx| Ok(cx.send_request(request))).await
}

pub async fn worktree_repository_status(
&self,
request: WorktreeRepositoryStatusRequest,
Expand Down
14 changes: 13 additions & 1 deletion src/crates/interfaces/app-server-protocol/src/schemas/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
//! are never returned by the server.

use agent_client_protocol::{JsonRpcRequest, JsonRpcResponse};
use bitfun_core_types::{ProviderCatalog, ReasoningConfig};
use bitfun_core_types::{
ProviderCatalog, ReasoningCatalogProjection, ReasoningCatalogProjectionRequest, ReasoningConfig,
};
use serde::{Deserialize, Serialize};

macro_rules! unit_response {
Expand Down Expand Up @@ -54,6 +56,16 @@ pub struct TuiModelCatalogResponse {
pub reasoning_presets_by_model: std::collections::BTreeMap<String, Vec<String>>,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcRequest)]
#[request(method = "model/projectReasoningCatalog", response = ProjectReasoningCatalogResponse)]
#[serde(transparent)]
pub struct ProjectReasoningCatalogRequest(pub ReasoningCatalogProjectionRequest);

#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcResponse)]
pub struct ProjectReasoningCatalogResponse {
pub projection: ReasoningCatalogProjection,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ModelSummary {
Expand Down
1 change: 1 addition & 0 deletions src/crates/interfaces/app-server/src/management.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ impl AppManagementCapabilities {
self.models.clone(),
&[
"config/getTuiModelCatalog",
"model/projectReasoningCatalog",
"model/list",
"model/get",
"model/add",
Expand Down
9 changes: 9 additions & 0 deletions src/crates/interfaces/app-server/src/management/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1352,6 +1352,15 @@ impl AppManagementService {
})
}

pub async fn project_reasoning_catalog(
&self,
request: ProjectReasoningCatalogRequest,
) -> AppManagementResult<ProjectReasoningCatalogResponse> {
Ok(ProjectReasoningCatalogResponse {
projection: bitfun_core::project_ai_model_reasoning_catalog(request.0).await,
})
}

pub async fn add_model(
&self,
request: AddModelRequest,
Expand Down
Loading