diff --git a/src-tauri/src/agent/protocols/anthropic.rs b/src-tauri/src/agent/protocols/anthropic.rs index 6e06b8699..e3cf90550 100644 --- a/src-tauri/src/agent/protocols/anthropic.rs +++ b/src-tauri/src/agent/protocols/anthropic.rs @@ -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> { 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, Option) { 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); diff --git a/src-tauri/src/agent/protocols/mod.rs b/src-tauri/src/agent/protocols/mod.rs index 198f3dfcb..fbdf98e4f 100644 --- a/src-tauri/src/agent/protocols/mod.rs +++ b/src-tauri/src/agent/protocols/mod.rs @@ -62,9 +62,13 @@ pub trait Protocol: Send + Sync { /// 根据 ProviderType 创建协议处理器 pub fn create_protocol(provider_type: ProviderType) -> Box { 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), diff --git a/src-tauri/src/agent/types.rs b/src-tauri/src/agent/types.rs index d6f63725d..c55887304 100644 --- a/src-tauri/src/agent/types.rs +++ b/src-tauri/src/agent/types.rs @@ -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 兼容协议 diff --git a/src-tauri/src/commands/api_key_provider_cmd.rs b/src-tauri/src/commands/api_key_provider_cmd.rs index ced6faba7..7d920385b 100644 --- a/src-tauri/src/commands/api_key_provider_cmd.rs +++ b/src-tauri/src/commands/api_key_provider_cmd.rs @@ -38,6 +38,9 @@ pub struct AddCustomProviderRequest { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UpdateProviderRequest { pub name: Option, + /// Provider 类型(仅自定义 Provider 可修改) + #[serde(rename = "type")] + pub provider_type: Option, pub api_host: Option, pub enabled: Option, pub sort_order: Option, @@ -241,10 +244,18 @@ pub fn update_api_key_provider( id: String, request: UpdateProviderRequest, ) -> Result { + // 解析 provider_type(如果提供) + let provider_type: Option = 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, diff --git a/src-tauri/src/converter/openai_to_antigravity.rs b/src-tauri/src/converter/openai_to_antigravity.rs index 2225aab26..5cfce66ac 100644 --- a/src-tauri/src/converter/openai_to_antigravity.rs +++ b/src-tauri/src/converter/openai_to_antigravity.rs @@ -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" }) } diff --git a/src-tauri/src/database/dao/api_key_provider.rs b/src-tauri/src/database/dao/api_key_provider.rs index 92263e7c8..953c48957 100644 --- a/src-tauri/src/database/dao/api_key_provider.rs +++ b/src-tauri/src/database/dao/api_key_provider.rs @@ -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), diff --git a/src-tauri/src/providers/antigravity.rs b/src-tauri/src/providers/antigravity.rs index ebfa56599..bf2886e26 100644 --- a/src-tauri/src/providers/antigravity.rs +++ b/src-tauri/src/providers/antigravity.rs @@ -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; diff --git a/src-tauri/src/services/api_key_provider_service.rs b/src-tauri/src/services/api_key_provider_service.rs index 5465d4580..14a9f2fcd 100644 --- a/src-tauri/src/services/api_key_provider_service.rs +++ b/src-tauri/src/services/api_key_provider_service.rs @@ -259,6 +259,7 @@ impl ApiKeyProviderService { db: &DbConnection, id: &str, name: Option, + provider_type: Option, api_host: Option, enabled: Option, sort_order: Option, @@ -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; } diff --git a/src-tauri/tests/api_key_provider_tests.rs b/src-tauri/tests/api_key_provider_tests.rs index d27560ce0..aa3289622 100644 --- a/src-tauri/tests/api_key_provider_tests.rs +++ b/src-tauri/tests/api_key_provider_tests.rs @@ -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"); diff --git a/src/components/agent/chat/components/ChatNavbar.tsx b/src/components/agent/chat/components/ChatNavbar.tsx index 892052c19..3fdb71f07 100644 --- a/src/components/agent/chat/components/ChatNavbar.tsx +++ b/src/components/agent/chat/components/ChatNavbar.tsx @@ -22,6 +22,7 @@ const getRegistryIdFromType = (providerType: string): string => { const typeMap: Record = { openai: "openai", anthropic: "anthropic", + "anthropic-compatible": "anthropic", // Anthropic 兼容格式使用 Anthropic 模型列表 gemini: "google", "azure-openai": "openai", vertexai: "google", diff --git a/src/components/api-server/ApiServerPage.tsx b/src/components/api-server/ApiServerPage.tsx index 7187b4cb0..8864ce94d 100644 --- a/src/components/api-server/ApiServerPage.tsx +++ b/src/components/api-server/ApiServerPage.tsx @@ -366,6 +366,7 @@ export function ApiServerPage() { const typeIconMap: Record = { openai: "openai", anthropic: "claude", + "anthropic-compatible": "claude", gemini: "gemini", "azure-openai": "openai", vertexai: "gemini", diff --git a/src/components/provider-pool/api-key/AddCustomProviderModal.tsx b/src/components/provider-pool/api-key/AddCustomProviderModal.tsx index 36faa750e..df6f4db9a 100644 --- a/src/components/provider-pool/api-key/AddCustomProviderModal.tsx +++ b/src/components/provider-pool/api-key/AddCustomProviderModal.tsx @@ -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 = { openai: [], "openai-response": [], anthropic: [], + "anthropic-compatible": [], // Anthropic 兼容格式,无需额外字段 gemini: [], "azure-openai": ["apiVersion"], vertexai: ["project", "location"], diff --git a/src/components/provider-pool/api-key/ProviderConfigForm.test.ts b/src/components/provider-pool/api-key/ProviderConfigForm.test.ts index 3020f7dd2..dc0655207 100644 --- a/src/components/provider-pool/api-key/ProviderConfigForm.test.ts +++ b/src/components/provider-pool/api-key/ProviderConfigForm.test.ts @@ -51,6 +51,7 @@ const EXPECTED_EXTRA_FIELDS: Record = { openai: [], "openai-response": [], anthropic: [], + "anthropic-compatible": [], gemini: [], "azure-openai": ["apiVersion"], vertexai: ["project", "location"], diff --git a/src/components/provider-pool/api-key/ProviderConfigForm.tsx b/src/components/provider-pool/api-key/ProviderConfigForm.tsx index c6c52671f..b53f74667 100644 --- a/src/components/provider-pool/api-key/ProviderConfigForm.tsx +++ b/src/components/provider-pool/api-key/ProviderConfigForm.tsx @@ -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 = { 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 = ({ }) => { // 表单状态 const [formState, setFormState] = useState({ + 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 = ({ // 当 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 = ({ setSaveError(null); }, [ provider.id, + provider.type, provider.api_host, provider.api_version, provider.project, @@ -174,6 +201,8 @@ export const ProviderConfigForm: React.FC = ({ .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 = ({ setIsSaving(false); } }, - [provider.id, onUpdate], + [provider.id, provider.is_system, onUpdate], ); // 防抖保存 @@ -224,8 +253,8 @@ export const ProviderConfigForm: React.FC = ({ }; // 获取当前 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 = ({ className={cn("space-y-4", className)} data-testid="provider-config-form" > + {/* Provider 类型选择器(仅自定义 Provider 显示) */} + {!provider.is_system && ( +
+ + +

+ 选择 API 协议类型,不同类型使用不同的请求格式 +

+
+ )} + {/* API Host 字段(所有 Provider 都有) */}