Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -527,3 +527,5 @@ their sign-off. When in doubt, check with your legal team.

*Thank you for contributing to Buzz. Every bug report, documentation fix,
and code contribution makes the project better for everyone. 🐝*

Ollama Cloud provider setup: [docs/ollama-cloud.md](docs/ollama-cloud.md).
7 changes: 7 additions & 0 deletions crates/buzz-agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ OPENAI_COMPAT_MODEL=gpt-5 \
OPENAI_COMPAT_BASE_URL=https://api.openai.com/v1 \
./target/release/buzz-agent

# Or Ollama Cloud (OpenAI-compatible hosted models)
BUZZ_AGENT_PROVIDER=ollama-cloud \
OPENAI_COMPAT_API_KEY=... \
OPENAI_COMPAT_MODEL=kimi-k2.5 \
./target/release/buzz-agent


# Or OpenRouter
BUZZ_AGENT_PROVIDER=openrouter \
OPENROUTER_API_KEY=sk-or-v1-... \
Expand Down
81 changes: 79 additions & 2 deletions crates/buzz-agent/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,11 @@ pub const HANDOFF_MAX_TOOL_NAMES: usize = 20;
const DEFAULT_SYSTEM_PROMPT: &str =
"You are buzz-agent. Use the provided tools to act. Tool calls are your only output.";

/// Default OpenAI-compatible endpoint for Ollama Cloud when
/// `BUZZ_AGENT_PROVIDER=ollama-cloud` and `OPENAI_COMPAT_BASE_URL` is unset.
/// See https://docs.ollama.com/cloud
pub const OLLAMA_CLOUD_DEFAULT_BASE_URL: &str = "https://ollama.com/v1";

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Provider {
Anthropic,
Expand Down Expand Up @@ -801,7 +806,10 @@ impl Config {
env("OPENAI_COMPAT_MODEL").as_deref(),
)
.ok_or_else(|| "config: OPENAI_COMPAT_MODEL required".to_string())?,
env_or("OPENAI_COMPAT_BASE_URL", "https://api.openai.com/v1"),
resolve_openai_base_url_for_provider(
env("BUZZ_AGENT_PROVIDER").as_deref(),
env("OPENAI_COMPAT_BASE_URL"),
),
parse_openai_api(env("OPENAI_COMPAT_API").as_deref())?,
),
Provider::Databricks | Provider::DatabricksV2 => (
Expand Down Expand Up @@ -1024,6 +1032,28 @@ fn present_nonempty(v: Option<&str>) -> bool {
v.map(str::trim).is_some_and(|s| !s.is_empty())
}

fn is_ollama_cloud_provider(requested: Option<&str>) -> bool {
requested
.map(str::trim)
.is_some_and(|s| {
let n = s.to_ascii_lowercase();
n == "ollama-cloud" || n == "ollama_cloud"
})
}

fn resolve_openai_base_url_for_provider(
requested_provider: Option<&str>,
base_url: Option<String>,
) -> String {
base_url.unwrap_or_else(|| {
if is_ollama_cloud_provider(requested_provider) {
OLLAMA_CLOUD_DEFAULT_BASE_URL.to_owned()
} else {
"https://api.openai.com/v1".to_owned()
}
})
}

fn resolve_provider(
requested: Option<&str>,
anthropic_key: Option<&str>,
Expand All @@ -1042,6 +1072,13 @@ fn resolve_provider(
"openai" | "openai-compat" => Err(
"config: OPENAI_COMPAT_API_KEY required".into(),
),
// Ollama Cloud is OpenAI-compatible; requires a real API key.
"ollama-cloud" | "ollama_cloud" if present_nonempty(openai_key) => {
Ok(Provider::OpenAi)
}
"ollama-cloud" | "ollama_cloud" => Err(
"config: OPENAI_COMPAT_API_KEY required for ollama-cloud".into(),
),
"databricks" => Ok(Provider::Databricks),
"databricks_v2" | "databricks-v2" => Ok(Provider::DatabricksV2),
"openrouter" if present_nonempty(openrouter_key) => Ok(Provider::OpenRouter),
Expand All @@ -1052,7 +1089,7 @@ fn resolve_provider(
}
}
None => Err(
"config: BUZZ_AGENT_PROVIDER is required — set it to your provider (e.g. anthropic, openai, databricks)".into(),
"config: BUZZ_AGENT_PROVIDER is required — set it to your provider (e.g. anthropic, openai, ollama-cloud, databricks)".into(),
),
}
}
Expand Down Expand Up @@ -1311,6 +1348,46 @@ mod tests {
assert!(err.contains("BUZZ_AGENT_PROVIDER=OpenAIish"));
}

#[test]
fn resolve_provider_ollama_cloud_requires_api_key() {
let err = resolve_provider(Some("ollama-cloud"), None, None, None).unwrap_err();
assert!(
err.contains("OPENAI_COMPAT_API_KEY"),
"{err}"
);
}

#[test]
fn resolve_provider_ollama_cloud_maps_to_openai() {
assert_eq!(
resolve_provider(Some("ollama-cloud"), None, Some("sk-test"), None).unwrap(),
Provider::OpenAi
);
assert_eq!(
resolve_provider(Some("ollama_cloud"), None, Some("sk-test"), None).unwrap(),
Provider::OpenAi
);
}

#[test]
fn ollama_cloud_default_base_url() {
assert_eq!(
resolve_openai_base_url_for_provider(Some("ollama-cloud"), None),
OLLAMA_CLOUD_DEFAULT_BASE_URL
);
assert_eq!(
resolve_openai_base_url_for_provider(Some("openai"), None),
"https://api.openai.com/v1"
);
assert_eq!(
resolve_openai_base_url_for_provider(
Some("ollama-cloud"),
Some("https://custom.example/v1".into())
),
"https://custom.example/v1"
);
}

#[test]
fn is_openai_host_matrix() {
// Lookalike-safe: `api.openai.com.evil.example` and malformed URLs
Expand Down
8 changes: 5 additions & 3 deletions desktop/src-tauri/src/managed_agents/readiness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -480,7 +480,9 @@ fn buzz_agent_requirements(effective: &EffectiveAgentEnv) -> Vec<Requirement> {
Some("DATABRICKS_MODEL")
}
Some("anthropic") => Some("ANTHROPIC_MODEL"),
Some("openai") | Some("openai-compat") => Some("OPENAI_COMPAT_MODEL"),
Some("openai") | Some("openai-compat") | Some("ollama-cloud") | Some("ollama_cloud") => {
Some("OPENAI_COMPAT_MODEL")
}
Some("openrouter") => Some("OPENROUTER_MODEL"),
_ => None,
};
Expand Down Expand Up @@ -510,7 +512,7 @@ fn buzz_agent_requirements(effective: &EffectiveAgentEnv) -> Vec<Requirement> {
key: "ANTHROPIC_API_KEY".to_string(),
});
}
Some("openai")
Some("openai") | Some("openai-compat") | Some("ollama-cloud") | Some("ollama_cloud")
if env_key_missing("OPENAI_COMPAT_API_KEY") => {
missing.push(Requirement::EnvKey {
key: "OPENAI_COMPAT_API_KEY".to_string(),
Expand Down Expand Up @@ -622,7 +624,7 @@ fn goose_requirements(
key: "ANTHROPIC_API_KEY".to_string(),
});
}
Some("openai")
Some("openai") | Some("openai-compat") | Some("ollama-cloud") | Some("ollama_cloud")
if env_key_missing("OPENAI_COMPAT_API_KEY")
&& !file_key_present("OPENAI_COMPAT_API_KEY") =>
{
Expand Down
9 changes: 8 additions & 1 deletion desktop/src/features/agents/ui/agentConfigOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const KNOWN_LLM_PROVIDER_IDS = [
"openai",
"openai-compat",
"openrouter",
"ollama-cloud",
] as const;

type PersonaLlmProviderId = (typeof KNOWN_LLM_PROVIDER_IDS)[number];
Expand Down Expand Up @@ -114,6 +115,10 @@ const PROVIDER_CREDENTIAL_CONFIG: Partial<
requiredEnvKeys: ["OPENROUTER_API_KEY"],
secretEnvVar: "OPENROUTER_API_KEY",
},
"ollama-cloud": {
requiredEnvKeys: ["OPENAI_COMPAT_API_KEY"],
secretEnvVar: "OPENAI_COMPAT_API_KEY",
},
};

const DEFAULT_MODEL_OPTION: PersonaModelOption = {
Expand All @@ -126,6 +131,7 @@ export const PERSONA_LLM_PROVIDER_OPTIONS: readonly PersonaModelOption[] = [
{ id: "openai", label: "OpenAI" },
{ id: "openai-compat", label: "OpenAI-compatible" },
{ id: "openrouter", label: "OpenRouter" },
{ id: "ollama-cloud", label: "Ollama Cloud" },
{ id: "relay-mesh", label: "Buzz shared compute" },
{ id: "databricks", label: "Databricks" },
{ id: "databricks_v2", label: "Databricks v2" },
Expand Down Expand Up @@ -286,7 +292,8 @@ export function providerRequiresExplicitModel(
trimmedProvider === "anthropic" ||
trimmedProvider === "openai" ||
trimmedProvider === "openai-compat" ||
trimmedProvider === "openrouter"
trimmedProvider === "openrouter" ||
trimmedProvider === "ollama-cloud"
);
}

Expand Down
1 change: 1 addition & 0 deletions desktop/src/features/agents/ui/bakedEnvHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ function providerModelEnvKey(provider: string): string | null {
return "ANTHROPIC_MODEL";
case "openai":
case "openai-compat":
case "ollama-cloud":
return "OPENAI_COMPAT_MODEL";
default:
return null;
Expand Down
30 changes: 30 additions & 0 deletions docs/ollama-cloud.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Ollama Cloud in Buzz

[Ollama Cloud](https://docs.ollama.com/cloud) exposes an OpenAI-compatible API.
Buzz treats it as a **named provider** so you do not have to pick
“OpenAI-compatible” and type the base URL by hand ([#3874](https://github.com/block/buzz/issues/3874)).

## Desktop

1. Agent defaults / persona → provider **Ollama Cloud**
2. Paste your Ollama API key into the credential field (`OPENAI_COMPAT_API_KEY`)
3. Set a model id from your Ollama Cloud account (e.g. a hosted open model)

Buzz maps this to `BUZZ_AGENT_PROVIDER=ollama-cloud` and defaults
`OPENAI_COMPAT_BASE_URL` to `https://ollama.com/v1`.

## CLI / buzz-agent

```bash
BUZZ_AGENT_PROVIDER=ollama-cloud \
OPENAI_COMPAT_API_KEY=... \
OPENAI_COMPAT_MODEL=... \
buzz-agent
```

Override the endpoint with `OPENAI_COMPAT_BASE_URL` if needed.

## Related

- Local Ollama (localhost): see open PR / docs for the `ollama` alias (#3145 / #3152)
- Generic OpenAI-compatible: provider `openai-compat` with any base URL