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
1 change: 1 addition & 0 deletions USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ export ANTHROPIC_AUTH_TOKEN="anthropic-oauth-or-proxy-bearer-token"
| `sk-ant-*` API key | `ANTHROPIC_API_KEY` | `x-api-key: sk-ant-...` | [console.anthropic.com](https://console.anthropic.com) |
| OAuth access token (opaque) | `ANTHROPIC_AUTH_TOKEN` | `Authorization: Bearer ...` | an Anthropic-compatible proxy or OAuth flow that mints bearer tokens |
| OpenRouter key (`sk-or-v1-*`) | `OPENAI_API_KEY` + `OPENAI_BASE_URL=https://openrouter.ai/api/v1` | `Authorization: Bearer ...` | [openrouter.ai/keys](https://openrouter.ai/keys) |
| Ollama local instance | `OLLAMA_HOST` | no auth header (Ollama requires none) | local Ollama server at `http://127.0.0.1:11434` |

**Why this matters:** if you paste an `sk-ant-*` key into `ANTHROPIC_AUTH_TOKEN`, Anthropic's API will return `401 Invalid bearer token` because `sk-ant-*` keys are rejected over the Bearer header. The fix is a one-line env var swap — move the key to `ANTHROPIC_API_KEY`. Recent `claw` builds detect this exact shape (401 + `sk-ant-*` in the Bearer slot) and append a hint to the error message pointing at the fix.

Expand Down
7 changes: 7 additions & 0 deletions docs/local-openai-compatible-providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ ollama serve

In another shell:

```bash
export OLLAMA_HOST="http://127.0.0.1:11434"
claw --model "qwen2.5-coder:7b" prompt "say hello"
```

`OLLAMA_HOST` is now the preferred env var for Ollama. The older `OPENAI_BASE_URL` + `OPENAI_API_KEY` workaround is also supported for existing setups that already rely on the OpenAI-compatible path.

```bash
export OPENAI_BASE_URL="http://127.0.0.1:11434/v1"
export OPENAI_API_KEY="local-dev-token"
Expand Down
27 changes: 17 additions & 10 deletions rust/crates/api/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,23 @@ impl ProviderClient {
OpenAiCompatConfig::xai(),
)?)),
ProviderKind::OpenAi => {
// DashScope models (qwen-*) also return ProviderKind::OpenAi because they
// speak the OpenAI wire format, but they need the DashScope config which
// reads DASHSCOPE_API_KEY and points at dashscope.aliyuncs.com.
let config = match providers::metadata_for_model(&resolved_model) {
Some(meta) if meta.auth_env == "DASHSCOPE_API_KEY" => {
OpenAiCompatConfig::dashscope()
}
_ => OpenAiCompatConfig::openai(),
};
Ok(Self::OpenAi(OpenAiCompatClient::from_env(config)?))
if std::env::var_os("OLLAMA_HOST").is_some() {
// unwrap is safe: from_ollama_env always returns Some
Ok(Self::OpenAi(
openai_compat::OpenAiCompatClient::from_ollama_env().unwrap(),
))
} else {
// DashScope models (qwen-*) also return ProviderKind::OpenAi because they
// speak the OpenAI wire format, but they need the DashScope config which
// reads DASHSCOPE_API_KEY and points at dashscope.aliyuncs.com.
let config = match providers::metadata_for_model(&resolved_model) {
Some(meta) if meta.auth_env == "DASHSCOPE_API_KEY" => {
OpenAiCompatConfig::dashscope()
}
_ => OpenAiCompatConfig::openai(),
};
Ok(Self::OpenAi(OpenAiCompatClient::from_env(config)?))
}
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions rust/crates/api/src/providers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,10 @@ pub fn provider_diagnostics_for_model(model: &str) -> ProviderDiagnostics {

#[must_use]
pub fn detect_provider_kind(model: &str) -> ProviderKind {
if std::env::var_os("OLLAMA_HOST").is_some() {
return ProviderKind::OpenAi;
}

if let Some(metadata) = metadata_for_model(model) {
return metadata.provider;
}
Expand Down
23 changes: 23 additions & 0 deletions rust/crates/api/src/providers/openai_compat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ const XAI_MAX_REQUEST_BODY_BYTES: usize = 52_428_800; // 50MB
const OPENAI_MAX_REQUEST_BODY_BYTES: usize = 104_857_600; // 100MB
const DASHSCOPE_MAX_REQUEST_BODY_BYTES: usize = 6_291_456; // 6MB (observed limit in dogfood)

pub const OLLAMA_CONFIG: OpenAiCompatConfig = OpenAiCompatConfig {
provider_name: "Ollama",
api_key_env: "OLLAMA_HOST",
base_url_env: "OLLAMA_HOST",
default_base_url: "http://127.0.0.1:11434/v1",
max_request_body_bytes: 104_857_600,
};

impl OpenAiCompatConfig {
#[must_use]
pub const fn xai() -> Self {
Expand Down Expand Up @@ -140,6 +148,21 @@ impl OpenAiCompatClient {
Ok(Self::new(api_key, config))
}

pub fn from_ollama_env() -> Option<Self> {
let host =
std::env::var("OLLAMA_HOST").unwrap_or_else(|_| "http://127.0.0.1:11434".to_string());
let base_url = format!("{}/v1", host.trim_end_matches('/'));
Some(Self {
http: build_http_client_or_default(),
api_key: "ollama".to_string(),
config: OLLAMA_CONFIG,
base_url,
max_retries: DEFAULT_MAX_RETRIES,
initial_backoff: DEFAULT_INITIAL_BACKOFF,
max_backoff: DEFAULT_MAX_BACKOFF,
})
}

#[must_use]
pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
self.base_url = base_url.into();
Expand Down
17 changes: 17 additions & 0 deletions rust/crates/rusty-claude-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1991,6 +1991,12 @@ fn resolve_model_alias_with_config(model: &str) -> String {
/// Rejects: empty, whitespace-only, strings with spaces, or invalid chars.
fn validate_model_syntax(model: &str) -> Result<(), String> {
let trimmed = model.trim();
if std::env::var_os("OLLAMA_HOST").is_some() {
if trimmed.is_empty() {
return Err("invalid model syntax: model string cannot be empty.\nUsage: --model <provider/model> e.g. --model anthropic/claude-opus-4-7".to_string());
}
return Ok(());
}
if trimmed.is_empty() {
return Err("invalid model syntax: model string cannot be empty.\nUsage: --model <provider/model> e.g. --model anthropic/claude-opus-4-7".to_string());
}
Expand Down Expand Up @@ -16659,4 +16665,15 @@ mod alias_resolution_tests {
assert_eq!(resolve_model_alias_with_config(model), model);
assert!(validate_model_syntax(model).is_ok());
}

#[test]
fn test_ollama_host_bypasses_provider_model_validation() {
// Safety: test sets and clears env var within the test.
// May be flaky if tests run in parallel with conflicting OLLAMA_HOST usage,
// but acceptable for a unit-level smoke test.
std::env::set_var("OLLAMA_HOST", "http://127.0.0.1:11434");
let result = validate_model_syntax("qwen2.5-coder:7b");
std::env::remove_var("OLLAMA_HOST");
assert!(result.is_ok());
}
}