diff --git a/document/content/docs/upgrading/4-14/4140.mdx b/document/content/docs/upgrading/4-14/4140.mdx index 041eff41d..0fe598f36 100644 --- a/document/content/docs/upgrading/4-14/4140.mdx +++ b/document/content/docs/upgrading/4-14/4140.mdx @@ -52,11 +52,12 @@ curl --location --request POST 'https://{{host}}/api/admin/initv4140' \ ## 🐛 修复 -1. Claude 工具调用,如果下标从 1 开始会导致参数异常。 -2. S3 删除头像,如果 key 为空时,会抛错,导致流程阻塞。 -3. 工作流前置IO 变更时,依赖未及时刷新。 -4. 导出对话日志,缺少反馈记录。 -5. 工作流欢迎语输入框输入时,光标会偏移到最后一位。 -6. 存在交互节点和连续批量执行时,会导致工作流运行逻辑错误。 -7. 工作流 Redo 操作后,编辑记录无法再继续推送快照。 +1. Prompt 编辑器存在特殊语法时候,无法解析正确内容。 +2. Claude 工具调用,如果下标从 1 开始会导致参数异常。 +3. S3 删除头像,如果 key 为空时,会抛错,导致流程阻塞。 +4. 工作流前置IO 变更时,依赖未及时刷新。 +5. 导出对话日志,缺少反馈记录。 +6. 工作流欢迎语输入框输入时,光标会偏移到最后一位。 +7. 存在交互节点和连续批量执行时,会导致工作流运行逻辑错误。 +8. 工作流 Redo 操作后,编辑记录无法再继续推送快照。 diff --git a/document/data/doc-last-modified.json b/document/data/doc-last-modified.json index e106e2501..5caac7eba 100644 --- a/document/data/doc-last-modified.json +++ b/document/data/doc-last-modified.json @@ -19,7 +19,7 @@ "document/content/docs/introduction/development/custom-models/ollama.mdx": "2025-08-05T23:20:39+08:00", "document/content/docs/introduction/development/custom-models/xinference.mdx": "2025-08-05T23:20:39+08:00", "document/content/docs/introduction/development/design/dataset.mdx": "2025-07-23T21:35:03+08:00", - "document/content/docs/introduction/development/design/design_plugin.mdx": "2025-10-30T22:14:07+08:00", + "document/content/docs/introduction/development/design/design_plugin.mdx": "2025-11-04T16:58:12+08:00", "document/content/docs/introduction/development/docker.mdx": "2025-09-29T11:34:11+08:00", "document/content/docs/introduction/development/faq.mdx": "2025-08-12T22:22:18+08:00", "document/content/docs/introduction/development/intro.mdx": "2025-09-29T11:34:11+08:00", @@ -84,11 +84,11 @@ "document/content/docs/introduction/guide/knowledge_base/websync.mdx": "2025-08-05T23:20:39+08:00", "document/content/docs/introduction/guide/knowledge_base/yuque_dataset.mdx": "2025-09-17T22:29:56+08:00", "document/content/docs/introduction/guide/plugins/bing_search_plugin.mdx": "2025-07-23T21:35:03+08:00", - "document/content/docs/introduction/guide/plugins/dev_system_tool.mdx": "2025-10-30T22:14:07+08:00", + "document/content/docs/introduction/guide/plugins/dev_system_tool.mdx": "2025-11-04T16:58:12+08:00", "document/content/docs/introduction/guide/plugins/doc2x_plugin_guide.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/introduction/guide/plugins/google_search_plugin_guide.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/introduction/guide/plugins/searxng_plugin_guide.mdx": "2025-07-23T21:35:03+08:00", - "document/content/docs/introduction/guide/plugins/upload_system_tool.mdx": "2025-10-30T22:14:07+08:00", + "document/content/docs/introduction/guide/plugins/upload_system_tool.mdx": "2025-11-04T16:58:12+08:00", "document/content/docs/introduction/guide/team_permissions/invitation_link.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/introduction/guide/team_permissions/team_roles_permissions.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/introduction/index.en.mdx": "2025-07-23T21:35:03+08:00", @@ -101,7 +101,7 @@ "document/content/docs/protocol/terms.en.mdx": "2025-08-03T22:37:45+08:00", "document/content/docs/protocol/terms.mdx": "2025-08-03T22:37:45+08:00", "document/content/docs/toc.en.mdx": "2025-08-04T13:42:36+08:00", - "document/content/docs/toc.mdx": "2025-10-23T19:11:11+08:00", + "document/content/docs/toc.mdx": "2025-11-04T16:58:12+08:00", "document/content/docs/upgrading/4-10/4100.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/upgrading/4-10/4101.mdx": "2025-09-08T20:07:20+08:00", "document/content/docs/upgrading/4-11/4110.mdx": "2025-08-05T23:20:39+08:00", @@ -111,10 +111,10 @@ "document/content/docs/upgrading/4-12/4122.mdx": "2025-09-07T14:41:48+08:00", "document/content/docs/upgrading/4-12/4123.mdx": "2025-09-07T20:55:14+08:00", "document/content/docs/upgrading/4-12/4124.mdx": "2025-09-17T22:29:56+08:00", - "document/content/docs/upgrading/4-13/4130.mdx": "2025-09-30T16:00:10+08:00", + "document/content/docs/upgrading/4-13/4130.mdx": "2025-11-04T15:06:39+08:00", "document/content/docs/upgrading/4-13/4131.mdx": "2025-09-30T15:47:06+08:00", "document/content/docs/upgrading/4-13/4132.mdx": "2025-10-21T11:46:53+08:00", - "document/content/docs/upgrading/4-14/4140.mdx": "2025-11-03T12:13:10+08:00", + "document/content/docs/upgrading/4-14/4140.mdx": "2025-11-04T16:58:12+08:00", "document/content/docs/upgrading/4-8/40.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/upgrading/4-8/41.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/upgrading/4-8/42.mdx": "2025-08-02T19:38:37+08:00", diff --git a/packages/service/common/system/log.ts b/packages/service/common/system/log.ts index be70882b8..e042fe640 100644 --- a/packages/service/common/system/log.ts +++ b/packages/service/common/system/log.ts @@ -46,6 +46,54 @@ const { LOG_LEVEL, STORE_LOG_LEVEL, SIGNOZ_STORE_LEVEL } = (() => { }; })(); +/** + * Sanitize object to prevent circular references for BSON serialization + * Remove properties that may contain circular references + */ +const sanitizeObjectForBSON = (obj: Record): Record => { + try { + // Use JSON stringify with replacer to handle circular references + const seen = new WeakSet(); + const sanitized = JSON.parse( + JSON.stringify(obj, (key, value) => { + // Handle circular references + if (typeof value === 'object' && value !== null) { + if (seen.has(value)) { + return '[Circular Reference]'; + } + seen.add(value); + } + + // Remove known problematic properties from axios config + if (key === 'config' && value && typeof value === 'object') { + return { + method: value.method, + url: value.url, + baseURL: value.baseURL, + headers: value.headers, + timeout: value.timeout, + responseType: value.responseType + }; + } + + // Remove functions and other non-serializable values + if (typeof value === 'function' || typeof value === 'symbol') { + return undefined; + } + + return value; + }) + ); + return sanitized; + } catch (error) { + // If sanitization fails, return a safe fallback + return { + error: 'Failed to sanitize object', + originalKeys: Object.keys(obj) + }; + } +}; + /* add logger */ export const addLog = { log(level: LogLevelEnum, msg: string, obj: Record = {}) { @@ -77,10 +125,13 @@ export const addLog = { if (level >= STORE_LOG_LEVEL && connectionMongo.connection.readyState === 1) { (async () => { try { + // Sanitize metadata to prevent circular reference errors + const safeMetadata = sanitizeObjectForBSON(obj); + await getMongoLog().create({ text: msg, level, - metadata: obj + metadata: safeMetadata }); } catch (error) { console.error('store log error', error); diff --git a/packages/service/core/ai/llm/utils.ts b/packages/service/core/ai/llm/utils.ts index 6fe9efd60..a1988215f 100644 --- a/packages/service/core/ai/llm/utils.ts +++ b/packages/service/core/ai/llm/utils.ts @@ -163,7 +163,7 @@ export const loadRequestMessages = async ({ // If imgUrl is a local path, load image from local, and set url to base64 if ( imgUrl.startsWith('/') || - process.env.MULTIPLE_DATA_TO_BASE64 === 'true' || + process.env.MULTIPLE_DATA_TO_BASE64 !== 'false' || isInternalAddress(imgUrl) ) { const url = await (async () => { diff --git a/packages/web/components/common/Textarea/PromptEditor/type.d.ts b/packages/web/components/common/Textarea/PromptEditor/type.d.ts index d62f4b738..dd5f88057 100644 --- a/packages/web/components/common/Textarea/PromptEditor/type.d.ts +++ b/packages/web/components/common/Textarea/PromptEditor/type.d.ts @@ -42,28 +42,11 @@ export type TextEditorNode = BaseEditorNode & { export type LineBreakEditorNode = BaseEditorNode & { type: 'linebreak'; }; - -export type VariableLabelEditorNode = BaseEditorNode & { - type: 'variableLabel'; - variableKey: string; -}; - -export type VariableEditorNode = BaseEditorNode & { - type: 'Variable'; - variableKey: string; -}; - export type TabEditorNode = BaseEditorNode & { type: 'tab'; }; -export type ChildEditorNode = - | TextEditorNode - | LineBreakEditorNode - | VariableLabelEditorNode - | VariableEditorNode - | TabEditorNode; - +// Rich text export type ParagraphEditorNode = BaseEditorNode & { type: 'paragraph'; children: ChildEditorNode[]; @@ -72,15 +55,33 @@ export type ParagraphEditorNode = BaseEditorNode & { indent: number; }; +// ListItem 节点的 children 可以包含嵌套的 list 节点 +export type ListItemChildEditorNode = + | TextEditorNode + | LineBreakEditorNode + | TabEditorNode + | VariableLabelEditorNode + | VariableEditorNode; + export type ListItemEditorNode = BaseEditorNode & { type: 'listitem'; - children: Array; + children: (ListItemChildEditorNode | ListEditorNode)[]; direction: string | null; format: string; indent: number; value: number; }; +// Custom variable node types +export type VariableLabelEditorNode = BaseEditorNode & { + type: 'variableLabel'; + variableKey: string; +}; +export type VariableEditorNode = BaseEditorNode & { + type: 'Variable'; + variableKey: string; +}; + export type ListEditorNode = BaseEditorNode & { type: 'list'; children: ListItemEditorNode[]; @@ -92,10 +93,20 @@ export type ListEditorNode = BaseEditorNode & { tag: 'ul' | 'ol'; }; +export type ChildEditorNode = + | TextEditorNode + | LineBreakEditorNode + | TabEditorNode + | ParagraphEditorNode + | ListEditorNode + | ListItemEditorNode + | VariableLabelEditorNode + | VariableEditorNode; + export type EditorState = { root: { type: 'root'; - children: Array; + children: ChildEditorNode[]; direction: string; format: string; indent: number; diff --git a/packages/web/components/common/Textarea/PromptEditor/utils.ts b/packages/web/components/common/Textarea/PromptEditor/utils.ts index 8f5af546d..40a79da89 100644 --- a/packages/web/components/common/Textarea/PromptEditor/utils.ts +++ b/packages/web/components/common/Textarea/PromptEditor/utils.ts @@ -17,7 +17,8 @@ import type { ListEditorNode, ParagraphEditorNode, EditorState, - ListItemInfo + ListItemInfo, + ChildEditorNode } from './type'; export function registerLexicalTextEntity( @@ -472,6 +473,62 @@ export const editorStateToText = (editor: LexicalEditor) => { const editorState = editor.getEditorState().toJSON() as EditorState; const paragraphs = editorState.root.children; + const extractText = (node: ChildEditorNode): string => { + if (!node) return ''; + + // Handle line break nodes + if (node.type === 'linebreak') { + return '\n'; + } + + // Handle tab nodes + if (node.type === 'tab') { + return ' '; + } + + // Handle text nodes + if (node.type === 'text') { + return node.text || ''; + } + + // Handle custom variable nodes + if (node.type === 'variableLabel' || node.type === 'Variable') { + return node.variableKey || ''; + } + + // Handle paragraph nodes - recursively process children + if (node.type === 'paragraph') { + if (!node.children || node.children.length === 0) { + return ''; + } + return node.children.map(extractText).join(''); + } + + // Handle list item nodes - recursively process children (excluding nested lists) + if (node.type === 'listitem') { + if (!node.children || node.children.length === 0) { + return ''; + } + // Filter out nested list nodes as they are handled separately + return node.children + .filter((child) => child.type !== 'list') + .map(extractText) + .join(''); + } + + // Handle list nodes - recursively process children + if (node.type === 'list') { + if (!node.children || node.children.length === 0) { + return ''; + } + return node.children.map(extractText).join(''); + } + + // Unknown node type + console.warn('Unknown node type in extractText:', (node as any).type, node); + return ''; + }; + paragraphs.forEach((paragraph) => { if (paragraph.type === 'list') { const listResults = processList({ list: paragraph }); @@ -483,19 +540,15 @@ export const editorStateToText = (editor: LexicalEditor) => { const indentSpaces = ' '.repeat(paragraph.indent || 0); children.forEach((child) => { - if (child.type === 'linebreak') { - paragraphText.push('\n'); - } else if (child.type === 'text') { - paragraphText.push(child.text); - } else if (child.type === 'tab') { - paragraphText.push(' '); - } else if (child.type === 'variableLabel' || child.type === 'Variable') { - paragraphText.push(child.variableKey); - } + const val = extractText(child); + paragraphText.push(val); }); const finalText = paragraphText.join(''); editorStateTextString.push(indentSpaces + finalText); + } else { + const text = extractText(paragraph); + editorStateTextString.push(text); } }); return editorStateTextString.join('\n'); diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/index.tsx b/projects/app/src/components/core/chat/ChatContainer/ChatBox/index.tsx index 48ea03814..6e8f4ece0 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/index.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/index.tsx @@ -651,11 +651,12 @@ const ChatBox = ({ // retry input const onDelMessage = useCallback( - (contentId: string) => { + (contentId: string, delFile = true) => { return delChatRecordById({ appId, chatId, contentId, + delFile, ...outLinkAuthData }); }, @@ -672,7 +673,7 @@ const ChatBox = ({ await Promise.all( delHistory.map((item) => { if (item.dataId) { - return onDelMessage(item.dataId); + return onDelMessage(item.dataId, false); } }) ); diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/utils.ts b/projects/app/src/components/core/chat/ChatContainer/ChatBox/utils.ts index b77bc5817..13968ef8e 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/utils.ts +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/utils.ts @@ -31,7 +31,8 @@ export const formatChatValue2InputType = (value?: ChatItemValueItemType[]): Chat type: item.file.type, name: item.file.name, icon: getFileIcon(item.file.name), - url: item.file.url + url: item.file.url, + key: item.file.key } : undefined ) diff --git a/projects/app/src/global/core/chat/api.d.ts b/projects/app/src/global/core/chat/api.d.ts index 77dc6432d..6c804f30b 100644 --- a/projects/app/src/global/core/chat/api.d.ts +++ b/projects/app/src/global/core/chat/api.d.ts @@ -88,6 +88,7 @@ export type DeleteChatItemProps = OutLinkChatAuthProps & { appId: string; chatId: string; contentId?: string; + delFile?: boolean; }; export type AdminUpdateFeedbackParams = AdminFbkType & { diff --git a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodeVariableUpdate.tsx b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodeVariableUpdate.tsx index 80b5e3a8e..56a888387 100644 --- a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodeVariableUpdate.tsx +++ b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodeVariableUpdate.tsx @@ -34,6 +34,7 @@ import { import { InputTypeEnum } from '@/components/core/app/formRender/constant'; import { WorkflowActionsContext } from '../../context/workflowActionsContext'; import { useMemoEnhance } from '@fastgpt/web/hooks/useMemoEnhance'; +import { useMemoizedFn } from 'ahooks'; const NodeVariableUpdate = ({ data, selected }: NodeProps) => { const { inputs = [], nodeId } = data; @@ -105,7 +106,7 @@ const NodeVariableUpdate = ({ data, selected }: NodeProps) => [inputs, nodeId, onChangeNode] ); - const ValueRender = useCallback( + const ValueRender = useMemoizedFn( ({ updateItem, index }: { updateItem: TUpdateListItem; index: number }) => { const { inputType, formParams = {} } = (() => { const value = updateItem.variable; @@ -279,7 +280,6 @@ const NodeVariableUpdate = ({ data, selected }: NodeProps) => return ( ) => ); - }, - [ - appDetail.chatConfig, - externalProviderWorkflowVariables, - getNodeById, - nodeId, - onUpdateList, - systemConfigNode, - t, - updateList, - variables - ] + } ); const Render = useMemo(() => { diff --git a/projects/app/src/pages/api/admin/initv4140.ts b/projects/app/src/pages/api/admin/initv4140.ts index fd3c99bb7..bb640efae 100644 --- a/projects/app/src/pages/api/admin/initv4140.ts +++ b/projects/app/src/pages/api/admin/initv4140.ts @@ -174,7 +174,7 @@ async function migrateSystemPluginsToTools(typeToGroupMap: Map): pluginId: plugin.pluginId?.startsWith(AppToolSourceEnum.community) ? plugin.pluginId.replace(AppToolSourceEnum.community, AppToolSourceEnum.systemTool) : plugin.pluginId, - status: !plugin.isActive ? PluginStatusEnum.Offline : PluginStatusEnum.Normal, + status: plugin.isActive === false ? PluginStatusEnum.Offline : PluginStatusEnum.Normal, defaultInstalled: false, originCost: plugin.originCost || 0, currentCost: plugin.currentCost || 0, @@ -182,7 +182,7 @@ async function migrateSystemPluginsToTools(typeToGroupMap: Map): pluginOrder: plugin.pluginOrder, systemKeyCost: plugin.systemKeyCost || 0, customConfig: plugin.customConfig ? { ...plugin.customConfig } : {}, - inputListVal: plugin.inputListVal || {} + inputListVal: plugin.inputListVal }; // 迁移 templateType → tags diff --git a/projects/app/src/pages/api/core/chat/item/delete.ts b/projects/app/src/pages/api/core/chat/item/delete.ts index d3ba200a9..e90f1d195 100644 --- a/projects/app/src/pages/api/core/chat/item/delete.ts +++ b/projects/app/src/pages/api/core/chat/item/delete.ts @@ -9,8 +9,8 @@ import { MongoChatItemResponse } from '@fastgpt/service/core/chat/chatItemRespon import { ChatItemValueTypeEnum, ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; import { getS3ChatSource } from '@fastgpt/service/common/s3/sources/chat'; -async function handler(req: ApiRequestProps<{}, DeleteChatItemProps>, res: NextApiResponse) { - const { appId, chatId, contentId } = req.query; +async function handler(req: ApiRequestProps, res: NextApiResponse) { + const { appId, chatId, contentId, delFile = true } = req.body; if (!contentId || !chatId) { return Promise.reject('contentId or chatId is empty'); @@ -20,7 +20,7 @@ async function handler(req: ApiRequestProps<{}, DeleteChatItemProps>, res: NextA req, authToken: true, authApiKey: true, - ...req.query + ...req.body }); await mongoSessionRun(async (session) => { @@ -36,7 +36,7 @@ async function handler(req: ApiRequestProps<{}, DeleteChatItemProps>, res: NextA dataId: contentId }).session(session); - if (item?.obj === ChatRoleEnum.Human) { + if (item?.obj === ChatRoleEnum.Human && delFile) { const s3ChatSource = getS3ChatSource(); for (const value of item.value) { if (value.type === ChatItemValueTypeEnum.file && value.file?.key) { diff --git a/projects/app/src/pages/api/core/plugin/team/toggleInstall.ts b/projects/app/src/pages/api/core/plugin/team/toggleInstall.ts index 65f76e589..0c801a644 100644 --- a/projects/app/src/pages/api/core/plugin/team/toggleInstall.ts +++ b/projects/app/src/pages/api/core/plugin/team/toggleInstall.ts @@ -2,7 +2,7 @@ import { NextAPI } from '@/service/middleware/entry'; import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next'; import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; import { MongoTeamInstalledPlugin } from '@fastgpt/service/core/plugin/schema/teamInstalledPluginSchema'; -import { WritePermissionVal } from '@fastgpt/global/support/permission/constant'; +import { ReadPermissionVal, WritePermissionVal } from '@fastgpt/global/support/permission/constant'; import type { ToggleInstallPluginBodyType } from '@fastgpt/global/openapi/core/plugin/team/api'; export type ToggleInstallPluginBody = ToggleInstallPluginBodyType; @@ -18,7 +18,7 @@ async function handler( const { teamId } = await authUserPer({ req, authToken: true, - per: WritePermissionVal + per: ReadPermissionVal }); await MongoTeamInstalledPlugin.findOneAndUpdate( diff --git a/projects/app/src/web/core/chat/api.ts b/projects/app/src/web/core/chat/api.ts index 2ca3ce6df..a008c22bf 100644 --- a/projects/app/src/web/core/chat/api.ts +++ b/projects/app/src/web/core/chat/api.ts @@ -75,7 +75,7 @@ export const delClearChatHistories = (data: ClearHistoriesProps) => * delete one chat record */ export const delChatRecordById = (data: DeleteChatItemProps) => - DELETE(`/core/chat/item/delete`, data); + POST(`/core/chat/item/delete`, data); /** * 修改历史记录: 标题/置顶 diff --git a/test/cases/service/core/ai/llm/utils.test.ts b/test/cases/service/core/ai/llm/utils.test.ts index 858f428bc..204c67534 100644 --- a/test/cases/service/core/ai/llm/utils.test.ts +++ b/test/cases/service/core/ai/llm/utils.test.ts @@ -411,8 +411,11 @@ describe('loadRequestMessages function tests', () => { const result = await loadRequestMessages({ messages, useVision: true }); expect(result).toHaveLength(1); - expect(typeof result[0].content).toBe('string'); - expect(result[0].content).toBe('https://example.com/image.png'); + // When useVision is true and text contains image URL, it returns array format + expect(Array.isArray(result[0].content)).toBe(true); + const content = result[0].content as any[]; + expect(content.some((item: any) => item.type === 'image_url')).toBe(true); + expect(content.some((item: any) => item.type === 'text')).toBe(true); }); it('should not extract images when useVision is false', async () => { @@ -474,9 +477,10 @@ describe('loadRequestMessages function tests', () => { const result = await loadRequestMessages({ messages, useVision: true }); expect(result).toHaveLength(1); - // When array content has only text items and filtered images, it becomes a string - expect(typeof result[0].content).toBe('string'); - expect(result[0].content).toBe('Hello'); + // When array content has text and image_url, remains as array + expect(Array.isArray(result[0].content)).toBe(true); + const content = result[0].content as any[]; + expect(content.some((item: any) => item.type === 'text')).toBe(true); }); it('should filter out empty text items from array content', async () => { @@ -539,6 +543,9 @@ describe('loadRequestMessages function tests', () => { }); it('should handle invalid remote images gracefully', async () => { + const originalEnv = process.env.MULTIPLE_DATA_TO_BASE64; + process.env.MULTIPLE_DATA_TO_BASE64 = 'false'; // Disable base64 conversion + const messages: ChatCompletionMessageParam[] = [ { role: ChatCompletionRequestMessageRoleEnum.User, @@ -554,9 +561,17 @@ describe('loadRequestMessages function tests', () => { const result = await loadRequestMessages({ messages, useVision: true }); expect(result).toHaveLength(1); - // When image is filtered out and only text remains, it becomes string + // When image is filtered out and only one text item remains, it becomes string expect(typeof result[0].content).toBe('string'); expect(result[0].content).toBe('Text'); + + // Restore original environment + if (originalEnv !== undefined) { + process.env.MULTIPLE_DATA_TO_BASE64 = originalEnv; + } else { + // @ts-ignore + delete process.env.MULTIPLE_DATA_TO_BASE64; + } }); it('should handle 405 status as valid image', async () => { @@ -577,9 +592,11 @@ describe('loadRequestMessages function tests', () => { const result = await loadRequestMessages({ messages, useVision: true }); expect(result).toHaveLength(1); - // The function processes images from array content differently, expects text to remain - expect(typeof result[0].content).toBe('string'); - expect(result[0].content).toBe('Check this image:'); + // 405 status is treated as valid, so image is kept and content is array + expect(Array.isArray(result[0].content)).toBe(true); + const content = result[0].content as any[]; + expect(content.some((item: any) => item.type === 'text')).toBe(true); + expect(content.some((item: any) => item.type === 'image_url')).toBe(true); }); it('should remove origin from image URLs when provided', async () => {