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
4 changes: 2 additions & 2 deletions docs/interactive-capabilities/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ OpenBitFun Playbook currently contains **22 features**, **21 settings pages**, a
- Generated per-item interaction audit: `docs/interactive-capabilities/technical/product-control-open-audit.json`
- Generated low-level audit map: `docs/interactive-capabilities/technical/tauri-command-map.json`

说明书、网站、搜索和智能体只看“功能 + 设置 + 子能力”。每项子能力都必须引用已注册 Tauri Command 或可解析的源码标记;这些证据不会进入公开目录。当前 **662** 个 Tauri 命令只用于实现覆盖审计。产品 UI 交互源码会在生成和检查时扫描并校验,但不会保存成随普通 UI 改动频繁变化的版本化快照。
说明书、网站、搜索和智能体只看“功能 + 设置 + 子能力”。每项子能力都必须引用已注册 Tauri Command 或可解析的源码标记;这些证据不会进入公开目录。当前 **663** 个 Tauri 命令只用于实现覆盖审计。产品 UI 交互源码会在生成和检查时扫描并校验,但不会保存成随普通 UI 改动频繁变化的版本化快照。

Docs, website, search, and agents see only features, settings, and documented sub-capabilities. Every sub-capability must reference a registered Tauri command or a resolvable source marker; evidence is stripped from public projections. The **662** Tauri commands remain implementation-audit evidence only. Product UI interaction sources are scanned and validated during generation and checks, but are not stored as a versioned snapshot that churns with ordinary UI changes.
Docs, website, search, and agents see only features, settings, and documented sub-capabilities. Every sub-capability must reference a registered Tauri command or a resolvable source marker; evidence is stripped from public projections. The **663** Tauri commands remain implementation-audit evidence only. Product UI interaction sources are scanned and validated during generation and checks, but are not stored as a versioned snapshot that churns with ordinary UI changes.

## 控制边界 / Control boundary

Expand Down
22 changes: 18 additions & 4 deletions docs/interactive-capabilities/technical/tauri-command-map.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
"schemaVersion": 2,
"generatedFrom": "src/shared/interactive-capabilities/catalog.json",
"catalogDigest": "6587344a6b5e75a80457ea4ba2670bf37cf3038bdb436089ae402eb7b5a5a025",
"commandCount": 662,
"commandCount": 663,
"coverage": {
"commandCount": 662,
"commandCount": 663,
"documentedCommandCount": 615,
"implementationCommandCount": 47,
"implementationDigest": "401fd0a5f64377e65573d1b0883fbfa844570096adfafd4c11a999d749ba5859"
"implementationCommandCount": 48,
"implementationDigest": "f6d38a24a70708988cb47ada81d07eccf0668684e8734c9ee5155b9ffa7e3db8"
},
"commands": [
{
Expand Down Expand Up @@ -3222,6 +3222,20 @@
"signature": "fn get_latest_insights() -> Result<Vec<InsightsReportMeta>, String>",
"remoteWorkspacePolicy": "LocalOnly"
},
{
"id": "get_local_models_dev_catalogs",
"moduleId": "commands",
"capabilityId": "feature.projects",
"capabilityIds": [
"feature.projects"
],
"documentedItemIds": [],
"visibility": "implementation",
"rustPath": "get_local_models_dev_catalogs",
"sourceFile": "src/apps/desktop/src/api/commands.rs",
"signature": "fn get_local_models_dev_catalogs( ) -> Result<openbitfun_core::LocalModelsDevCatalogs, String>",
"remoteWorkspacePolicy": "LocalOnly"
},
{
"id": "get_mcp_prompt",
"moduleId": "mcp",
Expand Down
5 changes: 4 additions & 1 deletion src/apps/cli/src/peer_host/commands/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,10 @@ async fn build_transient_client(
}

pub(crate) async fn get_ai_model_catalog() -> Result<Value, String> {
serde_json::to_value(openbitfun_core::get_ai_model_catalog().await?)
// A controller receives configured models, defaults and the session
// selection. The models.dev bodies stay on the host: they describe the
// public models.dev catalog, and a controller reads its own snapshot.
serde_json::to_value(openbitfun_core::get_remote_model_catalog().await?)
.map_err(|error| error.to_string())
}

Expand Down
16 changes: 15 additions & 1 deletion src/apps/desktop/src/api/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5373,9 +5373,23 @@ pub async fn get_model_configs(
}
}

/// Model catalog of the host that renders the current surface.
///
/// The models.dev bodies never travel: they describe the public models.dev
/// catalog, which every host refreshes for itself, and only Model Settings reads
/// them — through `get_local_models_dev_catalogs`, which stays on this machine.
#[tauri::command]
pub async fn get_ai_model_catalog() -> Result<openbitfun_core::AIModelCatalog, String> {
openbitfun_core::get_ai_model_catalog().await
openbitfun_core::get_remote_model_catalog().await
}

/// This machine's own models.dev projections, for a controller rendering the
/// Model Settings surface while a peer is selected. Declared controller-local in
/// the Product Operation Registry, so it never reaches a peer host.
#[tauri::command]
pub async fn get_local_models_dev_catalogs(
) -> Result<openbitfun_core::LocalModelsDevCatalogs, String> {
openbitfun_core::get_local_models_dev_catalogs().await
}

#[tauri::command]
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 @@ -1639,6 +1639,7 @@ pub async fn run() {
get_global_config_status,
get_model_configs,
get_ai_model_catalog,
get_local_models_dev_catalogs,
project_ai_model_reasoning_catalog,
get_models_dev_catalog_status,
refresh_models_dev_catalog_now,
Expand Down
58 changes: 45 additions & 13 deletions src/crates/assembly/core/src/infrastructure/ai/provider_catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,6 @@ pub(crate) fn resolve_builtin_provider_catalog(
source: ProviderCatalogSource,
) -> ProviderCatalog {
let overlay = parse_overlay().expect("built-in AI provider overlay must be valid");
let has_bound_catalog_data = models_dev.is_some_and(|catalog| {
overlay.providers.iter().any(|provider| {
provider
.catalog_provider_ids
.iter()
.any(|provider_id| catalog.provider_facts(provider_id).is_some())
})
});
let mut providers = overlay
.providers
.iter()
Expand All @@ -100,15 +92,55 @@ pub(crate) fn resolve_builtin_provider_catalog(
});
ProviderCatalog {
revision: resolved_catalog_revision(&revision),
source: if has_bound_catalog_data && source == ProviderCatalogSource::OpenBitFun {
ProviderCatalogSource::Mixed
} else {
source
},
source: promoted_catalog_source(&overlay, models_dev, source),
providers,
}
}

/// The identity of the built-in provider catalog without projecting its
/// providers.
///
/// A caller that must not ship the projected bodies — a peer answer, a session
/// poll, a mobile or bot controller — still has to report the same `revision`
/// and `source` the full build would: the revision drives
/// `RemoteModelCatalog::version`, and an attached controller only accepts a new
/// catalog when that version moves. Projecting the providers themselves is the
/// expensive part, so the slim build keeps the identity and drops the bodies.
pub(crate) fn builtin_provider_catalog_identity(
models_dev: Option<&ModelsDevCatalog>,
revision: String,
source: ProviderCatalogSource,
) -> ProviderCatalog {
let overlay = parse_overlay().expect("built-in AI provider overlay must be valid");
ProviderCatalog {
revision: resolved_catalog_revision(&revision),
source: promoted_catalog_source(&overlay, models_dev, source),
providers: Vec::new(),
}
}

/// A models.dev snapshot that contributes provider bindings makes the catalog
/// mixed even when the caller passed the OpenBitFun-only source.
fn promoted_catalog_source(
overlay: &ProviderOverlayDocument,
models_dev: Option<&ModelsDevCatalog>,
source: ProviderCatalogSource,
) -> ProviderCatalogSource {
let has_bound_catalog_data = models_dev.is_some_and(|catalog| {
overlay.providers.iter().any(|provider| {
provider
.catalog_provider_ids
.iter()
.any(|provider_id| catalog.provider_facts(provider_id).is_some())
})
});
if has_bound_catalog_data && source == ProviderCatalogSource::OpenBitFun {
ProviderCatalogSource::Mixed
} else {
source
}
}

fn resolved_catalog_revision(source_revision: &str) -> String {
let mut digest = Sha256::new();
digest.update(b"openbitfun-provider-catalog-v1\0");
Expand Down
28 changes: 27 additions & 1 deletion src/crates/assembly/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,9 @@ pub(crate) mod service_agent_runtime;
pub mod util; // General types, errors, helper functions

#[cfg(feature = "remote-connect")]
pub use openbitfun_services_integrations::remote_connect::RemoteModelCatalog as AIModelCatalog;
pub use openbitfun_services_integrations::remote_connect::{
LocalModelsDevCatalogs, RemoteModelCatalog as AIModelCatalog,
};

#[cfg(feature = "agent-runtime")]
pub fn get_builtin_ai_provider_catalog() -> openbitfun_core_types::ProviderCatalog {
Expand All @@ -81,11 +83,35 @@ pub fn get_builtin_ai_provider_catalog() -> openbitfun_core_types::ProviderCatal
)
}

/// Model catalog for an in-process local reader.
///
/// This machine's own models.dev projections are included: TUI/app-server
/// projections and the plugin host render them from the same process. A catalog
/// that crosses a machine boundary must use [`get_remote_model_catalog`]
/// instead, or a peer would ship a multi-MiB copy of the public catalog.
#[cfg(feature = "remote-connect")]
pub async fn get_ai_model_catalog() -> Result<AIModelCatalog, String> {
service_agent_runtime::CoreServiceAgentRuntime::load_local_model_catalog(None).await
}

/// Model catalog for a caller on another machine: configured models, defaults
/// and the session selection, with the models.dev bodies left out.
///
/// Every host keeps its own refreshed models.dev snapshot, so a controller
/// enriches its own Model Settings surface locally through
/// [`get_local_models_dev_catalogs`] instead of pulling the peer's copy.
#[cfg(feature = "remote-connect")]
pub async fn get_remote_model_catalog() -> Result<AIModelCatalog, String> {
service_agent_runtime::CoreServiceAgentRuntime::load_remote_model_catalog(None).await
}

/// This machine's own models.dev projections, for a controller that renders
/// Model Settings while a peer is selected.
#[cfg(feature = "remote-connect")]
pub async fn get_local_models_dev_catalogs() -> Result<LocalModelsDevCatalogs, String> {
service_agent_runtime::CoreServiceAgentRuntime::load_local_models_dev_catalogs().await
}

#[cfg(feature = "model-catalog")]
pub async fn project_ai_model_reasoning_catalog(
request: openbitfun_core_types::ReasoningCatalogProjectionRequest,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,9 @@ async fn start_switch_model(state: &mut BotChatState, s: &'static BotStrings) ->
}
};

// A bot menu reads the configured models and the session selection only: the
// models.dev bodies belong to the host's own settings surface, and a chat
// controller never offers provider or reasoning-catalog editing.
let catalog = match CoreServiceAgentRuntime::load_remote_model_catalog(Some(&session_id)).await
{
Ok(c) => c,
Expand Down
Loading
Loading