From 772cbdbe3813063e59e89f55c23c840784a4c80f Mon Sep 17 00:00:00 2001 From: jaberjaber23 Date: Sat, 7 Mar 2026 23:31:38 +0300 Subject: [PATCH] community fixes --- Cargo.lock | 28 ++++++------- Cargo.toml | 2 +- crates/openfang-api/src/routes.rs | 35 ++++++++++++++++- crates/openfang-api/static/index_body.html | 24 ++++++++++++ crates/openfang-api/static/js/pages/chat.js | 5 ++- .../openfang-api/static/js/pages/settings.js | 33 ++++++++++++++++ crates/openfang-api/static/js/pages/wizard.js | 10 +++-- crates/openfang-kernel/src/kernel.rs | 28 ++++++++++++- crates/openfang-runtime/src/drivers/mod.rs | 32 +++++++++++++++ crates/openfang-runtime/src/drivers/openai.rs | 39 ++++++++++++++++++- crates/openfang-types/src/agent.rs | 25 ++++++++++++ 11 files changed, 237 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0e58a826..8874f76a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3875,7 +3875,7 @@ dependencies = [ [[package]] name = "openfang-api" -version = "0.3.25" +version = "0.3.26" dependencies = [ "async-trait", "axum", @@ -3912,7 +3912,7 @@ dependencies = [ [[package]] name = "openfang-channels" -version = "0.3.25" +version = "0.3.26" dependencies = [ "async-trait", "axum", @@ -3944,7 +3944,7 @@ dependencies = [ [[package]] name = "openfang-cli" -version = "0.3.25" +version = "0.3.26" dependencies = [ "clap", "clap_complete", @@ -3971,7 +3971,7 @@ dependencies = [ [[package]] name = "openfang-desktop" -version = "0.3.25" +version = "0.3.26" dependencies = [ "axum", "open", @@ -3997,7 +3997,7 @@ dependencies = [ [[package]] name = "openfang-extensions" -version = "0.3.25" +version = "0.3.26" dependencies = [ "aes-gcm", "argon2", @@ -4025,7 +4025,7 @@ dependencies = [ [[package]] name = "openfang-hands" -version = "0.3.25" +version = "0.3.26" dependencies = [ "chrono", "dashmap", @@ -4042,7 +4042,7 @@ dependencies = [ [[package]] name = "openfang-kernel" -version = "0.3.25" +version = "0.3.26" dependencies = [ "async-trait", "chrono", @@ -4078,7 +4078,7 @@ dependencies = [ [[package]] name = "openfang-memory" -version = "0.3.25" +version = "0.3.26" dependencies = [ "async-trait", "chrono", @@ -4097,7 +4097,7 @@ dependencies = [ [[package]] name = "openfang-migrate" -version = "0.3.25" +version = "0.3.26" dependencies = [ "chrono", "dirs 6.0.0", @@ -4116,7 +4116,7 @@ dependencies = [ [[package]] name = "openfang-runtime" -version = "0.3.25" +version = "0.3.26" dependencies = [ "anyhow", "async-trait", @@ -4148,7 +4148,7 @@ dependencies = [ [[package]] name = "openfang-skills" -version = "0.3.25" +version = "0.3.26" dependencies = [ "chrono", "hex", @@ -4171,7 +4171,7 @@ dependencies = [ [[package]] name = "openfang-types" -version = "0.3.25" +version = "0.3.26" dependencies = [ "async-trait", "chrono", @@ -4190,7 +4190,7 @@ dependencies = [ [[package]] name = "openfang-wire" -version = "0.3.25" +version = "0.3.26" dependencies = [ "async-trait", "chrono", @@ -8818,7 +8818,7 @@ checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" [[package]] name = "xtask" -version = "0.3.25" +version = "0.3.26" [[package]] name = "yoke" diff --git a/Cargo.toml b/Cargo.toml index 4592c0ef..9bd1172b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ members = [ ] [workspace.package] -version = "0.3.26" +version = "0.3.27" edition = "2021" license = "Apache-2.0 OR MIT" repository = "https://github.com/RightNow-AI/openfang" diff --git a/crates/openfang-api/src/routes.rs b/crates/openfang-api/src/routes.rs index bacc4393..510352f8 100644 --- a/crates/openfang-api/src/routes.rs +++ b/crates/openfang-api/src/routes.rs @@ -395,12 +395,14 @@ pub async fn get_agent_session( // collects all tool_use entries keyed by id; pass 2 attaches results. // Pass 1: build messages and a lookup from tool_use_id → (msg_idx, tool_idx) + use base64::Engine as _; let mut built_messages: Vec = Vec::new(); let mut tool_use_index: std::collections::HashMap = std::collections::HashMap::new(); for m in &session.messages { let mut tools: Vec = Vec::new(); + let mut msg_images: Vec = Vec::new(); let content = match &m.content { openfang_types::message::MessageContent::Text(t) => t.clone(), openfang_types::message::MessageContent::Blocks(blocks) => { @@ -410,8 +412,36 @@ pub async fn get_agent_session( openfang_types::message::ContentBlock::Text { text } => { texts.push(text.clone()); } - openfang_types::message::ContentBlock::Image { .. } => { + openfang_types::message::ContentBlock::Image { + media_type, + data, + } => { texts.push("[Image]".to_string()); + // Persist image to upload dir so it can be + // served back when loading session history. + let file_id = uuid::Uuid::new_v4().to_string(); + let upload_dir = + std::env::temp_dir().join("openfang_uploads"); + let _ = std::fs::create_dir_all(&upload_dir); + if let Ok(bytes) = + base64::engine::general_purpose::STANDARD.decode(data) + { + let _ = std::fs::write( + upload_dir.join(&file_id), + &bytes, + ); + UPLOAD_REGISTRY.insert( + file_id.clone(), + UploadMeta { + filename: format!("image.{}", media_type.rsplit('/').next().unwrap_or("png")), + content_type: media_type.clone(), + }, + ); + msg_images.push(serde_json::json!({ + "file_id": file_id, + "filename": format!("image.{}", media_type.rsplit('/').next().unwrap_or("png")), + })); + } } openfang_types::message::ContentBlock::ToolUse { id, @@ -455,6 +485,9 @@ pub async fn get_agent_session( if !tools.is_empty() { msg["tools"] = serde_json::Value::Array(tools); } + if !msg_images.is_empty() { + msg["images"] = serde_json::Value::Array(msg_images); + } built_messages.push(msg); } diff --git a/crates/openfang-api/static/index_body.html b/crates/openfang-api/static/index_body.html index 4ed1f69d..f4d32aa9 100644 --- a/crates/openfang-api/static/index_body.html +++ b/crates/openfang-api/static/index_body.html @@ -3042,6 +3042,30 @@ args = ["-y", "@modelcontextprotocol/server-filesystem", "/path"] + +
+

Add Custom Provider

+

Connect any OpenAI-compatible API (vLLM, LiteLLM, LocalAI, etc.)

+
+
+ + +
+
+ + +
+
+
+ + +
+ + +

No providers found

Provider information could not be loaded. Check that the API is running.

diff --git a/crates/openfang-api/static/js/pages/chat.js b/crates/openfang-api/static/js/pages/chat.js index cd9fc57a..054d2252 100644 --- a/crates/openfang-api/static/js/pages/chat.js +++ b/crates/openfang-api/static/js/pages/chat.js @@ -531,7 +531,10 @@ function chatPage() { is_error: !!t.is_error }; }); - return { id: ++msgId, role: role, text: text, meta: '', tools: tools }; + var images = (m.images || []).map(function(img) { + return { file_id: img.file_id, filename: img.filename || 'image' }; + }); + return { id: ++msgId, role: role, text: text, meta: '', tools: tools, images: images }; }); self.$nextTick(function() { self.scrollToBottom(); }); } diff --git a/crates/openfang-api/static/js/pages/settings.js b/crates/openfang-api/static/js/pages/settings.js index 8b14788e..9454313f 100644 --- a/crates/openfang-api/static/js/pages/settings.js +++ b/crates/openfang-api/static/js/pages/settings.js @@ -26,6 +26,11 @@ function settingsPage() { providerTesting: {}, providerTestResults: {}, copilotOAuth: { polling: false, userCode: '', verificationUri: '', pollId: '', interval: 5 }, + customProviderName: '', + customProviderUrl: '', + customProviderKey: '', + customProviderStatus: '', + addingCustomProvider: false, loading: true, loadError: '', @@ -499,6 +504,34 @@ function settingsPage() { this.providerUrlSaving[provider.id] = false; }, + async addCustomProvider() { + var name = this.customProviderName.trim().toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-'); + if (!name) { OpenFangToast.error('Please enter a provider name'); return; } + var url = this.customProviderUrl.trim(); + if (!url) { OpenFangToast.error('Please enter a base URL'); return; } + if (url.indexOf('http://') !== 0 && url.indexOf('https://') !== 0) { + OpenFangToast.error('URL must start with http:// or https://'); return; + } + this.addingCustomProvider = true; + this.customProviderStatus = ''; + try { + var result = await OpenFangAPI.put('/api/providers/' + encodeURIComponent(name) + '/url', { base_url: url }); + if (this.customProviderKey.trim()) { + await OpenFangAPI.post('/api/providers/' + encodeURIComponent(name) + '/key', { key: this.customProviderKey.trim() }); + } + this.customProviderName = ''; + this.customProviderUrl = ''; + this.customProviderKey = ''; + this.customProviderStatus = ''; + OpenFangToast.success('Provider "' + name + '" added' + (result.reachable ? ' (reachable)' : ' (not reachable yet)')); + await this.loadProviders(); + } catch(e) { + this.customProviderStatus = 'Error: ' + (e.message || 'Failed'); + OpenFangToast.error('Failed to add provider: ' + e.message); + } + this.addingCustomProvider = false; + }, + // -- Security methods -- async loadSecurity() { this.secLoading = true; diff --git a/crates/openfang-api/static/js/pages/wizard.js b/crates/openfang-api/static/js/pages/wizard.js index 2e3f835f..4ba59e9d 100644 --- a/crates/openfang-api/static/js/pages/wizard.js +++ b/crates/openfang-api/static/js/pages/wizard.js @@ -320,7 +320,7 @@ function wizardPage() { }, get popularProviders() { - var popular = ['anthropic', 'openai', 'gemini', 'groq', 'deepseek', 'openrouter']; + var popular = ['anthropic', 'openai', 'gemini', 'groq', 'deepseek', 'openrouter', 'claude-code']; return this.providers.filter(function(p) { return popular.indexOf(p.id) >= 0; }).sort(function(a, b) { @@ -329,7 +329,7 @@ function wizardPage() { }, get otherProviders() { - var popular = ['anthropic', 'openai', 'gemini', 'groq', 'deepseek', 'openrouter']; + var popular = ['anthropic', 'openai', 'gemini', 'groq', 'deepseek', 'openrouter', 'claude-code']; return this.providers.filter(function(p) { return popular.indexOf(p.id) < 0; }); @@ -355,7 +355,8 @@ function wizardPage() { fireworks: { url: 'https://fireworks.ai/account/api-keys', text: 'Get your key from Fireworks AI' }, perplexity: { url: 'https://www.perplexity.ai/settings/api', text: 'Get your key from Perplexity Settings' }, cohere: { url: 'https://dashboard.cohere.com/api-keys', text: 'Get your key from the Cohere Dashboard' }, - xai: { url: 'https://console.x.ai/', text: 'Get your key from the xAI Console' } + xai: { url: 'https://console.x.ai/', text: 'Get your key from the xAI Console' }, + 'claude-code': { url: 'https://docs.anthropic.com/en/docs/claude-code', text: 'Install: npm install -g @anthropic-ai/claude-code && claude auth (no API key needed)' } }; return help[id] || null; }, @@ -474,7 +475,8 @@ function wizardPage() { fireworks: 'accounts/fireworks/models/llama-v3p1-70b-instruct', perplexity: 'llama-3.1-sonar-large-128k-online', cohere: 'command-r-plus', - xai: 'grok-2' + xai: 'grok-2', + 'claude-code': 'claude-code/sonnet' }; return defaults[providerId] || ''; }, diff --git a/crates/openfang-kernel/src/kernel.rs b/crates/openfang-kernel/src/kernel.rs index 88e0baf4..0b1a0701 100644 --- a/crates/openfang-kernel/src/kernel.rs +++ b/crates/openfang-kernel/src/kernel.rs @@ -567,8 +567,34 @@ impl OpenFangKernel { warn!( provider = %config.default_model.provider, error = %e, - "Primary LLM driver init failed — dashboard will still be accessible" + "Primary LLM driver init failed — trying auto-detect" ); + // Auto-detect: scan env for any configured provider key + if let Some((provider, model, env_var)) = drivers::detect_available_provider() { + let auto_config = DriverConfig { + provider: provider.to_string(), + api_key: std::env::var(env_var).ok(), + base_url: config.provider_urls.get(provider).cloned(), + }; + match drivers::create_driver(&auto_config) { + Ok(d) => { + info!( + provider = %provider, + model = %model, + "Auto-detected provider from {} — using as default", + env_var + ); + driver_chain.push(d); + // Update the running config so agents get the right model + config.default_model.provider = provider.to_string(); + config.default_model.model = model.to_string(); + config.default_model.api_key_env = env_var.to_string(); + } + Err(e2) => { + warn!(provider = %provider, error = %e2, "Auto-detected provider also failed"); + } + } + } } } diff --git a/crates/openfang-runtime/src/drivers/mod.rs b/crates/openfang-runtime/src/drivers/mod.rs index da3cf9ad..fe7b4c40 100644 --- a/crates/openfang-runtime/src/drivers/mod.rs +++ b/crates/openfang-runtime/src/drivers/mod.rs @@ -362,6 +362,38 @@ pub fn create_driver(config: &DriverConfig) -> Result, LlmErr }) } +/// Detect the first available provider by scanning environment variables. +/// +/// Returns `(provider, model, api_key_env)` for the first provider that has a +/// configured API key, checked in a user-friendly priority order. +pub fn detect_available_provider() -> Option<(&'static str, &'static str, &'static str)> { + // Priority: popular cloud providers first, then niche, then local + const PROBE_ORDER: &[(&str, &str, &str)] = &[ + ("openai", "gpt-4o", "OPENAI_API_KEY"), + ("anthropic", "claude-sonnet-4-20250514", "ANTHROPIC_API_KEY"), + ("gemini", "gemini-2.5-flash", "GEMINI_API_KEY"), + ("groq", "llama-3.3-70b-versatile", "GROQ_API_KEY"), + ("deepseek", "deepseek-chat", "DEEPSEEK_API_KEY"), + ("openrouter", "openrouter/auto", "OPENROUTER_API_KEY"), + ("mistral", "mistral-large-latest", "MISTRAL_API_KEY"), + ("together", "meta-llama/Llama-3-70b-chat-hf", "TOGETHER_API_KEY"), + ("fireworks", "accounts/fireworks/models/llama-v3p1-70b-instruct", "FIREWORKS_API_KEY"), + ("xai", "grok-2", "XAI_API_KEY"), + ("perplexity", "llama-3.1-sonar-large-128k-online", "PERPLEXITY_API_KEY"), + ("cohere", "command-r-plus", "COHERE_API_KEY"), + ]; + for &(provider, model, env_var) in PROBE_ORDER { + if std::env::var(env_var).ok().filter(|v| !v.is_empty()).is_some() { + return Some((provider, model, env_var)); + } + } + // Also check GOOGLE_API_KEY as alias for Gemini + if std::env::var("GOOGLE_API_KEY").ok().filter(|v| !v.is_empty()).is_some() { + return Some(("gemini", "gemini-2.5-flash", "GOOGLE_API_KEY")); + } + None +} + /// List all known provider names. pub fn known_providers() -> &'static [&'static str] { &[ diff --git a/crates/openfang-runtime/src/drivers/openai.rs b/crates/openfang-runtime/src/drivers/openai.rs index 212122f0..d8baed7f 100644 --- a/crates/openfang-runtime/src/drivers/openai.rs +++ b/crates/openfang-runtime/src/drivers/openai.rs @@ -67,6 +67,17 @@ fn uses_completion_tokens(model: &str) -> bool { || m.starts_with("o4") } +/// Returns true if a model rejects the `temperature` parameter. +/// +/// OpenAI's o-series reasoning models and some GPT-5 variants do not support +/// temperature and return 400 if it is included. +fn rejects_temperature(model: &str) -> bool { + let m = model.to_lowercase(); + m.starts_with("o1") + || m.starts_with("o3") + || m.starts_with("o4") +} + #[derive(Debug, Serialize)] struct OaiMessage { role: String, @@ -310,7 +321,7 @@ impl LlmDriver for OpenAIDriver { messages: oai_messages, max_tokens: mt, max_completion_tokens: mct, - temperature: Some(request.temperature), + temperature: if rejects_temperature(&request.model) { None } else { Some(request.temperature) }, tools: oai_tools, tool_choice, stream: false, @@ -372,6 +383,18 @@ impl LlmDriver for OpenAIDriver { } } + // o-series / reasoning models: strip temperature if rejected + if status == 400 + && body.contains("temperature") + && body.contains("unsupported_parameter") + && oai_request.temperature.is_some() + && attempt < max_retries + { + warn!(model = %oai_request.model, "Stripping temperature for this model"); + oai_request.temperature = None; + continue; + } + // GPT-5 / o-series: switch from max_tokens to max_completion_tokens if status == 400 && body.contains("max_tokens") @@ -635,7 +658,7 @@ impl LlmDriver for OpenAIDriver { messages: oai_messages, max_tokens: mt, max_completion_tokens: mct, - temperature: Some(request.temperature), + temperature: if rejects_temperature(&request.model) { None } else { Some(request.temperature) }, tools: oai_tools, tool_choice, stream: true, @@ -699,6 +722,18 @@ impl LlmDriver for OpenAIDriver { } } + // o-series / reasoning models: strip temperature if rejected + if status == 400 + && body.contains("temperature") + && body.contains("unsupported_parameter") + && oai_request.temperature.is_some() + && attempt < max_retries + { + warn!(model = %oai_request.model, "Stripping temperature for this model (stream)"); + oai_request.temperature = None; + continue; + } + // GPT-5 / o-series: switch from max_tokens to max_completion_tokens if status == 400 && body.contains("max_tokens") diff --git a/crates/openfang-types/src/agent.rs b/crates/openfang-types/src/agent.rs index 12f63f16..d9ae00f2 100644 --- a/crates/openfang-types/src/agent.rs +++ b/crates/openfang-types/src/agent.rs @@ -367,6 +367,7 @@ pub struct ModelConfig { /// LLM provider name. pub provider: String, /// Model identifier. + #[serde(alias = "name")] pub model: String, /// Maximum tokens for completion. pub max_tokens: u32, @@ -1144,4 +1145,28 @@ mod tests { let manifest: AgentManifest = serde_json::from_str(json).unwrap(); assert!(manifest.generate_identity_files); } + + // ----- ModelConfig alias tests ----- + + #[test] + fn test_model_config_name_alias_toml() { + let toml_str = r#" +name = "llama-3.3-70b-versatile" +provider = "groq" +"#; + let cfg: ModelConfig = toml::from_str(toml_str).unwrap(); + assert_eq!(cfg.model, "llama-3.3-70b-versatile"); + assert_eq!(cfg.provider, "groq"); + } + + #[test] + fn test_model_config_model_field_still_works() { + let toml_str = r#" +model = "gpt-4o" +provider = "openai" +"#; + let cfg: ModelConfig = toml::from_str(toml_str).unwrap(); + assert_eq!(cfg.model, "gpt-4o"); + assert_eq!(cfg.provider, "openai"); + } }