From beae0ec05c769f97f59ff6e89379ad49cbeefc22 Mon Sep 17 00:00:00 2001 From: Edward Tan Date: Wed, 19 Nov 2025 18:05:13 +0800 Subject: [PATCH 1/5] Add ChatOllama Cloud component Add a dedicated ChatOllama Cloud chat model component to integrate with Ollama Cloud API (https://ollama.com) with complete tool calling functionality. Features: - Dropdown selection for available Ollama Cloud models: * gpt-oss:120b, gpt-oss:20b * deepseek-v3.1:671b * qwen3-coder:480b, qwen3-vl:235b * minimax-m2, glm-4.6 - Configurable base URL (defaults to https://ollama.com) - Full tool calling support with bidirectional argument conversion Files added: - packages/components/nodes/chatmodels/ChatOllamaCloud/ChatOllamaCloud.ts - packages/components/nodes/chatmodels/ChatOllamaCloud/Ollama.svg - packages/components/credentials/OllamaCloudApi.credential.ts --- .../credentials/OllamaCloudApi.credential.ts | 27 + .../ChatOllamaCloud/ChatOllamaCloud.ts | 475 ++++++++++++++++++ .../chatmodels/ChatOllamaCloud/Ollama.svg | 1 + 3 files changed, 503 insertions(+) create mode 100644 packages/components/credentials/OllamaCloudApi.credential.ts create mode 100644 packages/components/nodes/chatmodels/ChatOllamaCloud/ChatOllamaCloud.ts create mode 100644 packages/components/nodes/chatmodels/ChatOllamaCloud/Ollama.svg diff --git a/packages/components/credentials/OllamaCloudApi.credential.ts b/packages/components/credentials/OllamaCloudApi.credential.ts new file mode 100644 index 00000000000..4a4027b18e9 --- /dev/null +++ b/packages/components/credentials/OllamaCloudApi.credential.ts @@ -0,0 +1,27 @@ +import { INodeParams, INodeCredential } from '../src/Interface' + +class OllamaCloudApi implements INodeCredential { + label: string + name: string + version: number + description?: string + inputs: INodeParams[] + + constructor() { + this.label = 'Ollama Cloud API' + this.name = 'ollamaCloudApi' + this.version = 1.0 + this.description = 'API key for Ollama Cloud (https://ollama.com)' + this.inputs = [ + { + label: 'Ollama Cloud API Key', + name: 'ollamaCloudApiKey', + type: 'password', + placeholder: 'sk-...' + } + ] + } +} + +module.exports = { credClass: OllamaCloudApi } + diff --git a/packages/components/nodes/chatmodels/ChatOllamaCloud/ChatOllamaCloud.ts b/packages/components/nodes/chatmodels/ChatOllamaCloud/ChatOllamaCloud.ts new file mode 100644 index 00000000000..3eba80a0ca4 --- /dev/null +++ b/packages/components/nodes/chatmodels/ChatOllamaCloud/ChatOllamaCloud.ts @@ -0,0 +1,475 @@ +import { BaseCache } from '@langchain/core/caches' +import { ICommonObject, INode, INodeData, INodeParams, IServerSideEventStreamer } from '../../../src/Interface' +import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' +import { ChatOpenAI, ClientOptions } from '@langchain/openai' + +class ChatOllamaCloud_ChatModels implements INode { + label: string + name: string + version: number + type: string + icon: string + category: string + description: string + baseClasses: string[] + credential: INodeParams + inputs: INodeParams[] + + constructor() { + this.label = 'ChatOllama Cloud' + this.name = 'chatOllamaCloud' + this.version = 1.0 + this.type = 'ChatOllamaCloud' + this.icon = 'Ollama.svg' + this.category = 'Chat Models' + this.description = 'Chat with Ollama Cloud API with full tool calling support' + this.baseClasses = [this.type, ...getBaseClasses(ChatOpenAI)] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['ollamaCloudApi'] + } + this.inputs = [ + { + label: 'Cache', + name: 'cache', + type: 'BaseCache', + optional: true + }, + { + label: 'Base URL', + name: 'baseUrl', + type: 'string', + default: 'https://ollama.com', + description: 'Base URL for Ollama Cloud or compatible API server', + optional: true, + additionalParams: true + }, + { + label: 'Model Name', + name: 'modelName', + type: 'options', + description: 'Select the Ollama Cloud model to use', + options: [ + { + label: 'GPT-OSS 120B', + name: 'gpt-oss:120b', + description: 'Large general-purpose model with 120B parameters' + }, + { + label: 'GPT-OSS 20B', + name: 'gpt-oss:20b', + description: 'Efficient general-purpose model with 20B parameters' + }, + { + label: 'DeepSeek V3.1 671B', + name: 'deepseek-v3.1:671b', + description: 'Advanced reasoning model with 671B parameters' + }, + { + label: 'Qwen3 Coder 480B', + name: 'qwen3-coder:480b', + description: 'Specialized coding model with 480B parameters' + }, + { + label: 'Qwen3 VL 235B', + name: 'qwen3-vl:235b', + description: 'Vision-language model with 235B parameters' + }, + { + label: 'MiniMax M2', + name: 'minimax-m2', + description: 'Efficient multi-modal model' + }, + { + label: 'GLM 4.6', + name: 'glm-4.6', + description: 'General Language Model version 4.6' + } + ], + default: 'gpt-oss:120b' + }, + { + label: 'Temperature', + name: 'temperature', + type: 'number', + step: 0.1, + default: 0.9, + optional: true, + description: 'Controls randomness in the output. Higher values = more creative, lower values = more focused.' + }, + { + label: 'Max Tokens', + name: 'maxTokens', + type: 'number', + step: 1, + optional: true, + additionalParams: true, + description: 'Maximum number of tokens to generate in the response' + }, + { + label: 'Top P', + name: 'topP', + type: 'number', + step: 0.1, + optional: true, + additionalParams: true, + description: 'Nucleus sampling parameter. Controls diversity of output.' + }, + { + label: 'Frequency Penalty', + name: 'frequencyPenalty', + type: 'number', + step: 0.1, + optional: true, + additionalParams: true, + description: 'Penalizes repeated tokens based on frequency' + }, + { + label: 'Presence Penalty', + name: 'presencePenalty', + type: 'number', + step: 0.1, + optional: true, + additionalParams: true, + description: 'Penalizes repeated tokens based on presence' + }, + { + label: 'Timeout', + name: 'timeout', + type: 'number', + step: 1, + optional: true, + additionalParams: true, + description: 'Timeout in milliseconds for API requests' + } + ] + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + const temperature = nodeData.inputs?.temperature as string + const modelName = nodeData.inputs?.modelName as string + const maxTokens = nodeData.inputs?.maxTokens as string + const topP = nodeData.inputs?.topP as string + const frequencyPenalty = nodeData.inputs?.frequencyPenalty as string + const presencePenalty = nodeData.inputs?.presencePenalty as string + const timeout = nodeData.inputs?.timeout as string + const cache = nodeData.inputs?.cache as BaseCache + const baseUrl = (nodeData.inputs?.baseUrl as string) || 'https://ollama.com' + + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + const ollamaCloudApiKey = getCredentialParam('ollamaCloudApiKey', credentialData, nodeData) + + const obj: any = { + temperature: parseFloat(temperature), + model: modelName + } + + if (maxTokens) obj.maxTokens = parseInt(maxTokens, 10) + if (topP) obj.topP = parseFloat(topP) + if (frequencyPenalty) obj.frequencyPenalty = parseFloat(frequencyPenalty) + if (presencePenalty) obj.presencePenalty = parseFloat(presencePenalty) + if (timeout) obj.timeout = parseInt(timeout, 10) + if (cache) obj.cache = cache + + // Set the API key in configuration to avoid early validation + // Construct the full API URL by appending /api to the base URL + const apiUrl = baseUrl.endsWith('/') ? `${baseUrl}api` : `${baseUrl}/api` + + obj.configuration = { + apiKey: ollamaCloudApiKey, + baseURL: apiUrl + } + + // Helper function to transform Ollama response to OpenAI format + const toOpenAIResponse = (ollamaResponse: any) => { + const role = ollamaResponse?.message?.role ?? 'assistant' + const content = ollamaResponse?.message?.content ?? ollamaResponse?.response ?? '' + const finishReason = ollamaResponse?.done ? 'stop' : null + const modelName = ollamaResponse?.model ?? 'unknown' + const created = ollamaResponse?.created_at ? Math.floor(new Date(ollamaResponse.created_at).getTime() / 1000) : Math.floor(Date.now() / 1000) + + const promptTokens = ollamaResponse?.prompt_eval_count ?? 0 + const completionTokens = ollamaResponse?.eval_count ?? 0 + const usage = promptTokens > 0 || completionTokens > 0 + ? { + prompt_tokens: promptTokens, + completion_tokens: completionTokens, + total_tokens: completionTokens + promptTokens + } + : undefined + + const choice: any = { + index: 0, + message: { + role, + content: content || '' + } + } + + // Preserve tool_calls if Ollama returned them + // CRITICAL: Convert arguments from object to JSON string for LangChain compatibility + if (ollamaResponse?.message?.tool_calls) { + choice.message.tool_calls = ollamaResponse.message.tool_calls.map((tc: any) => ({ + ...tc, + function: { + ...tc.function, + arguments: typeof tc.function.arguments === 'string' + ? tc.function.arguments + : JSON.stringify(tc.function.arguments) + } + })) + } + + if (finishReason) { + choice.finish_reason = finishReason + } + + return { + id: ollamaResponse?.id ?? `ollama-${Date.now()}`, + object: 'chat.completion', + created, + model: modelName, + choices: [choice], + ...(usage && { usage }) + } + } + + // Store tools in a closure so customFetch can access them + let boundTools: any[] | null = null + + const customFetch = async (input: any, init?: any): Promise => { + const originalUrl = + typeof input === 'string' + ? input + : input instanceof URL + ? input.toString() + : input?.url + + const isChatCompletions = typeof originalUrl === 'string' && originalUrl.includes('/chat/completions') + + let finalInput = input + let finalInit = init + + if (isChatCompletions) { + // Rewrite URL from /chat/completions to /chat for Ollama API + if (typeof originalUrl === 'string') { + const targetUrl = new URL(originalUrl) + targetUrl.pathname = targetUrl.pathname.replace(/\/chat\/completions$/, '/chat') + finalInput = targetUrl.toString() + } else if (input instanceof URL) { + input.pathname = input.pathname.replace(/\/chat\/completions$/, '/chat') + finalInput = input + } + + // Modify request body for Ollama compatibility + if (init?.body) { + try { + const bodyObj = typeof init.body === 'string' ? JSON.parse(init.body) : init.body + + // Inject bound tools if they're not in the request + if (!bodyObj.tools && boundTools && boundTools.length > 0) { + // Convert LangChain tools to OpenAI format with proper JSON Schema + const convertedTools = boundTools.map((tool: any) => { + if (typeof tool.convertToOpenAITool === 'function') { + return tool.convertToOpenAITool() + } + if (tool.type === 'function' && tool.function) { + return tool + } + // Manual conversion for LangChain tools with Zod schema + if (tool.name && tool.schema) { + try { + const { zodToJsonSchema } = require('zod-to-json-schema') + const jsonSchema = zodToJsonSchema(tool.schema) + delete jsonSchema.$schema + + return { + type: 'function', + function: { + name: tool.name, + description: tool.description || '', + parameters: jsonSchema + } + } + } catch (e) { + return { + type: 'function', + function: { + name: tool.name, + description: tool.description || '', + parameters: { + type: 'object', + properties: {}, + required: [] + } + } + } + } + } + return { + type: 'function', + function: { + name: tool.name || 'unknown', + description: tool.description || '', + parameters: { + type: 'object', + properties: {}, + required: [] + } + } + } + }) + + bodyObj.tools = convertedTools + } + + // Set tool_choice to auto if tools are present + if (bodyObj.tools && bodyObj.tools.length > 0 && !bodyObj.tool_choice) { + bodyObj.tool_choice = 'auto' + } + + // CRITICAL: Convert tool_calls arguments from STRING to OBJECT in messages + // LangChain sends arguments as JSON strings, but Ollama expects objects + if (bodyObj.messages && Array.isArray(bodyObj.messages)) { + bodyObj.messages = bodyObj.messages.map((msg: any) => { + // Convert tool_calls in additional_kwargs + if (msg.additional_kwargs?.tool_calls && Array.isArray(msg.additional_kwargs.tool_calls)) { + msg.additional_kwargs.tool_calls = msg.additional_kwargs.tool_calls.map((tc: any) => { + if (tc.function && typeof tc.function.arguments === 'string') { + try { + return { + ...tc, + function: { + ...tc.function, + arguments: JSON.parse(tc.function.arguments) + } + } + } catch (e) { + return tc + } + } + return tc + }) + } + + // Also convert top-level tool_calls if present + if (msg.tool_calls && Array.isArray(msg.tool_calls)) { + msg.tool_calls = msg.tool_calls.map((tc: any) => { + if (tc.function && typeof tc.function.arguments === 'string') { + try { + return { + ...tc, + function: { + ...tc.function, + arguments: JSON.parse(tc.function.arguments) + } + } + } catch (e) { + return tc + } + } + return tc + }) + } + + return msg + }) + } + + // Force non-streaming for Ollama compatibility + bodyObj.stream = false + delete bodyObj.stream_options + + const modifiedBody = JSON.stringify(bodyObj) + const newBodyLength = Buffer.byteLength(modifiedBody, 'utf8') + + // Update Content-Length header to match the new body + const headers = init.headers ? { ...init.headers } : {} + headers['content-length'] = String(newBodyLength) + + finalInit = { + ...init, + body: modifiedBody, + headers + } + } catch (error) { + finalInit = init + } + } + } + + const response = await fetch(finalInput as any, finalInit) + + if (!isChatCompletions) return response + + // Transform Ollama response to OpenAI format + const bodyText = await response.text() + let finalBody = bodyText + + try { + const parsed = JSON.parse(bodyText) + + // Check if already in OpenAI format + if (parsed?.choices && Array.isArray(parsed.choices) && parsed.choices[0]?.message) { + finalBody = bodyText + } else { + // Transform Ollama format to OpenAI format + const transformedResponse = toOpenAIResponse(parsed) + finalBody = JSON.stringify(transformedResponse) + } + } catch (error) { + finalBody = bodyText + } + + // Return proper Response object + const ResponseCtor = (globalThis as any).Response + if (!ResponseCtor) return response + + return new ResponseCtor(finalBody, { + status: response.status, + statusText: response.statusText, + headers: { + 'content-type': 'application/json', + 'content-length': String(finalBody.length) + } + }) + } + + // Add custom fetch to existing configuration + obj.configuration.fetch = customFetch + + const model = new ChatOpenAI(obj) + + // Force streaming to false for Ollama compatibility + model.streaming = false + + // Override _streamResponseChunks to delegate to non-streaming generation + const originalGenerate = model._generate.bind(model) + model._streamResponseChunks = async function* (messages: any, options: any, runManager: any) { + const result = await originalGenerate(messages, options, runManager) + if (result && result.generations && result.generations.length > 0) { + for (const generation of result.generations) { + yield generation + } + } + } as any + + // Store bound tools for injection into requests + const originalBindTools = model.bindTools?.bind(model) + if (originalBindTools) { + model.bindTools = function(tools: any) { + if (Array.isArray(tools)) { + boundTools = tools + } + return originalBindTools(tools) + } as any + } + + return model + } +} + +module.exports = { nodeClass: ChatOllamaCloud_ChatModels } + diff --git a/packages/components/nodes/chatmodels/ChatOllamaCloud/Ollama.svg b/packages/components/nodes/chatmodels/ChatOllamaCloud/Ollama.svg new file mode 100644 index 00000000000..2dc8df5311e --- /dev/null +++ b/packages/components/nodes/chatmodels/ChatOllamaCloud/Ollama.svg @@ -0,0 +1 @@ + \ No newline at end of file From 298e761064fd3e109c6da8bf136bad7c54c294d1 Mon Sep 17 00:00:00 2001 From: Edward Tan Date: Thu, 20 Nov 2025 09:52:46 +0800 Subject: [PATCH 2/5] feat: add ChatOllama Cloud component --- .../ChatOllamaCloud/ChatOllamaCloud.ts | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/packages/components/nodes/chatmodels/ChatOllamaCloud/ChatOllamaCloud.ts b/packages/components/nodes/chatmodels/ChatOllamaCloud/ChatOllamaCloud.ts index 3eba80a0ca4..6567a7a3d5f 100644 --- a/packages/components/nodes/chatmodels/ChatOllamaCloud/ChatOllamaCloud.ts +++ b/packages/components/nodes/chatmodels/ChatOllamaCloud/ChatOllamaCloud.ts @@ -255,12 +255,23 @@ class ChatOllamaCloud_ChatModels implements INode { if (isChatCompletions) { // Rewrite URL from /chat/completions to /chat for Ollama API if (typeof originalUrl === 'string') { - const targetUrl = new URL(originalUrl) - targetUrl.pathname = targetUrl.pathname.replace(/\/chat\/completions$/, '/chat') - finalInput = targetUrl.toString() + try { + const targetUrl = new URL(originalUrl) + targetUrl.pathname = targetUrl.pathname.replace(/\/chat\/completions$/, '/chat') + finalInput = targetUrl.toString() + } catch (e) { + // Fallback: simple string replacement if URL parsing fails + finalInput = originalUrl.replace(/\/chat\/completions$/, '/chat') + } } else if (input instanceof URL) { - input.pathname = input.pathname.replace(/\/chat\/completions$/, '/chat') - finalInput = input + try { + input.pathname = input.pathname.replace(/\/chat\/completions$/, '/chat') + finalInput = input + } catch (e) { + // If pathname is read-only, create new URL + const urlStr = input.toString() + finalInput = urlStr.replace(/\/chat\/completions$/, '/chat') + } } // Modify request body for Ollama compatibility From c0eef3e7a836738312f7e53f58d503f474f686ad Mon Sep 17 00:00:00 2001 From: Edward Tan Date: Tue, 9 Dec 2025 16:41:32 +0800 Subject: [PATCH 3/5] feat: add more Ollama Cloud models and custom model input support - Add 7 new cloud models: qwen3-next:80b-cloud, mistral-large-3:675b-cloud, ministral-3:3b-cloud, ministral-3:8b-cloud, ministral-3:14b-cloud, cogito-2.1:671b-cloud, kimi-k2-thinking:cloud - Split model selection into dropdown + custom text field for better UX - Allow users to enter custom model names while keeping preset options - Bump version to 1.2 --- .../ChatOllamaCloud/ChatOllamaCloud.ts | 48 ++++++++++++++++++- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/packages/components/nodes/chatmodels/ChatOllamaCloud/ChatOllamaCloud.ts b/packages/components/nodes/chatmodels/ChatOllamaCloud/ChatOllamaCloud.ts index 6567a7a3d5f..c6864b88aa5 100644 --- a/packages/components/nodes/chatmodels/ChatOllamaCloud/ChatOllamaCloud.ts +++ b/packages/components/nodes/chatmodels/ChatOllamaCloud/ChatOllamaCloud.ts @@ -18,7 +18,7 @@ class ChatOllamaCloud_ChatModels implements INode { constructor() { this.label = 'ChatOllama Cloud' this.name = 'chatOllamaCloud' - this.version = 1.0 + this.version = 1.2 this.type = 'ChatOllamaCloud' this.icon = 'Ollama.svg' this.category = 'Chat Models' @@ -77,6 +77,41 @@ class ChatOllamaCloud_ChatModels implements INode { name: 'qwen3-vl:235b', description: 'Vision-language model with 235B parameters' }, + { + label: 'Qwen3 Next 80B Cloud', + name: 'qwen3-next:80b-cloud', + description: 'Advanced Qwen3 model with 80B parameters, cloud version' + }, + { + label: 'Mistral Large 3 675B Cloud', + name: 'mistral-large-3:675b-cloud', + description: 'Large Mistral model with 675B parameters, cloud version' + }, + { + label: 'MiniStral 3 3B Cloud', + name: 'ministral-3:3b-cloud', + description: 'Efficient MiniStral model with 3B parameters, cloud version' + }, + { + label: 'MiniStral 3 8B Cloud', + name: 'ministral-3:8b-cloud', + description: 'Balanced MiniStral model with 8B parameters, cloud version' + }, + { + label: 'MiniStral 3 14B Cloud', + name: 'ministral-3:14b-cloud', + description: 'Advanced MiniStral model with 14B parameters, cloud version' + }, + { + label: 'Cogito 2.1 671B Cloud', + name: 'cogito-2.1:671b-cloud', + description: 'Reasoning model with 671B parameters, cloud version' + }, + { + label: 'Kimi K2 Thinking Cloud', + name: 'kimi-k2-thinking:cloud', + description: 'Advanced thinking model, cloud version' + }, { label: 'MiniMax M2', name: 'minimax-m2', @@ -90,6 +125,14 @@ class ChatOllamaCloud_ChatModels implements INode { ], default: 'gpt-oss:120b' }, + { + label: 'Custom Model Name', + name: 'customModelName', + type: 'string', + placeholder: 'e.g. llama3:70b', + description: 'Custom model name to use. If provided, it will override the model selected above.', + optional: true + }, { label: 'Temperature', name: 'temperature', @@ -150,6 +193,7 @@ class ChatOllamaCloud_ChatModels implements INode { async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { const temperature = nodeData.inputs?.temperature as string const modelName = nodeData.inputs?.modelName as string + const customModelName = nodeData.inputs?.customModelName as string const maxTokens = nodeData.inputs?.maxTokens as string const topP = nodeData.inputs?.topP as string const frequencyPenalty = nodeData.inputs?.frequencyPenalty as string @@ -163,7 +207,7 @@ class ChatOllamaCloud_ChatModels implements INode { const obj: any = { temperature: parseFloat(temperature), - model: modelName + model: customModelName || modelName } if (maxTokens) obj.maxTokens = parseInt(maxTokens, 10) From cb67fb3b95d19931275cba7619145b4b309d7202 Mon Sep 17 00:00:00 2001 From: Edward Tan Date: Fri, 12 Dec 2025 15:40:08 +0800 Subject: [PATCH 4/5] feat(ChatOllama): add Reasoning Effort parameter with robust error handling - Add Reasoning Effort parameter (low/medium/high) for thinking models - Fix 405 method not allowed error by adding fallback to standard streaming - Improve URL handling by removing trailing slashes from base URL - Document think parameter compatibility with GPT-OSS and other reasoning models - Gracefully handle non-native Ollama endpoints that don't support /api/chat --- .../nodes/chatmodels/ChatOllama/ChatOllama.ts | 32 +++- .../ChatOllama/FlowiseChatOllama.ts | 139 +++++++++++++++++- 2 files changed, 166 insertions(+), 5 deletions(-) diff --git a/packages/components/nodes/chatmodels/ChatOllama/ChatOllama.ts b/packages/components/nodes/chatmodels/ChatOllama/ChatOllama.ts index 25bee4364c9..a1fab1ad6d5 100644 --- a/packages/components/nodes/chatmodels/ChatOllama/ChatOllama.ts +++ b/packages/components/nodes/chatmodels/ChatOllama/ChatOllama.ts @@ -1,9 +1,8 @@ -import { ChatOllamaInput } from '@langchain/ollama' import { BaseChatModelParams } from '@langchain/core/language_models/chat_models' import { BaseCache } from '@langchain/core/caches' import { IMultiModalOption, INode, INodeData, INodeParams } from '../../../src/Interface' import { getBaseClasses } from '../../../src/utils' -import { ChatOllama } from './FlowiseChatOllama' +import { ChatOllama, FlowiseChatOllamaInput } from './FlowiseChatOllama' class ChatOllama_ChatModels implements INode { label: string @@ -20,7 +19,7 @@ class ChatOllama_ChatModels implements INode { constructor() { this.label = 'ChatOllama' this.name = 'chatOllama' - this.version = 5.0 + this.version = 5.1 this.type = 'ChatOllama' this.icon = 'Ollama.svg' this.category = 'Chat Models' @@ -210,6 +209,29 @@ class ChatOllama_ChatModels implements INode { step: 0.1, optional: true, additionalParams: true + }, + { + label: 'Reasoning Effort', + name: 'reasoningEffort', + type: 'options', + description: + 'Controls the thinking/reasoning depth for reasoning models (e.g., GPT-OSS, DeepSeek-R1, Qwen3). Higher effort = more thorough reasoning but slower responses.', + options: [ + { + label: 'Low', + name: 'low' + }, + { + label: 'Medium', + name: 'medium' + }, + { + label: 'High', + name: 'high' + } + ], + optional: true, + additionalParams: true } ] } @@ -230,13 +252,14 @@ class ChatOllama_ChatModels implements INode { const repeatLastN = nodeData.inputs?.repeatLastN as string const repeatPenalty = nodeData.inputs?.repeatPenalty as string const tfsZ = nodeData.inputs?.tfsZ as string + const reasoningEffort = nodeData.inputs?.reasoningEffort as string const allowImageUploads = nodeData.inputs?.allowImageUploads as boolean const jsonMode = nodeData.inputs?.jsonMode as boolean const streaming = nodeData.inputs?.streaming as boolean const cache = nodeData.inputs?.cache as BaseCache - const obj: ChatOllamaInput & BaseChatModelParams = { + const obj: FlowiseChatOllamaInput & BaseChatModelParams = { baseUrl, temperature: parseFloat(temperature), model: modelName, @@ -257,6 +280,7 @@ class ChatOllama_ChatModels implements INode { if (keepAlive) obj.keepAlive = keepAlive if (cache) obj.cache = cache if (jsonMode) obj.format = 'json' + if (reasoningEffort) obj.reasoningEffort = reasoningEffort as 'low' | 'medium' | 'high' const multiModalOption: IMultiModalOption = { image: { diff --git a/packages/components/nodes/chatmodels/ChatOllama/FlowiseChatOllama.ts b/packages/components/nodes/chatmodels/ChatOllama/FlowiseChatOllama.ts index 3089562eaaf..8ec57085672 100644 --- a/packages/components/nodes/chatmodels/ChatOllama/FlowiseChatOllama.ts +++ b/packages/components/nodes/chatmodels/ChatOllama/FlowiseChatOllama.ts @@ -1,16 +1,25 @@ import { ChatOllama as LCChatOllama, ChatOllamaInput } from '@langchain/ollama' import { IMultiModalOption, IVisionChatModal } from '../../../src' +import { BaseMessage, AIMessageChunk } from '@langchain/core/messages' +import { CallbackManagerForLLMRun } from '@langchain/core/callbacks/manager' +import { ChatGenerationChunk } from '@langchain/core/outputs' + +export interface FlowiseChatOllamaInput extends ChatOllamaInput { + reasoningEffort?: 'low' | 'medium' | 'high' +} export class ChatOllama extends LCChatOllama implements IVisionChatModal { configuredModel: string configuredMaxToken?: number multiModalOption: IMultiModalOption id: string + reasoningEffort?: 'low' | 'medium' | 'high' - constructor(id: string, fields?: ChatOllamaInput) { + constructor(id: string, fields?: FlowiseChatOllamaInput) { super(fields) this.id = id this.configuredModel = fields?.model ?? '' + this.reasoningEffort = fields?.reasoningEffort } revertToOriginalModel(): void { @@ -24,4 +33,132 @@ export class ChatOllama extends LCChatOllama implements IVisionChatModal { setVisionModel(): void { // pass } + + /** + * Override _streamResponseChunks to inject the 'think' parameter for reasoning models + */ + async *_streamResponseChunks( + messages: BaseMessage[], + options: this['ParsedCallOptions'], + runManager?: CallbackManagerForLLMRun + ): AsyncGenerator { + // If reasoningEffort is set, we need to use non-streaming with think parameter + // because streaming with think requires special handling + if (this.reasoningEffort) { + try { + // Call the non-streaming version and yield the result as a single chunk + const result = await this._generateNonStreaming(messages, options, runManager) + if (result) { + yield result + } + return + } catch (error: any) { + // If we get a 405 error, it means the endpoint doesn't support native Ollama API + // Fall back to regular streaming without the think parameter + if (error?.message?.includes('405')) { + console.warn( + 'Ollama reasoning effort requires native Ollama API endpoint. Falling back to standard mode.' + ) + // Fall through to use parent's streaming implementation + } else { + throw error + } + } + } + + // Otherwise, use the parent's streaming implementation + for await (const chunk of super._streamResponseChunks(messages, options, runManager)) { + yield chunk + } + } + + /** + * Non-streaming generation with think parameter support + */ + private async _generateNonStreaming( + messages: BaseMessage[], + options: this['ParsedCallOptions'], + runManager?: CallbackManagerForLLMRun + ): Promise { + let baseUrl = this.baseUrl || 'http://localhost:11434' + // Remove trailing slash if present + baseUrl = baseUrl.replace(/\/+$/, '') + const url = `${baseUrl}/api/chat` + + // Convert messages to Ollama format + const ollamaMessages = messages.map((msg) => ({ + role: msg._getType() === 'human' ? 'user' : msg._getType() === 'ai' ? 'assistant' : 'system', + content: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content) + })) + + const requestBody: any = { + model: this.model, + messages: ollamaMessages, + stream: false, + options: {} + } + + // Add think parameter for reasoning effort + // GPT-OSS model requires effort level: 'low', 'medium', 'high' + // Other models (DeepSeek R1, Qwen3) accept boolean true/false + // We pass the effort level string - Ollama handles this appropriately per model + if (this.reasoningEffort) { + requestBody.think = this.reasoningEffort + } + + // Add other Ollama options + if (this.temperature !== undefined) requestBody.options.temperature = this.temperature + if (this.topP !== undefined) requestBody.options.top_p = this.topP + if (this.topK !== undefined) requestBody.options.top_k = this.topK + if (this.numCtx !== undefined) requestBody.options.num_ctx = this.numCtx + if (this.repeatPenalty !== undefined) requestBody.options.repeat_penalty = this.repeatPenalty + if (this.mirostat !== undefined) requestBody.options.mirostat = this.mirostat + if (this.mirostatEta !== undefined) requestBody.options.mirostat_eta = this.mirostatEta + if (this.mirostatTau !== undefined) requestBody.options.mirostat_tau = this.mirostatTau + if (this.numGpu !== undefined) requestBody.options.num_gpu = this.numGpu + if (this.numThread !== undefined) requestBody.options.num_thread = this.numThread + if (this.repeatLastN !== undefined) requestBody.options.repeat_last_n = this.repeatLastN + if (this.tfsZ !== undefined) requestBody.options.tfs_z = this.tfsZ + if (this.format) requestBody.format = this.format + if (this.keepAlive) requestBody.keep_alive = this.keepAlive + + try { + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(requestBody) + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`Ollama API error: ${response.status} ${errorText}`) + } + + const data = await response.json() + + // Extract content and thinking from response + let content = data.message?.content || '' + const thinking = data.message?.thinking || '' + + // If there's thinking content, optionally prepend it (or handle separately) + // For now, we just return the main content + // The thinking is available in data.message.thinking if needed + + const chunk = new ChatGenerationChunk({ + message: new AIMessageChunk({ + content, + additional_kwargs: thinking ? { thinking } : {} + }), + text: content + }) + + await runManager?.handleLLMNewToken(content) + + return chunk + } catch (error) { + throw error + } + } } From 8d3cba6a439a5efc6af82812ba6ed2be3a273c3b Mon Sep 17 00:00:00 2001 From: Edward Tan Date: Mon, 15 Dec 2025 10:19:15 +0800 Subject: [PATCH 5/5] Fix ChromaDB type imports and embedding function parameter - Update ChromaClientParams to ChromaClientArgs in imports and type usage - Change IEmbeddingFunction to EmbeddingFunction in import - Update embeddingFunction parameter from null to undefined to match type requirements - Remove unused CollectionConfiguration import These changes fix TypeScript compilation errors with chromadb v3.1.6 --- packages/components/nodes/vectorstores/Chroma/core.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/components/nodes/vectorstores/Chroma/core.ts b/packages/components/nodes/vectorstores/Chroma/core.ts index eeb65161c22..ade3633b6a5 100644 --- a/packages/components/nodes/vectorstores/Chroma/core.ts +++ b/packages/components/nodes/vectorstores/Chroma/core.ts @@ -3,10 +3,10 @@ import type { ChromaClient as ChromaClientT, ChromaClientArgs, Collection, - CollectionConfiguration, CollectionMetadata, Metadata, - Where + Where, + EmbeddingFunction } from 'chromadb' import type { EmbeddingsInterface } from '@langchain/core/embeddings' @@ -18,7 +18,6 @@ type SharedChromaLibArgs = { collectionName?: string filter?: object collectionMetadata?: CollectionMetadata - collectionConfiguration?: CollectionConfiguration chromaCloudAPIKey?: string clientParams?: Omit } @@ -110,7 +109,7 @@ export class Chroma extends VectorStore { try { this.collection = await this.index.getOrCreateCollection({ name: this.collectionName, - embeddingFunction: null, + embeddingFunction: undefined, ...(this.collectionMetadata && { metadata: this.collectionMetadata }) }) } catch (err) {