Skip to content
Merged
14 changes: 14 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

## 常见任务

### 添加新的供应商类型
Expand Down
94 changes: 88 additions & 6 deletions src/app/v1/_lib/proxy/provider-selector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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<ProviderChainItem["decisionContext"]> = {
totalProviders: allProviders.length,
enabledProviders: 0,
targetType: requestedModel.startsWith("claude-") ? "claude" : "codex", // 根据模型名推断
targetType, // 根据原始请求格式推断目标供应商类型(修复:不再根据模型名推断)
requestedModel, // 新增:记录请求的模型
groupFilterApplied: false,
beforeHealthCheck: 0,
Expand All @@ -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";
Expand All @@ -440,6 +515,7 @@ export class ProxyProviderResolver {
| "circuit_open"
| "rate_limited"
| "excluded"
| "format_type_mismatch"
| "type_mismatch"
| "model_not_allowed"
| "disabled" = "disabled";
Expand All @@ -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}`;
Expand Down
5 changes: 4 additions & 1 deletion src/app/v1/_lib/proxy/response-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
8 changes: 5 additions & 3 deletions src/lib/utils/sse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 = "";
Expand Down
3 changes: 2 additions & 1 deletion src/types/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export interface ProviderChainItem {
// --- 供应商池状态 ---
totalProviders: number; // 系统总供应商数
enabledProviders: number; // 启用的供应商数
targetType: "claude" | "codex"; // 目标类型
targetType: "claude" | "codex" | "openai-compatible" | "gemini" | "gemini-cli"; // 目标类型(基于请求格式推断)
requestedModel?: string; // 请求的模型名称(用于追踪)

// --- 用户分组筛选 ---
Expand All @@ -113,6 +113,7 @@ export interface ProviderChainItem {
| "circuit_open"
| "rate_limited"
| "excluded"
| "format_type_mismatch" // 请求格式与供应商类型不兼容
| "type_mismatch"
| "model_not_allowed"
| "disabled";
Expand Down
Loading