mirror of
https://github.com/RightNow-AI/openfang.git
synced 2026-07-30 15:01:15 +00:00
community fixes
This commit is contained in:
Generated
+14
-14
@@ -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
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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] || '';
|
||||
},
|
||||
|
||||
@@ -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] {
|
||||
&[
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user