mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-27 13:29:15 +07:00
💄 style: Allow removal of top_p and similar request parameters (#9498)
* ✨ feat: 增强参数管理,支持删除 undefined 和 null 值的参数 * ✨ feat: 将模型参数中的 null 值转换为 undefined,以防止向 API 发送 null 值 * ✨ feat: 更新参数处理逻辑,使用 null 表示禁用标记,优化前端与后端的参数同步 * ✨ feat: 更新 Novita 和 SiliconCloud 模型,添加新模型并优化定价信息 * 🔧 refactor: remove deprecated model and update ID format in AI models * 🔧 feat: update pricing for novita models and add new Ling 1T model to siliconcloud * 🔧 feat: remove deprecated ERNIE model and add DeepSeek V3.2 Exp models to siliconcloud --------- Co-authored-by: Arvin Xu <arvinx@foxmail.com>
This commit is contained in:
@@ -17,7 +17,7 @@ export const MARKET_SIDEBAR_WIDTH = 400;
|
||||
export const FOLDER_WIDTH = 270;
|
||||
export const MAX_WIDTH = 1024;
|
||||
export const FORM_STYLE: FormProps = {
|
||||
itemMinWidth: 'max(30%,240px)',
|
||||
itemMinWidth: 'max(34%, 240px)',
|
||||
style: { maxWidth: MAX_WIDTH, width: '100%' },
|
||||
};
|
||||
export const MOBILE_HEADER_ICON_SIZE: ActionIconProps['size'] = { blockSize: 36, size: 22 };
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { and, eq, inArray } from 'drizzle-orm';
|
||||
import { LLMParams } from 'model-bank';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { DEFAULT_AGENT_CONFIG } from '@/const/settings';
|
||||
@@ -994,6 +995,113 @@ describe('SessionModel', () => {
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should properly delete params when value is undefined', async () => {
|
||||
// Create test session with agent having params
|
||||
const sessionId = 'test-session-delete-params';
|
||||
const agentId = 'test-agent-delete-params';
|
||||
|
||||
await serverDB.transaction(async (trx) => {
|
||||
await trx.insert(sessions).values({
|
||||
id: sessionId,
|
||||
userId,
|
||||
type: 'agent',
|
||||
});
|
||||
|
||||
await trx.insert(agents).values({
|
||||
id: agentId,
|
||||
userId,
|
||||
model: 'gpt-3.5-turbo',
|
||||
title: 'Test Agent',
|
||||
params: {
|
||||
temperature: 0.7,
|
||||
top_p: 1,
|
||||
presence_penalty: 0,
|
||||
frequency_penalty: 0,
|
||||
},
|
||||
});
|
||||
|
||||
await trx.insert(agentsToSessions).values({
|
||||
sessionId,
|
||||
agentId,
|
||||
userId,
|
||||
});
|
||||
});
|
||||
|
||||
// Update config with temperature set to undefined (delete it)
|
||||
await sessionModel.updateConfig(sessionId, {
|
||||
params: {
|
||||
temperature: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
// Verify temperature was deleted while other params remain
|
||||
const updatedAgent = await serverDB
|
||||
.select()
|
||||
.from(agents)
|
||||
.where(and(eq(agents.id, agentId), eq(agents.userId, userId)));
|
||||
|
||||
expect(updatedAgent[0].params).toMatchObject({
|
||||
top_p: 1,
|
||||
presence_penalty: 0,
|
||||
frequency_penalty: 0,
|
||||
});
|
||||
expect(updatedAgent[0].params).not.toHaveProperty('temperature');
|
||||
});
|
||||
|
||||
it('should mark params as null when value is null', async () => {
|
||||
// Create test session with agent having params
|
||||
const sessionId = 'test-session-delete-params-null';
|
||||
const agentId = 'test-agent-delete-params-null';
|
||||
|
||||
await serverDB.transaction(async (trx) => {
|
||||
await trx.insert(sessions).values({
|
||||
id: sessionId,
|
||||
userId,
|
||||
type: 'agent',
|
||||
});
|
||||
|
||||
await trx.insert(agents).values({
|
||||
id: agentId,
|
||||
userId,
|
||||
model: 'gpt-3.5-turbo',
|
||||
title: 'Test Agent',
|
||||
params: {
|
||||
temperature: 0.7,
|
||||
top_p: 1,
|
||||
presence_penalty: 0,
|
||||
frequency_penalty: 0,
|
||||
},
|
||||
});
|
||||
|
||||
await trx.insert(agentsToSessions).values({
|
||||
sessionId,
|
||||
agentId,
|
||||
userId,
|
||||
});
|
||||
});
|
||||
|
||||
// Update config with temperature set to null (mark it as disabled)
|
||||
await sessionModel.updateConfig(sessionId, {
|
||||
params: {
|
||||
temperature: null,
|
||||
} as any,
|
||||
});
|
||||
|
||||
// Verify temperature is marked as null while other params remain untouched
|
||||
const updatedAgent = await serverDB
|
||||
.select()
|
||||
.from(agents)
|
||||
.where(and(eq(agents.id, agentId), eq(agents.userId, userId)));
|
||||
|
||||
expect(updatedAgent[0].params).toMatchObject({
|
||||
top_p: 1,
|
||||
presence_penalty: 0,
|
||||
frequency_penalty: 0,
|
||||
temperature: null,
|
||||
});
|
||||
expect((updatedAgent[0].params as LLMParams)?.temperature).toBeNull();
|
||||
});
|
||||
|
||||
it('should throw error if session has no associated agent', async () => {
|
||||
// Create session without agent
|
||||
const sessionId = 'session-no-agent';
|
||||
|
||||
@@ -435,7 +435,47 @@ export class SessionModel {
|
||||
);
|
||||
}
|
||||
|
||||
const mergedValue = merge(session.agent, data);
|
||||
// 先处理参数字段:undefined 表示删除,null 表示禁用标记
|
||||
const existingParams = session.agent.params ?? {};
|
||||
const updatedParams: Record<string, any> = { ...existingParams };
|
||||
|
||||
if (data.params) {
|
||||
const incomingParams = data.params as Record<string, any>;
|
||||
Object.keys(incomingParams).forEach((key) => {
|
||||
const incomingValue = incomingParams[key];
|
||||
|
||||
// undefined 代表显式删除该字段
|
||||
if (incomingValue === undefined) {
|
||||
delete updatedParams[key];
|
||||
return;
|
||||
}
|
||||
|
||||
// 其余值(包括 null)都直接覆盖,null 表示在前端禁用该参数
|
||||
updatedParams[key] = incomingValue;
|
||||
});
|
||||
}
|
||||
|
||||
// 构建要合并的数据,排除 params(单独处理)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { params: _params, ...restData } = data;
|
||||
const mergedValue = merge(session.agent, restData);
|
||||
|
||||
// 应用处理后的参数
|
||||
mergedValue.params = Object.keys(updatedParams).length > 0 ? updatedParams : undefined;
|
||||
|
||||
// 最终清理:确保没有 undefined 或 null 值进入数据库
|
||||
if (mergedValue.params) {
|
||||
const params = mergedValue.params as Record<string, any>;
|
||||
Object.keys(params).forEach((key) => {
|
||||
if (params[key] === undefined) {
|
||||
delete params[key];
|
||||
}
|
||||
});
|
||||
if (Object.keys(params).length === 0) {
|
||||
mergedValue.params = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return this.db
|
||||
.update(agents)
|
||||
.set(mergedValue)
|
||||
|
||||
@@ -130,23 +130,6 @@ const groqChatModels: AIChatModelCard[] = [
|
||||
},
|
||||
type: 'chat',
|
||||
},
|
||||
{
|
||||
abilities: {
|
||||
functionCall: true,
|
||||
reasoning: true,
|
||||
},
|
||||
contextWindowTokens: 131_072,
|
||||
displayName: 'DeepSeek R1 Distill Llama 70B',
|
||||
id: 'deepseek-r1-distill-llama-70b',
|
||||
maxOutput: 131_072,
|
||||
pricing: {
|
||||
units: [
|
||||
{ name: 'textInput', rate: 0.75, strategy: 'fixed', unit: 'millionTokens' },
|
||||
{ name: 'textOutput', rate: 0.99, strategy: 'fixed', unit: 'millionTokens' },
|
||||
],
|
||||
},
|
||||
type: 'chat',
|
||||
},
|
||||
{
|
||||
abilities: {
|
||||
functionCall: true,
|
||||
|
||||
@@ -311,21 +311,6 @@ const novitaChatModels: AIChatModelCard[] = [
|
||||
},
|
||||
type: 'chat',
|
||||
},
|
||||
{
|
||||
abilities: {
|
||||
functionCall: true,
|
||||
},
|
||||
contextWindowTokens: 120_000,
|
||||
displayName: 'ERNIE 4.5 0.3B',
|
||||
id: 'baidu/ernie-4.5-0.3b',
|
||||
pricing: {
|
||||
units: [
|
||||
{ name: 'textInput', rate: 0, strategy: 'fixed', unit: 'millionTokens' },
|
||||
{ name: 'textOutput', rate: 0, strategy: 'fixed', unit: 'millionTokens' },
|
||||
],
|
||||
},
|
||||
type: 'chat',
|
||||
},
|
||||
{
|
||||
abilities: {
|
||||
functionCall: true,
|
||||
@@ -432,8 +417,8 @@ const novitaChatModels: AIChatModelCard[] = [
|
||||
id: 'qwen/qwen3-4b-fp8',
|
||||
pricing: {
|
||||
units: [
|
||||
{ name: 'textInput', rate: 0, strategy: 'fixed', unit: 'millionTokens' },
|
||||
{ name: 'textOutput', rate: 0, strategy: 'fixed', unit: 'millionTokens' },
|
||||
{ name: 'textInput', rate: 0.03, strategy: 'fixed', unit: 'millionTokens' },
|
||||
{ name: 'textOutput', rate: 0.03, strategy: 'fixed', unit: 'millionTokens' },
|
||||
],
|
||||
},
|
||||
type: 'chat',
|
||||
@@ -634,19 +619,6 @@ const novitaChatModels: AIChatModelCard[] = [
|
||||
},
|
||||
type: 'chat',
|
||||
},
|
||||
{
|
||||
contextWindowTokens: 32_768,
|
||||
description: 'Mistral 7B Instruct 是一款兼有速度优化和长上下文支持的高性能行业标准模型。',
|
||||
displayName: 'Mistral 7B Instruct',
|
||||
id: 'mistralai/mistral-7b-instruct',
|
||||
pricing: {
|
||||
units: [
|
||||
{ name: 'textInput', rate: 0.029, strategy: 'fixed', unit: 'millionTokens' },
|
||||
{ name: 'textOutput', rate: 0.059, strategy: 'fixed', unit: 'millionTokens' },
|
||||
],
|
||||
},
|
||||
type: 'chat',
|
||||
},
|
||||
{
|
||||
contextWindowTokens: 65_535,
|
||||
description: 'WizardLM-2 8x22B 是微软AI最先进的Wizard模型,显示出极其竞争力的表现。',
|
||||
@@ -834,21 +806,6 @@ const novitaChatModels: AIChatModelCard[] = [
|
||||
},
|
||||
type: 'chat',
|
||||
},
|
||||
{
|
||||
abilities: {
|
||||
reasoning: true,
|
||||
},
|
||||
contextWindowTokens: 32_000,
|
||||
displayName: 'Deepseek R1 Distill Llama 8B',
|
||||
id: 'deepseek/deepseek-r1-distill-llama-8b',
|
||||
pricing: {
|
||||
units: [
|
||||
{ name: 'textInput', rate: 0.04, strategy: 'fixed', unit: 'millionTokens' },
|
||||
{ name: 'textOutput', rate: 0.04, strategy: 'fixed', unit: 'millionTokens' },
|
||||
],
|
||||
},
|
||||
type: 'chat',
|
||||
},
|
||||
{
|
||||
abilities: {
|
||||
functionCall: true,
|
||||
@@ -988,21 +945,6 @@ const novitaChatModels: AIChatModelCard[] = [
|
||||
},
|
||||
type: 'chat',
|
||||
},
|
||||
{
|
||||
abilities: {
|
||||
functionCall: true,
|
||||
},
|
||||
contextWindowTokens: 32_000,
|
||||
displayName: 'GLM 4 32B 0414',
|
||||
id: 'thudm/glm-4-32b-0414',
|
||||
pricing: {
|
||||
units: [
|
||||
{ name: 'textInput', rate: 0.55, strategy: 'fixed', unit: 'millionTokens' },
|
||||
{ name: 'textOutput', rate: 1.66, strategy: 'fixed', unit: 'millionTokens' },
|
||||
],
|
||||
},
|
||||
type: 'chat',
|
||||
},
|
||||
];
|
||||
|
||||
export const allModels = [...novitaChatModels];
|
||||
|
||||
@@ -2,6 +2,25 @@ import { AIChatModelCard, AIImageModelCard } from '../types/aiModel';
|
||||
|
||||
// https://siliconflow.cn/zh-cn/models
|
||||
const siliconcloudChatModels: AIChatModelCard[] = [
|
||||
{
|
||||
abilities: {
|
||||
functionCall: true,
|
||||
},
|
||||
contextWindowTokens: 131_072,
|
||||
description:
|
||||
'Ling-1T 是 "灵 2.0" 系列的首款旗舰级 non-thinking 模型,拥有 1 万亿总参数和每 token 约 500 亿个活动参数。基于灵 2.0 架构构建,Ling-1T 旨在突破高效推理和可扩展认知的极限。Ling-1T-base 在超过 20 万亿个高质量、推理密集的 token 上进行训练,针对大型知识密集型任务与长文档理解进行了优化,具备出色的工具调用和上下文记忆能力。',
|
||||
displayName: 'Ling 1T',
|
||||
id: 'inclusionAI/Ling-1T',
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
units: [
|
||||
{ name: 'textInput', rate: 4, strategy: 'fixed', unit: 'millionTokens' },
|
||||
{ name: 'textOutput', rate: 16, strategy: 'fixed', unit: 'millionTokens' },
|
||||
],
|
||||
},
|
||||
releasedAt: '2025-10-09',
|
||||
type: 'chat',
|
||||
},
|
||||
{
|
||||
abilities: {
|
||||
functionCall: true,
|
||||
@@ -106,6 +125,130 @@ const siliconcloudChatModels: AIChatModelCard[] = [
|
||||
releasedAt: '2025-09-10',
|
||||
type: 'chat',
|
||||
},
|
||||
{
|
||||
abilities: {
|
||||
functionCall: true,
|
||||
vision: true,
|
||||
},
|
||||
contextWindowTokens: 256_000,
|
||||
description:
|
||||
'Qwen3-VL-30B-A3B-Instruct 是 Qwen3-VL 系列的指令微调版本,具有强大的视觉-语言理解与生成能力,原生支持 256K 上下文长度,适合多模态对话与图像条件生成任务。',
|
||||
displayName: 'Qwen3 VL 30B A3B Instruct',
|
||||
id: 'Qwen/Qwen3-VL-30B-A3B-Instruct',
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
units: [
|
||||
{ name: 'textInput', rate: 0.7, strategy: 'fixed', unit: 'millionTokens' },
|
||||
{ name: 'textOutput', rate: 2.8, strategy: 'fixed', unit: 'millionTokens' },
|
||||
],
|
||||
},
|
||||
type: 'chat',
|
||||
},
|
||||
{
|
||||
abilities: {
|
||||
functionCall: true,
|
||||
reasoning: true,
|
||||
vision: true,
|
||||
},
|
||||
contextWindowTokens: 256_000,
|
||||
description:
|
||||
'Qwen3-VL-30B-A3B-Thinking 是 Qwen3-VL 的推理增强版本(Thinking),在多模态推理、图像到代码和复杂视觉理解任务上进行了优化,支持 256K 上下文并具备更强的链式思考能力。',
|
||||
displayName: 'Qwen3 VL 30B A3B Thinking',
|
||||
id: 'Qwen/Qwen3-VL-30B-A3B-Thinking',
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
units: [
|
||||
{ name: 'textInput', rate: 0.7, strategy: 'fixed', unit: 'millionTokens' },
|
||||
{ name: 'textOutput', rate: 2.8, strategy: 'fixed', unit: 'millionTokens' },
|
||||
],
|
||||
},
|
||||
type: 'chat',
|
||||
},
|
||||
{
|
||||
abilities: {
|
||||
functionCall: true,
|
||||
vision: true,
|
||||
},
|
||||
contextWindowTokens: 256_000,
|
||||
description:
|
||||
'Qwen3-VL-235B-A22B-Instruct 是 Qwen3-VL 系列的大型指令微调模型,基于混合专家(MoE)架构,拥有卓越的多模态理解与生成能力,原生支持 256K 上下文,适用于高并发生产级多模态服务。',
|
||||
displayName: 'Qwen3 VL 235B A22B Instruct',
|
||||
id: 'Qwen/Qwen3-VL-235B-A22B-Instruct',
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
units: [
|
||||
{ name: 'textInput', rate: 2.5, strategy: 'fixed', unit: 'millionTokens' },
|
||||
{ name: 'textOutput', rate: 10, strategy: 'fixed', unit: 'millionTokens' },
|
||||
],
|
||||
},
|
||||
type: 'chat',
|
||||
},
|
||||
{
|
||||
abilities: {
|
||||
functionCall: true,
|
||||
reasoning: true,
|
||||
vision: true,
|
||||
},
|
||||
contextWindowTokens: 256_000,
|
||||
description:
|
||||
'Qwen3-VL-235B-A22B-Thinking 是 Qwen3-VL 系列中的旗舰思考版本,针对复杂多模态推理、长上下文推理与智能体交互进行了专项优化,适合需要深度思考与视觉推理的企业级场景。',
|
||||
displayName: 'Qwen3 VL 235B A22B Thinking',
|
||||
id: 'Qwen/Qwen3-VL-235B-A22B-Thinking',
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
units: [
|
||||
{ name: 'textInput', rate: 2.5, strategy: 'fixed', unit: 'millionTokens' },
|
||||
{ name: 'textOutput', rate: 10, strategy: 'fixed', unit: 'millionTokens' },
|
||||
],
|
||||
},
|
||||
type: 'chat',
|
||||
},
|
||||
{
|
||||
abilities: {
|
||||
functionCall: true,
|
||||
reasoning: true,
|
||||
},
|
||||
contextWindowTokens: 163_840,
|
||||
description:
|
||||
'DeepSeek-V3.2-Exp 是 DeepSeek 发布的实验性 V3.2 版本,作为迈向下一代架构的中间探索。它在 V3.1-Terminus 的基础上引入了 DeepSeek 稀疏注意力(DeepSeek Sparse Attention,DSA)机制以提升长上下文训练与推理效率,针对工具调用、长文档理解与多步推理进行了专项优化。V3.2-Exp 为研究与产品化之间的桥梁,适合希望在高上下文预算场景中探索更高推理效率的用户。',
|
||||
displayName: 'DeepSeek V3.2 Exp',
|
||||
id: 'deepseek-ai/DeepSeek-V3.2-Exp',
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
units: [
|
||||
{ name: 'textInput', rate: 2, strategy: 'fixed', unit: 'millionTokens' },
|
||||
{ name: 'textOutput', rate: 3, strategy: 'fixed', unit: 'millionTokens' },
|
||||
],
|
||||
},
|
||||
releasedAt: '2025-09-29',
|
||||
settings: {
|
||||
extendParams: ['enableReasoning', 'reasoningBudgetToken'],
|
||||
},
|
||||
type: 'chat',
|
||||
},
|
||||
{
|
||||
abilities: {
|
||||
functionCall: true,
|
||||
reasoning: true,
|
||||
},
|
||||
contextWindowTokens: 163_840,
|
||||
description:
|
||||
'DeepSeek-V3.2-Exp 是 DeepSeek 发布的实验性 V3.2 版本,作为迈向下一代架构的中间探索。它在 V3.1-Terminus 的基础上引入了 DeepSeek 稀疏注意力(DeepSeek Sparse Attention,DSA)机制以提升长上下文训练与推理效率,针对工具调用、长文档理解与多步推理进行了专项优化。V3.2-Exp 为研究与产品化之间的桥梁,适合希望在高上下文预算场景中探索更高推理效率的用户。',
|
||||
displayName: 'DeepSeek V3.2 Exp (Pro)',
|
||||
id: 'Pro/deepseek-ai/DeepSeek-V3.2-Exp',
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
units: [
|
||||
{ name: 'textInput', rate: 2, strategy: 'fixed', unit: 'millionTokens' },
|
||||
{ name: 'textOutput', rate: 3, strategy: 'fixed', unit: 'millionTokens' },
|
||||
],
|
||||
},
|
||||
releasedAt: '2025-09-29',
|
||||
settings: {
|
||||
extendParams: ['enableReasoning', 'reasoningBudgetToken'],
|
||||
},
|
||||
type: 'chat',
|
||||
},
|
||||
{
|
||||
abilities: {
|
||||
functionCall: true,
|
||||
@@ -151,50 +294,6 @@ const siliconcloudChatModels: AIChatModelCard[] = [
|
||||
},
|
||||
type: 'chat',
|
||||
},
|
||||
{
|
||||
abilities: {
|
||||
functionCall: true,
|
||||
reasoning: true,
|
||||
},
|
||||
contextWindowTokens: 163_840,
|
||||
description:
|
||||
'DeepSeek-V3.1 是由深度求索(DeepSeek AI)发布的混合模式大语言模型,它在前代模型的基础上进行了多方面的重要升级。该模型的一大创新是集成了“思考模式”(Thinking Mode)和“非思考模式”(Non-thinking Mode)于一体,用户可以通过调整聊天模板灵活切换,以适应不同的任务需求。通过专门的训练后优化,V3.1 在工具调用和 Agent 任务方面的性能得到了显著增强,能够更好地支持外部搜索工具和执行多步复杂任务。该模型基于 DeepSeek-V3.1-Base 进行后训练,通过两阶段长文本扩展方法,大幅增加了训练数据量,使其在处理长文档和长篇代码方面表现更佳。作为一个开源模型,DeepSeek-V3.1 在编码、数学和推理等多个基准测试中展现了与顶尖闭源模型相媲美的能力,同时凭借其混合专家(MoE)架构,在保持巨大模型容量的同时,有效降低了推理成本。',
|
||||
displayName: 'DeepSeek V3.1',
|
||||
id: 'deepseek-ai/DeepSeek-V3.1',
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
units: [
|
||||
{ name: 'textInput', rate: 4, strategy: 'fixed', unit: 'millionTokens' },
|
||||
{ name: 'textOutput', rate: 12, strategy: 'fixed', unit: 'millionTokens' },
|
||||
],
|
||||
},
|
||||
settings: {
|
||||
extendParams: ['enableReasoning', 'reasoningBudgetToken'],
|
||||
},
|
||||
type: 'chat',
|
||||
},
|
||||
{
|
||||
abilities: {
|
||||
functionCall: true,
|
||||
reasoning: true,
|
||||
},
|
||||
contextWindowTokens: 163_840,
|
||||
description:
|
||||
'DeepSeek-V3.1 是由深度求索(DeepSeek AI)发布的混合模式大语言模型,它在前代模型的基础上进行了多方面的重要升级。该模型的一大创新是集成了“思考模式”(Thinking Mode)和“非思考模式”(Non-thinking Mode)于一体,用户可以通过调整聊天模板灵活切换,以适应不同的任务需求。通过专门的训练后优化,V3.1 在工具调用和 Agent 任务方面的性能得到了显著增强,能够更好地支持外部搜索工具和执行多步复杂任务。该模型基于 DeepSeek-V3.1-Base 进行后训练,通过两阶段长文本扩展方法,大幅增加了训练数据量,使其在处理长文档和长篇代码方面表现更佳。作为一个开源模型,DeepSeek-V3.1 在编码、数学和推理等多个基准测试中展现了与顶尖闭源模型相媲美的能力,同时凭借其混合专家(MoE)架构,在保持巨大模型容量的同时,有效降低了推理成本。',
|
||||
displayName: 'DeepSeek V3.1 (Pro)',
|
||||
id: 'Pro/deepseek-ai/DeepSeek-V3.1',
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
units: [
|
||||
{ name: 'textInput', rate: 4, strategy: 'fixed', unit: 'millionTokens' },
|
||||
{ name: 'textOutput', rate: 12, strategy: 'fixed', unit: 'millionTokens' },
|
||||
],
|
||||
},
|
||||
settings: {
|
||||
extendParams: ['enableReasoning', 'reasoningBudgetToken'],
|
||||
},
|
||||
type: 'chat',
|
||||
},
|
||||
{
|
||||
abilities: {
|
||||
functionCall: true,
|
||||
|
||||
@@ -1,26 +1,259 @@
|
||||
'use client';
|
||||
|
||||
import { Form, type FormGroupItemType, Select, SliderWithInput } from '@lobehub/ui';
|
||||
import { Form as AntdForm, Switch } from 'antd';
|
||||
import {
|
||||
Form,
|
||||
type FormGroupItemType,
|
||||
type FormItemProps,
|
||||
Select,
|
||||
SliderWithInput,
|
||||
} from '@lobehub/ui';
|
||||
import { Form as AntdForm, Checkbox, Switch } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import { memo } from 'react';
|
||||
import { memo, useCallback, useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import InfoTooltip from '@/components/InfoTooltip';
|
||||
import { FORM_STYLE } from '@/const/layoutTokens';
|
||||
import ModelSelect from '@/features/ModelSelect';
|
||||
import { useProviderName } from '@/hooks/useProviderName';
|
||||
|
||||
import { selectors, useStore } from '../store';
|
||||
|
||||
type ParamKey = 'temperature' | 'top_p' | 'presence_penalty' | 'frequency_penalty';
|
||||
|
||||
const useStyles = createStyles(({ css, token }) => ({
|
||||
checkbox: css`
|
||||
.ant-checkbox-inner {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&:hover .ant-checkbox-inner {
|
||||
border-color: ${token.colorPrimary};
|
||||
}
|
||||
`,
|
||||
label: css`
|
||||
user-select: none;
|
||||
`,
|
||||
sliderWrapper: css`
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
`,
|
||||
}));
|
||||
|
||||
// Wrapper component for slider with checkbox
|
||||
interface SliderWithCheckboxProps {
|
||||
checked: boolean;
|
||||
disabled: boolean;
|
||||
max: number;
|
||||
min: number;
|
||||
onChange?: (value: number) => void;
|
||||
onToggle: (checked: boolean) => void;
|
||||
step: number;
|
||||
styles: any;
|
||||
value?: number;
|
||||
}
|
||||
|
||||
const SliderWithCheckbox = memo<SliderWithCheckboxProps>(
|
||||
({ value, onChange, disabled, checked, onToggle, styles, min, max, step }) => {
|
||||
return (
|
||||
<div className={styles.sliderWrapper}>
|
||||
<Checkbox
|
||||
checked={checked}
|
||||
className={styles.checkbox}
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
onToggle(e.target.checked);
|
||||
}}
|
||||
/>
|
||||
<div style={{ flex: 1 }}>
|
||||
<SliderWithInput
|
||||
disabled={disabled}
|
||||
max={max}
|
||||
min={min}
|
||||
onChange={onChange}
|
||||
step={step}
|
||||
value={value}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const PARAM_NAME_MAP: Record<ParamKey, (string | number)[]> = {
|
||||
frequency_penalty: ['params', 'frequency_penalty'],
|
||||
presence_penalty: ['params', 'presence_penalty'],
|
||||
temperature: ['params', 'temperature'],
|
||||
top_p: ['params', 'top_p'],
|
||||
};
|
||||
|
||||
const PARAM_DEFAULTS: Record<ParamKey, number> = {
|
||||
frequency_penalty: 0,
|
||||
presence_penalty: 0,
|
||||
temperature: 0.7,
|
||||
top_p: 1,
|
||||
};
|
||||
|
||||
const PARAM_CONFIG = {
|
||||
frequency_penalty: {
|
||||
descKey: 'settingModel.frequencyPenalty.desc',
|
||||
labelKey: 'settingModel.frequencyPenalty.title',
|
||||
slider: { max: 2, min: -2, step: 0.1 },
|
||||
tag: 'frequency_penalty',
|
||||
},
|
||||
presence_penalty: {
|
||||
descKey: 'settingModel.presencePenalty.desc',
|
||||
labelKey: 'settingModel.presencePenalty.title',
|
||||
slider: { max: 2, min: -2, step: 0.1 },
|
||||
tag: 'presence_penalty',
|
||||
},
|
||||
temperature: {
|
||||
descKey: 'settingModel.temperature.desc',
|
||||
labelKey: 'settingModel.temperature.title',
|
||||
slider: { max: 2, min: 0, step: 0.1 },
|
||||
tag: 'temperature',
|
||||
},
|
||||
top_p: {
|
||||
descKey: 'settingModel.topP.desc',
|
||||
labelKey: 'settingModel.topP.title',
|
||||
slider: { max: 1, min: 0, step: 0.1 },
|
||||
tag: 'top_p',
|
||||
},
|
||||
} satisfies Record<
|
||||
ParamKey,
|
||||
{
|
||||
descKey: string;
|
||||
labelKey: string;
|
||||
slider: { max: number; min: number; step: number };
|
||||
tag: string;
|
||||
}
|
||||
>;
|
||||
|
||||
const AgentModal = memo(() => {
|
||||
const { t } = useTranslation('setting');
|
||||
const [form] = Form.useForm();
|
||||
const config = useStore(selectors.currentAgentConfig, isEqual);
|
||||
const { styles } = useStyles();
|
||||
|
||||
const enableMaxTokens = AntdForm.useWatch(['chatConfig', 'enableMaxTokens'], form);
|
||||
const enableReasoningEffort = AntdForm.useWatch(['chatConfig', 'enableReasoningEffort'], form);
|
||||
const config = useStore(selectors.currentAgentConfig, isEqual);
|
||||
|
||||
const updateConfig = useStore((s) => s.setAgentConfig);
|
||||
const providerName = useProviderName(useStore((s) => s.config.provider) as string);
|
||||
const provider = useStore((s) => s.config.provider);
|
||||
const providerName = useProviderName(provider as string);
|
||||
|
||||
const { temperature, top_p, presence_penalty, frequency_penalty } = config.params ?? {};
|
||||
|
||||
const lastValuesRef = useRef<Record<ParamKey, number | undefined>>({
|
||||
frequency_penalty,
|
||||
presence_penalty,
|
||||
temperature,
|
||||
top_p,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldsValue({
|
||||
...config,
|
||||
_modalConfig: {
|
||||
model: config.model,
|
||||
provider: config.provider,
|
||||
},
|
||||
});
|
||||
|
||||
if (typeof temperature === 'number') lastValuesRef.current.temperature = temperature;
|
||||
if (typeof top_p === 'number') lastValuesRef.current.top_p = top_p;
|
||||
if (typeof presence_penalty === 'number') {
|
||||
lastValuesRef.current.presence_penalty = presence_penalty;
|
||||
}
|
||||
if (typeof frequency_penalty === 'number') {
|
||||
lastValuesRef.current.frequency_penalty = frequency_penalty;
|
||||
}
|
||||
}, [config, form, temperature, top_p, presence_penalty, frequency_penalty]);
|
||||
|
||||
const temperatureValue = AntdForm.useWatch(PARAM_NAME_MAP.temperature, form);
|
||||
const topPValue = AntdForm.useWatch(PARAM_NAME_MAP.top_p, form);
|
||||
const presencePenaltyValue = AntdForm.useWatch(PARAM_NAME_MAP.presence_penalty, form);
|
||||
const frequencyPenaltyValue = AntdForm.useWatch(PARAM_NAME_MAP.frequency_penalty, form);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof temperatureValue === 'number') lastValuesRef.current.temperature = temperatureValue;
|
||||
}, [temperatureValue]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof topPValue === 'number') lastValuesRef.current.top_p = topPValue;
|
||||
}, [topPValue]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof presencePenaltyValue === 'number') {
|
||||
lastValuesRef.current.presence_penalty = presencePenaltyValue;
|
||||
}
|
||||
}, [presencePenaltyValue]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof frequencyPenaltyValue === 'number') {
|
||||
lastValuesRef.current.frequency_penalty = frequencyPenaltyValue;
|
||||
}
|
||||
}, [frequencyPenaltyValue]);
|
||||
|
||||
const enabledMap: Record<ParamKey, boolean> = {
|
||||
frequency_penalty: typeof frequencyPenaltyValue === 'number',
|
||||
presence_penalty: typeof presencePenaltyValue === 'number',
|
||||
temperature: typeof temperatureValue === 'number',
|
||||
top_p: typeof topPValue === 'number',
|
||||
};
|
||||
|
||||
const handleToggle = useCallback(
|
||||
(key: ParamKey, enabled: boolean) => {
|
||||
const namePath = PARAM_NAME_MAP[key];
|
||||
|
||||
if (!enabled) {
|
||||
const currentValue = form.getFieldValue(namePath);
|
||||
if (typeof currentValue === 'number') {
|
||||
lastValuesRef.current[key] = currentValue;
|
||||
}
|
||||
form.setFieldValue(namePath, undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
const fallback = lastValuesRef.current[key];
|
||||
const nextValue = typeof fallback === 'number' ? fallback : PARAM_DEFAULTS[key];
|
||||
lastValuesRef.current[key] = nextValue;
|
||||
form.setFieldValue(namePath, nextValue);
|
||||
},
|
||||
[form],
|
||||
);
|
||||
|
||||
const paramItems: FormItemProps[] = (Object.keys(PARAM_CONFIG) as ParamKey[]).map((key) => {
|
||||
const meta = PARAM_CONFIG[key];
|
||||
const enabled = enabledMap[key];
|
||||
|
||||
return {
|
||||
children: (
|
||||
<SliderWithCheckbox
|
||||
checked={enabled}
|
||||
disabled={!enabled}
|
||||
max={meta.slider.max}
|
||||
min={meta.slider.min}
|
||||
onToggle={(checked) => handleToggle(key, checked)}
|
||||
step={meta.slider.step}
|
||||
styles={styles}
|
||||
/>
|
||||
),
|
||||
desc: t(meta.descKey as any),
|
||||
label: (
|
||||
<Flexbox align={'center'} className={styles.label} gap={8} horizontal>
|
||||
{t(meta.labelKey as any)}
|
||||
<InfoTooltip title={t(meta.descKey as any)} />
|
||||
</Flexbox>
|
||||
),
|
||||
name: PARAM_NAME_MAP[key],
|
||||
tag: meta.tag,
|
||||
} satisfies FormItemProps;
|
||||
});
|
||||
|
||||
const model: FormGroupItemType = {
|
||||
children: [
|
||||
@@ -40,34 +273,7 @@ const AgentModal = memo(() => {
|
||||
name: ['chatConfig', 'enableStreaming'],
|
||||
valuePropName: 'checked',
|
||||
},
|
||||
{
|
||||
children: <SliderWithInput max={2} min={0} step={0.1} />,
|
||||
desc: t('settingModel.temperature.desc'),
|
||||
label: t('settingModel.temperature.title'),
|
||||
name: ['params', 'temperature'],
|
||||
tag: 'temperature',
|
||||
},
|
||||
{
|
||||
children: <SliderWithInput max={1} min={0} step={0.1} />,
|
||||
desc: t('settingModel.topP.desc'),
|
||||
label: t('settingModel.topP.title'),
|
||||
name: ['params', 'top_p'],
|
||||
tag: 'top_p',
|
||||
},
|
||||
{
|
||||
children: <SliderWithInput max={2} min={-2} step={0.1} />,
|
||||
desc: t('settingModel.presencePenalty.desc'),
|
||||
label: t('settingModel.presencePenalty.title'),
|
||||
name: ['params', 'presence_penalty'],
|
||||
tag: 'presence_penalty',
|
||||
},
|
||||
{
|
||||
children: <SliderWithInput max={2} min={-2} step={0.1} />,
|
||||
desc: t('settingModel.frequencyPenalty.desc'),
|
||||
label: t('settingModel.frequencyPenalty.title'),
|
||||
name: ['params', 'frequency_penalty'],
|
||||
tag: 'frequency_penalty',
|
||||
},
|
||||
...paramItems,
|
||||
{
|
||||
children: <Switch />,
|
||||
label: t('settingModel.enableMaxTokens.title'),
|
||||
@@ -77,7 +283,15 @@ const AgentModal = memo(() => {
|
||||
valuePropName: 'checked',
|
||||
},
|
||||
{
|
||||
children: <SliderWithInput max={32_000} min={0} step={100} unlimitedInput={true} />,
|
||||
children: (
|
||||
<SliderWithInput
|
||||
disabled={!enableMaxTokens}
|
||||
max={32_000}
|
||||
min={0}
|
||||
step={100}
|
||||
unlimitedInput
|
||||
/>
|
||||
),
|
||||
desc: t('settingModel.maxTokens.desc'),
|
||||
divider: false,
|
||||
hidden: !enableMaxTokens,
|
||||
@@ -137,10 +351,23 @@ const AgentModal = memo(() => {
|
||||
items={[model]}
|
||||
itemsType={'group'}
|
||||
onFinish={({ _modalConfig, ...rest }) => {
|
||||
// 清理 params 中的 undefined 和 null 值,确保禁用的参数被正确移除
|
||||
const cleanedRest = { ...rest };
|
||||
if (cleanedRest.params) {
|
||||
const cleanedParams = { ...cleanedRest.params };
|
||||
(Object.keys(cleanedParams) as Array<keyof typeof cleanedParams>).forEach((key) => {
|
||||
// 使用 null 作为禁用标记(JSON 可以序列化 null,而 undefined 会被忽略)
|
||||
if (cleanedParams[key] === undefined) {
|
||||
cleanedParams[key] = null as any;
|
||||
}
|
||||
});
|
||||
cleanedRest.params = cleanedParams as any;
|
||||
}
|
||||
|
||||
updateConfig({
|
||||
model: _modalConfig?.model,
|
||||
provider: _modalConfig?.provider,
|
||||
...rest,
|
||||
...cleanedRest,
|
||||
});
|
||||
}}
|
||||
variant={'borderless'}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { Form, type FormItemProps, Tag } from '@lobehub/ui';
|
||||
import { Form as AntdForm, Checkbox } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { memo } from 'react';
|
||||
import { memo, useCallback, useEffect, useRef } from 'react';
|
||||
import type { ComponentType } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
@@ -20,75 +23,283 @@ interface ControlsProps {
|
||||
setUpdating: (updating: boolean) => void;
|
||||
updating: boolean;
|
||||
}
|
||||
|
||||
type ParamKey = 'temperature' | 'top_p' | 'presence_penalty' | 'frequency_penalty';
|
||||
|
||||
type ParamLabelKey =
|
||||
| 'settingModel.temperature.title'
|
||||
| 'settingModel.topP.title'
|
||||
| 'settingModel.presencePenalty.title'
|
||||
| 'settingModel.frequencyPenalty.title';
|
||||
|
||||
type ParamDescKey =
|
||||
| 'settingModel.temperature.desc'
|
||||
| 'settingModel.topP.desc'
|
||||
| 'settingModel.presencePenalty.desc'
|
||||
| 'settingModel.frequencyPenalty.desc';
|
||||
|
||||
const useStyles = createStyles(({ css, token }) => ({
|
||||
checkbox: css`
|
||||
.ant-checkbox-inner {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&:hover .ant-checkbox-inner {
|
||||
border-color: ${token.colorPrimary};
|
||||
}
|
||||
`,
|
||||
label: css`
|
||||
user-select: none;
|
||||
`,
|
||||
sliderWrapper: css`
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
`,
|
||||
}));
|
||||
|
||||
// Wrapper component to handle checkbox + slider
|
||||
interface ParamControlWrapperProps {
|
||||
Component: ComponentType<any>;
|
||||
checked: boolean;
|
||||
disabled: boolean;
|
||||
onChange?: (value: number) => void;
|
||||
onToggle: (checked: boolean) => void;
|
||||
styles: any;
|
||||
value?: number;
|
||||
}
|
||||
|
||||
const ParamControlWrapper = memo<ParamControlWrapperProps>(
|
||||
({ Component, value, onChange, disabled, checked, onToggle, styles }) => {
|
||||
return (
|
||||
<div className={styles.sliderWrapper}>
|
||||
<Checkbox
|
||||
checked={checked}
|
||||
className={styles.checkbox}
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
onToggle(e.target.checked);
|
||||
}}
|
||||
/>
|
||||
<div style={{ flex: 1 }}>
|
||||
<Component disabled={disabled} onChange={onChange} value={value} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const PARAM_NAME_MAP: Record<ParamKey, (string | number)[]> = {
|
||||
frequency_penalty: ['params', 'frequency_penalty'],
|
||||
presence_penalty: ['params', 'presence_penalty'],
|
||||
temperature: ['params', 'temperature'],
|
||||
top_p: ['params', 'top_p'],
|
||||
};
|
||||
|
||||
const PARAM_DEFAULTS: Record<ParamKey, number> = {
|
||||
frequency_penalty: 0,
|
||||
presence_penalty: 0,
|
||||
temperature: 0.7,
|
||||
top_p: 1,
|
||||
};
|
||||
|
||||
const PARAM_CONFIG = {
|
||||
frequency_penalty: {
|
||||
Component: FrequencyPenalty,
|
||||
descKey: 'settingModel.frequencyPenalty.desc',
|
||||
labelKey: 'settingModel.frequencyPenalty.title',
|
||||
tag: 'frequency_penalty',
|
||||
},
|
||||
presence_penalty: {
|
||||
Component: PresencePenalty,
|
||||
descKey: 'settingModel.presencePenalty.desc',
|
||||
labelKey: 'settingModel.presencePenalty.title',
|
||||
tag: 'presence_penalty',
|
||||
},
|
||||
temperature: {
|
||||
Component: Temperature,
|
||||
descKey: 'settingModel.temperature.desc',
|
||||
labelKey: 'settingModel.temperature.title',
|
||||
tag: 'temperature',
|
||||
},
|
||||
top_p: {
|
||||
Component: TopP,
|
||||
descKey: 'settingModel.topP.desc',
|
||||
labelKey: 'settingModel.topP.title',
|
||||
tag: 'top_p',
|
||||
},
|
||||
} satisfies Record<
|
||||
ParamKey,
|
||||
{
|
||||
Component: ComponentType<any>;
|
||||
descKey: ParamDescKey;
|
||||
labelKey: ParamLabelKey;
|
||||
tag: string;
|
||||
}
|
||||
>;
|
||||
|
||||
const Controls = memo<ControlsProps>(({ setUpdating }) => {
|
||||
const { t } = useTranslation('setting');
|
||||
const mobile = useServerConfigStore((s) => s.isMobile);
|
||||
const updateAgentConfig = useAgentStore((s) => s.updateAgentConfig);
|
||||
const { styles } = useStyles();
|
||||
|
||||
const config = useAgentStore(agentSelectors.currentAgentConfig, isEqual);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const items: FormItemProps[] = [
|
||||
{
|
||||
children: <Temperature />,
|
||||
const { frequency_penalty, presence_penalty, temperature, top_p } = config.params ?? {};
|
||||
|
||||
const lastValuesRef = useRef<Record<ParamKey, number | undefined>>({
|
||||
frequency_penalty,
|
||||
presence_penalty,
|
||||
temperature,
|
||||
top_p,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldsValue(config);
|
||||
|
||||
if (typeof temperature === 'number') lastValuesRef.current.temperature = temperature;
|
||||
if (typeof top_p === 'number') lastValuesRef.current.top_p = top_p;
|
||||
if (typeof presence_penalty === 'number') {
|
||||
lastValuesRef.current.presence_penalty = presence_penalty;
|
||||
}
|
||||
if (typeof frequency_penalty === 'number') {
|
||||
lastValuesRef.current.frequency_penalty = frequency_penalty;
|
||||
}
|
||||
}, [config, form, frequency_penalty, presence_penalty, temperature, top_p]);
|
||||
|
||||
const temperatureValue = AntdForm.useWatch(PARAM_NAME_MAP.temperature, form);
|
||||
const topPValue = AntdForm.useWatch(PARAM_NAME_MAP.top_p, form);
|
||||
const presencePenaltyValue = AntdForm.useWatch(PARAM_NAME_MAP.presence_penalty, form);
|
||||
const frequencyPenaltyValue = AntdForm.useWatch(PARAM_NAME_MAP.frequency_penalty, form);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof temperatureValue === 'number') lastValuesRef.current.temperature = temperatureValue;
|
||||
}, [temperatureValue]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof topPValue === 'number') lastValuesRef.current.top_p = topPValue;
|
||||
}, [topPValue]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof presencePenaltyValue === 'number') {
|
||||
lastValuesRef.current.presence_penalty = presencePenaltyValue;
|
||||
}
|
||||
}, [presencePenaltyValue]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof frequencyPenaltyValue === 'number') {
|
||||
lastValuesRef.current.frequency_penalty = frequencyPenaltyValue;
|
||||
}
|
||||
}, [frequencyPenaltyValue]);
|
||||
|
||||
const enabledMap: Record<ParamKey, boolean> = {
|
||||
frequency_penalty: typeof frequencyPenaltyValue === 'number',
|
||||
presence_penalty: typeof presencePenaltyValue === 'number',
|
||||
temperature: typeof temperatureValue === 'number',
|
||||
top_p: typeof topPValue === 'number',
|
||||
};
|
||||
|
||||
const handleToggle = useCallback(
|
||||
async (key: ParamKey, enabled: boolean) => {
|
||||
const namePath = PARAM_NAME_MAP[key];
|
||||
let newValue: number | undefined;
|
||||
|
||||
if (!enabled) {
|
||||
const currentValue = form.getFieldValue(namePath);
|
||||
if (typeof currentValue === 'number') {
|
||||
lastValuesRef.current[key] = currentValue;
|
||||
}
|
||||
newValue = undefined;
|
||||
form.setFieldValue(namePath, undefined);
|
||||
} else {
|
||||
const fallback = lastValuesRef.current[key];
|
||||
const nextValue = typeof fallback === 'number' ? fallback : PARAM_DEFAULTS[key];
|
||||
lastValuesRef.current[key] = nextValue;
|
||||
newValue = nextValue;
|
||||
form.setFieldValue(namePath, nextValue);
|
||||
}
|
||||
|
||||
// 立即保存变更 - 手动构造配置对象确保使用最新值
|
||||
setUpdating(true);
|
||||
const currentValues = form.getFieldsValue();
|
||||
const prevParams = (currentValues.params ?? {}) as Record<ParamKey, number | undefined>;
|
||||
const currentParams: Record<ParamKey, number | undefined> = { ...prevParams };
|
||||
|
||||
if (newValue === undefined) {
|
||||
// 显式删除该属性,而不是设置为 undefined
|
||||
// 这样可以确保 Form 表单状态同步
|
||||
delete currentParams[key];
|
||||
// 使用 null 作为禁用标记(数据库会保留 null,前端据此判断复选框状态)
|
||||
currentParams[key] = null as any;
|
||||
} else {
|
||||
currentParams[key] = newValue;
|
||||
}
|
||||
|
||||
const updatedConfig = {
|
||||
...currentValues,
|
||||
params: currentParams,
|
||||
};
|
||||
|
||||
await updateAgentConfig(updatedConfig);
|
||||
setUpdating(false);
|
||||
},
|
||||
[form, setUpdating, updateAgentConfig],
|
||||
);
|
||||
|
||||
// 使用 useMemo 确保防抖函数只创建一次
|
||||
const handleValuesChange = useCallback(
|
||||
debounce(async (values) => {
|
||||
setUpdating(true);
|
||||
await updateAgentConfig(values);
|
||||
setUpdating(false);
|
||||
}, 500),
|
||||
[updateAgentConfig, setUpdating],
|
||||
);
|
||||
|
||||
const baseItems: FormItemProps[] = (Object.keys(PARAM_CONFIG) as ParamKey[]).map((key) => {
|
||||
const meta = PARAM_CONFIG[key];
|
||||
const Component = meta.Component;
|
||||
const enabled = enabledMap[key];
|
||||
|
||||
return {
|
||||
children: (
|
||||
<ParamControlWrapper
|
||||
Component={Component}
|
||||
checked={enabled}
|
||||
disabled={!enabled}
|
||||
onToggle={(checked) => handleToggle(key, checked)}
|
||||
styles={styles}
|
||||
/>
|
||||
),
|
||||
label: (
|
||||
<Flexbox align={'center'} gap={8} horizontal justify={'space-between'}>
|
||||
{t('settingModel.temperature.title')}
|
||||
<InfoTooltip title={t('settingModel.temperature.desc')} />
|
||||
<Flexbox align={'center'} className={styles.label} gap={8} horizontal>
|
||||
{t(meta.labelKey)}
|
||||
<InfoTooltip title={t(meta.descKey)} />
|
||||
</Flexbox>
|
||||
),
|
||||
name: ['params', 'temperature'],
|
||||
tag: 'temperature',
|
||||
},
|
||||
{
|
||||
children: <TopP />,
|
||||
label: (
|
||||
<Flexbox gap={8} horizontal>
|
||||
{t('settingModel.topP.title')}
|
||||
<InfoTooltip title={t('settingModel.topP.desc')} />
|
||||
</Flexbox>
|
||||
),
|
||||
name: ['params', 'top_p'],
|
||||
tag: 'top_p',
|
||||
},
|
||||
{
|
||||
children: <PresencePenalty />,
|
||||
label: (
|
||||
<Flexbox gap={8} horizontal>
|
||||
{t('settingModel.presencePenalty.title')}
|
||||
<InfoTooltip title={t('settingModel.presencePenalty.desc')} />
|
||||
</Flexbox>
|
||||
),
|
||||
name: ['params', 'presence_penalty'],
|
||||
tag: 'presence_penalty',
|
||||
},
|
||||
{
|
||||
children: <FrequencyPenalty />,
|
||||
label: (
|
||||
<Flexbox gap={8} horizontal>
|
||||
{t('settingModel.frequencyPenalty.title')}
|
||||
<InfoTooltip title={t('settingModel.frequencyPenalty.desc')} />
|
||||
</Flexbox>
|
||||
),
|
||||
name: ['params', 'frequency_penalty'],
|
||||
tag: 'frequency_penalty',
|
||||
},
|
||||
];
|
||||
name: PARAM_NAME_MAP[key],
|
||||
tag: meta.tag,
|
||||
} satisfies FormItemProps;
|
||||
});
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
initialValues={config}
|
||||
itemMinWidth={200}
|
||||
itemMinWidth={220}
|
||||
items={
|
||||
mobile
|
||||
? items
|
||||
: items.map(({ tag, ...item }) => ({ ...item, desc: <Tag size={'small'}>{tag}</Tag> }))
|
||||
? baseItems
|
||||
: baseItems.map(({ tag, ...item }) => ({
|
||||
...item,
|
||||
desc: <Tag size={'small'}>{tag}</Tag>,
|
||||
}))
|
||||
}
|
||||
itemsType={'flat'}
|
||||
onValuesChange={debounce(async (values) => {
|
||||
setUpdating(true);
|
||||
await updateAgentConfig(values);
|
||||
setUpdating(false);
|
||||
}, 500)}
|
||||
onValuesChange={handleValuesChange}
|
||||
styles={{
|
||||
group: {
|
||||
background: 'transparent',
|
||||
|
||||
@@ -5,16 +5,20 @@ import { memo } from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
interface FrequencyPenaltyProps {
|
||||
disabled?: boolean;
|
||||
onChange?: (value: number) => void;
|
||||
value?: number;
|
||||
}
|
||||
|
||||
const FrequencyPenalty = memo<FrequencyPenaltyProps>(({ value, onChange }) => {
|
||||
const FrequencyPenalty = memo<FrequencyPenaltyProps>(({ value, onChange, disabled }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Flexbox style={{ paddingInlineStart: 8 }}>
|
||||
<Flexbox style={{ width: '100%' }}>
|
||||
<SliderWithInput
|
||||
changeOnWheel
|
||||
controls={false}
|
||||
disabled={disabled}
|
||||
marks={{
|
||||
'-2': (
|
||||
<Icon icon={FileIcon} size={'small'} style={{ color: theme.colorTextQuaternary }} />
|
||||
@@ -29,9 +33,10 @@ const FrequencyPenalty = memo<FrequencyPenaltyProps>(({ value, onChange }) => {
|
||||
onChange={onChange}
|
||||
size={'small'}
|
||||
step={0.1}
|
||||
style={{ height: 42 }}
|
||||
styles={{
|
||||
input: {
|
||||
maxWidth: 64,
|
||||
maxWidth: 43,
|
||||
},
|
||||
}}
|
||||
value={value}
|
||||
|
||||
@@ -5,16 +5,20 @@ import { memo } from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
interface PresencePenaltyProps {
|
||||
disabled?: boolean;
|
||||
onChange?: (value: number) => void;
|
||||
value?: number;
|
||||
}
|
||||
|
||||
const PresencePenalty = memo<PresencePenaltyProps>(({ value, onChange }) => {
|
||||
const PresencePenalty = memo<PresencePenaltyProps>(({ value, onChange, disabled }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Flexbox style={{ paddingInlineStart: 8 }}>
|
||||
<Flexbox style={{ width: '100%' }}>
|
||||
<SliderWithInput
|
||||
changeOnWheel
|
||||
controls={false}
|
||||
disabled={disabled}
|
||||
marks={{
|
||||
'-2': (
|
||||
<Icon icon={RepeatIcon} size={'small'} style={{ color: theme.colorTextQuaternary }} />
|
||||
@@ -27,9 +31,10 @@ const PresencePenalty = memo<PresencePenaltyProps>(({ value, onChange }) => {
|
||||
onChange={onChange}
|
||||
size={'small'}
|
||||
step={0.1}
|
||||
style={{ height: 42 }}
|
||||
styles={{
|
||||
input: {
|
||||
maxWidth: 64,
|
||||
maxWidth: 43,
|
||||
},
|
||||
}}
|
||||
value={value}
|
||||
|
||||
@@ -41,16 +41,19 @@ const Warning = memo(() => {
|
||||
});
|
||||
|
||||
interface TemperatureProps {
|
||||
disabled?: boolean;
|
||||
onChange?: (value: number) => void;
|
||||
value?: number;
|
||||
}
|
||||
|
||||
const Temperature = memo<TemperatureProps>(({ value, onChange }) => {
|
||||
const Temperature = memo<TemperatureProps>(({ value, onChange, disabled }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Flexbox gap={4} style={{ paddingInlineStart: 8 }}>
|
||||
<Flexbox gap={4} style={{ width: '100%' }}>
|
||||
<SliderWithInput
|
||||
changeOnWheel
|
||||
controls={false}
|
||||
disabled={disabled}
|
||||
marks={{
|
||||
0: <Icon icon={Sparkle} size={'small'} style={{ color: theme.colorTextQuaternary }} />,
|
||||
1: <div />,
|
||||
@@ -60,15 +63,15 @@ const Temperature = memo<TemperatureProps>(({ value, onChange }) => {
|
||||
onChange={onChange}
|
||||
size={'small'}
|
||||
step={0.1}
|
||||
style={{ height: 48 }}
|
||||
style={{ height: 42 }}
|
||||
styles={{
|
||||
input: {
|
||||
maxWidth: 64,
|
||||
maxWidth: 43,
|
||||
},
|
||||
}}
|
||||
value={value}
|
||||
/>
|
||||
<Warning />
|
||||
{!disabled && <Warning />}
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -5,16 +5,20 @@ import { memo } from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
interface TopPProps {
|
||||
disabled?: boolean;
|
||||
onChange?: (value: number) => void;
|
||||
value?: number;
|
||||
}
|
||||
|
||||
const TopP = memo<TopPProps>(({ value, onChange }) => {
|
||||
const TopP = memo<TopPProps>(({ value, onChange, disabled }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Flexbox style={{ paddingInlineStart: 8 }}>
|
||||
<Flexbox style={{ width: '100%' }}>
|
||||
<SliderWithInput
|
||||
changeOnWheel
|
||||
controls={false}
|
||||
disabled={disabled}
|
||||
marks={{
|
||||
0: (
|
||||
<Icon
|
||||
@@ -31,9 +35,10 @@ const TopP = memo<TopPProps>(({ value, onChange }) => {
|
||||
onChange={onChange}
|
||||
size={'small'}
|
||||
step={0.1}
|
||||
style={{ height: 42 }}
|
||||
styles={{
|
||||
input: {
|
||||
maxWidth: 64,
|
||||
maxWidth: 43,
|
||||
},
|
||||
}}
|
||||
value={value}
|
||||
|
||||
@@ -274,6 +274,12 @@ class ChatService {
|
||||
{ ...res, apiMode, model },
|
||||
);
|
||||
|
||||
// Convert null to undefined for model params to prevent sending null values to API
|
||||
if (payload.temperature === null) payload.temperature = undefined;
|
||||
if (payload.top_p === null) payload.top_p = undefined;
|
||||
if (payload.presence_penalty === null) payload.presence_penalty = undefined;
|
||||
if (payload.frequency_penalty === null) payload.frequency_penalty = undefined;
|
||||
|
||||
const sdkType = resolveRuntimeProvider(provider);
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user