diff --git a/CLAUDE.md b/CLAUDE.md index 5ac43a3a5..b27131b4e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -658,6 +658,20 @@ WITH latest_prices AS ( SELECT ... LIMIT 50 OFFSET 0; ``` +### 10. Database MCP + +Objective: +You are required to use the `db` MCP server to interact with a database. + +Capabilities: +With this server, you can perform the following actions: + +- View the structure of database tables. +- Query and inspect data within the tables. + +Prerequisite: +Before performing any operations, you must first consult the database schema definition to understand its structure. The schema is defined in the following file: @src/drizzle/schema.ts. + ## 常见任务 ### 添加新的供应商类型 diff --git a/src/app/v1/_lib/proxy/provider-selector.ts b/src/app/v1/_lib/proxy/provider-selector.ts index e7e2bb268..e65b2697b 100644 --- a/src/app/v1/_lib/proxy/provider-selector.ts +++ b/src/app/v1/_lib/proxy/provider-selector.ts @@ -6,6 +6,7 @@ import { isCircuitOpen, getCircuitState } from "@/lib/circuit-breaker"; import { ProxyResponses } from "./responses"; import { logger } from "@/lib/logger"; import type { ProxySession } from "./session"; +import type { ClientFormat } from "./format-mapper"; import type { ProviderChainItem } from "@/types/message"; /** @@ -87,6 +88,44 @@ function providerSupportsModel(provider: Provider, requestedModel: string): bool return false; } +/** + * 根据原始请求格式限制可选供应商类型 + * + * 核心逻辑:确保客户端请求格式与供应商类型兼容,避免格式错配 + * + * 映射关系: + * - claude → claude | claude-auth + * - response → codex + * - openai → openai-compatible + * - gemini → gemini + * - gemini-cli → gemini-cli + * + * @param format - 客户端请求格式(从 session.originalFormat 获取) + * @param providerType - 供应商类型 + * @returns 是否兼容 + * + * 向后兼容:调用方在 originalFormat 未设置时应跳过此检查 + */ +function checkFormatProviderTypeCompatibility( + format: ClientFormat, + providerType: Provider["providerType"] +): boolean { + switch (format) { + case "claude": + return providerType === "claude" || providerType === "claude-auth"; + case "response": + return providerType === "codex"; + case "openai": + return providerType === "openai-compatible"; + case "gemini": + return providerType === "gemini"; + case "gemini-cli": + return providerType === "gemini-cli"; + default: + return true; // 未知格式回退为兼容(不会主动过滤) + } +} + export class ProxyProviderResolver { static async ensure( session: ProxySession, @@ -115,7 +154,9 @@ export class ProxyProviderResolver { decisionContext: { totalProviders: 0, // 复用不需要筛选 enabledProviders: 0, - targetType: reusedProvider.providerType as "claude" | "codex", + targetType: reusedProvider.providerType as NonNullable< + ProviderChainItem["decisionContext"] + >["targetType"], requestedModel: session.getCurrentModel() || "", groupFilterApplied: false, beforeHealthCheck: 0, @@ -196,7 +237,9 @@ export class ProxyProviderResolver { : { totalProviders: 0, enabledProviders: 0, - targetType: session.provider.providerType as "claude" | "codex", + targetType: session.provider.providerType as NonNullable< + ProviderChainItem["decisionContext"] + >["targetType"], requestedModel: session.getCurrentModel() || "", groupFilterApplied: false, beforeHealthCheck: 0, @@ -251,7 +294,9 @@ export class ProxyProviderResolver { decisionContext: successContext || { totalProviders: 0, enabledProviders: 0, - targetType: session.provider.providerType as "claude" | "codex", + targetType: session.provider.providerType as NonNullable< + ProviderChainItem["decisionContext"] + >["targetType"], requestedModel: session.getCurrentModel() || "", groupFilterApplied: false, beforeHealthCheck: 0, @@ -399,11 +444,29 @@ export class ProxyProviderResolver { const allProviders = await findProviderList(); const requestedModel = session?.getCurrentModel() || ""; + // 原始请求格式映射到目标供应商类型;缺省为 claude 以兼容历史请求 + const targetType: "claude" | "codex" | "openai-compatible" | "gemini" | "gemini-cli" = (() => { + switch (session?.originalFormat) { + case "claude": + return "claude"; + case "response": + return "codex"; + case "openai": + return "openai-compatible"; + case "gemini": + return "gemini"; + case "gemini-cli": + return "gemini-cli"; + default: + return "claude"; // 默认回退到 claude(向后兼容) + } + })(); + // === 初始化决策上下文 === const context: NonNullable = { totalProviders: allProviders.length, enabledProviders: 0, - targetType: requestedModel.startsWith("claude-") ? "claude" : "codex", // 根据模型名推断 + targetType, // 根据原始请求格式推断目标供应商类型(修复:不再根据模型名推断) requestedModel, // 新增:记录请求的模型 groupFilterApplied: false, beforeHealthCheck: 0, @@ -415,14 +478,26 @@ export class ProxyProviderResolver { excludedProviderIds: excludeIds.length > 0 ? excludeIds : undefined, }; - // Step 1: 基础过滤 + 模型匹配(新逻辑) + // Step 1: 基础过滤 + 格式/模型匹配(新逻辑) const enabledProviders = allProviders.filter((provider) => { // 1a. 基础过滤 if (!provider.isEnabled || excludeIds.includes(provider.id)) { return false; } - // 1b. 模型匹配(新逻辑) + // 1b. 格式类型匹配(新增) + // 根据 session.originalFormat 限制候选供应商类型,避免格式错配 + if (session?.originalFormat) { + const isFormatCompatible = checkFormatProviderTypeCompatibility( + session.originalFormat, + provider.providerType + ); + if (!isFormatCompatible) { + return false; // 过滤掉格式不兼容的供应商 + } + } + + // 1c. 模型匹配(保留原有逻辑) if (!requestedModel) { // 没有模型信息时,只选择 Anthropic 提供商(向后兼容) return provider.providerType === "claude"; @@ -440,6 +515,7 @@ export class ProxyProviderResolver { | "circuit_open" | "rate_limited" | "excluded" + | "format_type_mismatch" | "type_mismatch" | "model_not_allowed" | "disabled" = "disabled"; @@ -451,6 +527,12 @@ export class ProxyProviderResolver { } else if (excludeIds.includes(p.id)) { reason = "excluded"; details = "已在前序尝试中失败"; + } else if ( + session?.originalFormat && + !checkFormatProviderTypeCompatibility(session.originalFormat, p.providerType) + ) { + reason = "format_type_mismatch"; + details = `原始格式 ${session.originalFormat} 与供应商类型 ${p.providerType} 不兼容`; } else if (requestedModel && !providerSupportsModel(p, requestedModel)) { reason = "model_not_allowed"; details = `不支持模型 ${requestedModel}`; diff --git a/src/app/v1/_lib/proxy/response-handler.ts b/src/app/v1/_lib/proxy/response-handler.ts index bfc0b34aa..2b1f2fea5 100644 --- a/src/app/v1/_lib/proxy/response-handler.ts +++ b/src/app/v1/_lib/proxy/response-handler.ts @@ -1103,7 +1103,10 @@ function parseUsageFromResponseText( // Fallback to SSE parsing when body is not valid JSON } - if (!usageMetrics && responseText.includes("event:")) { + // SSE 解析:支持两种格式 + // 1. 标准 SSE (event: + data:) - Claude/OpenAI + // 2. 纯 data: 格式 - Gemini + if (!usageMetrics && responseText.includes("data:")) { const events = parseSSEData(responseText); for (const event of events) { if (usageMetrics) { diff --git a/src/lib/utils/sse.ts b/src/lib/utils/sse.ts index a4fa4dfe0..efed88b7e 100644 --- a/src/lib/utils/sse.ts +++ b/src/lib/utils/sse.ts @@ -10,7 +10,9 @@ export function parseSSEData(sseText: string): ParsedSSEEvent[] { let dataLines: string[] = []; const flushEvent = () => { - if (!eventName || dataLines.length === 0) { + // 修改:支持没有 event: 前缀的纯 data: 格式(Gemini 流式响应) + // 如果没有 eventName,使用默认值 "message" + if (dataLines.length === 0) { eventName = ""; dataLines = []; return; @@ -20,9 +22,9 @@ export function parseSSEData(sseText: string): ParsedSSEEvent[] { try { const data = JSON.parse(dataStr); - events.push({ event: eventName, data }); + events.push({ event: eventName || "message", data }); } catch { - events.push({ event: eventName, data: dataStr }); + events.push({ event: eventName || "message", data: dataStr }); } eventName = ""; diff --git a/src/types/message.ts b/src/types/message.ts index 4048969c1..0c89315ae 100644 --- a/src/types/message.ts +++ b/src/types/message.ts @@ -91,7 +91,7 @@ export interface ProviderChainItem { // --- 供应商池状态 --- totalProviders: number; // 系统总供应商数 enabledProviders: number; // 启用的供应商数 - targetType: "claude" | "codex"; // 目标类型 + targetType: "claude" | "codex" | "openai-compatible" | "gemini" | "gemini-cli"; // 目标类型(基于请求格式推断) requestedModel?: string; // 请求的模型名称(用于追踪) // --- 用户分组筛选 --- @@ -113,6 +113,7 @@ export interface ProviderChainItem { | "circuit_open" | "rate_limited" | "excluded" + | "format_type_mismatch" // 请求格式与供应商类型不兼容 | "type_mismatch" | "model_not_allowed" | "disabled";