瀏覽代碼

feat(nodes): authenticate Ollama Chat against hosted ollama.com

The node could only reach an unauthenticated endpoint, so the hosted API was
out of reach and cloud models were only usable by proxying through a local
daemon.

Adds an authType of none or credential, plus a credentialId resolved through
the existing credential store. A Bearer credential yields the Authorization
header ollama.com expects, so the API key is never written into node config
or the workflow document. Point baseUrl at https://ollama.com to call the
hosted API directly.

The credential is resolved once before the retry loop rather than per
attempt: a missing or rejected credential is not transient, and retrying it
with exponential backoff would only delay a certain failure.
fszontagh 1 月之前
父節點
當前提交
292de3732c
共有 1 個文件被更改,包括 37 次插入 和 4 次删除
  1. 37 4
      nodes/ai/ollama-chat.js

+ 37 - 4
nodes/ai/ollama-chat.js

@@ -13,9 +13,24 @@ const configSchema = {
         baseUrl: {
             type: 'string',
             title: 'Ollama Base URL',
-            description: 'Base URL of the Ollama instance',
+            description: 'Base URL of the Ollama instance. Use https://ollama.com to call the hosted API directly instead of proxying through a local daemon.',
             default: 'http://localhost:11434'
         },
+        authType: {
+            type: 'string',
+            title: 'Authentication',
+            description: 'Hosted ollama.com requires a credential. A local daemon usually does not.',
+            enum: ['none', 'credential'],
+            enumLabels: ['None', 'Stored credential'],
+            default: 'none'
+        },
+        credentialId: {
+            type: 'string',
+            title: 'Credential',
+            description: 'Stored Bearer credential holding the ollama.com API key',
+            default: '',
+            showWhen: { field: 'authType', value: 'credential' }
+        },
         model: {
             type: 'string',
             title: 'Model',
@@ -213,7 +228,21 @@ function findImage(input, override) {
     return result;
 }
 
-function callOllama(config, base64) {
+function buildHeaders(config) {
+    const headers = { 'Content-Type': 'application/json' };
+
+    if (config.authType === 'credential' && config.credentialId) {
+        const auth = smartbotic.credentials.get(config.credentialId);
+        if (!auth.success) {
+            throw new Error('Failed to load credential: ' + auth.error);
+        }
+        headers[auth.headerName] = auth.headerValue;
+    }
+
+    return headers;
+}
+
+function callOllama(config, base64, headers) {
     const messages = [];
     if (config.systemPrompt && String(config.systemPrompt).trim().length > 0) {
         messages.push({ role: 'system', content: String(config.systemPrompt) });
@@ -235,7 +264,7 @@ function callOllama(config, base64) {
     const response = smartbotic.http.request({
         method: 'POST',
         url: String(config.baseUrl).replace(/\/+$/, '') + '/api/chat',
-        headers: { 'Content-Type': 'application/json' },
+        headers: headers,
         body: JSON.stringify(payload),
         timeout: Number(config.timeoutMs) || 120000
     });
@@ -277,6 +306,10 @@ module.exports = {
             }
         }
 
+        // Resolved once, outside the retry loop: a missing or broken credential is
+        // not transient, so retrying it with backoff only wastes time.
+        const headers = buildHeaders(config);
+
         const wantJson = config.responseFormat === 'json';
         const attempts = 1 + (Number(config.retryCount) > 0 ? Number(config.retryCount) : 0);
 
@@ -288,7 +321,7 @@ module.exports = {
         for (let attempt = 1; attempt <= attempts; attempt++) {
             used = attempt;
             try {
-                content = callOllama(config, image.base64);
+                content = callOllama(config, image.base64, headers);
                 if (wantJson) {
                     parsed = JSON.parse(stripFences(content));
                 }