Skip to content
Open
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
24 changes: 24 additions & 0 deletions crates/openfang-api/src/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ pub async fn list_agents(State(state): State<Arc<AppState>>) -> impl IntoRespons
"auth_status": auth_status,
"ready": ready,
"profile": e.manifest.profile,
"temperature": e.manifest.model.temperature,
"identity": {
"emoji": e.identity.emoji,
"avatar_url": e.identity.avatar_url,
Expand Down Expand Up @@ -1362,6 +1363,7 @@ pub async fn get_agent(
"mcp_servers": entry.manifest.mcp_servers,
"mcp_servers_mode": if entry.manifest.mcp_servers.is_empty() { "all" } else { "allowlist" },
"fallback_models": entry.manifest.fallback_models,
"temperature": entry.manifest.model.temperature,
})),
)
}
Expand Down Expand Up @@ -8644,6 +8646,7 @@ pub struct PatchAgentConfigRequest {
pub api_key_env: Option<String>,
pub base_url: Option<String>,
pub fallback_models: Option<Vec<openfang_types::agent::FallbackModel>>,
pub temperature: Option<f32>,
}

/// PATCH /api/agents/{id}/config — Hot-update agent name, description, system prompt, and identity.
Expand Down Expand Up @@ -8865,6 +8868,27 @@ pub async fn patch_agent_config(
}
}

// Update temperature
if let Some(temp) = req.temperature {
if !(0.0..=2.0).contains(&temp) {
return (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({"error": "Temperature must be between 0.0 and 2.0"})),
);
}
if state
.kernel
.registry
.update_temperature(agent_id, temp)
.is_err()
{
return (
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Agent not found"})),
);
}
}

// Persist updated manifest to database so changes survive restart
if let Some(entry) = state.kernel.registry.get(agent_id) {
if let Err(e) = state.kernel.memory.save_agent(&entry) {
Expand Down
1 change: 1 addition & 0 deletions crates/openfang-api/static/index_body.html
Original file line number Diff line number Diff line change
Expand Up @@ -1025,6 +1025,7 @@ <h3>
<div x-show="detailTab === 'config'">
<div class="form-group"><label>Name</label><input class="form-input" x-model="configForm.name"></div>
<div class="form-group"><label>System Prompt</label><textarea class="form-textarea" x-model="configForm.system_prompt" style="height:80px"></textarea></div>
<div class="form-group"><label>Temperature</label><input type="number" class="form-input" x-model.number="configForm.temperature" min="0" max="2" step="0.05" style="width:100px"></div>
<div class="form-group"><label>Emoji</label>
<div class="emoji-grid">
<template x-for="em in emojiOptions" :key="em">
Expand Down
3 changes: 2 additions & 1 deletion crates/openfang-api/static/js/pages/agents.js
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,8 @@ function agentsPage() {
emoji: (agent.identity && agent.identity.emoji) || '',
color: (agent.identity && agent.identity.color) || '#FF5C00',
archetype: (agent.identity && agent.identity.archetype) || '',
vibe: (agent.identity && agent.identity.vibe) || ''
vibe: (agent.identity && agent.identity.vibe) || '',
temperature: agent.temperature != null ? agent.temperature : 0.7
};
this.showDetailModal = true;
// Fetch full agent detail to get fallback_models
Expand Down
87 changes: 76 additions & 11 deletions crates/openfang-kernel/src/kernel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1996,19 +1996,24 @@ impl OpenFangKernel {
// Persist usage to database (same as non-streaming path)
let model = &manifest.model.model;
let cost = MeteringEngine::estimate_cost_with_catalog(
&kernel_clone.model_catalog.read().unwrap_or_else(|e| e.into_inner()),
&kernel_clone
.model_catalog
.read()
.unwrap_or_else(|e| e.into_inner()),
model,
result.total_usage.input_tokens,
result.total_usage.output_tokens,
);
let _ = kernel_clone.metering.record(&openfang_memory::usage::UsageRecord {
agent_id,
model: model.clone(),
input_tokens: result.total_usage.input_tokens,
output_tokens: result.total_usage.output_tokens,
cost_usd: cost,
tool_calls: result.iterations.saturating_sub(1),
});
let _ = kernel_clone
.metering
.record(&openfang_memory::usage::UsageRecord {
agent_id,
model: model.clone(),
input_tokens: result.total_usage.input_tokens,
output_tokens: result.total_usage.output_tokens,
cost_usd: cost,
tool_calls: result.iterations.saturating_sub(1),
});

let _ = kernel_clone
.registry
Expand Down Expand Up @@ -3216,6 +3221,24 @@ impl OpenFangKernel {
other => KernelError::OpenFang(OpenFangError::Internal(other.to_string())),
})?;

// Capture API-patched fields from the existing agent before manifest rebuild.
// The rebuild below overwrites everything from HAND.toml, so we save the live
// values here and reapply them after — making API changes durable across restarts.
let existing_agent = self
.registry
.list()
.into_iter()
.find(|e| e.name == def.agent.name);
let existing_tool_filters: (Vec<String>, Vec<String>) = existing_agent
.as_ref()
.map(|e| {
(
e.manifest.tool_allowlist.clone(),
e.manifest.tool_blocklist.clone(),
)
})
.unwrap_or_default();

// Build an agent manifest from the hand definition.
// If the hand declares provider/model as "default", inherit the kernel's configured LLM.
let hand_provider = if def.agent.provider == "default" {
Expand All @@ -3229,6 +3252,21 @@ impl OpenFangKernel {
def.agent.model.clone()
};

// Detect API-patched model config: compare the live agent's provider/model/temperature
// against what HAND.toml would produce. A difference means an API call changed it —
// preserve those values so they survive respawn.
let existing_model_override: Option<(String, String, f32)> = existing_agent.and_then(|e| {
let m = &e.manifest.model;
if m.provider != hand_provider
|| m.model != hand_model
|| (m.temperature - def.agent.temperature).abs() > f32::EPSILON
{
Some((m.provider.clone(), m.model.clone(), m.temperature))
} else {
None
}
});

let mut manifest = AgentManifest {
name: def.agent.name.clone(),
description: def.agent.description.clone(),
Expand Down Expand Up @@ -3287,6 +3325,25 @@ impl OpenFangKernel {
..Default::default()
};

// Restore API-patched tool filters so they survive respawn.
// Only apply if non-empty — an empty saved list means "no override set", not "block all".
let (saved_allowlist, saved_blocklist) = existing_tool_filters;
if !saved_allowlist.is_empty() {
manifest.tool_allowlist = saved_allowlist;
}
if !saved_blocklist.is_empty() {
manifest.tool_blocklist = saved_blocklist;
}

// Restore API-patched model config so provider/model/temperature survive respawn.
// system_prompt is intentionally excluded — it is assembled from HAND.toml + settings
// context by this function and must stay dynamic.
if let Some((provider, model, temperature)) = existing_model_override {
manifest.model.provider = provider;
manifest.model.model = model;
manifest.model.temperature = temperature;
}

// Resolve hand settings → prompt block + env vars
let resolved = openfang_hands::resolve_settings(&def.settings, &instance.config);
if !resolved.prompt_block.is_empty() {
Expand Down Expand Up @@ -5164,10 +5221,18 @@ impl OpenFangKernel {
.unwrap_or_default();

if !tool_allowlist.is_empty() {
all_tools.retain(|t| tool_allowlist.iter().any(|a| a == &t.name));
all_tools.retain(|t| {
tool_allowlist
.iter()
.any(|a| a.to_lowercase() == t.name.to_lowercase())
});
}
if !tool_blocklist.is_empty() {
all_tools.retain(|t| !tool_blocklist.iter().any(|b| b == &t.name));
all_tools.retain(|t| {
!tool_blocklist
.iter()
.any(|b| b.to_lowercase() == t.name.to_lowercase())
});
}

// Step 5: Remove shell_exec if exec_policy denies it.
Expand Down
11 changes: 11 additions & 0 deletions crates/openfang-kernel/src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,17 @@ impl AgentRegistry {
Ok(())
}

/// Update an agent's sampling temperature.
pub fn update_temperature(&self, id: AgentId, temperature: f32) -> OpenFangResult<()> {
let mut entry = self
.agents
.get_mut(&id)
.ok_or_else(|| OpenFangError::AgentNotFound(id.to_string()))?;
entry.manifest.model.temperature = temperature;
entry.last_active = chrono::Utc::now();
Ok(())
}

/// Update an agent's model AND provider together.
pub fn update_model_and_provider(
&self,
Expand Down
12 changes: 3 additions & 9 deletions crates/openfang-runtime/src/drivers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -791,9 +791,7 @@ mod tests {
let config = DriverConfig {
provider: "azure".to_string(),
api_key: Some("test-azure-key".to_string()),
base_url: Some(
"https://myresource.openai.azure.com/openai/deployments".to_string(),
),
base_url: Some("https://myresource.openai.azure.com/openai/deployments".to_string()),
skip_permissions: true,
};
let driver = create_driver(&config);
Expand All @@ -805,9 +803,7 @@ mod tests {
let config = DriverConfig {
provider: "azure".to_string(),
api_key: None,
base_url: Some(
"https://myresource.openai.azure.com/openai/deployments".to_string(),
),
base_url: Some("https://myresource.openai.azure.com/openai/deployments".to_string()),
skip_permissions: true,
};
let result = create_driver(&config);
Expand Down Expand Up @@ -843,9 +839,7 @@ mod tests {
let config = DriverConfig {
provider: "azure-openai".to_string(),
api_key: Some("test-azure-key".to_string()),
base_url: Some(
"https://myresource.openai.azure.com/openai/deployments".to_string(),
),
base_url: Some("https://myresource.openai.azure.com/openai/deployments".to_string()),
skip_permissions: true,
};
let driver = create_driver(&config);
Expand Down
3 changes: 1 addition & 2 deletions crates/openfang-runtime/src/drivers/openai.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,7 @@ impl OpenAIDriver {
if self.azure_mode {
builder = builder.header("api-key", self.api_key.as_str());
} else {
builder =
builder.header("authorization", format!("Bearer {}", self.api_key.as_str()));
builder = builder.header("authorization", format!("Bearer {}", self.api_key.as_str()));
}
builder
}
Expand Down
11 changes: 5 additions & 6 deletions crates/openfang-runtime/src/model_catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,11 @@

use openfang_types::model_catalog::{
AuthStatus, ModelCatalogEntry, ModelTier, ProviderInfo, AI21_BASE_URL, ANTHROPIC_BASE_URL,
AZURE_OPENAI_BASE_URL, BEDROCK_BASE_URL, CEREBRAS_BASE_URL, CHUTES_BASE_URL,
COHERE_BASE_URL, DEEPSEEK_BASE_URL, FIREWORKS_BASE_URL, GEMINI_BASE_URL,
GITHUB_COPILOT_BASE_URL, GROQ_BASE_URL, HUGGINGFACE_BASE_URL, KIMI_CODING_BASE_URL,
LEMONADE_BASE_URL, LMSTUDIO_BASE_URL, MINIMAX_BASE_URL, MISTRAL_BASE_URL,
MOONSHOT_BASE_URL, NVIDIA_NIM_BASE_URL, OLLAMA_BASE_URL, OPENAI_BASE_URL,
OPENROUTER_BASE_URL, PERPLEXITY_BASE_URL, QIANFAN_BASE_URL, QWEN_BASE_URL,
AZURE_OPENAI_BASE_URL, BEDROCK_BASE_URL, CEREBRAS_BASE_URL, CHUTES_BASE_URL, COHERE_BASE_URL,
DEEPSEEK_BASE_URL, FIREWORKS_BASE_URL, GEMINI_BASE_URL, GITHUB_COPILOT_BASE_URL, GROQ_BASE_URL,
HUGGINGFACE_BASE_URL, KIMI_CODING_BASE_URL, LEMONADE_BASE_URL, LMSTUDIO_BASE_URL,
MINIMAX_BASE_URL, MISTRAL_BASE_URL, MOONSHOT_BASE_URL, NVIDIA_NIM_BASE_URL, OLLAMA_BASE_URL,
OPENAI_BASE_URL, OPENROUTER_BASE_URL, PERPLEXITY_BASE_URL, QIANFAN_BASE_URL, QWEN_BASE_URL,
REPLICATE_BASE_URL, SAMBANOVA_BASE_URL, TOGETHER_BASE_URL, VENICE_BASE_URL, VLLM_BASE_URL,
VOLCENGINE_BASE_URL, VOLCENGINE_CODING_BASE_URL, XAI_BASE_URL, ZAI_BASE_URL,
ZAI_CODING_BASE_URL, ZHIPU_BASE_URL, ZHIPU_CODING_BASE_URL,
Expand Down
Loading