Kaynağa Gözat

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 ay önce
ebeveyn
işleme
292de3732c
1 değiştirilmiş dosya ile 37 ekleme ve 4 silme
  1. 37 4
      nodes/ai/ollama-chat.js

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

@@ -13,9 +13,24 @@ const configSchema = {
         baseUrl: {
         baseUrl: {
             type: 'string',
             type: 'string',
             title: 'Ollama Base URL',
             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'
             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: {
         model: {
             type: 'string',
             type: 'string',
             title: 'Model',
             title: 'Model',
@@ -213,7 +228,21 @@ function findImage(input, override) {
     return result;
     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 = [];
     const messages = [];
     if (config.systemPrompt && String(config.systemPrompt).trim().length > 0) {
     if (config.systemPrompt && String(config.systemPrompt).trim().length > 0) {
         messages.push({ role: 'system', content: String(config.systemPrompt) });
         messages.push({ role: 'system', content: String(config.systemPrompt) });
@@ -235,7 +264,7 @@ function callOllama(config, base64) {
     const response = smartbotic.http.request({
     const response = smartbotic.http.request({
         method: 'POST',
         method: 'POST',
         url: String(config.baseUrl).replace(/\/+$/, '') + '/api/chat',
         url: String(config.baseUrl).replace(/\/+$/, '') + '/api/chat',
-        headers: { 'Content-Type': 'application/json' },
+        headers: headers,
         body: JSON.stringify(payload),
         body: JSON.stringify(payload),
         timeout: Number(config.timeoutMs) || 120000
         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 wantJson = config.responseFormat === 'json';
         const attempts = 1 + (Number(config.retryCount) > 0 ? Number(config.retryCount) : 0);
         const attempts = 1 + (Number(config.retryCount) > 0 ? Number(config.retryCount) : 0);
 
 
@@ -288,7 +321,7 @@ module.exports = {
         for (let attempt = 1; attempt <= attempts; attempt++) {
         for (let attempt = 1; attempt <= attempts; attempt++) {
             used = attempt;
             used = attempt;
             try {
             try {
-                content = callOllama(config, image.base64);
+                content = callOllama(config, image.base64, headers);
                 if (wantJson) {
                 if (wantJson) {
                     parsed = JSON.parse(stripFences(content));
                     parsed = JSON.parse(stripFences(content));
                 }
                 }