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
20 changes: 8 additions & 12 deletions crates/openfang-api/src/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8813,20 +8813,16 @@ pub async fn patch_agent_config(
if !new_model.is_empty() {
if let Some(ref new_provider) = req.provider {
if !new_provider.is_empty() {
// Explicit provider given — use it directly
if state
.kernel
.registry
.update_model_and_provider(
agent_id,
new_model.clone(),
new_provider.clone(),
)
.is_err()
// Explicit provider given — still route through set_agent_model
// so provider-specific auth/env hints stay in sync.
if let Err(e) =
state
.kernel
.set_agent_model(agent_id, new_model, Some(new_provider))
{
return (
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Agent not found"})),
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": format!("{e}")})),
);
}
} else {
Expand Down
61 changes: 50 additions & 11 deletions crates/openfang-kernel/src/kernel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1311,6 +1311,30 @@ impl OpenFangKernel {
}
}

// Normalize catalog-backed model labels/aliases into canonical IDs and
// fill provider/auth hints when the manifest did not fully specify them.
if let Ok(catalog) = self.model_catalog.read() {
if let Some(entry) = catalog.find_model(&manifest.model.model) {
let provider_is_default =
manifest.model.provider.is_empty() || manifest.model.provider == "default";
if provider_is_default || manifest.model.provider == entry.provider {
manifest.model.provider = entry.provider.clone();
manifest.model.model = strip_provider_prefix(&entry.id, &entry.provider);
if manifest.model.api_key_env.is_none() {
manifest.model.api_key_env =
Some(self.config.resolve_api_key_env(&entry.provider));
}
}
}
}
if manifest.model.api_key_env.is_none()
&& !manifest.model.provider.is_empty()
&& manifest.model.provider != "default"
{
manifest.model.api_key_env =
Some(self.config.resolve_api_key_env(&manifest.model.provider));
}

// Normalize: strip provider prefix from model name if present
let normalized = strip_provider_prefix(&manifest.model.model, &manifest.model.provider);
if normalized != manifest.model.model {
Expand Down Expand Up @@ -2831,6 +2855,11 @@ impl OpenFangKernel {
model: &str,
explicit_provider: Option<&str>,
) -> KernelResult<()> {
let catalog_entry = self
.model_catalog
.read()
.ok()
.and_then(|catalog| catalog.find_model(model).cloned());
let provider = if let Some(ep) = explicit_provider {
// User explicitly set the provider — use it as-is
Some(ep.to_string())
Expand All @@ -2849,25 +2878,35 @@ impl OpenFangKernel {
None
} else {
// No custom base_url: safe to auto-detect from catalog / model name
let resolved_provider = self.model_catalog.read().ok().and_then(|catalog| {
catalog
.find_model(model)
.map(|entry| entry.provider.clone())
});
let resolved_provider = catalog_entry.as_ref().map(|entry| entry.provider.clone());
resolved_provider.or_else(|| infer_provider_from_model(model))
}
};

// Strip the provider prefix from the model name (e.g. "openrouter/deepseek/deepseek-chat" → "deepseek/deepseek-chat")
let normalized_model = if let Some(ref prov) = provider {
strip_provider_prefix(model, prov)
} else {
model.to_string()
};
let normalized_model =
if let (Some(entry), Some(prov)) = (catalog_entry.as_ref(), provider.as_ref()) {
if entry.provider == *prov {
strip_provider_prefix(&entry.id, prov)
} else {
strip_provider_prefix(model, prov)
}
} else if let Some(ref prov) = provider {
strip_provider_prefix(model, prov)
} else {
model.to_string()
};

if let Some(provider) = provider {
let api_key_env = Some(self.config.resolve_api_key_env(&provider));
self.registry
.update_model_and_provider(agent_id, normalized_model.clone(), provider.clone())
.update_model_provider_config(
agent_id,
normalized_model.clone(),
provider.clone(),
api_key_env,
None,
)
.map_err(KernelError::OpenFang)?;
info!(agent_id = %agent_id, model = %normalized_model, provider = %provider, "Agent model+provider updated");
} else {
Expand Down
21 changes: 21 additions & 0 deletions crates/openfang-kernel/src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,27 @@ impl AgentRegistry {
Ok(())
}

/// Update an agent's model, provider, and connection hints together.
pub fn update_model_provider_config(
&self,
id: AgentId,
new_model: String,
new_provider: String,
api_key_env: Option<String>,
base_url: Option<String>,
) -> OpenFangResult<()> {
let mut entry = self
.agents
.get_mut(&id)
.ok_or_else(|| OpenFangError::AgentNotFound(id.to_string()))?;
entry.manifest.model.model = new_model;
entry.manifest.model.provider = new_provider;
entry.manifest.model.api_key_env = api_key_env;
entry.manifest.model.base_url = base_url;
entry.last_active = chrono::Utc::now();
Ok(())
}

/// Update an agent's fallback model chain.
pub fn update_fallback_models(
&self,
Expand Down
18 changes: 17 additions & 1 deletion crates/openfang-runtime/src/model_catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,13 +107,21 @@ impl ModelCatalog {
&self.models
}

/// Find a model by its canonical ID or by alias.
/// Find a model by its canonical ID, display name, or alias.
pub fn find_model(&self, id_or_alias: &str) -> Option<&ModelCatalogEntry> {
let lower = id_or_alias.to_lowercase();
// Direct ID match first
if let Some(entry) = self.models.iter().find(|m| m.id.to_lowercase() == lower) {
return Some(entry);
}
// Display-name match for dashboard/UI payloads that send labels.
if let Some(entry) = self
.models
.iter()
.find(|m| m.display_name.to_lowercase() == lower)
{
return Some(entry);
}
// Alias resolution
if let Some(canonical) = self.aliases.get(&lower) {
return self.models.iter().find(|m| m.id == *canonical);
Expand Down Expand Up @@ -3938,6 +3946,14 @@ mod tests {
assert_eq!(entry.provider, "xai");
}

#[test]
fn test_find_model_by_display_name() {
let catalog = ModelCatalog::new();
let entry = catalog.find_model("Grok 4").unwrap();
assert_eq!(entry.id, "grok-4-0709");
assert_eq!(entry.provider, "xai");
}

#[test]
fn test_new_providers_in_catalog() {
let catalog = ModelCatalog::new();
Expand Down