From cadbf73d803b95055d3e692a363c2cc17f4d64ff Mon Sep 17 00:00:00 2001 From: Hikari Date: Fri, 13 Mar 2026 00:33:19 -0700 Subject: [PATCH] feat: expose modelOverrides setting in ConfigSidebar --- src-tauri/src/config.rs | 20 +++ src-tauri/src/wsl_bridge.rs | 140 ++++++++++++++++++-- src/lib/commands/slashCommands.test.ts | 1 + src/lib/commands/slashCommands.ts | 2 + src/lib/components/ConfigSidebar.svelte | 38 ++++++ src/lib/components/InputBar.svelte | 1 + src/lib/components/PermissionModal.svelte | 1 + src/lib/components/StatusBar.svelte | 3 + src/lib/components/TaskLoopPanel.svelte | 1 + src/lib/components/UserQuestionModal.svelte | 1 + src/lib/stores/config.test.ts | 3 + src/lib/stores/config.ts | 3 + vitest.setup.ts | 1 + 13 files changed, 203 insertions(+), 12 deletions(-) diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index e4268a3..ab26da7 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize, Default)] @@ -46,6 +48,9 @@ pub struct ClaudeStartOptions { #[serde(default)] pub auto_memory_directory: Option, + + #[serde(default)] + pub model_overrides: Option>, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -192,6 +197,9 @@ pub struct HikariConfig { #[serde(default)] pub auto_memory_directory: Option, + + #[serde(default)] + pub model_overrides: Option>, } impl Default for HikariConfig { @@ -242,6 +250,7 @@ impl Default for HikariConfig { include_git_instructions: true, enable_claudeai_mcp_servers: true, auto_memory_directory: None, + model_overrides: None, } } } @@ -392,6 +401,7 @@ mod tests { assert!(config.include_git_instructions); assert!(config.enable_claudeai_mcp_servers); assert!(config.auto_memory_directory.is_none()); + assert!(config.model_overrides.is_none()); } #[test] @@ -442,6 +452,10 @@ mod tests { include_git_instructions: false, enable_claudeai_mcp_servers: false, auto_memory_directory: Some("/custom/memory".to_string()), + model_overrides: Some(HashMap::from([( + "claude-opus-4-6".to_string(), + "arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-opus-4-6-v1".to_string(), + )])), }; let json = serde_json::to_string(&config).unwrap(); @@ -466,6 +480,12 @@ mod tests { deserialized.auto_memory_directory, Some("/custom/memory".to_string()) ); + assert!(deserialized.model_overrides.is_some()); + let overrides = deserialized.model_overrides.unwrap(); + assert_eq!( + overrides.get("claude-opus-4-6").map(String::as_str), + Some("arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-opus-4-6-v1") + ); } #[test] diff --git a/src-tauri/src/wsl_bridge.rs b/src-tauri/src/wsl_bridge.rs index 0ea63bc..452d5b3 100644 --- a/src-tauri/src/wsl_bridge.rs +++ b/src-tauri/src/wsl_bridge.rs @@ -291,10 +291,39 @@ impl WslBridge { cmd.arg("--worktree"); } - // Pass auto-memory directory via settings if specified - if let Some(ref dir) = options.auto_memory_directory { - if !dir.is_empty() { - cmd.args(["--settings", &format!(r#"{{"autoMemoryDirectory":"{}"}}"#, dir)]); + // Pass combined settings via --settings flag if any settings are specified + { + let has_memory_dir = options + .auto_memory_directory + .as_deref() + .map(|d| !d.is_empty()) + .unwrap_or(false); + let has_overrides = options + .model_overrides + .as_ref() + .map(|m| !m.is_empty()) + .unwrap_or(false); + + if has_memory_dir || has_overrides { + let mut settings = serde_json::Map::new(); + if let Some(ref dir) = options.auto_memory_directory { + if !dir.is_empty() { + settings.insert( + "autoMemoryDirectory".to_string(), + serde_json::Value::String(dir.clone()), + ); + } + } + if let Some(ref overrides) = options.model_overrides { + if !overrides.is_empty() { + if let Ok(val) = serde_json::to_value(overrides) { + settings.insert("modelOverrides".to_string(), val); + } + } + } + if let Ok(settings_json) = serde_json::to_string(&settings) { + cmd.args(["--settings", &settings_json]); + } } } @@ -441,14 +470,40 @@ impl WslBridge { claude_cmd.push_str(" --worktree"); } - // Pass auto-memory directory via settings if specified - if let Some(ref dir) = options.auto_memory_directory { - if !dir.is_empty() { - let escaped_dir = dir.replace('\'', "'\\''"); - claude_cmd.push_str(&format!( - " --settings '{{\"autoMemoryDirectory\":\"{}\"}}'", - escaped_dir - )); + // Pass combined settings via --settings flag if any settings are specified + { + let has_memory_dir = options + .auto_memory_directory + .as_deref() + .map(|d| !d.is_empty()) + .unwrap_or(false); + let has_overrides = options + .model_overrides + .as_ref() + .map(|m| !m.is_empty()) + .unwrap_or(false); + + if has_memory_dir || has_overrides { + let mut settings = serde_json::Map::new(); + if let Some(ref dir) = options.auto_memory_directory { + if !dir.is_empty() { + settings.insert( + "autoMemoryDirectory".to_string(), + serde_json::Value::String(dir.clone()), + ); + } + } + if let Some(ref overrides) = options.model_overrides { + if !overrides.is_empty() { + if let Ok(val) = serde_json::to_value(overrides) { + settings.insert("modelOverrides".to_string(), val); + } + } + } + if let Ok(settings_json) = serde_json::to_string(&settings) { + let escaped = settings_json.replace('\'', "'\\''"); + claude_cmd.push_str(&format!(" --settings '{}'", escaped)); + } } } @@ -3075,4 +3130,65 @@ mod tests { let dir = ""; assert!(dir.is_empty(), "Empty directory should be skipped"); } + + /// Build the combined settings JSON for both memory directory and model overrides (for testing) + #[cfg(test)] + fn build_combined_settings_arg( + memory_dir: Option<&str>, + model_overrides: Option<&std::collections::HashMap>, + ) -> String { + let mut settings = serde_json::Map::new(); + if let Some(dir) = memory_dir { + if !dir.is_empty() { + settings.insert( + "autoMemoryDirectory".to_string(), + serde_json::Value::String(dir.to_string()), + ); + } + } + if let Some(overrides) = model_overrides { + if !overrides.is_empty() { + if let Ok(val) = serde_json::to_value(overrides) { + settings.insert("modelOverrides".to_string(), val); + } + } + } + serde_json::to_string(&settings).unwrap_or_default() + } + + #[test] + fn test_e2e_combined_settings_memory_only() { + let result = build_combined_settings_arg(Some("/custom/dir"), None); + assert_eq!(result, r#"{"autoMemoryDirectory":"/custom/dir"}"#); + } + + #[test] + fn test_e2e_combined_settings_overrides_only() { + let mut overrides = std::collections::HashMap::new(); + overrides.insert( + "claude-opus-4-6".to_string(), + "arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-opus-4-6-v1".to_string(), + ); + let result = build_combined_settings_arg(None, Some(&overrides)); + assert!(result.contains("modelOverrides")); + assert!(result.contains("claude-opus-4-6")); + assert!(result.contains("arn:aws:bedrock")); + } + + #[test] + fn test_e2e_combined_settings_both_fields() { + let mut overrides = std::collections::HashMap::new(); + overrides.insert("claude-opus-4-6".to_string(), "custom-model-id".to_string()); + let result = build_combined_settings_arg(Some("/mem/dir"), Some(&overrides)); + assert!(result.contains("autoMemoryDirectory")); + assert!(result.contains("modelOverrides")); + assert!(result.contains("/mem/dir")); + assert!(result.contains("custom-model-id")); + } + + #[test] + fn test_e2e_combined_settings_empty_produces_empty_object() { + let result = build_combined_settings_arg(Some(""), None); + assert_eq!(result, "{}"); + } } diff --git a/src/lib/commands/slashCommands.test.ts b/src/lib/commands/slashCommands.test.ts index 4b48783..a333df2 100644 --- a/src/lib/commands/slashCommands.test.ts +++ b/src/lib/commands/slashCommands.test.ts @@ -68,6 +68,7 @@ vi.mock("$lib/stores/config", () => ({ include_git_instructions: true, enable_claudeai_mcp_servers: true, auto_memory_directory: null, + model_overrides: null, }), }, })); diff --git a/src/lib/commands/slashCommands.ts b/src/lib/commands/slashCommands.ts index c4fe697..79145b3 100644 --- a/src/lib/commands/slashCommands.ts +++ b/src/lib/commands/slashCommands.ts @@ -69,6 +69,7 @@ async function changeDirectory(path: string): Promise { include_git_instructions: config.include_git_instructions ?? true, enable_claudeai_mcp_servers: config.enable_claudeai_mcp_servers ?? true, auto_memory_directory: config.auto_memory_directory || null, + model_overrides: config.model_overrides || null, }, }); @@ -149,6 +150,7 @@ async function startNewConversation(): Promise { include_git_instructions: config.include_git_instructions ?? true, enable_claudeai_mcp_servers: config.enable_claudeai_mcp_servers ?? true, auto_memory_directory: config.auto_memory_directory || null, + model_overrides: config.model_overrides || null, }, }); diff --git a/src/lib/components/ConfigSidebar.svelte b/src/lib/components/ConfigSidebar.svelte index c07528d..e7b17d2 100644 --- a/src/lib/components/ConfigSidebar.svelte +++ b/src/lib/components/ConfigSidebar.svelte @@ -62,6 +62,7 @@ include_git_instructions: true, enable_claudeai_mcp_servers: true, auto_memory_directory: null, + model_overrides: null, max_output_tokens: null, trusted_workspaces: [], background_image_path: null, @@ -82,6 +83,8 @@ let customUiFontPathInput = $state(""); let customUiFontFamilyInput = $state(""); let customUiFontStatus: string | null = $state(null); + let modelOverridesJson = $state(""); + let modelOverridesError: string | null = $state(null); interface AuthStatus { is_logged_in: boolean; @@ -111,6 +114,7 @@ customFontFamilyInput = c.custom_font_family ?? ""; customUiFontPathInput = c.custom_ui_font_path ?? ""; customUiFontFamilyInput = c.custom_ui_font_family ?? ""; + modelOverridesJson = c.model_overrides ? JSON.stringify(c.model_overrides, null, 2) : ""; }); configStore.isSidebarOpen.subscribe((open) => { @@ -196,6 +200,18 @@ async function handleSave() { isSaving = true; saveError = null; + modelOverridesError = null; + try { + if (modelOverridesJson.trim()) { + config.model_overrides = JSON.parse(modelOverridesJson) as Record; + } else { + config.model_overrides = null; + } + } catch { + modelOverridesError = "Invalid JSON — please check your model overrides."; + isSaving = false; + return; + } try { await configStore.saveConfig(config); configStore.closeSidebar(); @@ -622,6 +638,28 @@ default (working directory).

+ + +
+ + + {#if modelOverridesError} +

{modelOverridesError}

+ {/if} +

+ JSON map of model names to provider-specific IDs (for AWS Bedrock, Google Vertex, etc.). + Passed via --settings modelOverrides. Leave blank to use + defaults. +

+
diff --git a/src/lib/components/InputBar.svelte b/src/lib/components/InputBar.svelte index 221a189..fbb799f 100644 --- a/src/lib/components/InputBar.svelte +++ b/src/lib/components/InputBar.svelte @@ -405,6 +405,7 @@ User: ${formattedMessage}`; include_git_instructions: config.include_git_instructions ?? true, enable_claudeai_mcp_servers: config.enable_claudeai_mcp_servers ?? true, auto_memory_directory: config.auto_memory_directory || null, + model_overrides: config.model_overrides || null, }, }); diff --git a/src/lib/components/PermissionModal.svelte b/src/lib/components/PermissionModal.svelte index ce240d0..f4035f3 100644 --- a/src/lib/components/PermissionModal.svelte +++ b/src/lib/components/PermissionModal.svelte @@ -92,6 +92,7 @@ include_git_instructions: config.include_git_instructions ?? true, enable_claudeai_mcp_servers: config.enable_claudeai_mcp_servers ?? true, auto_memory_directory: config.auto_memory_directory || null, + model_overrides: config.model_overrides || null, }, }); diff --git a/src/lib/components/StatusBar.svelte b/src/lib/components/StatusBar.svelte index 9c0d8fb..9b69a63 100644 --- a/src/lib/components/StatusBar.svelte +++ b/src/lib/components/StatusBar.svelte @@ -92,6 +92,7 @@ include_git_instructions: true, enable_claudeai_mcp_servers: true, auto_memory_directory: null, + model_overrides: null, }); let streamerModeActive = $state(false); @@ -175,6 +176,7 @@ include_git_instructions: currentConfig.include_git_instructions ?? true, enable_claudeai_mcp_servers: currentConfig.enable_claudeai_mcp_servers ?? true, auto_memory_directory: currentConfig.auto_memory_directory || null, + model_overrides: currentConfig.model_overrides || null, }, }); @@ -335,6 +337,7 @@ include_git_instructions: currentConfig.include_git_instructions ?? true, enable_claudeai_mcp_servers: currentConfig.enable_claudeai_mcp_servers ?? true, auto_memory_directory: currentConfig.auto_memory_directory || null, + model_overrides: currentConfig.model_overrides || null, }, }); diff --git a/src/lib/components/TaskLoopPanel.svelte b/src/lib/components/TaskLoopPanel.svelte index c5ab836..cacd72a 100644 --- a/src/lib/components/TaskLoopPanel.svelte +++ b/src/lib/components/TaskLoopPanel.svelte @@ -221,6 +221,7 @@ include_git_instructions: cfg.include_git_instructions ?? true, enable_claudeai_mcp_servers: cfg.enable_claudeai_mcp_servers ?? true, auto_memory_directory: cfg.auto_memory_directory || null, + model_overrides: cfg.model_overrides || null, }, }); } catch (error) { diff --git a/src/lib/components/UserQuestionModal.svelte b/src/lib/components/UserQuestionModal.svelte index 96dc7f1..bcaf500 100644 --- a/src/lib/components/UserQuestionModal.svelte +++ b/src/lib/components/UserQuestionModal.svelte @@ -111,6 +111,7 @@ include_git_instructions: config.include_git_instructions ?? true, enable_claudeai_mcp_servers: config.enable_claudeai_mcp_servers ?? true, auto_memory_directory: config.auto_memory_directory || null, + model_overrides: config.model_overrides || null, }, }); diff --git a/src/lib/stores/config.test.ts b/src/lib/stores/config.test.ts index 3cf9ccb..018c240 100644 --- a/src/lib/stores/config.test.ts +++ b/src/lib/stores/config.test.ts @@ -224,6 +224,7 @@ describe("config store", () => { include_git_instructions: true, enable_claudeai_mcp_servers: true, auto_memory_directory: null, + model_overrides: null, }; expect(config.model).toBe("claude-sonnet-4"); @@ -287,6 +288,7 @@ describe("config store", () => { include_git_instructions: true, enable_claudeai_mcp_servers: true, auto_memory_directory: null, + model_overrides: null, }; expect(config.model).toBeNull(); @@ -905,6 +907,7 @@ describe("config store", () => { include_git_instructions: true, enable_claudeai_mcp_servers: true, auto_memory_directory: null, + model_overrides: null, }; const mockInvokeImpl = vi.mocked(invoke); diff --git a/src/lib/stores/config.ts b/src/lib/stores/config.ts index 6d2b102..6977cdd 100644 --- a/src/lib/stores/config.ts +++ b/src/lib/stores/config.ts @@ -89,6 +89,8 @@ export interface HikariConfig { enable_claudeai_mcp_servers: boolean; // Auto-memory directory auto_memory_directory: string | null; + // Model overrides for provider-specific model IDs (AWS Bedrock, Google Vertex, etc.) + model_overrides: Record | null; } const defaultConfig: HikariConfig = { @@ -146,6 +148,7 @@ const defaultConfig: HikariConfig = { include_git_instructions: true, enable_claudeai_mcp_servers: true, auto_memory_directory: null, + model_overrides: null, }; function createConfigStore() { diff --git a/vitest.setup.ts b/vitest.setup.ts index 204b9ac..7e80f5a 100644 --- a/vitest.setup.ts +++ b/vitest.setup.ts @@ -50,6 +50,7 @@ vi.mock("@tauri-apps/api/core", () => ({ profile_bio: null, custom_theme_colors: {}, auto_memory_directory: null, + model_overrides: null, }); case "list_quick_actions": return Promise.resolve([]);