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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ archive/
.claw/sessions/
.clawhip/
status-help.txt
.DS_Store
141 changes: 141 additions & 0 deletions USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,147 @@ The OpenAI-compatible backend also serves as the gateway for **OpenRouter**, **O

**Model-name prefix routing:** If a model name starts with `openai/`, `gpt-`, `qwen/`, or `qwen-`, the provider is selected by the prefix regardless of which env vars are set. This prevents accidental misrouting to Anthropic when multiple credentials exist in the environment.

### Configured compatible providers

If you use several compatible providers, define named provider profiles in `settings.json` instead of changing `OPENAI_BASE_URL` before every run. Each profile gets its own protocol, base URL, credential env var, and model allow-list.

Run `claw login` or `/login` to create these profiles interactively for Z.AI, MiniMax, OpenAI, Kimi, Moonshot, or a custom compatible endpoint. The wizard can store a pasted local token in `~/.claw/settings.json`, but `apiKeyEnv` is preferred when you can keep the secret in your shell environment:

```json
{
"model": "zai/glm-5.1",
"modelProviders": {
"zai": {
"type": "openai-compatible",
"baseUrl": "https://api.z.ai/api/paas/v4",
"apiKeyEnv": "Z_AI_API_KEY",
"models": [
"glm-5.1",
"glm-5",
"glm-5-turbo",
"glm-4.7",
"glm-4.7-flashx",
"glm-4.7-flash",
"glm-4.6",
"glm-4.5",
"glm-4.5-x",
"glm-4.5-air",
"glm-4.5-airx",
"glm-4.5-flash",
"glm-4-32b-0414-128k"
],
"defaultModel": "glm-5.1"
},
"zai-coding-plan": {
"type": "openai-compatible",
"baseUrl": "https://api.z.ai/api/coding/paas/v4",
"apiKeyEnv": "Z_AI_API_KEY",
"models": [
"glm-4.5-air",
"glm-4.7",
"glm-5-turbo",
"glm-5.1",
"glm-5v-turbo"
],
"defaultModel": "glm-5.1"
},
"minimax-coding-plan": {
"type": "anthropic-compatible",
"baseUrl": "https://api.minimax.io/anthropic/v1",
"apiKeyEnv": "MINIMAX_API_KEY",
"models": [
"MiniMax-M2",
"MiniMax-M2.1",
"MiniMax-M2.5",
"MiniMax-M2.5-highspeed",
"MiniMax-M2.7",
"MiniMax-M2.7-highspeed"
],
"defaultModel": "MiniMax-M2.7-highspeed"
},
"openai": {
"type": "openai-compatible",
"baseUrl": "https://api.openai.com/v1",
"apiKeyEnv": "OPENAI_API_KEY",
"models": [
"gpt-5-codex",
"gpt-5.1-codex",
"gpt-5.1-codex-max",
"gpt-5.1-codex-mini",
"gpt-5.2",
"gpt-5.2-codex",
"gpt-5.3-codex",
"gpt-5.3-codex-spark",
"gpt-5.4",
"gpt-5.4-fast",
"gpt-5.4-mini",
"gpt-5.4-mini-fast",
"gpt-5.5",
"gpt-5.5-fast",
"gpt-5.5-pro"
],
"defaultModel": "gpt-5.5"
},
"kimi-for-coding": {
"type": "anthropic-compatible",
"baseUrl": "https://api.kimi.com/coding/v1",
"apiKeyEnv": "KIMI_API_KEY",
"models": [
"k2p5",
"k2p6",
"kimi-k2-thinking"
],
"defaultModel": "k2p6"
},
"moonshot": {
"type": "openai-compatible",
"baseUrl": "https://api.moonshot.ai/v1",
"apiKeyEnv": "MOONSHOT_API_KEY",
"models": [
"kimi-k2.6",
"kimi-k2.5",
"kimi-k2-0905-preview",
"kimi-k2-0711-preview",
"kimi-k2-turbo-preview",
"kimi-k2-thinking",
"kimi-k2-thinking-turbo",
"moonshot-v1-8k",
"moonshot-v1-32k",
"moonshot-v1-128k",
"moonshot-v1-8k-vision-preview",
"moonshot-v1-32k-vision-preview",
"moonshot-v1-128k-vision-preview"
],
"defaultModel": "kimi-k2.6"
}
}
}
```

Use `/model provider/model` in the REPL to switch without restarting:

```text
/model zai/glm-5.1
/model zai-coding-plan/glm-5.1
/model minimax-coding-plan/MiniMax-M2.7-highspeed
/model openai/gpt-5.5
/model kimi-for-coding/k2p6
/model moonshot/kimi-k2.6
```

You can also use the provider name alone when it has `defaultModel` configured:

```text
/model zai
/model zai-coding-plan
/model minimax-coding-plan
/model openai
/model kimi-for-coding
/model moonshot
```

Prefer `apiKeyEnv` so secrets stay out of source-controlled project settings. `apiKey` is supported for local-only files when an environment variable is not practical.

### Tested models and aliases

These are the models registered in the built-in alias table with known token limits:
Expand Down
3 changes: 3 additions & 0 deletions rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

120 changes: 110 additions & 10 deletions rust/crates/api/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use crate::error::ApiError;
use crate::prompt_cache::{PromptCache, PromptCacheRecord, PromptCacheStats};
use crate::providers::anthropic::{self, AnthropicClient, AuthSource};
use crate::providers::openai_compat::{self, OpenAiCompatClient, OpenAiCompatConfig};
use crate::providers::wham::{self, WhamClient};
use crate::providers::{self, ProviderKind};
use crate::types::{MessageRequest, MessageResponse, StreamEvent};

Expand All @@ -11,6 +12,8 @@ pub enum ProviderClient {
Anthropic(AnthropicClient),
Xai(OpenAiCompatClient),
OpenAi(OpenAiCompatClient),
/// OpenAI WHAM backend (chatgpt.com/backend-api/wham) using ChatGPT OAuth tokens.
Wham(WhamClient),
}

impl ProviderClient {
Expand All @@ -32,26 +35,115 @@ 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) {
// Use metadata_for_model for prefix-aware config selection.
// DashScope models (qwen-*, kimi-*) and Moonshot models (moonshot/*)
// all speak the OpenAI wire format but need different configs.
let (config, oauth_provider_id) = match providers::metadata_for_model(&resolved_model) {
Some(meta) if meta.auth_env == "DASHSCOPE_API_KEY" => {
OpenAiCompatConfig::dashscope()
(OpenAiCompatConfig::dashscope(), None)
}
_ => OpenAiCompatConfig::openai(),
Some(meta) if meta.auth_env == "MOONSHOT_API_KEY" => {
(OpenAiCompatConfig::moonshot(), Some("moonshot"))
}
_ => (OpenAiCompatConfig::openai(), Some("openai")),
};
Ok(Self::OpenAi(OpenAiCompatClient::from_env(config)?))
// Try OAuth if the provider supports it and env var is not set
if let Some(provider_id) = oauth_provider_id {
if provider_id == "openai" {
// OpenAI OAuth tokens are WHAM-backend tokens (chatgpt.com/backend-api/wham),
// NOT Platform API tokens. Route to WhamClient when using OAuth.
if let Ok(Some(token_set)) = runtime::load_provider_oauth(provider_id) {
let account_id = token_set
.id_token
.as_deref()
.and_then(runtime::extract_chatgpt_account_id)
.or_else(|| {
runtime::extract_chatgpt_account_id(&token_set.access_token)
});
return Ok(Self::Wham(WhamClient::from_oauth_token_set(
token_set,
account_id,
"https://auth.openai.com/oauth/token",
"app_EMoamEEZ73f0CkXaXp7hrann",
)));
}
}
if provider_id == "moonshot" {
Ok(Self::OpenAi(
OpenAiCompatClient::from_env_or_oauth_with_refresh(
config,
provider_id,
"https://auth.kimi.com/api/oauth/token",
"17e5f671-d194-4dfb-9706-5516cb48c098",
)?,
))
} else {
Ok(Self::OpenAi(OpenAiCompatClient::from_env_or_oauth(
config, provider_id,
)?))
}
} else {
Ok(Self::OpenAi(OpenAiCompatClient::from_env(config)?))
}
}
}
}

#[must_use]
pub fn from_openai_compatible_profile(
api_key: impl Into<String>,
base_url: impl Into<String>,
) -> Self {
Self::OpenAi(
OpenAiCompatClient::new(api_key, OpenAiCompatConfig::openai()).with_base_url(base_url),
)
}

/// Create an OpenAI-compatible client from an OAuth token set with automatic refresh.
/// Used for custom providers that authenticate via OAuth rather than API keys.
#[must_use]
pub fn from_openai_compatible_oauth(
base_url: impl Into<String>,
token_set: runtime::OAuthTokenSet,
token_url: impl Into<String>,
client_id: impl Into<String>,
) -> Self {
Self::OpenAi(
OpenAiCompatClient::from_oauth_token_set(
token_set,
OpenAiCompatConfig::openai(),
token_url,
client_id,
"custom",
)
.with_base_url(base_url),
)
}

#[must_use]
pub fn from_anthropic_compatible_profile(
api_key: impl Into<String>,
base_url: impl Into<String>,
) -> Self {
Self::Anthropic(AnthropicClient::new(api_key).with_base_url(base_url))
}

/// Set a custom User-Agent header on the underlying OpenAI-compatible client.
/// No-op for Anthropic, xAI, or WHAM variants.
#[must_use]
pub fn with_user_agent(self, user_agent: impl Into<String>) -> Self {
match self {
Self::OpenAi(client) => Self::OpenAi(client.with_user_agent(user_agent)),
other => other,
}
}

#[must_use]
pub const fn provider_kind(&self) -> ProviderKind {
match self {
Self::Anthropic(_) => ProviderKind::Anthropic,
Self::Xai(_) => ProviderKind::Xai,
Self::OpenAi(_) => ProviderKind::OpenAi,
Self::OpenAi(_) | Self::Wham(_) => ProviderKind::OpenAi,
}
}

Expand All @@ -67,15 +159,15 @@ impl ProviderClient {
pub fn prompt_cache_stats(&self) -> Option<PromptCacheStats> {
match self {
Self::Anthropic(client) => client.prompt_cache_stats(),
Self::Xai(_) | Self::OpenAi(_) => None,
Self::Xai(_) | Self::OpenAi(_) | Self::Wham(_) => None,
}
}

#[must_use]
pub fn take_last_prompt_cache_record(&self) -> Option<PromptCacheRecord> {
match self {
Self::Anthropic(client) => client.take_last_prompt_cache_record(),
Self::Xai(_) | Self::OpenAi(_) => None,
Self::Xai(_) | Self::OpenAi(_) | Self::Wham(_) => None,
}
}

Expand All @@ -86,6 +178,7 @@ impl ProviderClient {
match self {
Self::Anthropic(client) => client.send_message(request).await,
Self::Xai(client) | Self::OpenAi(client) => client.send_message(request).await,
Self::Wham(client) => client.send_message(request).await,
}
}

Expand All @@ -102,6 +195,10 @@ impl ProviderClient {
.stream_message(request)
.await
.map(MessageStream::OpenAiCompat),
Self::Wham(client) => client
.stream_message(request)
.await
.map(MessageStream::Wham),
}
}
}
Expand All @@ -110,6 +207,7 @@ impl ProviderClient {
pub enum MessageStream {
Anthropic(anthropic::MessageStream),
OpenAiCompat(openai_compat::MessageStream),
Wham(wham::WhamMessageStream),
}

impl MessageStream {
Expand All @@ -118,13 +216,15 @@ impl MessageStream {
match self {
Self::Anthropic(stream) => stream.request_id(),
Self::OpenAiCompat(stream) => stream.request_id(),
Self::Wham(stream) => stream.request_id(),
}
}

pub async fn next_event(&mut self) -> Result<Option<StreamEvent>, ApiError> {
match self {
Self::Anthropic(stream) => stream.next_event().await,
Self::OpenAiCompat(stream) => stream.next_event().await,
Self::Wham(stream) => stream.next_event().await,
}
}
}
Expand Down
7 changes: 4 additions & 3 deletions rust/crates/api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@ pub use prompt_cache::{
CacheBreakEvent, PromptCache, PromptCacheConfig, PromptCachePaths, PromptCacheRecord,
PromptCacheStats,
};
pub use providers::anthropic::{AnthropicClient, AnthropicClient as ApiClient, AuthSource};
pub use providers::anthropic::{has_auth_from_env_or_saved as anthropic_has_auth, AnthropicClient, AnthropicClient as ApiClient, AuthSource};
pub use providers::openai_compat::{
build_chat_completion_request, flatten_tool_result_content, is_reasoning_model,
build_chat_completion_request, flatten_tool_result_content, has_api_key, is_reasoning_model,
model_rejects_is_error_field, translate_message, OpenAiCompatClient, OpenAiCompatConfig,
};
pub use providers::wham::{WhamClient, DEFAULT_WHAM_BASE_URL};
pub use providers::{
detect_provider_kind, max_tokens_for_model, max_tokens_for_model_with_override,
resolve_model_alias, ProviderKind,
metadata_for_model, resolve_model_alias, ProviderKind, ProviderMetadata,
};
pub use sse::{parse_frame, SseParser};
pub use types::{
Expand Down
Loading