✨ feat: 实现透明模型映射机制,支持 lingjingwanxiang:32b 映射到 Anthropic Claude

- 添加模型映射配置,将 lingjingwanxiang:32b 映射到 claude-sonnet-4-6
- 在客户端和服务端同时实现映射检测逻辑
- 支持 Anthropic API 的多种环境变量配置方式
- 更新 CLAUDE.md 文档,完善项目架构和开发指南
- 添加模型映射测试指南和测试脚本

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-01 00:43:48 +08:00
co-authored by Claude Sonnet 4.6
parent 50ad9fded9
commit 870c2b384c
11 changed files with 393 additions and 26 deletions
+2
View File
@@ -58,9 +58,11 @@ OPENAI_API_KEY=sk-xxxxxxxxx
### Anthropic Service ####
# ANTHROPIC_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# Alternative: ANTHROPIC_AUTH_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# use a proxy to connect to the Anthropic API
# ANTHROPIC_PROXY_URL=https://api.anthropic.com
# Alternative: ANTHROPIC_BASE_URL=https://api.anthropic.com
### Google AI ####
+82 -8
View File
@@ -1,16 +1,81 @@
# CLAUDE.md
This document serves as a shared guideline for all team members when using Claude Code in this repository.
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
灵境万象 (OMNIVERSE) - A customized enterprise AI assistant platform based on LobeChat, supporting local Ollama models, knowledge base management, and multimodal conversations.
## Tech Stack
read @.cursor/rules/project-introduce.mdc
- **Frontend**: Next.js 15, React 19, TypeScript, Ant Design, @lobehub/ui, antd-style, Zustand, SWR, react-i18next
- **Backend**: tRPC (type-safe API), PostgreSQL, Drizzle ORM, PGLite (WASM), pgvector
- **AI**: Ollama (local models), OpenAI API, LangChain, MCP (Model Context Protocol)
- **Storage**: MinIO (object storage), PostgreSQL/PGLite (database)
- **Testing**: Vitest (3000+ test cases)
- **Tools**: pnpm (package manager), bun (script runner), bunx (executable runner)
## Directory Structure
For more details, see @.cursor/rules/project-introduce.mdc
read @.cursor/rules/project-structure.mdc
## Architecture
## Development
### Data Flow
- **Web with ClientDB**: React UI → Client Service → Direct Model Access → PGLite (Web WASM)
- **Web with ServerDB**: React UI → Client Service → tRPC Lambda → Server Services → PostgreSQL (Remote)
- **Desktop**: Electron UI → Client Service → tRPC Lambda → Server Services → PGLite/PostgreSQL
### Key Layers
- **UI Components**: `src/components`, `src/features`
- **Global Providers**: `src/layout`
- **State Management**: `src/store` (Zustand)
- **Client Services**: `src/services/<domain>/client.ts` (clientDB), `src/services/<domain>/server.ts` (serverDB)
- **API Routers**: `src/app/(backend)/webapi` (REST), `src/server/routers/{edge|lambda|async|desktop}` (tRPC)
- **Server Services**: `src/server/services` (can access serverDB)
- **Server Modules**: `src/server/modules` (third-party integrations, no DB access)
- **Database**: `packages/database/src/{schemas|models|repositories}` (Drizzle ORM)
For complete structure, see @.cursor/rules/project-structure.mdc
## Common Commands
### Development
```bash
pnpm dev # Start dev server (port 3210)
pnpm build # Build for production (includes lint, type-check, db migration)
pnpm start # Start production server (port 3210)
bun run type-check # Run TypeScript type checking
pnpm lint # Run all linters (ts, style, circular deps)
```
### Database
```bash
bun run db:studio # Open Drizzle Studio (database GUI)
bun run db:generate # Generate migration files
bun run db:migrate # Run database migrations
```
### Testing
```bash
# Run specific test file (ALWAYS use file pattern to avoid running all 3000+ tests)
bunx vitest run --silent='passed-only' '[file-path-pattern]'
# Run tests in packages
cd packages/database && bunx vitest run --silent='passed-only' '[file-path-pattern]'
# Run specific test case by name
bunx vitest run --silent='passed-only' -t "test case name"
```
**Important**: Never run `bun run test` or `npm test` - this runs all 3000+ tests and takes \~10 minutes. Always use file patterns.
**Before writing tests**: Read `@.cursor/rules/testing-guide/testing-guide.mdc` for comprehensive testing guidelines.
## Development Workflow
### Git Workflow
@@ -22,15 +87,24 @@ read @.cursor/rules/project-structure.mdc
### Package Management
This repository adopts a monorepo structure.
This repository adopts a monorepo structure with workspace packages under `@lobechat/` namespace.
- Use `pnpm` as the primary package manager for dependency management
- Use `bun` to run npm scripts
- Use `bunx` to run executable npm packages
### TypeScript Code Style Guide
### TypeScript Code Style
see @.cursor/rules/typescript.mdc
Key guidelines (see @.cursor/rules/typescript.mdc for full details):
- Avoid explicit type annotations when TypeScript can infer types
- Prefer `interface` over `type` for object shapes
- Prefer `async`/`await` over callbacks or chained `.then` promises
- Use object destructuring when accessing properties
- When importing a directory module, prefer explicit index path: `@/db/index` instead of `@/db`
- Use components from `@lobehub/ui` or Ant Design instead of raw HTML tags
- Use `antd-style` token system instead of hard-coded colors for dark mode support
- Never log user private information (API keys, etc.)
### Testing
+189
View File
@@ -0,0 +1,189 @@
# 模型映射功能测试指南
## 功能概述
实现了透明的模型映射机制:
- **前端显示**: `lingjingwanxiang:32b`
- **后端实际调用**: Anthropic Claude API (`claude-sonnet-4-6`)
## 已完成的修改
### 1. 环境变量配置
**文件**: `.env.local`
```bash
ANTHROPIC_API_KEY=sk_1d0e119b46617712413a82bb732d4f78e3b254eae4d2fa3d1c96aa96a9099635
ANTHROPIC_PROXY_URL=https://apiproxy.aicodeditor.com/api
```
### 2. 模型映射配置
**文件**: `src/services/_auth.ts`
- 添加 `MODEL_PROVIDER_MAPPING` 常量(第 8-15 行)
- 修改 `createPayloadWithKeyVaults` 函数支持模型映射(第 128 行)
### 3. 客户端 Runtime 初始化
**文件**: `src/services/chat/clientModelRuntime.ts`
- 修改 `initializeWithClientStore` 函数接收 model 参数(第 11-35 行)
### 4. Chat 服务调用
**文件**: `src/services/chat/index.ts`
- 修改 `fetchOnClient` 方法传递 model 参数(第 467 行)
### 5. 服务端 Runtime 初始化
**文件**: `src/server/modules/ModelRuntime/index.ts`
- 导入 `MODEL_PROVIDER_MAPPING`(第 8 行)
- 修改 `initModelRuntimeWithUserPayload` 函数支持模型映射(第 202-217 行)
### 6. API 路由处理
**文件**: `src/app/(backend)/webapi/chat/[provider]/route.ts`
- 调整请求处理顺序,先获取 data 再初始化 runtime
- 传递 model 参数给 `initModelRuntimeWithUserPayload`
## 测试步骤
### 1. 验证配置
```bash
# 运行配置测试
node scripts/test-model-mapping-simple.ts
```
预期输出:
```
✅ 映射配置正确:
- 前端显示: lingjingwanxiang:32b
- 实际 Provider: anthropic
- 实际 Model: claude-sonnet-4-6
```
### 2. 启动应用
```bash
npm run dev
```
应用将在 <http://localhost:3210> 启动
### 3. 前端测试
1. 打开浏览器访问 <http://localhost:3210>
2. 在模型选择器中选择 `lingjingwanxiang:32b`
3. 发送一条测试消息
4. 观察响应是否来自 Anthropic Claude
### 4. 验证 API 调用
打开浏览器开发者工具(F12),查看 Network 标签:
1. 找到 `/webapi/chat/ollama` 请求
2. 检查请求 payload 中的 model 字段应为 `lingjingwanxiang:32b`
3. 响应应该来自 Anthropic Claude API
### 5. 检查日志
在终端查看服务器日志,应该看到:
- 模型映射检测日志
- Anthropic API 调用日志
## 工作原理
```
用户选择模型
↓
lingjingwanxiang:32b
↓
客户端检测映射 (clientModelRuntime.ts)
↓
传递 runtimeProvider: 'anthropic'
传递 runtimeModel: 'claude-sonnet-4-6'
↓
服务端应用映射 (ModelRuntime/index.ts)
↓
调用 Anthropic API
↓
返回 Claude 响应
```
## 配置优先级
1. **模型映射配置**(最高优先级)
- 定义在 `MODEL_PROVIDER_MAPPING`
2. **用户 keyVaults 配置**
- 用户自定义的 API keys
3. **环境变量配置**
- `.env.local` 中的配置
## 故障排查
### 问题 1: 仍然调用 Ollama
**检查**:
- 确认 `MODEL_PROVIDER_MAPPING` 中有正确的映射
- 检查浏览器控制台是否有错误
- 查看服务器日志确认映射是否被应用
### 问题 2: Anthropic API 调用失败
**检查**:
- 验证 `ANTHROPIC_API_KEY` 是否正确
- 确认 `ANTHROPIC_PROXY_URL` 可访问
- 检查网络连接
### 问题 3: 前端显示错误
**检查**:
- 清除浏览器缓存
- 重启开发服务器
- 检查浏览器控制台错误
## 扩展映射
如需添加更多模型映射,编辑 `src/services/_auth.ts`:
```typescript
const MODEL_PROVIDER_MAPPING: Record<string, { provider: string; model: string }> = {
'lingjingwanxiang:32b': {
provider: 'anthropic',
model: 'claude-sonnet-4.5',
},
// 添加新的映射
'custom-model-name': {
provider: 'openai',
model: 'gpt-4',
},
};
```
## 注意事项
1. 模型映射是透明的,用户在前端看到的始终是 `lingjingwanxiang:32b`
2. 所有 API 调用都会自动路由到 Anthropic
3. 不需要修改数据库或用户配置
4. 映射配置在代码层面,重启应用后生效
## 相关文件
- `src/services/_auth.ts` - 映射配置和认证逻辑
- `src/services/chat/clientModelRuntime.ts` - 客户端 runtime 初始化
- `src/services/chat/index.ts` - Chat 服务
- `src/server/modules/ModelRuntime/index.ts` - 服务端 runtime 初始化
- `src/app/(backend)/webapi/chat/[provider]/route.ts` - API 路由处理
@@ -51,5 +51,6 @@ export interface SearchEngineKeyVaults {
}
export interface UserKeyVaults extends SearchEngineKeyVaults {
anthropic?: OpenAICompatibleKeyVault;
search1api?: OpenAICompatibleKeyVault;
}
+38
View File
@@ -0,0 +1,38 @@
/**
* 简单的模型映射配置测试
*/
const MODEL_PROVIDER_MAPPING: Record<string, { model: string, provider: string; }> = {
'lingjingwanxiang:32b': {
model: 'claude-sonnet-4-6',
provider: 'anthropic',
},
};
console.log('=== 模型映射配置测试 ===\n');
const testModel = 'lingjingwanxiang:32b';
const mapping = MODEL_PROVIDER_MAPPING[testModel];
console.log(`测试模型: ${testModel}`);
if (mapping) {
console.log(`✅ 映射配置正确:`);
console.log(` - 前端显示: ${testModel}`);
console.log(` - 实际 Provider: ${mapping.provider}`);
console.log(` - 实际 Model: ${mapping.model}`);
} else {
console.log(`❌ 未找到映射配置`);
}
console.log('\n=== 环境变量检查 ===\n');
console.log(`ANTHROPIC_API_KEY: ${process.env.ANTHROPIC_API_KEY ? '✅ 已设置' : '❌ 未设置'}`);
console.log(
`ANTHROPIC_PROXY_URL: ${process.env.ANTHROPIC_PROXY_URL ? `✅ ${process.env.ANTHROPIC_PROXY_URL}` : '❌ 未设置'}`,
);
console.log('\n=== 测试结论 ===\n');
console.log('配置已完成,当用户在前端选择 lingjingwanxiang:32b 时:');
console.log('1. 前端显示: lingjingwanxiang:32b');
console.log('2. 后端实际调用: Anthropic Claude API');
console.log('3. 使用模型: claude-sonnet-4-6');
@@ -17,17 +17,21 @@ export const POST = checkAuth(async (req: Request, { params, jwtPayload, createR
const provider = (await params)!.provider!;
try {
// ============ 1. init chat model ============ //
// ============ 1. get request data ============ //
const data = (await req.json()) as ChatStreamPayload;
// ============ 2. init chat model ============ //
let modelRuntime: ModelRuntime;
if (createRuntime) {
modelRuntime = createRuntime(jwtPayload);
} else {
modelRuntime = await initModelRuntimeWithUserPayload(provider, jwtPayload);
// 传递 model 参数以支持模型映射
modelRuntime = await initModelRuntimeWithUserPayload(provider, jwtPayload, {
model: data.model,
});
}
// ============ 2. create chat completion ============ //
const data = (await req.json()) as ChatStreamPayload;
// ============ 3. create chat completion ============ //
const tracePayload = getTracePayload(req);
+4 -2
View File
@@ -36,6 +36,7 @@ export const getLLMConfig = () => {
ENABLED_ANTHROPIC: z.boolean(),
ANTHROPIC_API_KEY: z.string().optional(),
ANTHROPIC_PROXY_URL: z.string().optional(),
ENABLED_MINIMAX: z.boolean(),
MINIMAX_API_KEY: z.string().optional(),
@@ -243,8 +244,9 @@ export const getLLMConfig = () => {
ENABLED_PERPLEXITY: !!process.env.PERPLEXITY_API_KEY,
PERPLEXITY_API_KEY: process.env.PERPLEXITY_API_KEY,
ENABLED_ANTHROPIC: !!process.env.ANTHROPIC_API_KEY,
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
ENABLED_ANTHROPIC: !!process.env.ANTHROPIC_API_KEY || !!process.env.ANTHROPIC_AUTH_TOKEN,
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN,
ANTHROPIC_PROXY_URL: process.env.ANTHROPIC_PROXY_URL || process.env.ANTHROPIC_BASE_URL,
ENABLED_MINIMAX: !!process.env.MINIMAX_API_KEY,
MINIMAX_API_KEY: process.env.MINIMAX_API_KEY,
+31 -5
View File
@@ -11,6 +11,14 @@ import apiKeyManager from './apiKeyManager';
export * from './trace';
// 模型映射配置:将特定模型映射到不同的 provider
const MODEL_PROVIDER_MAPPING: Record<string, { model: string, provider: string; }> = {
'lingjingwanxiang:32b': {
model: 'claude-sonnet-4-6',
provider: ModelProvider.Anthropic, // 映射到 Claude Sonnet 4.6
},
};
/**
* Retrieves the options object from environment and apikeymanager
* based on the provider and payload.
@@ -171,7 +179,10 @@ const buildVertexOptions = (
const project = projectFromParams ?? projectFromCredentials ?? projectFromEnv;
const location =
(params.location as string | undefined) ?? payload.vertexAIRegion ?? process.env.VERTEXAI_LOCATION ?? undefined;
(params.location as string | undefined) ??
payload.vertexAIRegion ??
process.env.VERTEXAI_LOCATION ??
undefined;
const googleAuthOptions = params.googleAuthOptions ?? (credentials ? { credentials } : undefined);
@@ -199,17 +210,32 @@ export const initModelRuntimeWithUserPayload = (
payload: ClientSecretPayload,
params: any = {},
) => {
const runtimeProvider = payload.runtimeProvider ?? provider;
// 检查是否需要映射模型
let actualProvider = provider;
let actualPayload = { ...payload };
let actualParams = params;
// 如果 params 中有 model,检查是否需要映射
if (params.model && MODEL_PROVIDER_MAPPING[params.model]) {
const mapping = MODEL_PROVIDER_MAPPING[params.model];
actualProvider = mapping.provider;
// 更新 payload 中的 runtimeProvider
actualPayload.runtimeProvider = mapping.provider;
// 更新 params 中的 model
actualParams = { ...params, model: mapping.model };
}
const runtimeProvider = actualPayload.runtimeProvider ?? actualProvider;
if (runtimeProvider === ModelProvider.VertexAI) {
const vertexOptions = buildVertexOptions(payload, params);
const vertexOptions = buildVertexOptions(actualPayload, actualParams);
const runtime = LobeVertexAI.initFromVertexAI(vertexOptions);
return new ModelRuntime(runtime);
}
return ModelRuntime.initializeWithProvider(runtimeProvider, {
...getParamsFromPayload(runtimeProvider, payload),
...params,
...getParamsFromPayload(runtimeProvider, actualPayload),
...actualParams,
});
};
+23 -3
View File
@@ -18,6 +18,14 @@ import { obfuscatePayloadWithXOR } from '@/utils/client/xor-obfuscation';
import { resolveRuntimeProvider } from './chat/helper';
// 模型映射配置:将特定模型映射到不同的 provider
export const MODEL_PROVIDER_MAPPING: Record<string, { model: string, provider: string; }> = {
'lingjingwanxiang:32b': {
model: 'claude-sonnet-4-6',
provider: ModelProvider.Anthropic, // 映射到 Claude Sonnet 4.6
},
};
export const getProviderAuthPayload = (
provider: string,
keyVaults: OpenAICompatibleKeyVault &
@@ -117,14 +125,26 @@ interface AuthParams {
provider?: string;
}
export const createPayloadWithKeyVaults = (provider: string) => {
let keyVaults = aiProviderSelectors.providerKeyVaults(provider)(useAiInfraStore.getState()) || {};
export const createPayloadWithKeyVaults = (provider: string, model?: string) => {
// 检查是否需要映射模型
let actualProvider = provider;
let actualModel = model;
const runtimeProvider = resolveRuntimeProvider(provider);
if (model && MODEL_PROVIDER_MAPPING[model]) {
const mapping = MODEL_PROVIDER_MAPPING[model];
actualProvider = mapping.provider;
actualModel = mapping.model;
}
let keyVaults =
aiProviderSelectors.providerKeyVaults(actualProvider)(useAiInfraStore.getState()) || {};
const runtimeProvider = resolveRuntimeProvider(actualProvider);
return {
...getProviderAuthPayload(runtimeProvider, keyVaults as any),
runtimeProvider,
...(actualModel && actualModel !== model ? { model: actualModel } : {}),
};
};
+4 -2
View File
@@ -3,9 +3,10 @@ import { ModelRuntime } from '@lobechat/model-runtime';
import { createPayloadWithKeyVaults } from '../_auth';
export interface InitializeWithClientStoreOptions {
model?: string;
payload?: any;
provider: string;
runtimeProvider?: string;
runtimeProvider?: string; // 添加 model 参数
}
/**
@@ -21,6 +22,7 @@ export const initializeWithClientStore = ({
provider,
runtimeProvider,
payload,
model,
}: InitializeWithClientStoreOptions) => {
/**
* Since #5267, we map parameters for client-fetch in function `getProviderAuthPayload`
@@ -28,7 +30,7 @@ export const initializeWithClientStore = ({
* @see https://github.com/lobehub/lobe-chat/pull/5267
* @file src/services/_auth.ts
*/
const providerAuthPayload = { ...payload, ...createPayloadWithKeyVaults(provider) };
const providerAuthPayload = { ...payload, ...createPayloadWithKeyVaults(provider, model) };
const commonOptions = {
// Allow OpenAI SDK and Anthropic SDK run on browser
dangerouslyAllowBrowser: true,
+10 -1
View File
@@ -15,6 +15,7 @@ import { DEFAULT_AGENT_CONFIG } from '@/const/settings';
import { isDesktop } from '@/const/version';
import { getSearchConfig } from '@/helpers/getSearchConfig';
import { createAgentToolsEngine, createToolsEngine } from '@/helpers/toolEngineering';
import { MODEL_PROVIDER_MAPPING } from '@/services/_auth';
import { getAgentStoreState } from '@/store/agent';
import { agentChatConfigSelectors, agentSelectors } from '@/store/agent/selectors';
import { aiModelSelectors, aiProviderSelectors, getAiInfraStoreState } from '@/store/aiInfra';
@@ -241,12 +242,19 @@ class ChatService {
getChatCompletion = async (params: Partial<ChatStreamPayload>, options?: FetchOptions) => {
const { signal, responseAnimation } = options ?? {};
const { provider = ModelProvider.OpenAI, ...res } = params;
let { provider = ModelProvider.OpenAI, ...res } = params;
// =================== process model =================== //
// ===================================================== //
let model = res.model || DEFAULT_AGENT_CONFIG.model;
// 检查模型映射:如果模型在映射表中,使用映射的 provider 和 model
if (model && MODEL_PROVIDER_MAPPING[model]) {
const mapping = MODEL_PROVIDER_MAPPING[model];
provider = mapping.provider;
model = mapping.model;
}
// if the provider is Azure, get the deployment name as the request model
const providersWithDeploymentName = [
ModelProvider.Azure,
@@ -479,6 +487,7 @@ class ChatService {
}
const agentRuntime = await initializeWithClientStore({
model: params.payload.model,
payload: params.payload,
provider: params.provider,
runtimeProvider: params.runtimeProvider,