Commit 8271ef53 by Finley Ge Committed by GitHub

fix: sanitize private tool schema fields (#7207)

parent ff1a1a10
import type { ChatCompletionCreateParams } from '@fastgpt/global/core/ai/llm/type'; import type {
ChatCompletionCreateParams,
ChatCompletionTool
} from '@fastgpt/global/core/ai/llm/type';
import { getLLMSupportParams } from '@fastgpt/global/core/ai/llm/utils'; import { getLLMSupportParams } from '@fastgpt/global/core/ai/llm/utils';
import json5 from 'json5'; import json5 from 'json5';
import { computedMaxToken, computedTemperature } from '../../utils'; import { computedMaxToken, computedTemperature } from '../../utils';
...@@ -6,6 +9,38 @@ import { getLLMModel } from '../../model'; ...@@ -6,6 +9,38 @@ import { getLLMModel } from '../../model';
import type { InferCompletionsBody, LLMRequestBodyType } from './types'; import type { InferCompletionsBody, LLMRequestBodyType } from './types';
import type { LLMModelItemType } from '@fastgpt/global/core/ai/model.schema'; import type { LLMModelItemType } from '@fastgpt/global/core/ai/model.schema';
const privateToolSchemaKeys = new Set(['toolDescription', 'x-tool-description', 'isSecret']);
/**
* 清理 FastGPT 内部工具参数扩展字段,避免 OpenAI-compatible SDK 转成 Gemini 等原生
* function declaration 时,把 toolDescription 这类非供应商 schema 字段透传出去。
*/
const sanitizeToolParametersSchema = (schema: unknown): unknown => {
if (Array.isArray(schema)) {
return schema.map(sanitizeToolParametersSchema);
}
if (!schema || typeof schema !== 'object') {
return schema;
}
return Object.fromEntries(
Object.entries(schema as Record<string, unknown>)
.filter(([key]) => !privateToolSchemaKeys.has(key))
.map(([key, value]) => [key, sanitizeToolParametersSchema(value)])
);
};
const sanitizeCompletionTools = (tools?: ChatCompletionTool[]): ChatCompletionTool[] | undefined =>
tools?.map((tool) => ({
...tool,
function: {
...tool.function,
parameters: sanitizeToolParametersSchema(
tool.function.parameters
) as ChatCompletionTool['function']['parameters']
}
}));
/** /**
* 把 FastGPT 内部 LLM body 转成 OpenAI SDK 可请求的 completions body。 * 把 FastGPT 内部 LLM body 转成 OpenAI SDK 可请求的 completions body。
* *
...@@ -22,6 +57,7 @@ export const llmCompletionsBodyFormat = async <T extends ChatCompletionCreatePar ...@@ -22,6 +57,7 @@ export const llmCompletionsBodyFormat = async <T extends ChatCompletionCreatePar
modelData: LLMModelItemType; modelData: LLMModelItemType;
}> => { }> => {
const { tools, tool_choice, parallel_tool_calls, toolCallMode, ...body } = input; const { tools, tool_choice, parallel_tool_calls, toolCallMode, ...body } = input;
const sanitizedTools = sanitizeCompletionTools(tools);
// 这些字段只影响 FastGPT 自身逻辑,不能透传给模型供应商。 // 这些字段只影响 FastGPT 自身逻辑,不能透传给模型供应商。
delete body.retainDatasetCite; delete body.retainDatasetCite;
delete body.useVision; delete body.useVision;
...@@ -82,8 +118,8 @@ export const llmCompletionsBodyFormat = async <T extends ChatCompletionCreatePar ...@@ -82,8 +118,8 @@ export const llmCompletionsBodyFormat = async <T extends ChatCompletionCreatePar
stop: formatStop?.length ? formatStop : undefined, stop: formatStop?.length ? formatStop : undefined,
// prompt tool 模式通过 prompt 描述工具,直接传 tools 会让部分模型同时触发两套协议。 // prompt tool 模式通过 prompt 描述工具,直接传 tools 会让部分模型同时触发两套协议。
...(toolCallMode === 'toolChoice' && ...(toolCallMode === 'toolChoice' &&
tools?.length && { sanitizedTools?.length && {
tools, tools: sanitizedTools,
tool_choice, tool_choice,
parallel_tool_calls parallel_tool_calls
}) })
......
...@@ -151,6 +151,81 @@ describe('llmCompletionsBodyFormat', () => { ...@@ -151,6 +151,81 @@ describe('llmCompletionsBodyFormat', () => {
expect(requestBody).not.toHaveProperty('tools'); expect(requestBody).not.toHaveProperty('tools');
}); });
it('should remove FastGPT private tool schema fields before sending tools to model', async () => {
mockGetLLMModel.mockReturnValue(createModel());
const { requestBody } = await llmCompletionsBodyFormat({
model: 'gpt-4o',
messages,
stream: false,
tools: [
{
type: 'function',
function: {
name: 'search',
description: 'search',
parameters: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query',
toolDescription: 'Query for model',
'x-tool-description': 'HTTP query',
isSecret: true
},
filters: {
type: 'array',
items: {
type: 'object',
properties: {
key: {
type: 'string',
toolDescription: 'Filter key'
}
},
toolDescription: 'Filter object'
}
}
}
}
}
}
],
toolCallMode: 'toolChoice'
});
expect(requestBody.tools).toEqual([
{
type: 'function',
function: {
name: 'search',
description: 'search',
parameters: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query'
},
filters: {
type: 'array',
items: {
type: 'object',
properties: {
key: {
type: 'string'
}
}
}
}
}
}
}
}
]);
});
it('should apply field map after base formatting', async () => { it('should apply field map after base formatting', async () => {
mockGetLLMModel.mockReturnValue( mockGetLLMModel.mockReturnValue(
createModel({ createModel({
......
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