diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..9eb6e0e1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,62 @@ +name: Bug Report +description: Report a bug or unexpected behavior +labels: ["bug"] +body: + - type: textarea + id: description + attributes: + label: Description + description: What happened? + placeholder: Describe the bug clearly and concisely. + validations: + required: true + + - type: textarea + id: expected + attributes: + label: Expected Behavior + description: What did you expect to happen? + validations: + required: true + + - type: textarea + id: steps + attributes: + label: Steps to Reproduce + description: How can we reproduce this? + placeholder: | + 1. Run `openfang start` + 2. Open dashboard at http://localhost:4200 + 3. Click ... + validations: + required: true + + - type: input + id: version + attributes: + label: OpenFang Version + description: Output of `openfang -V` + placeholder: "0.3.23" + validations: + required: true + + - type: dropdown + id: os + attributes: + label: Operating System + options: + - Linux (x86_64) + - Linux (aarch64/ARM64) + - macOS (Apple Silicon) + - macOS (Intel) + - Windows + - Android (Termux) + - Other + validations: + required: true + + - type: textarea + id: logs + attributes: + label: Logs / Screenshots + description: Paste relevant logs or attach screenshots. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 00000000..210c593e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,24 @@ +name: Feature Request +description: Suggest a new feature or improvement +labels: ["enhancement"] +body: + - type: textarea + id: description + attributes: + label: Description + description: What feature would you like? + placeholder: Describe the feature and why it would be useful. + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Alternatives Considered + description: Have you tried any workarounds? + + - type: textarea + id: context + attributes: + label: Additional Context + description: Any other context, screenshots, or references. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..ff3edab2 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,17 @@ +version: 2 +updates: + - package-ecosystem: "cargo" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 + labels: + - "dependencies" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 3 + labels: + - "ci" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..e4dff9c9 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,19 @@ +## Summary + + + +## Changes + + + +## Testing + +- [ ] `cargo clippy --workspace --all-targets -- -D warnings` passes +- [ ] `cargo test --workspace` passes +- [ ] Live integration tested (if applicable) + +## Security + +- [ ] No new unsafe code +- [ ] No secrets or API keys in diff +- [ ] User input validated at boundaries diff --git a/Cargo.lock b/Cargo.lock index fb58a377..b764783a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3866,7 +3866,7 @@ dependencies = [ [[package]] name = "openfang-api" -version = "0.3.22" +version = "0.3.23" dependencies = [ "async-trait", "axum", @@ -3903,7 +3903,7 @@ dependencies = [ [[package]] name = "openfang-channels" -version = "0.3.22" +version = "0.3.23" dependencies = [ "async-trait", "axum", @@ -3934,7 +3934,7 @@ dependencies = [ [[package]] name = "openfang-cli" -version = "0.3.22" +version = "0.3.23" dependencies = [ "clap", "clap_complete", @@ -3961,7 +3961,7 @@ dependencies = [ [[package]] name = "openfang-desktop" -version = "0.3.22" +version = "0.3.23" dependencies = [ "axum", "open", @@ -3987,7 +3987,7 @@ dependencies = [ [[package]] name = "openfang-extensions" -version = "0.3.22" +version = "0.3.23" dependencies = [ "aes-gcm", "argon2", @@ -4015,7 +4015,7 @@ dependencies = [ [[package]] name = "openfang-hands" -version = "0.3.22" +version = "0.3.23" dependencies = [ "chrono", "dashmap", @@ -4032,7 +4032,7 @@ dependencies = [ [[package]] name = "openfang-kernel" -version = "0.3.22" +version = "0.3.23" dependencies = [ "async-trait", "chrono", @@ -4068,7 +4068,7 @@ dependencies = [ [[package]] name = "openfang-memory" -version = "0.3.22" +version = "0.3.23" dependencies = [ "async-trait", "chrono", @@ -4087,7 +4087,7 @@ dependencies = [ [[package]] name = "openfang-migrate" -version = "0.3.22" +version = "0.3.23" dependencies = [ "chrono", "dirs 6.0.0", @@ -4106,7 +4106,7 @@ dependencies = [ [[package]] name = "openfang-runtime" -version = "0.3.22" +version = "0.3.23" dependencies = [ "anyhow", "async-trait", @@ -4138,7 +4138,7 @@ dependencies = [ [[package]] name = "openfang-skills" -version = "0.3.22" +version = "0.3.23" dependencies = [ "chrono", "hex", @@ -4161,7 +4161,7 @@ dependencies = [ [[package]] name = "openfang-types" -version = "0.3.22" +version = "0.3.23" dependencies = [ "async-trait", "chrono", @@ -4180,7 +4180,7 @@ dependencies = [ [[package]] name = "openfang-wire" -version = "0.3.22" +version = "0.3.23" dependencies = [ "async-trait", "chrono", @@ -8802,7 +8802,7 @@ checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" [[package]] name = "xtask" -version = "0.3.22" +version = "0.3.23" [[package]] name = "yoke" diff --git a/Cargo.toml b/Cargo.toml index 939bcbef..e9c080e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ members = [ ] [workspace.package] -version = "0.3.23" +version = "0.3.24" edition = "2021" license = "Apache-2.0 OR MIT" repository = "https://github.com/RightNow-AI/openfang" diff --git a/crates/openfang-cli/src/main.rs b/crates/openfang-cli/src/main.rs index afaf6030..967eb3c7 100644 --- a/crates/openfang-cli/src/main.rs +++ b/crates/openfang-cli/src/main.rs @@ -2075,20 +2075,23 @@ fn cmd_doctor(json: bool, repair: bool) { } let answer = prompt_input(" Create default config? [Y/n] "); if answer.is_empty() || answer.starts_with('y') || answer.starts_with('Y') { - let default_config = r#"# OpenFang Agent OS configuration + let (provider, api_key_env, model) = detect_best_provider(); + let default_config = format!( + r#"# OpenFang Agent OS configuration # See https://github.com/RightNow-AI/openfang for documentation # For Docker, change to "0.0.0.0:4200" or set OPENFANG_LISTEN env var. api_listen = "127.0.0.1:4200" [default_model] -provider = "groq" -model = "llama-3.3-70b-versatile" -api_key_env = "GROQ_API_KEY" +provider = "{provider}" +model = "{model}" +api_key_env = "{api_key_env}" [memory] decay_rate = 0.05 -"#; +"# + ); let _ = std::fs::create_dir_all(&openfang_dir); if std::fs::write(&config_path, default_config).is_ok() { restrict_file_permissions(&config_path); diff --git a/crates/openfang-kernel/src/kernel.rs b/crates/openfang-kernel/src/kernel.rs index e6a0d869..7e17cf2d 100644 --- a/crates/openfang-kernel/src/kernel.rs +++ b/crates/openfang-kernel/src/kernel.rs @@ -1027,24 +1027,31 @@ impl OpenFangKernel { &mut restored_entry.manifest.resources, ); - // Apply default_model to restored agents (same logic as spawn) + // Apply default_model to restored agents. + // + // Two cases: + // 1. Agent has empty/default provider → always apply default_model + // 2. Agent named "assistant" (auto-spawned) → update to match + // default_model so config.toml changes take effect on restart { + let dm = &kernel.config.default_model; let is_default_provider = restored_entry.manifest.model.provider.is_empty() || restored_entry.manifest.model.provider == "default"; let is_default_model = restored_entry.manifest.model.model.is_empty() || restored_entry.manifest.model.model == "default"; - if is_default_provider && is_default_model { - let dm = &kernel.config.default_model; + let is_auto_spawned = restored_entry.name == "assistant" + && restored_entry.manifest.description == "General-purpose assistant"; + if is_default_provider && is_default_model || is_auto_spawned { if !dm.provider.is_empty() { restored_entry.manifest.model.provider = dm.provider.clone(); } if !dm.model.is_empty() { restored_entry.manifest.model.model = dm.model.clone(); } - if !dm.api_key_env.is_empty() && restored_entry.manifest.model.api_key_env.is_none() { + if !dm.api_key_env.is_empty() { restored_entry.manifest.model.api_key_env = Some(dm.api_key_env.clone()); } - if dm.base_url.is_some() && restored_entry.manifest.model.base_url.is_none() { + if dm.base_url.is_some() { restored_entry.manifest.model.base_url.clone_from(&dm.base_url); } } diff --git a/crates/openfang-kernel/src/wizard.rs b/crates/openfang-kernel/src/wizard.rs index a99f21ef..ad6dafe8 100644 --- a/crates/openfang-kernel/src/wizard.rs +++ b/crates/openfang-kernel/src/wizard.rs @@ -54,10 +54,11 @@ impl SetupWizard { /// model configuration, capabilities, and schedule. pub fn build_plan(intent: AgentIntent) -> SetupPlan { // Map model tier to provider/model + // Use "default" so the kernel applies config.toml's [default_model]. + // Only "complex" tier gets an explicit Anthropic override. let (provider, model) = match intent.model_tier.as_str() { - "simple" => ("groq", "llama-3.3-70b-versatile"), "complex" => ("anthropic", "claude-sonnet-4-20250514"), - _ => ("groq", "llama-3.3-70b-versatile"), // medium default + _ => ("default", "default"), }; // Build capabilities from intent @@ -285,7 +286,7 @@ mod tests { let plan = SetupWizard::build_plan(intent); assert_eq!(plan.manifest.name, "research-bot"); - assert_eq!(plan.manifest.model.provider, "groq"); + assert_eq!(plan.manifest.model.provider, "default"); assert!(plan .manifest .capabilities diff --git a/crates/openfang-runtime/src/model_catalog.rs b/crates/openfang-runtime/src/model_catalog.rs index a2dc6b0a..3c4198e9 100644 --- a/crates/openfang-runtime/src/model_catalog.rs +++ b/crates/openfang-runtime/src/model_catalog.rs @@ -26,9 +26,17 @@ impl ModelCatalog { /// Create a new catalog populated with builtin models and providers. pub fn new() -> Self { let models = builtin_models(); - let aliases = builtin_aliases(); + let mut aliases = builtin_aliases(); let mut providers = builtin_providers(); + // Auto-register aliases defined on model entries + for model in &models { + for alias in &model.aliases { + let lower = alias.to_lowercase(); + aliases.entry(lower).or_insert_with(|| model.id.clone()); + } + } + // Set model counts on providers for provider in &mut providers { provider.model_count = models.iter().filter(|m| m.provider == provider.id).count(); @@ -2589,6 +2597,76 @@ fn builtin_models() -> Vec { supports_streaming: true, aliases: vec![], }, + ModelCatalogEntry { + id: "qwen3-235b-a22b".into(), + display_name: "Qwen3 235B".into(), + provider: "qwen".into(), + tier: ModelTier::Frontier, + context_window: 131_072, + max_output_tokens: 8_192, + input_cost_per_m: 4.00, + output_cost_per_m: 12.00, + supports_tools: true, + supports_vision: false, + supports_streaming: true, + aliases: vec!["qwen3".into()], + }, + ModelCatalogEntry { + id: "qwen3-30b-a3b".into(), + display_name: "Qwen3 30B".into(), + provider: "qwen".into(), + tier: ModelTier::Fast, + context_window: 131_072, + max_output_tokens: 8_192, + input_cost_per_m: 0.30, + output_cost_per_m: 0.60, + supports_tools: true, + supports_vision: false, + supports_streaming: true, + aliases: vec![], + }, + ModelCatalogEntry { + id: "qwen-coder-plus-latest".into(), + display_name: "Qwen Coder Plus (Latest)".into(), + provider: "qwen".into(), + tier: ModelTier::Smart, + context_window: 131_072, + max_output_tokens: 8_192, + input_cost_per_m: 0.80, + output_cost_per_m: 2.00, + supports_tools: true, + supports_vision: false, + supports_streaming: true, + aliases: vec!["qwen-coder".into()], + }, + ModelCatalogEntry { + id: "qwen2.5-coder-32b-instruct".into(), + display_name: "Qwen 2.5 Coder 32B".into(), + provider: "qwen".into(), + tier: ModelTier::Balanced, + context_window: 131_072, + max_output_tokens: 8_192, + input_cost_per_m: 0.80, + output_cost_per_m: 2.00, + supports_tools: true, + supports_vision: false, + supports_streaming: true, + aliases: vec![], + }, + ModelCatalogEntry { + id: "qwen-vl-max".into(), + display_name: "Qwen VL Max".into(), + provider: "qwen".into(), + tier: ModelTier::Frontier, + context_window: 32_768, + max_output_tokens: 8_192, + input_cost_per_m: 3.00, + output_cost_per_m: 9.00, + supports_tools: false, + supports_vision: true, + supports_streaming: true, + aliases: vec![], + }, // ══════════════════════════════════════════════════════════════ // MiniMax (4) // ══════════════════════════════════════════════════════════════