diff --git a/crates/goose/src/providers/ollama.rs b/crates/goose/src/providers/ollama.rs index e29901d6..d397cf50 100644 --- a/crates/goose/src/providers/ollama.rs +++ b/crates/goose/src/providers/ollama.rs @@ -122,6 +122,10 @@ fn apply_ollama_options(payload: &mut Value, model_config: &ModelConfig) { } } +fn ollama_host_configured(config: &crate::config::Config) -> bool { + config.get_param::("OLLAMA_HOST").is_ok() +} + impl OllamaProvider { pub async fn from_env(model: ModelConfig) -> Result { let config = crate::config::Config::global(); @@ -262,6 +266,10 @@ impl ProviderDef for OllamaProvider { true } + fn inventory_configured() -> bool { + ollama_host_configured(crate::config::Config::global()) + } + fn inventory_identity() -> Result { let config = crate::config::Config::global(); Ok( @@ -465,6 +473,51 @@ fn stream_ollama(response: Response, mut log: RequestLog) -> Result)]); + let config_file = tempfile::NamedTempFile::new().unwrap(); + let secrets_file = tempfile::NamedTempFile::new().unwrap(); + let config = crate::config::Config::new_with_config_paths( + vec![config_file.path().to_path_buf()], + secrets_file.path(), + ) + .unwrap(); + + assert!(!ollama_host_configured(&config)); + } + + #[test] + fn test_ollama_host_env_marks_inventory_configured() { + let _guard = env_lock::lock_env([("OLLAMA_HOST", Some("http://127.0.0.1:11435"))]); + let config_file = tempfile::NamedTempFile::new().unwrap(); + let secrets_file = tempfile::NamedTempFile::new().unwrap(); + let config = crate::config::Config::new_with_config_paths( + vec![config_file.path().to_path_buf()], + secrets_file.path(), + ) + .unwrap(); + + assert!(ollama_host_configured(&config)); + } + + #[test] + fn test_ollama_host_config_marks_inventory_configured() { + let _guard = env_lock::lock_env([("OLLAMA_HOST", None::<&str>)]); + let config_file = tempfile::NamedTempFile::new().unwrap(); + let secrets_file = tempfile::NamedTempFile::new().unwrap(); + let config = crate::config::Config::new_with_config_paths( + vec![config_file.path().to_path_buf()], + secrets_file.path(), + ) + .unwrap(); + config + .set_param("OLLAMA_HOST", "http://127.0.0.1:11435") + .unwrap(); + + assert!(ollama_host_configured(&config)); + } + #[test] fn test_apply_ollama_options_uses_input_limit() { let _guard = env_lock::lock_env([("GOOSE_INPUT_LIMIT", Some("8192"))]); diff --git a/ui/goose2/src/features/providers/providerCatalog.test.ts b/ui/goose2/src/features/providers/providerCatalog.test.ts index 7a1f0ddf..f150ffdb 100644 --- a/ui/goose2/src/features/providers/providerCatalog.test.ts +++ b/ui/goose2/src/features/providers/providerCatalog.test.ts @@ -1,5 +1,26 @@ import { describe, expect, it } from "vitest"; -import { resolveAgentProviderCatalogId } from "./providerCatalog"; +import { + getCatalogEntry, + resolveAgentProviderCatalogId, +} from "./providerCatalog"; + +describe("provider catalog", () => { + it("exposes Ollama host configuration", () => { + const ollama = getCatalogEntry("ollama"); + + expect(ollama?.setupMethod).toBe("config_fields"); + expect(ollama?.fields).toEqual([ + { + key: "OLLAMA_HOST", + label: "Host", + secret: false, + required: true, + placeholder: "localhost or http://localhost:11434", + defaultValue: "http://localhost:11434", + }, + ]); + }); +}); describe("resolveAgentProviderCatalogId", () => { it("matches direct catalog ids", () => { diff --git a/ui/goose2/src/features/providers/providerCatalog.ts b/ui/goose2/src/features/providers/providerCatalog.ts index ba802fe9..eb2bc1db 100644 --- a/ui/goose2/src/features/providers/providerCatalog.ts +++ b/ui/goose2/src/features/providers/providerCatalog.ts @@ -6,7 +6,6 @@ import { } from "./providerCatalogAliases"; export const PROVIDER_CATALOG: ProviderCatalogEntry[] = [ - // ── Agent providers ────────────────────────────────────────────── { id: "goose", displayName: "Goose", @@ -92,7 +91,6 @@ export const PROVIDER_CATALOG: ProviderCatalogEntry[] = [ showOnlyWhenInstalled: true, }, - // ── Model providers (power Goose) ──────────────────────────────── { id: "anthropic", displayName: "Anthropic", @@ -164,8 +162,18 @@ export const PROVIDER_CATALOG: ProviderCatalogEntry[] = [ id: "ollama", displayName: "Ollama", category: "model", - description: "Run models locally", - setupMethod: "local", + description: "Run local or self-hosted models", + setupMethod: "config_fields", + fields: [ + { + key: "OLLAMA_HOST", + label: "Host", + secret: false, + required: true, + placeholder: "localhost or http://localhost:11434", + defaultValue: "http://localhost:11434", + }, + ], docsUrl: "https://ollama.com", tier: "promoted", }, diff --git a/ui/goose2/src/features/settings/ui/__tests__/ModelProviderRow.test.tsx b/ui/goose2/src/features/settings/ui/__tests__/ModelProviderRow.test.tsx index 2809174d..6b526c5a 100644 --- a/ui/goose2/src/features/settings/ui/__tests__/ModelProviderRow.test.tsx +++ b/ui/goose2/src/features/settings/ui/__tests__/ModelProviderRow.test.tsx @@ -73,6 +73,37 @@ describe("ModelProviderRow", () => { ]); }); + it("pre-fills and saves provider field defaults", async () => { + const user = userEvent.setup(); + + render( + , + ); + + await user.click(screen.getByRole("button", { name: /ollama/i })); + + expect( + await screen.findByDisplayValue("http://localhost:11434"), + ).toBeVisible(); + + await user.click(screen.getByRole("button", { name: /^save$/i })); + + await waitFor(() => expect(onSaveFields).toHaveBeenCalledTimes(1)); + expect(onSaveFields).toHaveBeenCalledWith([ + { + key: "OLLAMA_HOST", + value: "http://localhost:11434", + isSecret: false, + }, + ]); + }); + it("shows the connected row while model inventory is still loading", async () => { const user = userEvent.setup(); diff --git a/ui/goose2/src/features/settings/ui/modelProviderHelpers.tsx b/ui/goose2/src/features/settings/ui/modelProviderHelpers.tsx index d8cbe424..8c0c509c 100644 --- a/ui/goose2/src/features/settings/ui/modelProviderHelpers.tsx +++ b/ui/goose2/src/features/settings/ui/modelProviderHelpers.tsx @@ -49,7 +49,7 @@ export function createDraftValues( if (field.secret) { return [field.key, ""]; } - return [field.key, currentValue?.value ?? ""]; + return [field.key, currentValue?.value ?? field.defaultValue ?? ""]; }), ); } diff --git a/ui/goose2/src/shared/types/providers.ts b/ui/goose2/src/shared/types/providers.ts index 1c11da26..30e935df 100644 --- a/ui/goose2/src/shared/types/providers.ts +++ b/ui/goose2/src/shared/types/providers.ts @@ -19,6 +19,7 @@ export interface ProviderField { secret: boolean; required: boolean; placeholder?: string; + defaultValue?: string; } export interface ProviderFieldValue {