feat: add anthropic-compatible provider type

Add a new provider type "anthropic-compatible" to support Anthropic API
variants that use array format for the system field instead of string.

Changes:
- Add AnthropicCompatible to ApiProviderType enum (backend)
- Add AnthropicCompatible to Agent ProviderType enum
- Modify AnthropicProtocol to support array system format
- Add provider type selector in ProviderConfigForm for custom providers
- Update icon mappings to show Claude icon for anthropic-compatible
- Update model registry mappings to show Anthropic models

This allows users to connect to Anthropic-compatible proxies (like One API)
that expect the system field in array format:
  {"system": [{"type": "text", "text": "..."}]}

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
coso
2026-01-12 20:06:08 +08:00
co-authored by Claude Opus 4.5
parent 2b32211e85
commit 86e2a0db33
18 changed files with 184 additions and 25 deletions
+44 -5
View File
@@ -41,9 +41,40 @@ struct AnthropicTool {
}
/// Anthropic 协议处理器
pub struct AnthropicProtocol;
pub struct AnthropicProtocol {
/// 是否使用数组格式的 system 字段
/// 标准 Anthropic: system: "prompt"
/// 兼容格式: system: [{"type": "text", "text": "prompt"}]
use_array_system_format: bool,
}
impl AnthropicProtocol {
/// 创建标准 Anthropic 协议处理器
pub fn new() -> Self {
Self {
use_array_system_format: false,
}
}
/// 创建兼容格式的 Anthropic 协议处理器
pub fn with_array_system_format() -> Self {
Self {
use_array_system_format: true,
}
}
/// 构建 system 字段
fn build_system_field(&self, prompt: &str) -> serde_json::Value {
if self.use_array_system_format {
serde_json::json!([{
"type": "text",
"text": prompt
}])
} else {
serde_json::json!(prompt)
}
}
/// 将 OpenAI Tool 转换为 Anthropic Tool
fn convert_tools(tools: Option<&[Tool]>) -> Option<Vec<AnthropicTool>> {
tools.map(|t| {
@@ -170,6 +201,7 @@ impl AnthropicProtocol {
/// 构建消息列表
fn build_messages(
&self,
history: &[AgentMessage],
user_message: &str,
images: Option<&[ImageData]>,
@@ -178,7 +210,10 @@ impl AnthropicProtocol {
let mut messages = Vec::new();
// 系统提示词(Anthropic 使用单独的 system 字段)
let system_prompt = config.system_prompt.as_ref().map(|s| serde_json::json!(s));
let system_prompt = config
.system_prompt
.as_ref()
.map(|s| self.build_system_field(s));
// 添加历史消息(跳过 system 消息)
for msg in history {
@@ -220,13 +255,17 @@ impl AnthropicProtocol {
/// 从历史构建消息(不添加新用户消息)
fn build_messages_from_history(
&self,
history: &[AgentMessage],
config: &AgentConfig,
) -> (Vec<AnthropicMessage>, Option<serde_json::Value>) {
let mut messages = Vec::new();
// 系统提示词
let system_prompt = config.system_prompt.as_ref().map(|s| serde_json::json!(s));
let system_prompt = config
.system_prompt
.as_ref()
.map(|s| self.build_system_field(s));
// 添加所有历史消息(跳过 system)
for msg in history {
@@ -386,7 +425,7 @@ impl Protocol for AnthropicProtocol {
);
let (anthropic_messages, system) =
Self::build_messages(messages, user_message, images, config);
self.build_messages(messages, user_message, images, config);
let anthropic_tools = Self::convert_tools(tools);
@@ -454,7 +493,7 @@ impl Protocol for AnthropicProtocol {
provider_id
);
let (anthropic_messages, system) = Self::build_messages_from_history(messages, config);
let (anthropic_messages, system) = self.build_messages_from_history(messages, config);
let anthropic_tools = Self::convert_tools(tools);
+6 -2
View File
@@ -62,9 +62,13 @@ pub trait Protocol: Send + Sync {
/// 根据 ProviderType 创建协议处理器
pub fn create_protocol(provider_type: ProviderType) -> Box<dyn Protocol> {
match provider_type {
// Claude 和 Kiro 都使用 Anthropic SSE 协议
// Claude 和 Kiro 使用标准 Anthropic SSE 协议
ProviderType::Claude | ProviderType::ClaudeOauth | ProviderType::Kiro => {
Box::new(AnthropicProtocol)
Box::new(AnthropicProtocol::new())
}
// Anthropic 兼容格式(system 为数组格式)
ProviderType::AnthropicCompatible => {
Box::new(AnthropicProtocol::with_array_system_format())
}
// 其他使用 OpenAI 兼容协议
_ => Box::new(OpenAIProtocol),
+13 -2
View File
@@ -15,6 +15,8 @@ pub enum ProviderType {
Claude,
/// Claude OAuth (Anthropic 协议)
ClaudeOauth,
/// Anthropic 兼容格式(支持 system 数组格式等变体)
AnthropicCompatible,
/// Kiro/CodeWhisperer (AWS Event Stream 协议)
Kiro,
/// Gemini (Gemini 协议)
@@ -38,6 +40,7 @@ impl ProviderType {
match s.to_lowercase().as_str() {
"claude" | "anthropic" => Self::Claude,
"claude_oauth" => Self::ClaudeOauth,
"anthropic_compatible" | "anthropic-compatible" => Self::AnthropicCompatible,
"kiro" => Self::Kiro,
"gemini" | "gemini_api_key" => Self::Gemini,
"openai" => Self::OpenAI,
@@ -52,7 +55,7 @@ impl ProviderType {
/// 获取 API 端点路径
pub fn endpoint(&self) -> &'static str {
match self {
Self::Claude | Self::ClaudeOauth => "/v1/messages",
Self::Claude | Self::ClaudeOauth | Self::AnthropicCompatible => "/v1/messages",
Self::Kiro => "/v1/chat/completions", // Kiro 使用 OpenAI 兼容格式,但后端会转换
Self::Gemini => "/v1/gemini/chat/completions",
_ => "/v1/chat/completions",
@@ -61,7 +64,15 @@ impl ProviderType {
/// 是否使用 Anthropic 协议
pub fn is_anthropic(&self) -> bool {
matches!(self, Self::Claude | Self::ClaudeOauth)
matches!(
self,
Self::Claude | Self::ClaudeOauth | Self::AnthropicCompatible
)
}
/// 是否使用 Anthropic 兼容格式(system 为数组格式)
pub fn uses_array_system_format(&self) -> bool {
matches!(self, Self::AnthropicCompatible)
}
/// 是否使用 OpenAI 兼容协议
@@ -38,6 +38,9 @@ pub struct AddCustomProviderRequest {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateProviderRequest {
pub name: Option<String>,
/// Provider 类型(仅自定义 Provider 可修改)
#[serde(rename = "type")]
pub provider_type: Option<String>,
pub api_host: Option<String>,
pub enabled: Option<bool>,
pub sort_order: Option<i32>,
@@ -241,10 +244,18 @@ pub fn update_api_key_provider(
id: String,
request: UpdateProviderRequest,
) -> Result<ProviderDisplay, String> {
// 解析 provider_type(如果提供)
let provider_type: Option<ApiProviderType> = request
.provider_type
.map(|t| t.parse())
.transpose()
.map_err(|e: String| format!("无效的 Provider 类型: {}", e))?;
let provider = service.0.update_provider(
&db,
&id,
request.name,
provider_type,
request.api_host,
request.enabled,
request.sort_order,
@@ -664,13 +664,24 @@ pub fn convert_openai_to_antigravity_with_context(
safety_settings: Some(default_safety_settings()),
};
// 确定 requestType(配额类型)
// - "agent": 默认类型,用于普通对话和工具调用
// - "web_search": 联网搜索请求
// - "image_gen": 图片生成请求
let request_type = if is_image_generation_model(actual_model) {
"image_gen"
} else {
"agent"
};
// 构建完整的 Antigravity 请求体
let result = serde_json::json!({
"project": project_id,
"requestId": generate_request_id(),
"request": inner,
"model": actual_model,
"userAgent": "antigravity"
"userAgent": "antigravity",
"requestType": request_type
});
eprintln!(
@@ -1076,7 +1087,8 @@ pub fn convert_image_request_to_antigravity(
"safetySettings": safety_settings
},
"model": actual_model,
"userAgent": "antigravity"
"userAgent": "antigravity",
"requestType": "image_gen"
})
}
@@ -20,6 +20,8 @@ pub enum ApiProviderType {
Openai,
OpenaiResponse,
Anthropic,
/// Anthropic 兼容格式(支持 system 数组格式等变体)
AnthropicCompatible,
Gemini,
AzureOpenai,
Vertexai,
@@ -35,6 +37,7 @@ impl std::fmt::Display for ApiProviderType {
ApiProviderType::Openai => write!(f, "openai"),
ApiProviderType::OpenaiResponse => write!(f, "openai-response"),
ApiProviderType::Anthropic => write!(f, "anthropic"),
ApiProviderType::AnthropicCompatible => write!(f, "anthropic-compatible"),
ApiProviderType::Gemini => write!(f, "gemini"),
ApiProviderType::AzureOpenai => write!(f, "azure-openai"),
ApiProviderType::Vertexai => write!(f, "vertexai"),
@@ -54,6 +57,7 @@ impl std::str::FromStr for ApiProviderType {
"openai" => Ok(ApiProviderType::Openai),
"openai-response" => Ok(ApiProviderType::OpenaiResponse),
"anthropic" => Ok(ApiProviderType::Anthropic),
"anthropic-compatible" => Ok(ApiProviderType::AnthropicCompatible),
"gemini" => Ok(ApiProviderType::Gemini),
"azure-openai" => Ok(ApiProviderType::AzureOpenai),
"vertexai" => Ok(ApiProviderType::Vertexai),
+2 -2
View File
@@ -858,7 +858,7 @@ impl AntigravityProvider {
.post(&url)
.header("Authorization", format!("Bearer {token}"))
.header("Content-Type", "application/json")
.header("User-Agent", "antigravity/1.11.5 windows/amd64")
.header("User-Agent", "antigravity/1.11.9 windows/amd64")
.json(body)
.send()
.await
@@ -2172,7 +2172,7 @@ impl StreamingProvider for AntigravityProvider {
.header("Authorization", format!("Bearer {}", token))
.header("Content-Type", "application/json")
.header("Accept", "text/event-stream")
.header("User-Agent", "antigravity/1.11.5 windows/amd64")
.header("User-Agent", "antigravity/1.11.9 windows/amd64")
.json(&payload)
.send()
.await;
@@ -259,6 +259,7 @@ impl ApiKeyProviderService {
db: &DbConnection,
id: &str,
name: Option<String>,
provider_type: Option<ApiProviderType>,
api_host: Option<String>,
enabled: Option<bool>,
sort_order: Option<i32>,
@@ -277,6 +278,13 @@ impl ApiKeyProviderService {
if let Some(n) = name {
provider.name = n;
}
// 只有自定义 Provider 才能修改类型
if let Some(t) = provider_type {
if provider.is_system {
return Err("系统 Provider 不允许修改类型".to_string());
}
provider.provider_type = t;
}
if let Some(h) = api_host {
provider.api_host = h;
}
+9 -8
View File
@@ -703,14 +703,15 @@ mod unit_tests {
&ctx.db,
&provider.id,
Some("Updated Name".to_string()),
None,
Some(false),
None,
None,
None,
None,
None,
None,
None, // provider_type
None, // api_host
Some(false), // enabled
None, // sort_order
None, // api_version
None, // project
None, // location
None, // region
None, // custom_models
)
.expect("Failed to update provider");
@@ -22,6 +22,7 @@ const getRegistryIdFromType = (providerType: string): string => {
const typeMap: Record<string, string> = {
openai: "openai",
anthropic: "anthropic",
"anthropic-compatible": "anthropic", // Anthropic 兼容格式使用 Anthropic 模型列表
gemini: "google",
"azure-openai": "openai",
vertexai: "google",
@@ -366,6 +366,7 @@ export function ApiServerPage() {
const typeIconMap: Record<string, string> = {
openai: "openai",
anthropic: "claude",
"anthropic-compatible": "claude",
gemini: "gemini",
"azure-openai": "openai",
vertexai: "gemini",
@@ -40,6 +40,7 @@ const PROVIDER_TYPES: { value: ProviderType; label: string }[] = [
{ value: "openai", label: "OpenAI 兼容" },
{ value: "openai-response", label: "OpenAI Responses API" },
{ value: "anthropic", label: "Anthropic" },
{ value: "anthropic-compatible", label: "Anthropic 兼容" },
{ value: "gemini", label: "Gemini" },
{ value: "azure-openai", label: "Azure OpenAI" },
{ value: "vertexai", label: "VertexAI" },
@@ -54,6 +55,7 @@ const PROVIDER_TYPE_EXTRA_FIELDS: Record<ProviderType, string[]> = {
openai: [],
"openai-response": [],
anthropic: [],
"anthropic-compatible": [], // Anthropic 兼容格式,无需额外字段
gemini: [],
"azure-openai": ["apiVersion"],
vertexai: ["project", "location"],
@@ -51,6 +51,7 @@ const EXPECTED_EXTRA_FIELDS: Record<ProviderType, string[]> = {
openai: [],
"openai-response": [],
anthropic: [],
"anthropic-compatible": [],
gemini: [],
"azure-openai": ["apiVersion"],
vertexai: ["project", "location"],
@@ -11,6 +11,13 @@ import React, { useState, useEffect, useCallback, useRef } from "react";
import { cn } from "@/lib/utils";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import type {
ProviderWithKeysDisplay,
UpdateProviderRequest,
@@ -24,11 +31,27 @@ import type { ProviderType } from "@/lib/types/provider";
/** 防抖延迟时间(毫秒) */
const DEBOUNCE_DELAY = 500;
/** 支持的 Provider 类型列表 */
const PROVIDER_TYPES: { value: ProviderType; label: string }[] = [
{ value: "openai", label: "OpenAI 兼容" },
{ value: "openai-response", label: "OpenAI Responses API" },
{ value: "anthropic", label: "Anthropic" },
{ value: "anthropic-compatible", label: "Anthropic 兼容" },
{ value: "gemini", label: "Gemini" },
{ value: "azure-openai", label: "Azure OpenAI" },
{ value: "vertexai", label: "VertexAI" },
{ value: "aws-bedrock", label: "AWS Bedrock" },
{ value: "ollama", label: "Ollama" },
{ value: "new-api", label: "New API" },
{ value: "gateway", label: "Vercel AI Gateway" },
];
/** Provider 类型对应的额外字段配置 */
const PROVIDER_TYPE_FIELDS: Record<ProviderType, string[]> = {
openai: [],
"openai-response": [],
anthropic: [],
"anthropic-compatible": [], // Anthropic 兼容格式,无需额外字段
gemini: [],
"azure-openai": ["apiVersion"],
vertexai: ["project", "location"],
@@ -81,6 +104,7 @@ export interface ProviderConfigFormProps {
}
interface FormState {
providerType: ProviderType;
apiHost: string;
apiVersion: string;
project: string;
@@ -121,6 +145,7 @@ export const ProviderConfigForm: React.FC<ProviderConfigFormProps> = ({
}) => {
// 表单状态
const [formState, setFormState] = useState<FormState>({
providerType: (provider.type as ProviderType) || "openai",
apiHost: provider.api_host || "",
apiVersion: provider.api_version || "",
project: provider.project || "",
@@ -140,6 +165,7 @@ export const ProviderConfigForm: React.FC<ProviderConfigFormProps> = ({
// 当 provider 变化时,重置表单状态
useEffect(() => {
setFormState({
providerType: (provider.type as ProviderType) || "openai",
apiHost: provider.api_host || "",
apiVersion: provider.api_version || "",
project: provider.project || "",
@@ -150,6 +176,7 @@ export const ProviderConfigForm: React.FC<ProviderConfigFormProps> = ({
setSaveError(null);
}, [
provider.id,
provider.type,
provider.api_host,
provider.api_version,
provider.project,
@@ -174,6 +201,8 @@ export const ProviderConfigForm: React.FC<ProviderConfigFormProps> = ({
.filter((m) => m.length > 0);
const request: UpdateProviderRequest = {
// 只有自定义 Provider 才发送 type 字段
type: !provider.is_system ? state.providerType : undefined,
api_host: state.apiHost || undefined,
api_version: state.apiVersion || undefined,
project: state.project || undefined,
@@ -190,7 +219,7 @@ export const ProviderConfigForm: React.FC<ProviderConfigFormProps> = ({
setIsSaving(false);
}
},
[provider.id, onUpdate],
[provider.id, provider.is_system, onUpdate],
);
// 防抖保存
@@ -224,8 +253,8 @@ export const ProviderConfigForm: React.FC<ProviderConfigFormProps> = ({
};
// 获取当前 Provider 类型需要显示的额外字段
const providerType = provider.type as ProviderType;
const extraFields = PROVIDER_TYPE_FIELDS[providerType] || [];
// 使用 formState 中的 providerType,这样修改类型后会立即更新显示的字段
const extraFields = PROVIDER_TYPE_FIELDS[formState.providerType] || [];
// 格式化最后保存时间
const formatLastSaved = (date: Date | null): string => {
@@ -238,6 +267,36 @@ export const ProviderConfigForm: React.FC<ProviderConfigFormProps> = ({
className={cn("space-y-4", className)}
data-testid="provider-config-form"
>
{/* Provider 类型选择器(仅自定义 Provider 显示) */}
{!provider.is_system && (
<div className="space-y-1.5">
<Label htmlFor="provider-type" className="text-sm font-medium">
Provider 类型
</Label>
<Select
value={formState.providerType}
onValueChange={(value) =>
handleFieldChange("providerType", value as ProviderType)
}
disabled={loading || isSaving}
>
<SelectTrigger data-testid="provider-type-select">
<SelectValue placeholder="选择 Provider 类型" />
</SelectTrigger>
<SelectContent>
{PROVIDER_TYPES.map((type) => (
<SelectItem key={type.value} value={type.value}>
{type.label}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
选择 API 协议类型,不同类型使用不同的请求格式
</p>
</div>
)}
{/* API Host 字段(所有 Provider 都有) */}
<div className="space-y-1.5">
<Label htmlFor="api-host" className="text-sm font-medium">
@@ -58,6 +58,7 @@ const PROVIDER_ID_TO_REGISTRY_ID: Record<string, string> = {
*/
const PROVIDER_TYPE_TO_REGISTRY_ID: Record<string, string> = {
anthropic: "anthropic",
"anthropic-compatible": "anthropic", // Anthropic 兼容格式
openai: "openai",
"openai-response": "openai",
gemini: "google",
+2 -1
View File
@@ -114,7 +114,8 @@ export const providerTypeToIcon: Record<string, string> = {
antigravity: "antigravity",
openai: "openai",
claude: "claude",
anthropic: "anthropic",
anthropic: "claude",
"anthropic-compatible": "claude", // Anthropic 兼容格式使用 Claude 图标
codex: "openai",
claude_oauth: "claude",
iflow: "alibaba",
+2
View File
@@ -31,6 +31,8 @@ export interface AddCustomProviderRequest {
*/
export interface UpdateProviderRequest {
name?: string;
/** Provider 类型(仅自定义 Provider 可修改) */
type?: string;
api_host?: string;
enabled?: boolean;
sort_order?: number;
+1
View File
@@ -19,6 +19,7 @@ export type ProviderType =
| "openai" // 标准 OpenAI Chat Completions API
| "openai-response" // OpenAI Responses API (支持 Reasoning)
| "anthropic" // Anthropic Messages API
| "anthropic-compatible" // Anthropic 兼容格式 (system 为数组格式)
| "gemini" // Google Gemini API
| "azure-openai" // Azure OpenAI API
| "vertexai" // Google Vertex AI API