Commit 34842fc1 by DigHuang Committed by GitHub

feat(reason): configurable reasoning effort with tool-call propagation (#6837)

* feat(reason): reasoning effort config

* feat: reasoning effort config ui

* refactor: price UI and add bordered table style

* feat(reason): add default reasoning effort and propagate to tool call node

* chore: ai-proxy version

* refactor(ai): narrow openai v6 tool types and tidy reasoning UI

* chore(ai): drop unused httpsAgent global and restore error-path

* feat(ai): add reasoning effort tip in AI settings modal
parent be0a41a9
...@@ -323,7 +323,7 @@ services: ...@@ -323,7 +323,7 @@ services:
# AI Proxy # AI Proxy
fastgpt-aiproxy: fastgpt-aiproxy:
image: registry.cn-hangzhou.aliyuncs.com/labring/aiproxy:v0.5.3 image: registry.cn-hangzhou.aliyuncs.com/labring/aiproxy:v0.5.6
container_name: fastgpt-aiproxy container_name: fastgpt-aiproxy
restart: unless-stopped restart: unless-stopped
ports: ports:
......
...@@ -323,7 +323,7 @@ services: ...@@ -323,7 +323,7 @@ services:
# AI Proxy # AI Proxy
fastgpt-aiproxy: fastgpt-aiproxy:
image: ghcr.io/labring/aiproxy:v0.5.3 image: ghcr.io/labring/aiproxy:v0.5.6
container_name: fastgpt-aiproxy container_name: fastgpt-aiproxy
restart: unless-stopped restart: unless-stopped
ports: ports:
......
...@@ -463,7 +463,7 @@ services: ...@@ -463,7 +463,7 @@ services:
# AI Proxy # AI Proxy
fastgpt-aiproxy: fastgpt-aiproxy:
image: registry.cn-hangzhou.aliyuncs.com/labring/aiproxy:v0.5.3 image: registry.cn-hangzhou.aliyuncs.com/labring/aiproxy:v0.5.6
container_name: fastgpt-aiproxy container_name: fastgpt-aiproxy
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
......
...@@ -441,7 +441,7 @@ services: ...@@ -441,7 +441,7 @@ services:
# AI Proxy # AI Proxy
fastgpt-aiproxy: fastgpt-aiproxy:
image: registry.cn-hangzhou.aliyuncs.com/labring/aiproxy:v0.5.3 image: registry.cn-hangzhou.aliyuncs.com/labring/aiproxy:v0.5.6
container_name: fastgpt-aiproxy container_name: fastgpt-aiproxy
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
......
...@@ -424,7 +424,7 @@ services: ...@@ -424,7 +424,7 @@ services:
# AI Proxy # AI Proxy
fastgpt-aiproxy: fastgpt-aiproxy:
image: registry.cn-hangzhou.aliyuncs.com/labring/aiproxy:v0.5.3 image: registry.cn-hangzhou.aliyuncs.com/labring/aiproxy:v0.5.6
container_name: fastgpt-aiproxy container_name: fastgpt-aiproxy
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
......
...@@ -423,7 +423,7 @@ services: ...@@ -423,7 +423,7 @@ services:
# AI Proxy # AI Proxy
fastgpt-aiproxy: fastgpt-aiproxy:
image: registry.cn-hangzhou.aliyuncs.com/labring/aiproxy:v0.5.3 image: registry.cn-hangzhou.aliyuncs.com/labring/aiproxy:v0.5.6
container_name: fastgpt-aiproxy container_name: fastgpt-aiproxy
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
......
...@@ -428,7 +428,7 @@ services: ...@@ -428,7 +428,7 @@ services:
# AI Proxy # AI Proxy
fastgpt-aiproxy: fastgpt-aiproxy:
image: registry.cn-hangzhou.aliyuncs.com/labring/aiproxy:v0.5.3 image: registry.cn-hangzhou.aliyuncs.com/labring/aiproxy:v0.5.6
container_name: fastgpt-aiproxy container_name: fastgpt-aiproxy
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
......
...@@ -406,7 +406,7 @@ services: ...@@ -406,7 +406,7 @@ services:
# AI Proxy # AI Proxy
fastgpt-aiproxy: fastgpt-aiproxy:
image: registry.cn-hangzhou.aliyuncs.com/labring/aiproxy:v0.5.3 image: registry.cn-hangzhou.aliyuncs.com/labring/aiproxy:v0.5.6
container_name: fastgpt-aiproxy container_name: fastgpt-aiproxy
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
......
...@@ -463,7 +463,7 @@ services: ...@@ -463,7 +463,7 @@ services:
# AI Proxy # AI Proxy
fastgpt-aiproxy: fastgpt-aiproxy:
image: ghcr.io/labring/aiproxy:v0.5.3 image: ghcr.io/labring/aiproxy:v0.5.6
container_name: fastgpt-aiproxy container_name: fastgpt-aiproxy
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
......
...@@ -441,7 +441,7 @@ services: ...@@ -441,7 +441,7 @@ services:
# AI Proxy # AI Proxy
fastgpt-aiproxy: fastgpt-aiproxy:
image: ghcr.io/labring/aiproxy:v0.5.3 image: ghcr.io/labring/aiproxy:v0.5.6
container_name: fastgpt-aiproxy container_name: fastgpt-aiproxy
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
......
...@@ -424,7 +424,7 @@ services: ...@@ -424,7 +424,7 @@ services:
# AI Proxy # AI Proxy
fastgpt-aiproxy: fastgpt-aiproxy:
image: ghcr.io/labring/aiproxy:v0.5.3 image: ghcr.io/labring/aiproxy:v0.5.6
container_name: fastgpt-aiproxy container_name: fastgpt-aiproxy
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
......
...@@ -423,7 +423,7 @@ services: ...@@ -423,7 +423,7 @@ services:
# AI Proxy # AI Proxy
fastgpt-aiproxy: fastgpt-aiproxy:
image: ghcr.io/labring/aiproxy:v0.5.3 image: ghcr.io/labring/aiproxy:v0.5.6
container_name: fastgpt-aiproxy container_name: fastgpt-aiproxy
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
......
...@@ -428,7 +428,7 @@ services: ...@@ -428,7 +428,7 @@ services:
# AI Proxy # AI Proxy
fastgpt-aiproxy: fastgpt-aiproxy:
image: ghcr.io/labring/aiproxy:v0.5.3 image: ghcr.io/labring/aiproxy:v0.5.6
container_name: fastgpt-aiproxy container_name: fastgpt-aiproxy
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
......
...@@ -406,7 +406,7 @@ services: ...@@ -406,7 +406,7 @@ services:
# AI Proxy # AI Proxy
fastgpt-aiproxy: fastgpt-aiproxy:
image: ghcr.io/labring/aiproxy:v0.5.3 image: ghcr.io/labring/aiproxy:v0.5.6
container_name: fastgpt-aiproxy container_name: fastgpt-aiproxy
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
......
import { i18nT } from '../../../web/i18n/utils'; import { i18nT } from '../../../web/i18n/utils';
import type { CompletionUsage } from './llm/type'; import type { CompletionUsage, ReasoningEffort } from './llm/type';
import type { LLMModelItemType, EmbeddingModelItemType, STTModelType } from './model.schema'; import type { LLMModelItemType, EmbeddingModelItemType, STTModelType } from './model.schema';
export const getLLMDefaultUsage = (): CompletionUsage => { export const getLLMDefaultUsage = (): CompletionUsage => {
...@@ -88,6 +88,16 @@ export enum EmbeddingTypeEnm { ...@@ -88,6 +88,16 @@ export enum EmbeddingTypeEnm {
db = 'db' db = 'db'
} }
export const reasoningEffortList: { label: string; value: ReasoningEffort }[] = [
{ label: i18nT('common:reasoning_effort.default'), value: null },
{ label: i18nT('common:reasoning_effort.none'), value: 'none' },
{ label: i18nT('common:reasoning_effort.minimal'), value: 'minimal' },
{ label: i18nT('common:reasoning_effort.low'), value: 'low' },
{ label: i18nT('common:reasoning_effort.medium'), value: 'medium' },
{ label: i18nT('common:reasoning_effort.high'), value: 'high' },
{ label: i18nT('common:reasoning_effort.xhigh'), value: 'xhigh' }
];
export const completionFinishReasonMap = { export const completionFinishReasonMap = {
error: i18nT('chat:completion_finish_error'), error: i18nT('chat:completion_finish_error'),
close: i18nT('chat:completion_finish_close'), close: i18nT('chat:completion_finish_close'),
......
...@@ -242,6 +242,14 @@ export type CompletionFinishReason = z.infer<typeof CompletionFinishReasonSchema ...@@ -242,6 +242,14 @@ export type CompletionFinishReason = z.infer<typeof CompletionFinishReasonSchema
export * from 'openai'; export * from 'openai';
export * from 'openai/resources'; export * from 'openai/resources';
// openai v6 把 ChatCompletionTool 拆成 function | custom 联合,FastGPT 内部仅产/消费 function
import type {
ChatCompletionFunctionTool,
ChatCompletionReasoningEffort
} from 'openai/resources/chat/completions';
export type ChatCompletionTool = ChatCompletionFunctionTool;
export type ReasoningEffort = ChatCompletionReasoningEffort;
export type PromptTemplateItem = { export type PromptTemplateItem = {
title: string; title: string;
desc: string; desc: string;
......
...@@ -13,6 +13,7 @@ export const getLLMSupportParams = (llm?: LLMModelItemType) => { ...@@ -13,6 +13,7 @@ export const getLLMSupportParams = (llm?: LLMModelItemType) => {
vision: !!llm?.vision, vision: !!llm?.vision,
temperature: typeof llm?.maxTemperature === 'number', temperature: typeof llm?.maxTemperature === 'number',
reasoning: !!llm?.reasoning, reasoning: !!llm?.reasoning,
reasoningEffort: !!llm?.reasoningEffort,
topP: !!llm?.showTopP, topP: !!llm?.showTopP,
stop: !!llm?.showStopSign, stop: !!llm?.showStopSign,
responseFormat: !!(llm?.responseFormatList && llm?.responseFormatList.length > 0), responseFormat: !!(llm?.responseFormatList && llm?.responseFormatList.length > 0),
......
...@@ -67,6 +67,7 @@ export const LLMModelItemSchema = PriceTypeSchema.extend(BaseModelItemSchema.sha ...@@ -67,6 +67,7 @@ export const LLMModelItemSchema = PriceTypeSchema.extend(BaseModelItemSchema.sha
censor: z.boolean().optional(), censor: z.boolean().optional(),
vision: z.boolean().optional(), vision: z.boolean().optional(),
reasoning: z.boolean().optional(), reasoning: z.boolean().optional(),
reasoningEffort: z.boolean().optional(),
functionCall: z.boolean(), functionCall: z.boolean(),
toolChoice: z.boolean(), toolChoice: z.boolean(),
......
...@@ -41,6 +41,9 @@ export const AppFormEditFormV1TypeSchema = z.object({ ...@@ -41,6 +41,9 @@ export const AppFormEditFormV1TypeSchema = z.object({
[NodeInputKeyEnum.aiChatIsResponseText]: z.boolean(), [NodeInputKeyEnum.aiChatIsResponseText]: z.boolean(),
maxHistories: z.int().min(0).max(100), maxHistories: z.int().min(0).max(100),
[NodeInputKeyEnum.aiChatReasoning]: z.boolean().optional(), [NodeInputKeyEnum.aiChatReasoning]: z.boolean().optional(),
[NodeInputKeyEnum.aiChatReasoningEffort]: z
.enum(['none', 'minimal', 'low', 'medium', 'high', 'xhigh'])
.nullish(),
[NodeInputKeyEnum.aiChatTopP]: z.number().optional(), [NodeInputKeyEnum.aiChatTopP]: z.number().optional(),
[NodeInputKeyEnum.aiChatStopSign]: z.string().optional(), [NodeInputKeyEnum.aiChatStopSign]: z.string().optional(),
[NodeInputKeyEnum.aiChatResponseFormat]: z.string().optional(), [NodeInputKeyEnum.aiChatResponseFormat]: z.string().optional(),
......
...@@ -4,6 +4,7 @@ import type { NodeInputKeyEnum } from '../workflow/constants'; ...@@ -4,6 +4,7 @@ import type { NodeInputKeyEnum } from '../workflow/constants';
import { VariableInputEnum } from '../workflow/constants'; import { VariableInputEnum } from '../workflow/constants';
import { InputComponentPropsTypeSchema } from '../workflow/type/io'; import { InputComponentPropsTypeSchema } from '../workflow/type/io';
import { DatasetSearchModeEnum } from '../dataset/constants'; import { DatasetSearchModeEnum } from '../dataset/constants';
import type { ReasoningEffort } from '../ai/llm/type';
import { StoreEdgeItemTypeSchema } from '../workflow/type/edge'; import { StoreEdgeItemTypeSchema } from '../workflow/type/edge';
import type { AppPermission } from '../../support/permission/app/controller'; import type { AppPermission } from '../../support/permission/app/controller';
import { ParentIdSchema, type ParentIdType } from '../../common/parentFolder/type'; import { ParentIdSchema, type ParentIdType } from '../../common/parentFolder/type';
...@@ -180,6 +181,7 @@ export type SettingAIDataType = { ...@@ -180,6 +181,7 @@ export type SettingAIDataType = {
maxHistories?: number; maxHistories?: number;
[NodeInputKeyEnum.aiChatVision]?: boolean; // Is open vision mode [NodeInputKeyEnum.aiChatVision]?: boolean; // Is open vision mode
[NodeInputKeyEnum.aiChatReasoning]?: boolean; // Is open reasoning mode [NodeInputKeyEnum.aiChatReasoning]?: boolean; // Is open reasoning mode
[NodeInputKeyEnum.aiChatReasoningEffort]?: ReasoningEffort;
[NodeInputKeyEnum.aiChatTopP]?: number; [NodeInputKeyEnum.aiChatTopP]?: number;
[NodeInputKeyEnum.aiChatStopSign]?: string; [NodeInputKeyEnum.aiChatStopSign]?: string;
[NodeInputKeyEnum.aiChatResponseFormat]?: string; [NodeInputKeyEnum.aiChatResponseFormat]?: string;
......
import type { Agent } from 'http';
declare global {
var httpsAgent: Agent;
}
...@@ -163,6 +163,7 @@ export enum NodeInputKeyEnum { ...@@ -163,6 +163,7 @@ export enum NodeInputKeyEnum {
aiChatVision = 'aiChatVision', aiChatVision = 'aiChatVision',
stringQuoteText = 'stringQuoteText', stringQuoteText = 'stringQuoteText',
aiChatReasoning = 'aiChatReasoning', aiChatReasoning = 'aiChatReasoning',
aiChatReasoningEffort = 'aiChatReasoningEffort',
aiChatTopP = 'aiChatTopP', aiChatTopP = 'aiChatTopP',
aiChatStopSign = 'aiChatStopSign', aiChatStopSign = 'aiChatStopSign',
aiChatResponseFormat = 'aiChatResponseFormat', aiChatResponseFormat = 'aiChatResponseFormat',
......
...@@ -19,6 +19,7 @@ import type { WorkflowResponseType } from '../../../../service/core/workflow/dis ...@@ -19,6 +19,7 @@ import type { WorkflowResponseType } from '../../../../service/core/workflow/dis
import type { AiChatQuoteRoleType } from '../template/system/aiChat/type'; import type { AiChatQuoteRoleType } from '../template/system/aiChat/type';
import type { OpenaiAccountType } from '../../../support/user/team/type'; import type { OpenaiAccountType } from '../../../support/user/team/type';
import { CompletionFinishReasonSchema } from '../../ai/llm/type'; import { CompletionFinishReasonSchema } from '../../ai/llm/type';
import type { ReasoningEffort } from '../../ai/llm/type';
import type { import type {
InteractiveNodeResponseType, InteractiveNodeResponseType,
WorkflowInteractiveResponseType WorkflowInteractiveResponseType
...@@ -380,6 +381,7 @@ export type AIChatNodeProps = { ...@@ -380,6 +381,7 @@ export type AIChatNodeProps = {
[NodeInputKeyEnum.aiChatIsResponseText]: boolean; [NodeInputKeyEnum.aiChatIsResponseText]: boolean;
[NodeInputKeyEnum.aiChatVision]?: boolean; [NodeInputKeyEnum.aiChatVision]?: boolean;
[NodeInputKeyEnum.aiChatReasoning]?: boolean; [NodeInputKeyEnum.aiChatReasoning]?: boolean;
[NodeInputKeyEnum.aiChatReasoningEffort]?: ReasoningEffort;
[NodeInputKeyEnum.aiChatTopP]?: number; [NodeInputKeyEnum.aiChatTopP]?: number;
[NodeInputKeyEnum.aiChatStopSign]?: string; [NodeInputKeyEnum.aiChatStopSign]?: string;
[NodeInputKeyEnum.aiChatResponseFormat]?: string; [NodeInputKeyEnum.aiChatResponseFormat]?: string;
......
...@@ -99,6 +99,12 @@ export const AiChatModule: FlowNodeTemplateType = { ...@@ -99,6 +99,12 @@ export const AiChatModule: FlowNodeTemplateType = {
value: true value: true
}, },
{ {
key: NodeInputKeyEnum.aiChatReasoningEffort,
renderTypeList: [FlowNodeInputTypeEnum.hidden],
label: '',
valueType: WorkflowIOValueTypeEnum.string
},
{
key: NodeInputKeyEnum.aiChatTopP, key: NodeInputKeyEnum.aiChatTopP,
renderTypeList: [FlowNodeInputTypeEnum.hidden], renderTypeList: [FlowNodeInputTypeEnum.hidden],
label: '', label: '',
......
...@@ -72,6 +72,12 @@ export const ToolCallNode: FlowNodeTemplateType = { ...@@ -72,6 +72,12 @@ export const ToolCallNode: FlowNodeTemplateType = {
value: true value: true
}, },
{ {
key: NodeInputKeyEnum.aiChatReasoningEffort,
renderTypeList: [FlowNodeInputTypeEnum.hidden],
label: '',
valueType: WorkflowIOValueTypeEnum.string
},
{
key: NodeInputKeyEnum.aiChatTopP, key: NodeInputKeyEnum.aiChatTopP,
renderTypeList: [FlowNodeInputTypeEnum.hidden], renderTypeList: [FlowNodeInputTypeEnum.hidden],
label: '', label: '',
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
"pnpm": "10.x" "pnpm": "10.x"
}, },
"dependencies": { "dependencies": {
"@fastgpt-sdk/plugin": "0.6.0", "@fastgpt-sdk/plugin": "0.6.1",
"@apidevtools/swagger-parser": "^10.1.0", "@apidevtools/swagger-parser": "^10.1.0",
"@bany/curl-to-json": "^1.2.8", "@bany/curl-to-json": "^1.2.8",
"axios": "catalog:", "axios": "catalog:",
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
"json5": "catalog:", "json5": "catalog:",
"nanoid": "catalog:", "nanoid": "catalog:",
"next": "catalog:", "next": "catalog:",
"openai": "4.104.0", "openai": "6.34.0",
"openapi-types": "^12.1.3", "openapi-types": "^12.1.3",
"timezones-list": "^3.0.2", "timezones-list": "^3.0.2",
"lodash": "catalog:", "lodash": "catalog:",
......
import tunnel from 'tunnel';
export function initHttpAgent() {
// proxy obj
if (process.env.AXIOS_PROXY_HOST && process.env.AXIOS_PROXY_PORT) {
global.httpsAgent = tunnel.httpsOverHttp({
proxy: {
host: process.env.AXIOS_PROXY_HOST,
port: +process.env.AXIOS_PROXY_PORT
}
});
}
}
...@@ -7,6 +7,7 @@ const aiProxyBaseUrl = process.env.AIPROXY_API_ENDPOINT ...@@ -7,6 +7,7 @@ const aiProxyBaseUrl = process.env.AIPROXY_API_ENDPOINT
const openaiBaseUrl = aiProxyBaseUrl || process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1'; const openaiBaseUrl = aiProxyBaseUrl || process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1';
const openaiBaseKey = process.env.AIPROXY_API_TOKEN || process.env.CHAT_API_KEY || ''; const openaiBaseKey = process.env.AIPROXY_API_TOKEN || process.env.CHAT_API_KEY || '';
// 代理走 packages/service/common/proxy/index.ts 里的 EnvHttpProxyAgent + setGlobalDispatcher
export const getAIApi = (props?: { userKey?: OpenaiAccountType; timeout?: number }) => { export const getAIApi = (props?: { userKey?: OpenaiAccountType; timeout?: number }) => {
const { userKey, timeout } = props || {}; const { userKey, timeout } = props || {};
...@@ -15,7 +16,6 @@ export const getAIApi = (props?: { userKey?: OpenaiAccountType; timeout?: number ...@@ -15,7 +16,6 @@ export const getAIApi = (props?: { userKey?: OpenaiAccountType; timeout?: number
return new OpenAI({ return new OpenAI({
baseURL: baseUrl, baseURL: baseUrl,
apiKey, apiKey,
httpAgent: global.httpsAgent,
timeout, timeout,
maxRetries: 2 maxRetries: 2
}); });
......
...@@ -128,6 +128,8 @@ export const loadSystemModels = async (init = false, language = 'en') => { ...@@ -128,6 +128,8 @@ export const loadSystemModels = async (init = false, language = 'en') => {
const dbModel = dbModels.find((item) => item.model === model.model); const dbModel = dbModels.find((item) => item.model === model.model);
const provider = getModelProvider(dbModel?.metadata?.provider || model.provider, language); const provider = getModelProvider(dbModel?.metadata?.provider || model.provider, language);
const dbLlmMetadata =
dbModel?.metadata?.type === ModelTypeEnum.llm ? dbModel.metadata : undefined;
const modelData: any = { const modelData: any = {
...model, ...model,
...@@ -138,7 +140,9 @@ export const loadSystemModels = async (init = false, language = 'en') => { ...@@ -138,7 +140,9 @@ export const loadSystemModels = async (init = false, language = 'en') => {
isCustom: false, isCustom: false,
...(model.type === ModelTypeEnum.llm && { ...(model.type === ModelTypeEnum.llm && {
maxResponse: model.maxTokens ?? 16000 maxResponse: model.maxTokens ?? 16000,
reasoning: dbLlmMetadata?.reasoning ?? model.reasoning ?? false,
reasoningEffort: dbLlmMetadata?.reasoningEffort ?? model.reasoningEffort ?? false
}), }),
...(model.type === ModelTypeEnum.llm && dbModel?.metadata?.type === ModelTypeEnum.llm ...(model.type === ModelTypeEnum.llm && dbModel?.metadata?.type === ModelTypeEnum.llm
......
...@@ -4,6 +4,7 @@ import type { ...@@ -4,6 +4,7 @@ import type {
ChatCompletionCreateParamsStreaming, ChatCompletionCreateParamsStreaming,
ChatCompletionMessageParam, ChatCompletionMessageParam,
ChatCompletionMessageToolCall, ChatCompletionMessageToolCall,
ChatCompletionTool,
CompletionFinishReason, CompletionFinishReason,
CompletionUsage, CompletionUsage,
OpenAI, OpenAI,
...@@ -258,10 +259,14 @@ export const createLLMResponse = async <T extends ChatCompletionCreateParams>( ...@@ -258,10 +259,14 @@ export const createLLMResponse = async <T extends ChatCompletionCreateParams>(
...(toolCalls?.length && { tool_calls: toolCalls }) ...(toolCalls?.length && { tool_calls: toolCalls })
}; };
// Usage count // requestBody 运行时是经 LLMRequestBodyType narrow 进来的 FastGPT 数据,
// 但类型层 InferCompletionsBody 落到了 SDK 形态(messages/tools 是 v6 后的 union)
const inputTokens = const inputTokens =
usage?.prompt_tokens || usage?.prompt_tokens ||
(await countGptMessagesTokens(requestBody.messages, requestBody.tools)); (await countGptMessagesTokens(
requestBody.messages as ChatCompletionMessageParam[],
requestBody.tools as ChatCompletionTool[] | undefined
));
const outputTokens = const outputTokens =
usage?.completion_tokens || (await countGptMessagesTokens([assistantMessage])); usage?.completion_tokens || (await countGptMessagesTokens([assistantMessage]));
...@@ -358,8 +363,9 @@ export const createLLMResponse = async <T extends ChatCompletionCreateParams>( ...@@ -358,8 +363,9 @@ export const createLLMResponse = async <T extends ChatCompletionCreateParams>(
inputTokens: 0, inputTokens: 0,
outputTokens: 0 outputTokens: 0
}, },
requestMessages: requestBody.messages, // requestBody.messages 在类型层是 SDK v6 的联合(含 custom tool),运行时 FastGPT 仅产 function 形态
completeMessages: [...requestBody.messages] requestMessages: requestBody.messages as ChatCompletionMessageParam[],
completeMessages: [...requestBody.messages] as ChatCompletionMessageParam[]
}; };
} }
}; };
...@@ -634,8 +640,12 @@ export const createCompleteResponse = async ({ ...@@ -634,8 +640,12 @@ export const createCompleteResponse = async ({
const { toolCalls } = (() => { const { toolCalls } = (() => {
if (tools?.length) { if (tools?.length) {
if (toolCallMode === 'toolChoice') { if (toolCallMode === 'toolChoice') {
// openai v6 的 tool_calls 是 function | custom 联合,FastGPT 仅消费 function 形态。
return { return {
toolCalls: response.choices?.[0]?.message?.tool_calls || [] toolCalls:
response.choices?.[0]?.message?.tool_calls?.filter(
(call): call is ChatCompletionMessageToolCall => call.type === 'function'
) || []
}; };
} }
...@@ -682,7 +692,12 @@ type InferCompletionsBody<T> = T extends { stream: true } ...@@ -682,7 +692,12 @@ type InferCompletionsBody<T> = T extends { stream: true }
? ChatCompletionCreateParamsNonStreaming ? ChatCompletionCreateParamsNonStreaming
: ChatCompletionCreateParams; : ChatCompletionCreateParams;
type LLMRequestBodyType<T> = Omit<T, 'model' | 'stop' | 'response_format' | 'messages'> & { // tools 同步用 FastGPT narrow 后的 ChatCompletionTool(function-only),
// 避免 SDK 联合类型透过 T['tools'] 漏到下游。
type LLMRequestBodyType<T> = Omit<
T,
'model' | 'stop' | 'response_format' | 'messages' | 'tools'
> & {
model: string | LLMModelItemType; model: string | LLMModelItemType;
stop?: string; stop?: string;
response_format?: { response_format?: {
...@@ -690,6 +705,7 @@ type LLMRequestBodyType<T> = Omit<T, 'model' | 'stop' | 'response_format' | 'mes ...@@ -690,6 +705,7 @@ type LLMRequestBodyType<T> = Omit<T, 'model' | 'stop' | 'response_format' | 'mes
json_schema?: string; json_schema?: string;
}; };
messages: ChatCompletionMessageParam[]; messages: ChatCompletionMessageParam[];
tools?: ChatCompletionTool[];
// Custom field // Custom field
retainDatasetCite?: boolean; retainDatasetCite?: boolean;
...@@ -786,6 +802,9 @@ const llmCompletionsBodyFormat = async <T extends ChatCompletionCreateParams>({ ...@@ -786,6 +802,9 @@ const llmCompletionsBodyFormat = async <T extends ChatCompletionCreateParams>({
if (!supportParams.responseFormat) { if (!supportParams.responseFormat) {
delete requestBody.response_format; delete requestBody.response_format;
} }
if (!supportParams.reasoningEffort) {
delete requestBody.reasoning_effort;
}
// field map // field map
if (modelData.fieldMap) { if (modelData.fieldMap) {
......
...@@ -86,6 +86,7 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise<ChatResp ...@@ -86,6 +86,7 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise<ChatResp
quotePrompt, quotePrompt,
aiChatVision, aiChatVision,
aiChatReasoning = true, aiChatReasoning = true,
aiChatReasoningEffort,
aiChatTopP, aiChatTopP,
aiChatStopSign, aiChatStopSign,
aiChatResponseFormat, aiChatResponseFormat,
...@@ -186,6 +187,7 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise<ChatResp ...@@ -186,6 +187,7 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise<ChatResp
max_tokens, max_tokens,
top_p: aiChatTopP, top_p: aiChatTopP,
stop: aiChatStopSign, stop: aiChatStopSign,
reasoning_effort: aiChatReasoningEffort,
response_format: { response_format: {
type: aiChatResponseFormat, type: aiChatResponseFormat,
json_schema: aiChatJsonSchema json_schema: aiChatJsonSchema
......
...@@ -75,6 +75,7 @@ export const runToolCall = async (props: DispatchToolModuleProps): Promise<Respo ...@@ -75,6 +75,7 @@ export const runToolCall = async (props: DispatchToolModuleProps): Promise<Respo
aiChatResponseFormat, aiChatResponseFormat,
aiChatJsonSchema, aiChatJsonSchema,
aiChatReasoning, aiChatReasoning,
aiChatReasoningEffort,
isResponseAnswerText = true, isResponseAnswerText = true,
useAgentSandbox useAgentSandbox
} }
...@@ -208,6 +209,7 @@ export const runToolCall = async (props: DispatchToolModuleProps): Promise<Respo ...@@ -208,6 +209,7 @@ export const runToolCall = async (props: DispatchToolModuleProps): Promise<Respo
temperature, temperature,
top_p: aiChatTopP, top_p: aiChatTopP,
stop: aiChatStopSign, stop: aiChatStopSign,
reasoning_effort: aiChatReasoningEffort,
response_format: { response_format: {
type: aiChatResponseFormat, type: aiChatResponseFormat,
json_schema: aiChatJsonSchema json_schema: aiChatJsonSchema
......
...@@ -7,6 +7,7 @@ import type { ChatItemMiniType } from '@fastgpt/global/core/chat/type'; ...@@ -7,6 +7,7 @@ import type { ChatItemMiniType } from '@fastgpt/global/core/chat/type';
import type { ToolCallChildrenInteractive } from '@fastgpt/global/core/workflow/template/system/interactive/type'; import type { ToolCallChildrenInteractive } from '@fastgpt/global/core/workflow/template/system/interactive/type';
import type { LLMModelItemType } from '@fastgpt/global/core/ai/model.schema'; import type { LLMModelItemType } from '@fastgpt/global/core/ai/model.schema';
import type { JSONSchemaInputType } from '@fastgpt/global/core/app/jsonschema'; import type { JSONSchemaInputType } from '@fastgpt/global/core/app/jsonschema';
import type { ReasoningEffort } from '@fastgpt/global/core/ai/llm/type';
export type DispatchToolModuleProps = ModuleDispatchProps<{ export type DispatchToolModuleProps = ModuleDispatchProps<{
[NodeInputKeyEnum.history]?: ChatItemMiniType[]; [NodeInputKeyEnum.history]?: ChatItemMiniType[];
...@@ -20,6 +21,7 @@ export type DispatchToolModuleProps = ModuleDispatchProps<{ ...@@ -20,6 +21,7 @@ export type DispatchToolModuleProps = ModuleDispatchProps<{
[NodeInputKeyEnum.aiChatIsResponseText]?: boolean; [NodeInputKeyEnum.aiChatIsResponseText]?: boolean;
[NodeInputKeyEnum.aiChatVision]?: boolean; [NodeInputKeyEnum.aiChatVision]?: boolean;
[NodeInputKeyEnum.aiChatReasoning]?: boolean; [NodeInputKeyEnum.aiChatReasoning]?: boolean;
[NodeInputKeyEnum.aiChatReasoningEffort]?: ReasoningEffort;
[NodeInputKeyEnum.aiChatTopP]?: number; [NodeInputKeyEnum.aiChatTopP]?: number;
[NodeInputKeyEnum.aiChatStopSign]?: string; [NodeInputKeyEnum.aiChatStopSign]?: string;
[NodeInputKeyEnum.aiChatResponseFormat]?: string; [NodeInputKeyEnum.aiChatResponseFormat]?: string;
......
...@@ -72,7 +72,6 @@ ...@@ -72,7 +72,6 @@
"proxy-from-env": "^1.1.0", "proxy-from-env": "^1.1.0",
"request-ip": "catalog:", "request-ip": "catalog:",
"tiktoken": "1.0.17", "tiktoken": "1.0.17",
"tunnel": "^0.0.6",
"turndown": "^7.1.2", "turndown": "^7.1.2",
"undici": "^7.18.2", "undici": "^7.18.2",
"winston": "^3.17.0", "winston": "^3.17.0",
......
...@@ -73,6 +73,8 @@ import { getLLMModel } from '@fastgpt/service/core/ai/model'; ...@@ -73,6 +73,8 @@ import { getLLMModel } from '@fastgpt/service/core/ai/model';
import { loadRequestMessages } from '@fastgpt/service/core/ai/llm/utils'; import { loadRequestMessages } from '@fastgpt/service/core/ai/llm/utils';
import { countGptMessagesTokens } from '@fastgpt/service/common/string/tiktoken/index'; import { countGptMessagesTokens } from '@fastgpt/service/common/string/tiktoken/index';
import { parseLLMStreamResponse, parseReasoningContent } from '@fastgpt/service/core/ai/utils'; import { parseLLMStreamResponse, parseReasoningContent } from '@fastgpt/service/core/ai/utils';
import { getLLMSupportParams } from '@fastgpt/global/core/ai/llm/utils';
import { promptToolCallMessageRewrite } from '@fastgpt/service/core/ai/llm/promptCall';
// Import the function to test // Import the function to test
import { import {
...@@ -87,6 +89,19 @@ const mockLoadRequestMessages = vi.mocked(loadRequestMessages); ...@@ -87,6 +89,19 @@ const mockLoadRequestMessages = vi.mocked(loadRequestMessages);
const mockCountGptMessagesTokens = vi.mocked(countGptMessagesTokens); const mockCountGptMessagesTokens = vi.mocked(countGptMessagesTokens);
const mockParseLLMStreamResponse = vi.mocked(parseLLMStreamResponse); const mockParseLLMStreamResponse = vi.mocked(parseLLMStreamResponse);
const mockParseReasoningContent = vi.mocked(parseReasoningContent); const mockParseReasoningContent = vi.mocked(parseReasoningContent);
const mockGetLLMSupportParams = vi.mocked(getLLMSupportParams);
const mockPromptToolCallMessageRewrite = vi.mocked(promptToolCallMessageRewrite);
const defaultSupportParams = {
vision: false,
temperature: true,
reasoning: false,
reasoningEffort: false,
topP: true,
stop: true,
responseFormat: false,
supportToolCall: true
};
// Helper to create mock model data // Helper to create mock model data
const createMockModelData = (overrides?: Partial<LLMModelItemType>): LLMModelItemType => ({ const createMockModelData = (overrides?: Partial<LLMModelItemType>): LLMModelItemType => ({
...@@ -1250,6 +1265,117 @@ describe('createLLMResponse', () => { ...@@ -1250,6 +1265,117 @@ describe('createLLMResponse', () => {
expect(result.finish_reason).toBe('length'); expect(result.finish_reason).toBe('length');
}); });
}); });
describe('reasoning_effort handling', () => {
const buildOkResponse = () => ({
choices: [
{
message: { role: 'assistant', content: 'ok' },
finish_reason: 'stop'
}
],
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }
});
it('should preserve reasoning_effort when model supports it', async () => {
mockGetLLMModel.mockReturnValue(createMockModelData({ reasoning: true }));
mockGetLLMSupportParams.mockReturnValueOnce({
...defaultSupportParams,
reasoning: true,
reasoningEffort: true
});
const mockCreate = vi.fn().mockResolvedValue(buildOkResponse());
mockGetAIApi.mockReturnValue({
chat: { completions: { create: mockCreate } }
} as any);
await createLLMResponse({
body: {
model: 'gpt-4',
messages: [{ role: ChatCompletionRequestMessageRoleEnum.User, content: 'hi' }],
reasoning_effort: 'high',
stream: false
}
});
expect(mockCreate).toHaveBeenCalledTimes(1);
expect(mockCreate.mock.calls[0][0].reasoning_effort).toBe('high');
});
it('should strip reasoning_effort when model does not support it', async () => {
mockGetLLMSupportParams.mockReturnValueOnce({
...defaultSupportParams,
reasoningEffort: false
});
const mockCreate = vi.fn().mockResolvedValue(buildOkResponse());
mockGetAIApi.mockReturnValue({
chat: { completions: { create: mockCreate } }
} as any);
await createLLMResponse({
body: {
model: 'gpt-4',
messages: [{ role: ChatCompletionRequestMessageRoleEnum.User, content: 'hi' }],
reasoning_effort: 'high',
stream: false
}
});
expect(mockCreate).toHaveBeenCalledTimes(1);
expect(mockCreate.mock.calls[0][0]).not.toHaveProperty('reasoning_effort');
});
});
describe('Error handling - requestMessages provenance', () => {
it('should return prompt-rewritten requestMessages on API error', async () => {
const loaded: ChatCompletionMessageParam[] = [
{ role: ChatCompletionRequestMessageRoleEnum.System, content: 'sys' },
{ role: ChatCompletionRequestMessageRoleEnum.User, content: 'q' }
];
const rewritten: ChatCompletionMessageParam[] = [
...loaded,
{ role: ChatCompletionRequestMessageRoleEnum.User, content: 'rewritten-suffix' }
];
mockLoadRequestMessages.mockResolvedValueOnce(loaded as any);
mockPromptToolCallMessageRewrite.mockReturnValueOnce(rewritten as any);
const mockAI = {
chat: {
completions: {
create: vi.fn().mockRejectedValue(new Error('boom'))
}
}
};
mockGetAIApi.mockReturnValue(mockAI as any);
const result = await createLLMResponse({
throwError: false,
body: {
model: 'gpt-4',
messages: [{ role: ChatCompletionRequestMessageRoleEnum.User, content: 'q' }],
tools: [
{
type: 'function' as const,
function: {
name: 'noop',
description: 'n',
parameters: { type: 'object', properties: {} }
}
}
],
toolCallMode: 'prompt',
stream: false
}
});
expect(result.error).toBeDefined();
expect(result.requestMessages).toEqual(rewritten);
expect(result.completeMessages).toEqual(rewritten);
});
});
}); });
describe('createCompleteResponse', () => { describe('createCompleteResponse', () => {
...@@ -1341,6 +1467,58 @@ describe('createCompleteResponse', () => { ...@@ -1341,6 +1467,58 @@ describe('createCompleteResponse', () => {
expect(result.toolCalls![0].function.name).toBe('test_tool'); expect(result.toolCalls![0].function.name).toBe('test_tool');
expect(toolCalls).toHaveLength(1); expect(toolCalls).toHaveLength(1);
}); });
it('should drop non-function tool_calls (e.g. custom type) in toolChoice mode', async () => {
const response = {
choices: [
{
message: {
role: 'assistant',
content: null,
tool_calls: [
{
id: 'call_fn',
type: 'function',
function: { name: 'test_tool', arguments: '{}' }
},
{
id: 'call_custom',
type: 'custom',
custom: { name: 'shell', input: 'ls' }
}
]
},
finish_reason: 'tool_calls'
}
],
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }
};
const tools = [
{
type: 'function' as const,
function: {
name: 'test_tool',
description: 'Test',
parameters: { type: 'object', properties: {} }
}
}
];
const result = await createCompleteResponse({
body: {
model: 'gpt-4',
messages: [],
tools,
stream: false
},
response: response as any
});
expect(result.toolCalls).toHaveLength(1);
expect(result.toolCalls![0].id).toBe('call_fn');
expect(result.toolCalls![0].type).toBe('function');
});
}); });
describe('createStreamResponse', () => { describe('createStreamResponse', () => {
......
...@@ -35,7 +35,7 @@ import EmptyTip from '../EmptyTip'; ...@@ -35,7 +35,7 @@ import EmptyTip from '../EmptyTip';
* customOnOpen: 自定义打开回调 * customOnOpen: 自定义打开回调
* customOnClose: 自定义关闭回调 * customOnClose: 自定义关闭回调
* */ * */
export type SelectProps<T = any> = Omit<ButtonProps, 'onChange'> & { export type SelectProps<T = any> = Omit<ButtonProps, 'onChange' | 'value'> & {
value?: T; value?: T;
valueLabel?: string | React.ReactNode; valueLabel?: string | React.ReactNode;
placeholder?: string; placeholder?: string;
......
...@@ -84,8 +84,9 @@ ...@@ -84,8 +84,9 @@
"model.price_tier_range_invalid": "Invalid price tier range. Please make sure upper bounds keep increasing", "model.price_tier_range_invalid": "Invalid price tier range. Please make sure upper bounds keep increasing",
"model.price_tiers": "Price tiers", "model.price_tiers": "Price tiers",
"model.price_tiers_tip": "Match a tier by input token range, then bill input and output tokens with that tier's prices. Each next range starts after the previous upper bound. Leave the last upper bound empty to mean and above.", "model.price_tiers_tip": "Match a tier by input token range, then bill input and output tokens with that tier's prices. Each next range starts after the previous upper bound. Leave the last upper bound empty to mean and above.",
"model.reasoning": "Support output thinking", "model.reasoning": "Model supports thinking",
"model.reasoning_tip": "For example, Deepseek-reasoner can output the thinking process.", "model.reasoning_tip": "For example, Deepseek-reasoner can output the thinking process.",
"model.reasoning_effort": "Support thinking config",
"model.request_auth": "Custom key", "model.request_auth": "Custom key",
"model.request_auth_tip": "When making a request to a custom request address, carry the request header: Authorization: Bearer xxx to make the request.", "model.request_auth_tip": "When making a request to a custom request address, carry the request header: Authorization: Bearer xxx to make the request.",
"model.request_url": "Custom url", "model.request_url": "Custom url",
...@@ -118,7 +119,7 @@ ...@@ -118,7 +119,7 @@
"personal_information": "Personal", "personal_information": "Personal",
"promotion_records": "Promotions", "promotion_records": "Promotions",
"requests_per_minute": "Requests per minute", "requests_per_minute": "Requests per minute",
"reset_default": "Restore the default configuration", "reset_default": "Restore default",
"status": "state", "status": "state",
"subscription_mode_month": "Duration", "subscription_mode_month": "Duration",
"subscription_package": "Subscription package", "subscription_package": "Subscription package",
......
...@@ -35,6 +35,9 @@ ...@@ -35,6 +35,9 @@
"Tool_description": "Tool description", "Tool_description": "Tool description",
"Tool_name": "Tool name", "Tool_name": "Tool name",
"ai_point_price": "Billing", "ai_point_price": "Billing",
"ai_setting_basic_config": "Basic settings",
"ai_setting_reasoning_config": "Reasoning settings",
"ai_setting_reasoning_config_tip": "Some models do not fully support every reasoning option. If you see an error, try a different reasoning option or use the default.",
"ai_settings": "AI Settings", "ai_settings": "AI Settings",
"all_apps": "All Applications", "all_apps": "All Applications",
"app.Version name": "Version Name", "app.Version name": "Version Name",
...@@ -289,7 +292,11 @@ ...@@ -289,7 +292,11 @@
"publish_success": "Publish Successful", "publish_success": "Publish Successful",
"question_guide_tip": "After the conversation, 3 guiding questions will be generated for you.", "question_guide_tip": "After the conversation, 3 guiding questions will be generated for you.",
"raw_params": "original parameters", "raw_params": "original parameters",
"reasoning_response": "Output thinking", "reasoning_response": "Hide AI reasoning",
"hide_response": "Hide AI output",
"hide_response_tip": "When checked, the model output is not shown to the user directly, but it can still be retrieved from the AI reply for downstream processing.",
"reasoning_effort": "Reasoning effort",
"reasoning_effort_unsupported": "This model does not support disabling reasoning",
"recharge": "Go to recharge", "recharge": "Go to recharge",
"reference_variable": "Reference variables", "reference_variable": "Reference variables",
"refresh_templates": "Refresh", "refresh_templates": "Refresh",
......
...@@ -955,6 +955,13 @@ ...@@ -955,6 +955,13 @@
"question_feedback": "Work order", "question_feedback": "Work order",
"read_course": "Read Course", "read_course": "Read Course",
"read_doc": "Read Document", "read_doc": "Read Document",
"reasoning_effort.default": "Default",
"reasoning_effort.high": "High",
"reasoning_effort.low": "Low",
"reasoning_effort.medium": "Medium",
"reasoning_effort.minimal": "Minimal",
"reasoning_effort.none": "None",
"reasoning_effort.xhigh": "Extra high",
"redo_tip": "Redo ctrl shift z", "redo_tip": "Redo ctrl shift z",
"redo_tip_mac": "Redo ⌘ shift z", "redo_tip_mac": "Redo ⌘ shift z",
"refresh": "Refresh", "refresh": "Refresh",
......
...@@ -84,8 +84,9 @@ ...@@ -84,8 +84,9 @@
"model.price_tier_range_invalid": "价格梯度区间设置有误,请检查区间上限是否递增", "model.price_tier_range_invalid": "价格梯度区间设置有误,请检查区间上限是否递增",
"model.price_tiers": "模型价格梯度", "model.price_tiers": "模型价格梯度",
"model.price_tiers_tip": "按输入 Token 所在区间匹配一个梯度,再使用该梯度的输入价格和输出价格分别计费。区间起点自动按上一梯度递增,最后一个梯度上限留空表示及以上。", "model.price_tiers_tip": "按输入 Token 所在区间匹配一个梯度,再使用该梯度的输入价格和输出价格分别计费。区间起点自动按上一梯度递增,最后一个梯度上限留空表示及以上。",
"model.reasoning": "支持输出思考", "model.reasoning": "模型支持思考",
"model.reasoning_tip": "例如 Deepseek-reasoner,可以输出思考过程。", "model.reasoning_tip": "例如 Deepseek-reasoner,可以输出思考过程。",
"model.reasoning_effort": "支持思考配置",
"model.request_auth": "自定义请求 Key", "model.request_auth": "自定义请求 Key",
"model.request_auth_tip": "向自定义请求地址发起请求时候,携带请求头:Authorization: Bearer xxx 进行请求", "model.request_auth_tip": "向自定义请求地址发起请求时候,携带请求头:Authorization: Bearer xxx 进行请求",
"model.request_url": "自定义请求地址", "model.request_url": "自定义请求地址",
...@@ -118,7 +119,7 @@ ...@@ -118,7 +119,7 @@
"personal_information": "个人信息", "personal_information": "个人信息",
"promotion_records": "促销记录", "promotion_records": "促销记录",
"requests_per_minute": "QPM", "requests_per_minute": "QPM",
"reset_default": "恢复默认配置", "reset_default": "恢复默认",
"status": "状态", "status": "状态",
"subscription_mode_month": "时长", "subscription_mode_month": "时长",
"subscription_package": "订阅套餐", "subscription_package": "订阅套餐",
......
...@@ -35,6 +35,9 @@ ...@@ -35,6 +35,9 @@
"Tool_description": "工具描述", "Tool_description": "工具描述",
"Tool_name": "工具名称", "Tool_name": "工具名称",
"ai_point_price": "AI积分计费", "ai_point_price": "AI积分计费",
"ai_setting_basic_config": "基础配置",
"ai_setting_reasoning_config": "思考配置",
"ai_setting_reasoning_config_tip": "部分模型未完全适配所有思考配置,如提示报错,请尝试切换其他思考配置选项或使用默认选项。",
"ai_settings": "AI 配置", "ai_settings": "AI 配置",
"all_apps": "全部应用", "all_apps": "全部应用",
"app.Version name": "版本名称", "app.Version name": "版本名称",
...@@ -289,7 +292,11 @@ ...@@ -289,7 +292,11 @@
"publish_success": "发布成功", "publish_success": "发布成功",
"question_guide_tip": "对话结束后,会为你生成 3 个引导性问题。", "question_guide_tip": "对话结束后,会为你生成 3 个引导性问题。",
"raw_params": "原始参数", "raw_params": "原始参数",
"reasoning_response": "输出思考", "reasoning_response": "隐藏 AI 思考",
"hide_response": "隐藏 AI 输出",
"hide_response_tip": "勾选后,模型输出内容不会直接展示给用户,但可以在 AI 回复的输出中获取本次模型输出的内容进行二次处理。",
"reasoning_effort": "思考配置",
"reasoning_effort_unsupported": "该模型不支持关闭思考",
"recharge": "去充值", "recharge": "去充值",
"reference_variable": "引用变量", "reference_variable": "引用变量",
"refresh_templates": "换一批", "refresh_templates": "换一批",
......
...@@ -955,6 +955,13 @@ ...@@ -955,6 +955,13 @@
"question_feedback": "工单咨询", "question_feedback": "工单咨询",
"read_course": "查看教程", "read_course": "查看教程",
"read_doc": "查看文档", "read_doc": "查看文档",
"reasoning_effort.default": "默认",
"reasoning_effort.high": "深度思考",
"reasoning_effort.low": "轻量思考",
"reasoning_effort.medium": "标准思考",
"reasoning_effort.minimal": "极简思考",
"reasoning_effort.none": "不思考",
"reasoning_effort.xhigh": "极致思考",
"redo_tip": "恢复 ctrl shift z", "redo_tip": "恢复 ctrl shift z",
"redo_tip_mac": "恢复 ⌘ shift z", "redo_tip_mac": "恢复 ⌘ shift z",
"refresh": "刷新", "refresh": "刷新",
......
...@@ -84,8 +84,9 @@ ...@@ -84,8 +84,9 @@
"model.price_tier_range_invalid": "價格梯度區間設定有誤,請確認區間上限有持續遞增", "model.price_tier_range_invalid": "價格梯度區間設定有誤,請確認區間上限有持續遞增",
"model.price_tiers": "模型價格梯度", "model.price_tiers": "模型價格梯度",
"model.price_tiers_tip": "依輸入 Token 所在區間匹配一個梯度,再使用該梯度的輸入價格和輸出價格分別計費。區間起點會依上一梯度自動遞增,最後一個梯度上限留空表示及以上。", "model.price_tiers_tip": "依輸入 Token 所在區間匹配一個梯度,再使用該梯度的輸入價格和輸出價格分別計費。區間起點會依上一梯度自動遞增,最後一個梯度上限留空表示及以上。",
"model.reasoning": "支援輸出思考", "model.reasoning": "模型支援思考",
"model.reasoning_tip": "例如 Deepseek-reasoner,可以輸出思考過程。", "model.reasoning_tip": "例如 Deepseek-reasoner,可以輸出思考過程。",
"model.reasoning_effort": "支援思考配置",
"model.request_auth": "自訂請求 Key", "model.request_auth": "自訂請求 Key",
"model.request_auth_tip": "向自訂請求地址發起請求時候,攜帶請求頭:Authorization: Bearer xxx 進行請求", "model.request_auth_tip": "向自訂請求地址發起請求時候,攜帶請求頭:Authorization: Bearer xxx 進行請求",
"model.request_url": "自訂請求地址", "model.request_url": "自訂請求地址",
...@@ -118,7 +119,7 @@ ...@@ -118,7 +119,7 @@
"personal_information": "個人資訊", "personal_information": "個人資訊",
"promotion_records": "促銷記錄", "promotion_records": "促銷記錄",
"requests_per_minute": "QPM", "requests_per_minute": "QPM",
"reset_default": "恢復預設設定", "reset_default": "恢復預設",
"status": "狀態", "status": "狀態",
"subscription_mode_month": "時長", "subscription_mode_month": "時長",
"subscription_package": "訂閱套餐", "subscription_package": "訂閱套餐",
......
...@@ -33,6 +33,9 @@ ...@@ -33,6 +33,9 @@
"Tool_description": "工具描述", "Tool_description": "工具描述",
"Tool_name": "工具名稱", "Tool_name": "工具名稱",
"ai_point_price": "AI 積分計費", "ai_point_price": "AI 積分計費",
"ai_setting_basic_config": "基礎設定",
"ai_setting_reasoning_config": "思考設定",
"ai_setting_reasoning_config_tip": "部分模型未完全適配所有思考設定,如提示報錯,請嘗試切換其他思考設定選項或使用預設選項。",
"ai_settings": "AI 設定", "ai_settings": "AI 設定",
"all_apps": "所有應用程式", "all_apps": "所有應用程式",
"app.Version name": "版本名稱", "app.Version name": "版本名稱",
...@@ -280,7 +283,11 @@ ...@@ -280,7 +283,11 @@
"publish_success": "發布成功", "publish_success": "發布成功",
"question_guide_tip": "對話結束後,會為你產生 3 個引導性問題。", "question_guide_tip": "對話結束後,會為你產生 3 個引導性問題。",
"raw_params": "原始參數", "raw_params": "原始參數",
"reasoning_response": "輸出思考", "reasoning_response": "隱藏 AI 思考",
"hide_response": "隱藏 AI 輸出",
"hide_response_tip": "勾選後,模型輸出內容不會直接展示給用戶,但可以在 AI 回覆的輸出中取得本次模型輸出的內容進行二次處理。",
"reasoning_effort": "思考配置",
"reasoning_effort_unsupported": "該模型不支援關閉思考",
"recharge": "去充值", "recharge": "去充值",
"reference_variable": "引用變量", "reference_variable": "引用變量",
"refresh_templates": "換一批", "refresh_templates": "換一批",
......
...@@ -945,6 +945,13 @@ ...@@ -945,6 +945,13 @@
"question_feedback": "工單諮詢", "question_feedback": "工單諮詢",
"read_course": "閱讀教學", "read_course": "閱讀教學",
"read_doc": "閱讀文件", "read_doc": "閱讀文件",
"reasoning_effort.default": "預設",
"reasoning_effort.high": "深度思考",
"reasoning_effort.low": "輕量思考",
"reasoning_effort.medium": "標準思考",
"reasoning_effort.minimal": "極簡思考",
"reasoning_effort.none": "不思考",
"reasoning_effort.xhigh": "極致思考",
"redo_tip": "重做 ctrl shift z", "redo_tip": "重做 ctrl shift z",
"redo_tip_mac": "重做 ⌘ shift z", "redo_tip_mac": "重做 ⌘ shift z",
"request_end": "已載入全部", "request_end": "已載入全部",
......
...@@ -709,6 +709,57 @@ const Table = tableMultiStyle({ ...@@ -709,6 +709,57 @@ const Table = tableMultiStyle({
} }
} }
} }
},
bordered: {
table: {
bg: 'white'
},
thead: {
tr: {
bg: 'myGray.25',
th: {
px: 3,
py: 2,
height: '32px',
bg: 'myGray.25',
color: 'myGray.500',
fontSize: 'mini',
fontWeight: 'medium',
letterSpacing: '0.5px',
textTransform: 'none',
borderRadius: 'none !important',
borderBottom: '1px solid',
borderColor: 'myGray.200',
'&:not(:first-of-type)': {
borderLeft: '1px solid',
borderColor: 'myGray.200'
}
}
}
},
tbody: {
tr: {
td: {
px: 3,
py: 2,
color: 'myGray.500',
fontSize: 'mini',
letterSpacing: '0.4px',
borderRadius: 'none !important',
borderBottom: 'none',
'&:not(:first-of-type)': {
borderLeft: '1px solid',
borderColor: 'myGray.200'
}
},
'&:not(:first-of-type)': {
td: {
borderTop: '1px solid',
borderColor: 'myGray.200'
}
}
}
}
} }
}, },
defaultProps: { defaultProps: {
......
import React, { useMemo } from 'react'; import React from 'react';
import { Box, Flex, Table, Tbody, Td, Th, Thead, Tr } from '@chakra-ui/react'; import { Box, Flex, Table, Tbody, Td, Th, Thead, Tr } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import type { ModelPriceTierType, PriceType } from '@fastgpt/global/core/ai/model.schema'; import type { ModelPriceTierType, PriceType } from '@fastgpt/global/core/ai/model.schema';
...@@ -40,18 +40,33 @@ const formatPriceSummary = ({ ...@@ -40,18 +40,33 @@ const formatPriceSummary = ({
); );
}; };
const PriceTiersLabel = ({ config, unitLabel }: { config: PriceType; unitLabel: string }) => { const SummaryLine = ({
tiers,
priceKey,
unitLabel,
fontSize
}: {
tiers: ModelPriceTierType[];
priceKey: 'input' | 'output';
unitLabel: string;
fontSize: string;
}) => {
const { t } = useTranslation(); const { t } = useTranslation();
const tiers = useMemo(() => config.priceTiers || [], [config]); return (
<Flex fontSize={fontSize} lineHeight={1.5} gap={0.5}>
if (tiers.length === 0) { <Box>{priceKey === 'input' ? t('common:Input') : t('common:Output')}:</Box>
return <Box>-</Box>; {formatPriceSummary({
} tiers,
priceKey: priceKey === 'input' ? 'inputPrice' : 'outputPrice',
unitLabel
})}
</Flex>
);
};
const TierBreakdownTable = ({ tiers }: { tiers: ModelPriceTierType[] }) => {
const { t } = useTranslation();
return ( return (
<MyTooltip
label={
tiers.length > 1 ? (
<Box <Box
p={2} p={2}
sx={{ sx={{
...@@ -114,9 +129,7 @@ const PriceTiersLabel = ({ config, unitLabel }: { config: PriceType; unitLabel: ...@@ -114,9 +129,7 @@ const PriceTiersLabel = ({ config, unitLabel }: { config: PriceType; unitLabel:
</Thead> </Thead>
<Tbody> <Tbody>
{tiers.map((tier, index) => ( {tiers.map((tier, index) => (
<Tr <Tr key={`${tier.minInputTokens ?? 1}-${tier.maxInputTokens ?? 'open'}-${index}`}>
key={`${tier.minInputTokens ?? 1}-${tier.maxInputTokens ?? 'open'}-${index}`}
>
<Td <Td
px={3} px={3}
py={2} py={2}
...@@ -167,31 +180,62 @@ const PriceTiersLabel = ({ config, unitLabel }: { config: PriceType; unitLabel: ...@@ -167,31 +180,62 @@ const PriceTiersLabel = ({ config, unitLabel }: { config: PriceType; unitLabel:
</Table> </Table>
</Box> </Box>
</Box> </Box>
) : null );
} };
const TierTooltip = ({
tiers,
color,
children
}: {
tiers: ModelPriceTierType[];
color?: string;
children: React.ReactNode;
}) => (
<MyTooltip
label={tiers.length > 1 ? <TierBreakdownTable tiers={tiers} /> : null}
px={0} px={0}
py={0} py={0}
maxW={'420px'} maxW={'420px'}
> >
<Box cursor={'default'}> <Box cursor={'default'} color={color}>
<Flex fontSize={'sm'} color={'myGray.700'} lineHeight={1.5} gap={0.5}> {children}
<Box>{t('common:Input')}:</Box>
{formatPriceSummary({
tiers,
priceKey: 'inputPrice',
unitLabel
})}
</Flex>
<Flex fontSize={'sm'} color={'myGray.700'} lineHeight={1.5} gap={0.5}>
<Box>{t('common:Output')}:</Box>
{formatPriceSummary({
tiers,
priceKey: 'outputPrice',
unitLabel
})}
</Flex>
</Box> </Box>
</MyTooltip> </MyTooltip>
);
export const PriceLine = React.memo(
({
config,
unitLabel,
priceKey,
fontSize = 'sm',
color
}: {
config: PriceType;
unitLabel: string;
priceKey: 'input' | 'output';
fontSize?: string;
color?: string;
}) => {
const tiers = config.priceTiers || [];
if (tiers.length === 0) return <Box>-</Box>;
return (
<TierTooltip tiers={tiers} color={color}>
<SummaryLine tiers={tiers} priceKey={priceKey} unitLabel={unitLabel} fontSize={fontSize} />
</TierTooltip>
);
}
);
const PriceTiersLabel = ({ config, unitLabel }: { config: PriceType; unitLabel: string }) => {
const tiers = config.priceTiers || [];
if (tiers.length === 0) return <Box>-</Box>;
return (
<TierTooltip tiers={tiers} color={'myGray.700'}>
<SummaryLine tiers={tiers} priceKey={'input'} unitLabel={unitLabel} fontSize={'sm'} />
<SummaryLine tiers={tiers} priceKey={'output'} unitLabel={unitLabel} fontSize={'sm'} />
</TierTooltip>
); );
}; };
......
...@@ -769,6 +769,14 @@ export const ModelEditModal = ({ ...@@ -769,6 +769,14 @@ export const ModelEditModal = ({
} }
}); });
const reasoningEnabled = useWatch({ control, name: 'reasoning' });
useEffect(() => {
// 仅在 reasoning 关闭且 reasoningEffort 实际为 true 时才清,避免挂载即把表单标 dirty
if (!reasoningEnabled && getValues('reasoningEffort')) {
setValue('reasoningEffort', false, { shouldDirty: false });
}
}, [reasoningEnabled, getValues, setValue]);
const isCustom = !!modelData.isCustom; const isCustom = !!modelData.isCustom;
const isLLMModel = modelData?.type === ModelTypeEnum.llm; const isLLMModel = modelData?.type === ModelTypeEnum.llm;
const isEmbeddingModel = modelData?.type === ModelTypeEnum.embedding; const isEmbeddingModel = modelData?.type === ModelTypeEnum.embedding;
...@@ -1074,6 +1082,13 @@ export const ModelEditModal = ({ ...@@ -1074,6 +1082,13 @@ export const ModelEditModal = ({
field={'reasoning'} field={'reasoning'}
register={register} register={register}
/> />
{reasoningEnabled && (
<SwitchField
label={t('account:model.reasoning_effort')}
field={'reasoningEffort'}
register={register}
/>
)}
{feConfigs?.isPlus && ( {feConfigs?.isPlus && (
<SwitchField <SwitchField
label={t('account:model.censor')} label={t('account:model.censor')}
......
...@@ -198,6 +198,7 @@ const EditForm = ({ ...@@ -198,6 +198,7 @@ const EditForm = ({
maxToken: appForm.aiSettings.maxToken, maxToken: appForm.aiSettings.maxToken,
maxHistories: appForm.aiSettings.maxHistories, maxHistories: appForm.aiSettings.maxHistories,
aiChatReasoning: appForm.aiSettings.aiChatReasoning ?? true, aiChatReasoning: appForm.aiSettings.aiChatReasoning ?? true,
aiChatReasoningEffort: appForm.aiSettings.aiChatReasoningEffort,
aiChatTopP: appForm.aiSettings.aiChatTopP, aiChatTopP: appForm.aiSettings.aiChatTopP,
aiChatStopSign: appForm.aiSettings.aiChatStopSign, aiChatStopSign: appForm.aiSettings.aiChatStopSign,
aiChatResponseFormat: appForm.aiSettings.aiChatResponseFormat, aiChatResponseFormat: appForm.aiSettings.aiChatResponseFormat,
......
...@@ -56,6 +56,9 @@ const SelectAiModelRender = ({ inputs = [], nodeId }: RenderInputProps) => { ...@@ -56,6 +56,9 @@ const SelectAiModelRender = ({ inputs = [], nodeId }: RenderInputProps) => {
inputs.find((input) => input.key === NodeInputKeyEnum.aiChatVision)?.value ?? true, inputs.find((input) => input.key === NodeInputKeyEnum.aiChatVision)?.value ?? true,
aiChatReasoning: aiChatReasoning:
inputs.find((input) => input.key === NodeInputKeyEnum.aiChatReasoning)?.value ?? true, inputs.find((input) => input.key === NodeInputKeyEnum.aiChatReasoning)?.value ?? true,
aiChatReasoningEffort: inputs.find(
(input) => input.key === NodeInputKeyEnum.aiChatReasoningEffort
)?.value,
aiChatTopP: inputs.find((input) => input.key === NodeInputKeyEnum.aiChatTopP)?.value, aiChatTopP: inputs.find((input) => input.key === NodeInputKeyEnum.aiChatTopP)?.value,
aiChatStopSign: inputs.find((input) => input.key === NodeInputKeyEnum.aiChatStopSign)?.value, aiChatStopSign: inputs.find((input) => input.key === NodeInputKeyEnum.aiChatStopSign)?.value,
aiChatResponseFormat: inputs.find( aiChatResponseFormat: inputs.find(
......
import { initHttpAgent } from '@fastgpt/service/common/middle/httpAgent';
import fs, { existsSync } from 'fs'; import fs, { existsSync } from 'fs';
import type { FastGPTFeConfigsType } from '@fastgpt/global/common/system/types/index'; import type { FastGPTFeConfigsType } from '@fastgpt/global/common/system/types/index';
import type { FastGPTConfigFileType } from '@fastgpt/global/common/system/types/index'; import type { FastGPTConfigFileType } from '@fastgpt/global/common/system/types/index';
...@@ -84,7 +83,6 @@ export function initGlobalVariables() { ...@@ -84,7 +83,6 @@ export function initGlobalVariables() {
global.datasetParseQueueLen = global.datasetParseQueueLen ?? 0; global.datasetParseQueueLen = global.datasetParseQueueLen ?? 0;
global.qaQueueLen = global.qaQueueLen ?? 0; global.qaQueueLen = global.qaQueueLen ?? 0;
global.vectorQueueLen = global.vectorQueueLen ?? 0; global.vectorQueueLen = global.vectorQueueLen ?? 0;
initHttpAgent();
initPlusRequest(); initPlusRequest();
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment