community fixes

This commit is contained in:
jaberjaber23
2026-03-07 23:31:38 +03:00
parent b2e2b1a038
commit 772cbdbe38
11 changed files with 237 additions and 24 deletions
Generated
+14 -14
View File
@@ -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"
+1 -1
View File
@@ -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"
+34 -1
View File
@@ -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<serde_json::Value> = Vec::new();
let mut tool_use_index: std::collections::HashMap<String, (usize, usize)> =
std::collections::HashMap::new();
for m in &session.messages {
let mut tools: Vec<serde_json::Value> = Vec::new();
let mut msg_images: Vec<serde_json::Value> = 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);
}
@@ -3042,6 +3042,30 @@ args = ["-y", "@modelcontextprotocol/server-filesystem", "/path"]</pre>
</div>
</template>
</div>
<!-- Add Custom Provider -->
<div class="info-card mt-4" style="border:1px solid var(--border)">
<h4 style="margin-top:0">Add Custom Provider</h4>
<p class="text-xs text-dim mb-2">Connect any OpenAI-compatible API (vLLM, LiteLLM, LocalAI, etc.)</p>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:0.5rem">
<div>
<label class="text-xs text-dim">Provider Name</label>
<input class="form-input" x-model="customProviderName" placeholder="e.g. my-local-llm">
</div>
<div>
<label class="text-xs text-dim">Base URL (required)</label>
<input class="form-input" x-model="customProviderUrl" placeholder="http://localhost:8080/v1">
</div>
</div>
<div class="mt-2">
<label class="text-xs text-dim">API Key (optional)</label>
<input class="form-input" type="password" x-model="customProviderKey" placeholder="sk-... (leave blank if not needed)">
</div>
<button class="btn btn-primary btn-sm mt-2" @click="addCustomProvider()" :disabled="!customProviderName.trim() || !customProviderUrl.trim() || addingCustomProvider">
<span x-show="!addingCustomProvider">Add Provider</span>
<span x-show="addingCustomProvider" class="spinner" style="width:10px;height:10px;border-width:2px"></span>
</button>
<span class="text-xs text-dim ml-2" x-text="customProviderStatus"></span>
</div>
<div class="empty-state" x-show="!providers.length">
<h4>No providers found</h4>
<p class="hint">Provider information could not be loaded. Check that the API is running.</p>
+4 -1
View File
@@ -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(); });
}
@@ -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;
@@ -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] || '';
},
+27 -1
View File
@@ -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");
}
}
}
}
}
@@ -362,6 +362,38 @@ pub fn create_driver(config: &DriverConfig) -> Result<Arc<dyn LlmDriver>, 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] {
&[
+37 -2
View File
@@ -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")
+25
View File
@@ -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");
}
}