品牌重塑: - Claude Code → Claw Code - .claude → .claw 配置目录 - CLAUDE_* → CLAW_* 环境变量 新增功能: - 多 Provider 架构 (ClawApi/Xai/OpenAI) - 插件系统 (生命周期/钩子/工具扩展) - LSP 集成 (诊断/代码智能) - Hook 系统 (PreToolUse/PostToolUse) - 独立 CLI (claw-cli) - HTTP Server (Axum/SSE) - Slash Commands 扩展 (branch/worktree/commit/pr/plugin等) 优化改进: - Compaction 支持增量压缩 - 全局工具注册表 - 配置文件统一为 .claw.json
240 lines
6.8 KiB
Rust
240 lines
6.8 KiB
Rust
use std::future::Future;
|
|
use std::pin::Pin;
|
|
|
|
use crate::error::ApiError;
|
|
use crate::types::{MessageRequest, MessageResponse};
|
|
|
|
pub mod claw_provider;
|
|
pub mod openai_compat;
|
|
|
|
pub type ProviderFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T, ApiError>> + Send + 'a>>;
|
|
|
|
pub trait Provider {
|
|
type Stream;
|
|
|
|
fn send_message<'a>(
|
|
&'a self,
|
|
request: &'a MessageRequest,
|
|
) -> ProviderFuture<'a, MessageResponse>;
|
|
|
|
fn stream_message<'a>(
|
|
&'a self,
|
|
request: &'a MessageRequest,
|
|
) -> ProviderFuture<'a, Self::Stream>;
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum ProviderKind {
|
|
ClawApi,
|
|
Xai,
|
|
OpenAi,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct ProviderMetadata {
|
|
pub provider: ProviderKind,
|
|
pub auth_env: &'static str,
|
|
pub base_url_env: &'static str,
|
|
pub default_base_url: &'static str,
|
|
}
|
|
|
|
const MODEL_REGISTRY: &[(&str, ProviderMetadata)] = &[
|
|
(
|
|
"opus",
|
|
ProviderMetadata {
|
|
provider: ProviderKind::ClawApi,
|
|
auth_env: "ANTHROPIC_API_KEY",
|
|
base_url_env: "ANTHROPIC_BASE_URL",
|
|
default_base_url: claw_provider::DEFAULT_BASE_URL,
|
|
},
|
|
),
|
|
(
|
|
"sonnet",
|
|
ProviderMetadata {
|
|
provider: ProviderKind::ClawApi,
|
|
auth_env: "ANTHROPIC_API_KEY",
|
|
base_url_env: "ANTHROPIC_BASE_URL",
|
|
default_base_url: claw_provider::DEFAULT_BASE_URL,
|
|
},
|
|
),
|
|
(
|
|
"haiku",
|
|
ProviderMetadata {
|
|
provider: ProviderKind::ClawApi,
|
|
auth_env: "ANTHROPIC_API_KEY",
|
|
base_url_env: "ANTHROPIC_BASE_URL",
|
|
default_base_url: claw_provider::DEFAULT_BASE_URL,
|
|
},
|
|
),
|
|
(
|
|
"claude-opus-4-6",
|
|
ProviderMetadata {
|
|
provider: ProviderKind::ClawApi,
|
|
auth_env: "ANTHROPIC_API_KEY",
|
|
base_url_env: "ANTHROPIC_BASE_URL",
|
|
default_base_url: claw_provider::DEFAULT_BASE_URL,
|
|
},
|
|
),
|
|
(
|
|
"claude-sonnet-4-6",
|
|
ProviderMetadata {
|
|
provider: ProviderKind::ClawApi,
|
|
auth_env: "ANTHROPIC_API_KEY",
|
|
base_url_env: "ANTHROPIC_BASE_URL",
|
|
default_base_url: claw_provider::DEFAULT_BASE_URL,
|
|
},
|
|
),
|
|
(
|
|
"claude-haiku-4-5-20251213",
|
|
ProviderMetadata {
|
|
provider: ProviderKind::ClawApi,
|
|
auth_env: "ANTHROPIC_API_KEY",
|
|
base_url_env: "ANTHROPIC_BASE_URL",
|
|
default_base_url: claw_provider::DEFAULT_BASE_URL,
|
|
},
|
|
),
|
|
(
|
|
"grok",
|
|
ProviderMetadata {
|
|
provider: ProviderKind::Xai,
|
|
auth_env: "XAI_API_KEY",
|
|
base_url_env: "XAI_BASE_URL",
|
|
default_base_url: openai_compat::DEFAULT_XAI_BASE_URL,
|
|
},
|
|
),
|
|
(
|
|
"grok-3",
|
|
ProviderMetadata {
|
|
provider: ProviderKind::Xai,
|
|
auth_env: "XAI_API_KEY",
|
|
base_url_env: "XAI_BASE_URL",
|
|
default_base_url: openai_compat::DEFAULT_XAI_BASE_URL,
|
|
},
|
|
),
|
|
(
|
|
"grok-mini",
|
|
ProviderMetadata {
|
|
provider: ProviderKind::Xai,
|
|
auth_env: "XAI_API_KEY",
|
|
base_url_env: "XAI_BASE_URL",
|
|
default_base_url: openai_compat::DEFAULT_XAI_BASE_URL,
|
|
},
|
|
),
|
|
(
|
|
"grok-3-mini",
|
|
ProviderMetadata {
|
|
provider: ProviderKind::Xai,
|
|
auth_env: "XAI_API_KEY",
|
|
base_url_env: "XAI_BASE_URL",
|
|
default_base_url: openai_compat::DEFAULT_XAI_BASE_URL,
|
|
},
|
|
),
|
|
(
|
|
"grok-2",
|
|
ProviderMetadata {
|
|
provider: ProviderKind::Xai,
|
|
auth_env: "XAI_API_KEY",
|
|
base_url_env: "XAI_BASE_URL",
|
|
default_base_url: openai_compat::DEFAULT_XAI_BASE_URL,
|
|
},
|
|
),
|
|
];
|
|
|
|
#[must_use]
|
|
pub fn resolve_model_alias(model: &str) -> String {
|
|
let trimmed = model.trim();
|
|
let lower = trimmed.to_ascii_lowercase();
|
|
MODEL_REGISTRY
|
|
.iter()
|
|
.find_map(|(alias, metadata)| {
|
|
(*alias == lower).then_some(match metadata.provider {
|
|
ProviderKind::ClawApi => match *alias {
|
|
"opus" => "claude-opus-4-6",
|
|
"sonnet" => "claude-sonnet-4-6",
|
|
"haiku" => "claude-haiku-4-5-20251213",
|
|
_ => trimmed,
|
|
},
|
|
ProviderKind::Xai => match *alias {
|
|
"grok" | "grok-3" => "grok-3",
|
|
"grok-mini" | "grok-3-mini" => "grok-3-mini",
|
|
"grok-2" => "grok-2",
|
|
_ => trimmed,
|
|
},
|
|
ProviderKind::OpenAi => trimmed,
|
|
})
|
|
})
|
|
.map_or_else(|| trimmed.to_string(), ToOwned::to_owned)
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn metadata_for_model(model: &str) -> Option<ProviderMetadata> {
|
|
let canonical = resolve_model_alias(model);
|
|
let lower = canonical.to_ascii_lowercase();
|
|
if let Some((_, metadata)) = MODEL_REGISTRY.iter().find(|(alias, _)| *alias == lower) {
|
|
return Some(*metadata);
|
|
}
|
|
if lower.starts_with("grok") {
|
|
return Some(ProviderMetadata {
|
|
provider: ProviderKind::Xai,
|
|
auth_env: "XAI_API_KEY",
|
|
base_url_env: "XAI_BASE_URL",
|
|
default_base_url: openai_compat::DEFAULT_XAI_BASE_URL,
|
|
});
|
|
}
|
|
None
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn detect_provider_kind(model: &str) -> ProviderKind {
|
|
if let Some(metadata) = metadata_for_model(model) {
|
|
return metadata.provider;
|
|
}
|
|
if claw_provider::has_auth_from_env_or_saved().unwrap_or(false) {
|
|
return ProviderKind::ClawApi;
|
|
}
|
|
if openai_compat::has_api_key("OPENAI_API_KEY") {
|
|
return ProviderKind::OpenAi;
|
|
}
|
|
if openai_compat::has_api_key("XAI_API_KEY") {
|
|
return ProviderKind::Xai;
|
|
}
|
|
ProviderKind::ClawApi
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn max_tokens_for_model(model: &str) -> u32 {
|
|
let canonical = resolve_model_alias(model);
|
|
if canonical.contains("opus") {
|
|
32_000
|
|
} else {
|
|
64_000
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{detect_provider_kind, max_tokens_for_model, resolve_model_alias, ProviderKind};
|
|
|
|
#[test]
|
|
fn resolves_grok_aliases() {
|
|
assert_eq!(resolve_model_alias("grok"), "grok-3");
|
|
assert_eq!(resolve_model_alias("grok-mini"), "grok-3-mini");
|
|
assert_eq!(resolve_model_alias("grok-2"), "grok-2");
|
|
}
|
|
|
|
#[test]
|
|
fn detects_provider_from_model_name_first() {
|
|
assert_eq!(detect_provider_kind("grok"), ProviderKind::Xai);
|
|
assert_eq!(
|
|
detect_provider_kind("claude-sonnet-4-6"),
|
|
ProviderKind::ClawApi
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn keeps_existing_max_token_heuristic() {
|
|
assert_eq!(max_tokens_for_model("opus"), 32_000);
|
|
assert_eq!(max_tokens_for_model("grok-3"), 64_000);
|
|
}
|
|
}
|