discord bugfixes

This commit is contained in:
jaberjaber23
2026-03-04 01:17:37 +03:00
parent fe96cd1004
commit fac4ad33e5
10 changed files with 204 additions and 84 deletions
Generated
+14 -14
View File
@@ -3866,7 +3866,7 @@ dependencies = [
[[package]]
name = "openfang-api"
version = "0.3.6"
version = "0.3.7"
dependencies = [
"async-trait",
"axum",
@@ -3902,7 +3902,7 @@ dependencies = [
[[package]]
name = "openfang-channels"
version = "0.3.6"
version = "0.3.7"
dependencies = [
"async-trait",
"axum",
@@ -3933,7 +3933,7 @@ dependencies = [
[[package]]
name = "openfang-cli"
version = "0.3.6"
version = "0.3.7"
dependencies = [
"clap",
"clap_complete",
@@ -3960,7 +3960,7 @@ dependencies = [
[[package]]
name = "openfang-desktop"
version = "0.3.6"
version = "0.3.7"
dependencies = [
"axum",
"open",
@@ -3986,7 +3986,7 @@ dependencies = [
[[package]]
name = "openfang-extensions"
version = "0.3.6"
version = "0.3.7"
dependencies = [
"aes-gcm",
"argon2",
@@ -4014,7 +4014,7 @@ dependencies = [
[[package]]
name = "openfang-hands"
version = "0.3.6"
version = "0.3.7"
dependencies = [
"chrono",
"dashmap",
@@ -4031,7 +4031,7 @@ dependencies = [
[[package]]
name = "openfang-kernel"
version = "0.3.6"
version = "0.3.7"
dependencies = [
"async-trait",
"chrono",
@@ -4067,7 +4067,7 @@ dependencies = [
[[package]]
name = "openfang-memory"
version = "0.3.6"
version = "0.3.7"
dependencies = [
"async-trait",
"chrono",
@@ -4086,7 +4086,7 @@ dependencies = [
[[package]]
name = "openfang-migrate"
version = "0.3.6"
version = "0.3.7"
dependencies = [
"chrono",
"dirs 6.0.0",
@@ -4105,7 +4105,7 @@ dependencies = [
[[package]]
name = "openfang-runtime"
version = "0.3.6"
version = "0.3.7"
dependencies = [
"anyhow",
"async-trait",
@@ -4137,7 +4137,7 @@ dependencies = [
[[package]]
name = "openfang-skills"
version = "0.3.6"
version = "0.3.7"
dependencies = [
"chrono",
"hex",
@@ -4160,7 +4160,7 @@ dependencies = [
[[package]]
name = "openfang-types"
version = "0.3.6"
version = "0.3.7"
dependencies = [
"async-trait",
"chrono",
@@ -4179,7 +4179,7 @@ dependencies = [
[[package]]
name = "openfang-wire"
version = "0.3.6"
version = "0.3.7"
dependencies = [
"async-trait",
"chrono",
@@ -8791,7 +8791,7 @@ checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
[[package]]
name = "xtask"
version = "0.3.6"
version = "0.3.7"
[[package]]
name = "yoke"
+1 -1
View File
@@ -18,7 +18,7 @@ members = [
]
[workspace.package]
version = "0.3.7"
version = "0.3.8"
edition = "2021"
license = "Apache-2.0 OR MIT"
repository = "https://github.com/RightNow-AI/openfang"
+6 -1
View File
@@ -796,7 +796,12 @@ async fn handle_command(
} else {
match state.kernel.set_agent_model(agent_id, args) {
Ok(()) => {
serde_json::json!({"type": "command_result", "command": cmd, "message": format!("Model switched to: {args}")})
let msg = if let Some(entry) = state.kernel.registry.get(agent_id) {
format!("Model switched to: {} (provider: {})", entry.manifest.model.model, entry.manifest.model.provider)
} else {
format!("Model switched to: {args}")
};
serde_json::json!({"type": "command_result", "command": cmd, "message": msg})
}
Err(e) => {
serde_json::json!({"type": "error", "content": format!("Model switch failed: {e}")})
@@ -222,10 +222,10 @@ async fn test_status_endpoint() {
assert_eq!(resp.status(), 200);
let body: serde_json::Value = resp.json().await.unwrap();
assert_eq!(body["status"], "running");
assert_eq!(body["agent_count"], 0);
assert_eq!(body["agent_count"], 1); // default assistant auto-spawned
assert!(body["uptime_seconds"].is_number());
assert_eq!(body["default_provider"], "ollama");
assert_eq!(body["agents"].as_array().unwrap().len(), 0);
assert_eq!(body["agents"].as_array().unwrap().len(), 1);
}
#[tokio::test]
@@ -247,7 +247,7 @@ async fn test_spawn_list_kill_agent() {
let agent_id = body["agent_id"].as_str().unwrap().to_string();
assert!(!agent_id.is_empty());
// --- List (1 agent) ---
// --- List (2 agents: default assistant + test-agent) ---
let resp = client
.get(format!("{}/api/agents", server.base_url))
.send()
@@ -255,10 +255,10 @@ async fn test_spawn_list_kill_agent() {
.unwrap();
assert_eq!(resp.status(), 200);
let agents: Vec<serde_json::Value> = resp.json().await.unwrap();
assert_eq!(agents.len(), 1);
assert_eq!(agents[0]["name"], "test-agent");
assert_eq!(agents[0]["id"], agent_id);
assert_eq!(agents[0]["model_provider"], "ollama");
assert_eq!(agents.len(), 2);
let test_agent = agents.iter().find(|a| a["name"] == "test-agent").unwrap();
assert_eq!(test_agent["id"], agent_id);
assert_eq!(test_agent["model_provider"], "ollama");
// --- Kill ---
let resp = client
@@ -270,7 +270,7 @@ async fn test_spawn_list_kill_agent() {
let body: serde_json::Value = resp.json().await.unwrap();
assert_eq!(body["status"], "killed");
// --- List (empty) ---
// --- List (only default assistant remains) ---
let resp = client
.get(format!("{}/api/agents", server.base_url))
.send()
@@ -278,7 +278,8 @@ async fn test_spawn_list_kill_agent() {
.unwrap();
assert_eq!(resp.status(), 200);
let agents: Vec<serde_json::Value> = resp.json().await.unwrap();
assert_eq!(agents.len(), 0);
assert_eq!(agents.len(), 1);
assert_eq!(agents[0]["name"], "assistant");
}
#[tokio::test]
@@ -617,14 +618,14 @@ memory_write = ["self.*"]
ids.push(body["agent_id"].as_str().unwrap().to_string());
}
// List should show 3
// List should show 4 (3 spawned + default assistant)
let resp = client
.get(format!("{}/api/agents", server.base_url))
.send()
.await
.unwrap();
let agents: Vec<serde_json::Value> = resp.json().await.unwrap();
assert_eq!(agents.len(), 3);
assert_eq!(agents.len(), 4);
// Status should agree
let resp = client
@@ -633,7 +634,7 @@ memory_write = ["self.*"]
.await
.unwrap();
let status: serde_json::Value = resp.json().await.unwrap();
assert_eq!(status["agent_count"], 3);
assert_eq!(status["agent_count"], 4);
// Kill one
let resp = client
@@ -643,14 +644,14 @@ memory_write = ["self.*"]
.unwrap();
assert_eq!(resp.status(), 200);
// List should show 2
// List should show 3 (2 spawned + default assistant)
let resp = client
.get(format!("{}/api/agents", server.base_url))
.send()
.await
.unwrap();
let agents: Vec<serde_json::Value> = resp.json().await.unwrap();
assert_eq!(agents.len(), 2);
assert_eq!(agents.len(), 3);
// Kill the rest
for id in [&ids[0], &ids[2]] {
@@ -661,14 +662,14 @@ memory_write = ["self.*"]
.unwrap();
}
// List should be empty
// List should have only default assistant
let resp = client
.get(format!("{}/api/agents", server.base_url))
.send()
.await
.unwrap();
let agents: Vec<serde_json::Value> = resp.json().await.unwrap();
assert_eq!(agents.len(), 0);
assert_eq!(agents.len(), 1);
}
// ---------------------------------------------------------------------------
+1 -1
View File
@@ -541,7 +541,7 @@ async fn load_spawn_kill_cycle() {
.await
.unwrap();
let remaining = agents.as_array().map(|a| a.len()).unwrap_or(0);
assert_eq!(remaining, 0, "All agents should be killed");
assert_eq!(remaining, 1, "Only default assistant should remain");
}
/// Test: Prometheus metrics endpoint under sustained load.
+44 -1
View File
@@ -1054,6 +1054,33 @@ impl OpenFangKernel {
}
}
// If no agents exist (fresh install), spawn a default assistant
if kernel.registry.list().is_empty() {
info!("No agents found — spawning default assistant");
let dm = &kernel.config.default_model;
let manifest = AgentManifest {
name: "assistant".to_string(),
description: "General-purpose assistant".to_string(),
model: openfang_types::agent::ModelConfig {
provider: dm.provider.clone(),
model: dm.model.clone(),
system_prompt: "You are a helpful AI assistant.".to_string(),
api_key_env: if dm.api_key_env.is_empty() {
None
} else {
Some(dm.api_key_env.clone())
},
base_url: dm.base_url.clone(),
..Default::default()
},
..Default::default()
};
match kernel.spawn_agent(manifest) {
Ok(id) => info!(id = %id, "Default assistant spawned"),
Err(e) => warn!("Failed to spawn default assistant: {e}"),
}
}
// Validate routing configs against model catalog
for entry in kernel.registry.list() {
if let Some(ref routing_config) = entry.manifest.routing {
@@ -2089,7 +2116,16 @@ impl OpenFangKernel {
routed_model = %routed_model,
"Model routing applied"
);
manifest.model.model = routed_model;
manifest.model.model = routed_model.clone();
// Also update provider if the routed model belongs to a different provider
if let Ok(cat) = self.model_catalog.read() {
if let Some(entry) = cat.find_model(&routed_model) {
if entry.provider != manifest.model.provider {
info!(old = %manifest.model.provider, new = %entry.provider, "Model routing changed provider");
manifest.model.provider = entry.provider.clone();
}
}
}
}
let driver = self.resolve_driver(&manifest)?;
@@ -2923,6 +2959,13 @@ impl OpenFangKernel {
);
}
// If an agent with this hand's name already exists, remove it first
let existing = self.registry.list().into_iter().find(|e| e.name == def.agent.name);
if let Some(old) = existing {
info!(agent = %old.name, id = %old.id, "Removing existing hand agent for reactivation");
let _ = self.kill_agent(old.id);
}
// Spawn the agent
let agent_id = self.spawn_agent(manifest)?;
+1 -1
View File
@@ -429,7 +429,7 @@ fn estimate_cost_rates(model: &str) -> (f64, f64) {
}
// ── xAI / Grok ──────────────────────────────────────────────
if model.contains("grok-4.1") {
if model.contains("grok-4-1") {
return (0.20, 0.50);
}
if model.contains("grok-4") {
@@ -349,9 +349,9 @@ async fn test_multiple_wasm_agents() {
assert_eq!(hello_result.response, "hello from wasm");
assert!(echo_result.response.contains("test data"));
// Verify agent list shows both
// Verify agent list shows both + default assistant
let agents = kernel.registry.list();
assert_eq!(agents.len(), 2);
assert_eq!(agents.len(), 3);
kernel.shutdown();
}
@@ -391,9 +391,9 @@ memory_write = ["self.*"]
let llm_manifest: AgentManifest = toml::from_str(llm_toml).unwrap();
let llm_id = kernel.spawn_agent(llm_manifest).unwrap();
// Verify both agents exist
// Verify both agents exist + default assistant
let agents = kernel.registry.list();
assert_eq!(agents.len(), 2);
assert_eq!(agents.len(), 3);
// WASM agent should work
let result = kernel.send_message(wasm_id, "hello").await.unwrap();
@@ -404,7 +404,7 @@ memory_write = ["self.*"]
// Kill WASM agent
kernel.kill_agent(wasm_id).unwrap();
assert_eq!(kernel.registry.list().len(), 1);
assert_eq!(kernel.registry.list().len(), 2);
kernel.shutdown();
}
+34 -22
View File
@@ -305,32 +305,44 @@ fn convert_response(resp: GeminiResponse) -> Result<CompletionResponse, LlmError
let mut content = Vec::new();
let mut tool_calls = Vec::new();
if let Some(gemini_content) = candidate.content {
for part in gemini_content.parts {
match part {
GeminiPart::Text { text } => {
if !text.is_empty() {
content.push(ContentBlock::Text { text });
match candidate.content {
Some(gemini_content) => {
for part in gemini_content.parts {
match part {
GeminiPart::Text { text } => {
if !text.is_empty() {
content.push(ContentBlock::Text { text });
}
}
GeminiPart::FunctionCall { function_call } => {
let id = format!("call_{}", uuid::Uuid::new_v4().simple());
content.push(ContentBlock::ToolUse {
id: id.clone(),
name: function_call.name.clone(),
input: function_call.args.clone(),
});
tool_calls.push(ToolCall {
id,
name: function_call.name,
input: function_call.args,
});
}
GeminiPart::InlineData { .. } | GeminiPart::FunctionResponse { .. } => {
// Shouldn't normally appear in responses, ignore
}
}
GeminiPart::FunctionCall { function_call } => {
let id = format!("call_{}", uuid::Uuid::new_v4().simple());
content.push(ContentBlock::ToolUse {
id: id.clone(),
name: function_call.name.clone(),
input: function_call.args.clone(),
});
tool_calls.push(ToolCall {
id,
name: function_call.name,
input: function_call.args,
});
}
GeminiPart::InlineData { .. } | GeminiPart::FunctionResponse { .. } => {
// Shouldn't normally appear in responses, ignore
}
}
}
None => {
let reason = candidate
.finish_reason
.as_deref()
.unwrap_or("unknown");
warn!(finish_reason = %reason, "Gemini returned candidate with no content");
return Err(LlmError::Parse(format!(
"Gemini returned empty response (finish_reason: {reason})"
)));
}
}
// Gemini uses "STOP" for both end-of-turn and function calls,
+81 -22
View File
@@ -648,8 +648,8 @@ fn builtin_aliases() -> HashMap<String, String> {
("gpt5", "gpt-5.2"),
("gpt5-mini", "gpt-5-mini"),
("flash", "gemini-2.5-flash"),
("gemini-flash", "gemini-2.5-flash"),
("gemini-pro", "gemini-3.1-pro"),
("gemini-pro", "gemini-3.1-pro-preview"),
("gemini-flash", "gemini-3-flash-preview"),
("deepseek", "deepseek-chat"),
("llama", "llama-3.3-70b-versatile"),
("llama-70b", "llama-3.3-70b-versatile"),
@@ -663,10 +663,11 @@ fn builtin_aliases() -> HashMap<String, String> {
("mistral-nemo", "open-mistral-nemo"),
("pixtral", "pixtral-large-latest"),
// xAI aliases
("grok", "grok-4"),
("grok", "grok-4-0709"),
("grok-4", "grok-4-0709"),
("grok-mini", "grok-2-mini"),
("grok3", "grok-3"),
("grok-fast", "grok-4.1-fast"),
("grok-fast", "grok-4-1-fast-reasoning"),
// Perplexity alias
("sonar", "sonar-pro"),
// AI21 aliases
@@ -1039,8 +1040,8 @@ fn builtin_models() -> Vec<ModelCatalogEntry> {
// Google Gemini (10)
// ══════════════════════════════════════════════════════════════
ModelCatalogEntry {
id: "gemini-3.1-pro".into(),
display_name: "Gemini 3.1 Pro".into(),
id: "gemini-3.1-pro-preview".into(),
display_name: "Gemini 3.1 Pro Preview".into(),
provider: "gemini".into(),
tier: ModelTier::Frontier,
context_window: 1_048_576,
@@ -1052,16 +1053,29 @@ fn builtin_models() -> Vec<ModelCatalogEntry> {
supports_streaming: true,
aliases: vec!["gemini-pro".into()],
},
// gemini-3-flash removed: model doesn't exist. Use gemini-2.5-flash instead.
ModelCatalogEntry {
id: "gemini-3-deep-think".into(),
display_name: "Gemini 3 Deep Think".into(),
id: "gemini-3-flash-preview".into(),
display_name: "Gemini 3 Flash Preview".into(),
provider: "gemini".into(),
tier: ModelTier::Frontier,
tier: ModelTier::Smart,
context_window: 1_048_576,
max_output_tokens: 65_536,
input_cost_per_m: 2.50,
output_cost_per_m: 15.0,
input_cost_per_m: 0.15,
output_cost_per_m: 0.60,
supports_tools: true,
supports_vision: true,
supports_streaming: true,
aliases: vec!["gemini-flash".into()],
},
ModelCatalogEntry {
id: "gemini-3.1-flash-lite-preview".into(),
display_name: "Gemini 3.1 Flash Lite Preview".into(),
provider: "gemini".into(),
tier: ModelTier::Fast,
context_window: 1_048_576,
max_output_tokens: 8_192,
input_cost_per_m: 0.04,
output_cost_per_m: 0.15,
supports_tools: true,
supports_vision: true,
supports_streaming: true,
@@ -2174,10 +2188,10 @@ fn builtin_models() -> Vec<ModelCatalogEntry> {
aliases: vec![],
},
// ══════════════════════════════════════════════════════════════
// xAI (6)
// xAI (9)
// ══════════════════════════════════════════════════════════════
ModelCatalogEntry {
id: "grok-4".into(),
id: "grok-4-0709".into(),
display_name: "Grok 4".into(),
provider: "xai".into(),
tier: ModelTier::Frontier,
@@ -2188,11 +2202,39 @@ fn builtin_models() -> Vec<ModelCatalogEntry> {
supports_tools: true,
supports_vision: true,
supports_streaming: true,
aliases: vec!["grok".into()],
aliases: vec!["grok".into(), "grok-4".into()],
},
ModelCatalogEntry {
id: "grok-4.1-fast".into(),
display_name: "Grok 4.1 Fast".into(),
id: "grok-4-fast-reasoning".into(),
display_name: "Grok 4 Fast Reasoning".into(),
provider: "xai".into(),
tier: ModelTier::Smart,
context_window: 256_000,
max_output_tokens: 32_768,
input_cost_per_m: 1.0,
output_cost_per_m: 5.0,
supports_tools: true,
supports_vision: false,
supports_streaming: true,
aliases: vec![],
},
ModelCatalogEntry {
id: "grok-4-fast-non-reasoning".into(),
display_name: "Grok 4 Fast Non-Reasoning".into(),
provider: "xai".into(),
tier: ModelTier::Smart,
context_window: 256_000,
max_output_tokens: 32_768,
input_cost_per_m: 1.0,
output_cost_per_m: 5.0,
supports_tools: true,
supports_vision: false,
supports_streaming: true,
aliases: vec![],
},
ModelCatalogEntry {
id: "grok-4-1-fast-reasoning".into(),
display_name: "Grok 4.1 Fast Reasoning".into(),
provider: "xai".into(),
tier: ModelTier::Fast,
context_window: 2_000_000,
@@ -2204,6 +2246,20 @@ fn builtin_models() -> Vec<ModelCatalogEntry> {
supports_streaming: true,
aliases: vec!["grok-fast".into()],
},
ModelCatalogEntry {
id: "grok-4-1-fast-non-reasoning".into(),
display_name: "Grok 4.1 Fast Non-Reasoning".into(),
provider: "xai".into(),
tier: ModelTier::Fast,
context_window: 2_000_000,
max_output_tokens: 32_768,
input_cost_per_m: 0.20,
output_cost_per_m: 0.50,
supports_tools: true,
supports_vision: false,
supports_streaming: true,
aliases: vec![],
},
ModelCatalogEntry {
id: "grok-3".into(),
display_name: "Grok 3".into(),
@@ -3020,7 +3076,7 @@ mod tests {
assert!(aliases.len() >= 20);
assert_eq!(aliases.get("sonnet").unwrap(), "claude-sonnet-4-6");
// New aliases
assert_eq!(aliases.get("grok").unwrap(), "grok-4");
assert_eq!(aliases.get("grok").unwrap(), "grok-4-0709");
assert_eq!(aliases.get("jamba").unwrap(), "jamba-1.5-large");
}
@@ -3028,7 +3084,7 @@ mod tests {
fn test_find_grok_by_alias() {
let catalog = ModelCatalog::new();
let entry = catalog.find_model("grok").unwrap();
assert_eq!(entry.id, "grok-4");
assert_eq!(entry.id, "grok-4-0709");
assert_eq!(entry.provider, "xai");
}
@@ -3049,9 +3105,12 @@ mod tests {
fn test_xai_models() {
let catalog = ModelCatalog::new();
let xai = catalog.models_by_provider("xai");
assert_eq!(xai.len(), 6);
assert!(xai.iter().any(|m| m.id == "grok-4"));
assert!(xai.iter().any(|m| m.id == "grok-4.1-fast"));
assert_eq!(xai.len(), 9);
assert!(xai.iter().any(|m| m.id == "grok-4-0709"));
assert!(xai.iter().any(|m| m.id == "grok-4-fast-reasoning"));
assert!(xai.iter().any(|m| m.id == "grok-4-fast-non-reasoning"));
assert!(xai.iter().any(|m| m.id == "grok-4-1-fast-reasoning"));
assert!(xai.iter().any(|m| m.id == "grok-4-1-fast-non-reasoning"));
assert!(xai.iter().any(|m| m.id == "grok-3"));
assert!(xai.iter().any(|m| m.id == "grok-3-mini"));
assert!(xai.iter().any(|m| m.id == "grok-2"));