Skip to content

Commit 000d7c5

Browse files
authored
feat: make ollama host configurable in goose2 (#8912)
1 parent 5fcdf31 commit 000d7c5

6 files changed

Lines changed: 120 additions & 6 deletions

File tree

crates/goose/src/providers/ollama.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,10 @@ fn apply_ollama_options(payload: &mut Value, model_config: &ModelConfig) {
122122
}
123123
}
124124

125+
fn ollama_host_configured(config: &crate::config::Config) -> bool {
126+
config.get_param::<String>("OLLAMA_HOST").is_ok()
127+
}
128+
125129
impl OllamaProvider {
126130
pub async fn from_env(model: ModelConfig) -> Result<Self> {
127131
let config = crate::config::Config::global();
@@ -262,6 +266,10 @@ impl ProviderDef for OllamaProvider {
262266
true
263267
}
264268

269+
fn inventory_configured() -> bool {
270+
ollama_host_configured(crate::config::Config::global())
271+
}
272+
265273
fn inventory_identity() -> Result<InventoryIdentityInput> {
266274
let config = crate::config::Config::global();
267275
Ok(
@@ -465,6 +473,51 @@ fn stream_ollama(response: Response, mut log: RequestLog) -> Result<MessageStrea
465473
mod tests {
466474
use super::*;
467475

476+
#[test]
477+
fn test_ollama_host_default_does_not_mark_inventory_configured() {
478+
let _guard = env_lock::lock_env([("OLLAMA_HOST", None::<&str>)]);
479+
let config_file = tempfile::NamedTempFile::new().unwrap();
480+
let secrets_file = tempfile::NamedTempFile::new().unwrap();
481+
let config = crate::config::Config::new_with_config_paths(
482+
vec![config_file.path().to_path_buf()],
483+
secrets_file.path(),
484+
)
485+
.unwrap();
486+
487+
assert!(!ollama_host_configured(&config));
488+
}
489+
490+
#[test]
491+
fn test_ollama_host_env_marks_inventory_configured() {
492+
let _guard = env_lock::lock_env([("OLLAMA_HOST", Some("http://127.0.0.1:11435"))]);
493+
let config_file = tempfile::NamedTempFile::new().unwrap();
494+
let secrets_file = tempfile::NamedTempFile::new().unwrap();
495+
let config = crate::config::Config::new_with_config_paths(
496+
vec![config_file.path().to_path_buf()],
497+
secrets_file.path(),
498+
)
499+
.unwrap();
500+
501+
assert!(ollama_host_configured(&config));
502+
}
503+
504+
#[test]
505+
fn test_ollama_host_config_marks_inventory_configured() {
506+
let _guard = env_lock::lock_env([("OLLAMA_HOST", None::<&str>)]);
507+
let config_file = tempfile::NamedTempFile::new().unwrap();
508+
let secrets_file = tempfile::NamedTempFile::new().unwrap();
509+
let config = crate::config::Config::new_with_config_paths(
510+
vec![config_file.path().to_path_buf()],
511+
secrets_file.path(),
512+
)
513+
.unwrap();
514+
config
515+
.set_param("OLLAMA_HOST", "http://127.0.0.1:11435")
516+
.unwrap();
517+
518+
assert!(ollama_host_configured(&config));
519+
}
520+
468521
#[test]
469522
fn test_apply_ollama_options_uses_input_limit() {
470523
let _guard = env_lock::lock_env([("GOOSE_INPUT_LIMIT", Some("8192"))]);

ui/goose2/src/features/providers/providerCatalog.test.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,26 @@
11
import { describe, expect, it } from "vitest";
2-
import { resolveAgentProviderCatalogId } from "./providerCatalog";
2+
import {
3+
getCatalogEntry,
4+
resolveAgentProviderCatalogId,
5+
} from "./providerCatalog";
6+
7+
describe("provider catalog", () => {
8+
it("exposes Ollama host configuration", () => {
9+
const ollama = getCatalogEntry("ollama");
10+
11+
expect(ollama?.setupMethod).toBe("config_fields");
12+
expect(ollama?.fields).toEqual([
13+
{
14+
key: "OLLAMA_HOST",
15+
label: "Host",
16+
secret: false,
17+
required: true,
18+
placeholder: "localhost or http://localhost:11434",
19+
defaultValue: "http://localhost:11434",
20+
},
21+
]);
22+
});
23+
});
324

425
describe("resolveAgentProviderCatalogId", () => {
526
it("matches direct catalog ids", () => {

ui/goose2/src/features/providers/providerCatalog.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import {
66
} from "./providerCatalogAliases";
77

88
export const PROVIDER_CATALOG: ProviderCatalogEntry[] = [
9-
// ── Agent providers ──────────────────────────────────────────────
109
{
1110
id: "goose",
1211
displayName: "Goose",
@@ -92,7 +91,6 @@ export const PROVIDER_CATALOG: ProviderCatalogEntry[] = [
9291
showOnlyWhenInstalled: true,
9392
},
9493

95-
// ── Model providers (power Goose) ────────────────────────────────
9694
{
9795
id: "anthropic",
9896
displayName: "Anthropic",
@@ -164,8 +162,18 @@ export const PROVIDER_CATALOG: ProviderCatalogEntry[] = [
164162
id: "ollama",
165163
displayName: "Ollama",
166164
category: "model",
167-
description: "Run models locally",
168-
setupMethod: "local",
165+
description: "Run local or self-hosted models",
166+
setupMethod: "config_fields",
167+
fields: [
168+
{
169+
key: "OLLAMA_HOST",
170+
label: "Host",
171+
secret: false,
172+
required: true,
173+
placeholder: "localhost or http://localhost:11434",
174+
defaultValue: "http://localhost:11434",
175+
},
176+
],
169177
docsUrl: "https://ollama.com",
170178
tier: "promoted",
171179
},

ui/goose2/src/features/settings/ui/__tests__/ModelProviderRow.test.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,37 @@ describe("ModelProviderRow", () => {
7373
]);
7474
});
7575

76+
it("pre-fills and saves provider field defaults", async () => {
77+
const user = userEvent.setup();
78+
79+
render(
80+
<ModelProviderRow
81+
provider={modelProvider("ollama", "not_configured")}
82+
onGetConfig={onGetConfig}
83+
onSaveFields={onSaveFields}
84+
onRemoveConfig={onRemoveConfig}
85+
onCompleteNativeSetup={onCompleteNativeSetup}
86+
/>,
87+
);
88+
89+
await user.click(screen.getByRole("button", { name: /ollama/i }));
90+
91+
expect(
92+
await screen.findByDisplayValue("http://localhost:11434"),
93+
).toBeVisible();
94+
95+
await user.click(screen.getByRole("button", { name: /^save$/i }));
96+
97+
await waitFor(() => expect(onSaveFields).toHaveBeenCalledTimes(1));
98+
expect(onSaveFields).toHaveBeenCalledWith([
99+
{
100+
key: "OLLAMA_HOST",
101+
value: "http://localhost:11434",
102+
isSecret: false,
103+
},
104+
]);
105+
});
106+
76107
it("shows the connected row while model inventory is still loading", async () => {
77108
const user = userEvent.setup();
78109

ui/goose2/src/features/settings/ui/modelProviderHelpers.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export function createDraftValues(
4949
if (field.secret) {
5050
return [field.key, ""];
5151
}
52-
return [field.key, currentValue?.value ?? ""];
52+
return [field.key, currentValue?.value ?? field.defaultValue ?? ""];
5353
}),
5454
);
5555
}

ui/goose2/src/shared/types/providers.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export interface ProviderField {
1919
secret: boolean;
2020
required: boolean;
2121
placeholder?: string;
22+
defaultValue?: string;
2223
}
2324

2425
export interface ProviderFieldValue {

0 commit comments

Comments
 (0)