n8n-nodes-ollama-reranker 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { ISupplyDataFunctions, SupplyData, INodeType, INodeTypeDescription } from 'n8n-workflow';
1
+ import { ILoadOptionsFunctions, INodePropertyOptions, ISupplyDataFunctions, SupplyData, INodeType, INodeTypeDescription } from 'n8n-workflow';
2
2
  /**
3
3
  * Ollama Reranker Provider
4
4
  *
@@ -12,6 +12,15 @@ import { ISupplyDataFunctions, SupplyData, INodeType, INodeTypeDescription } fro
12
12
  */
13
13
  export declare class OllamaReranker implements INodeType {
14
14
  description: INodeTypeDescription;
15
+ methods: {
16
+ loadOptions: {
17
+ /**
18
+ * Load models from Ollama/Custom Rerank API
19
+ * Dynamically fetches available models from /api/tags endpoint
20
+ */
21
+ getModels(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
22
+ };
23
+ };
15
24
  /**
16
25
  * Supply Data Method (NOT execute!)
17
26
  *
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.OllamaReranker = void 0;
4
4
  const n8n_workflow_1 = require("n8n-workflow");
5
+ const reranker_logic_1 = require("../shared/reranker-logic");
5
6
  /**
6
7
  * Ollama Reranker Provider
7
8
  *
@@ -55,48 +56,30 @@ class OllamaReranker {
55
56
  displayName: 'Model',
56
57
  name: 'model',
57
58
  type: 'options',
59
+ typeOptions: {
60
+ loadOptionsMethod: 'getModels',
61
+ },
62
+ default: '',
63
+ description: 'The reranker model to use - models are loaded from your configured Ollama/Custom API',
64
+ },
65
+ {
66
+ displayName: 'API Type',
67
+ name: 'apiType',
68
+ type: 'options',
58
69
  options: [
59
70
  {
60
- name: 'BGE Reranker v2-M3 (Recommended)',
61
- value: 'bge-reranker-v2-m3',
62
- description: 'Best general-purpose reranker, excellent balance of speed and accuracy',
63
- },
64
- {
65
- name: 'Qwen3-Reranker-0.6B (Fast)',
66
- value: 'dengcao/Qwen3-Reranker-0.6B:Q5_K_M',
67
- description: 'Fastest option, best for resource-limited environments',
68
- },
69
- {
70
- name: 'Qwen3-Reranker-4B (Balanced)',
71
- value: 'dengcao/Qwen3-Reranker-4B:Q5_K_M',
72
- description: 'Recommended for Qwen family - best balance of speed and accuracy',
71
+ name: 'Ollama Generate API',
72
+ value: 'ollama',
73
+ description: 'Standard Ollama /api/generate endpoint (for BGE, Qwen prompt-based rerankers)',
73
74
  },
74
75
  {
75
- name: 'Qwen3-Reranker-8B (Most Accurate)',
76
- value: 'dengcao/Qwen3-Reranker-8B:Q5_K_M',
77
- description: 'Highest accuracy, requires more resources',
78
- },
79
- {
80
- name: 'Custom Model',
76
+ name: 'Custom Rerank API',
81
77
  value: 'custom',
82
- description: 'Specify your own Ollama reranker model',
78
+ description: 'Custom /api/rerank endpoint (for deposium-embeddings-turbov2, etc.)',
83
79
  },
84
80
  ],
85
- default: 'bge-reranker-v2-m3',
86
- description: 'The Ollama reranker model to use',
87
- },
88
- {
89
- displayName: 'Custom Model Name',
90
- name: 'customModel',
91
- type: 'string',
92
- default: '',
93
- placeholder: 'your-reranker-model:tag',
94
- description: 'Name of your custom Ollama reranker model',
95
- displayOptions: {
96
- show: {
97
- model: ['custom'],
98
- },
99
- },
81
+ default: 'ollama',
82
+ description: 'Which API endpoint to use for reranking',
100
83
  },
101
84
  {
102
85
  displayName: 'Top K',
@@ -168,6 +151,47 @@ class OllamaReranker {
168
151
  },
169
152
  ],
170
153
  };
154
+ this.methods = {
155
+ loadOptions: {
156
+ /**
157
+ * Load models from Ollama/Custom Rerank API
158
+ * Dynamically fetches available models from /api/tags endpoint
159
+ */
160
+ async getModels() {
161
+ const credentials = await this.getCredentials('ollamaApi');
162
+ if (!(credentials === null || credentials === void 0 ? void 0 : credentials.host)) {
163
+ return [];
164
+ }
165
+ const baseUrl = credentials.host.replace(/\/$/, '');
166
+ try {
167
+ const response = await this.helpers.httpRequest({
168
+ method: 'GET',
169
+ url: `${baseUrl}/api/tags`,
170
+ json: true,
171
+ timeout: 5000,
172
+ });
173
+ if (!(response === null || response === void 0 ? void 0 : response.models) || !Array.isArray(response.models)) {
174
+ return [];
175
+ }
176
+ // Sort models alphabetically
177
+ const models = response.models.sort((a, b) => {
178
+ const nameA = a.name || '';
179
+ const nameB = b.name || '';
180
+ return nameA.localeCompare(nameB);
181
+ });
182
+ return models.map((model) => ({
183
+ name: model.name,
184
+ value: model.name,
185
+ description: model.details || `Size: ${Math.round((model.size || 0) / 1024 / 1024)}MB`,
186
+ }));
187
+ }
188
+ catch (error) {
189
+ // If API call fails, return empty array (user can still type model name manually)
190
+ return [];
191
+ }
192
+ },
193
+ },
194
+ };
171
195
  }
172
196
  /**
173
197
  * Supply Data Method (NOT execute!)
@@ -180,13 +204,11 @@ class OllamaReranker {
180
204
  this.logger.debug('Initializing Ollama Reranker Provider');
181
205
  const self = this;
182
206
  // Get node parameters once (provider nodes use index 0)
183
- let model = this.getNodeParameter('model', 0);
184
- if (model === 'custom') {
185
- model = this.getNodeParameter('customModel', 0);
186
- if (!(model === null || model === void 0 ? void 0 : model.trim())) {
187
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Custom model name is required when "Custom Model" is selected');
188
- }
207
+ const model = this.getNodeParameter('model', 0);
208
+ if (!(model === null || model === void 0 ? void 0 : model.trim())) {
209
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Model selection is required. Please select a model from the dropdown.');
189
210
  }
211
+ const apiType = this.getNodeParameter('apiType', 0, 'ollama');
190
212
  const instruction = this.getNodeParameter('instruction', 0);
191
213
  const additionalOptions = this.getNodeParameter('additionalOptions', 0, {});
192
214
  const timeout = (_a = additionalOptions.timeout) !== null && _a !== void 0 ? _a : 30000;
@@ -261,8 +283,8 @@ class OllamaReranker {
261
283
  });
262
284
  self.logger.debug(`Reranking ${processedDocs.length} documents with model: ${model}`);
263
285
  try {
264
- // Rerank documents using Ollama
265
- const rerankedDocs = await rerankDocuments(self, {
286
+ // Rerank documents using Ollama or Custom API
287
+ const rerankedDocs = await (0, reranker_logic_1.rerankDocuments)(self, {
266
288
  ollamaHost,
267
289
  model,
268
290
  query,
@@ -273,6 +295,7 @@ class OllamaReranker {
273
295
  batchSize,
274
296
  timeout,
275
297
  includeOriginalScores,
298
+ apiType,
276
299
  });
277
300
  self.logger.debug(`Reranking complete: ${rerankedDocs.length} documents returned`);
278
301
  // Log output for n8n execution tracking
@@ -310,277 +333,3 @@ class OllamaReranker {
310
333
  }
311
334
  }
312
335
  exports.OllamaReranker = OllamaReranker;
313
- /**
314
- * Rerank documents using Ollama reranker model
315
- */
316
- async function rerankDocuments(context, config) {
317
- const { ollamaHost, model, query, documents, instruction, topK, threshold, batchSize, timeout, includeOriginalScores } = config;
318
- const results = [];
319
- // Process all documents concurrently with controlled concurrency
320
- const promises = [];
321
- for (let i = 0; i < documents.length; i++) {
322
- const doc = documents[i];
323
- const promise = scoreDocument(context, ollamaHost, model, query, doc.pageContent, instruction, timeout).then(score => ({
324
- index: i,
325
- score,
326
- }));
327
- promises.push(promise);
328
- // Process in batches to avoid overwhelming the API
329
- if (promises.length >= batchSize || i === documents.length - 1) {
330
- const batchResults = await Promise.all(promises);
331
- results.push(...batchResults);
332
- promises.length = 0; // Clear the array
333
- }
334
- }
335
- // Filter by threshold and sort by score (descending)
336
- const filteredResults = results
337
- .filter(r => r.score >= threshold)
338
- .sort((a, b) => b.score - a.score)
339
- .slice(0, topK);
340
- // Map back to original documents with scores
341
- return filteredResults.map(result => {
342
- const originalDoc = documents[result.index];
343
- const rerankedDoc = {
344
- ...originalDoc,
345
- _rerankScore: result.score,
346
- _originalIndex: result.index,
347
- };
348
- if (includeOriginalScores && originalDoc._originalScore !== undefined) {
349
- rerankedDoc._originalScore = originalDoc._originalScore;
350
- }
351
- return rerankedDoc;
352
- });
353
- }
354
- /**
355
- * Score a single document against the query using Ollama reranker model with retry logic
356
- */
357
- async function scoreDocument(context, ollamaHost, model, query, documentContent, instruction, timeout) {
358
- var _a, _b, _c, _d;
359
- // Format prompt based on model type
360
- const prompt = formatRerankerPrompt(model, query, documentContent, instruction);
361
- const maxRetries = 3;
362
- let lastError;
363
- for (let attempt = 0; attempt < maxRetries; attempt++) {
364
- try {
365
- // Use Ollama /api/generate endpoint for reranker models
366
- const response = await context.helpers.httpRequest({
367
- method: 'POST',
368
- url: `${ollamaHost}/api/generate`,
369
- headers: {
370
- 'Content-Type': 'application/json',
371
- Accept: 'application/json',
372
- },
373
- body: {
374
- model,
375
- prompt,
376
- stream: false,
377
- options: {
378
- temperature: 0.0, // Deterministic scoring
379
- },
380
- },
381
- json: true,
382
- timeout,
383
- });
384
- // Parse the response to extract relevance score
385
- const score = parseRerankerResponse(model, response);
386
- return score;
387
- }
388
- catch (error) {
389
- lastError = error;
390
- // Don't retry on permanent errors
391
- if (((_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.statusCode) === 404 || ((_b = error === null || error === void 0 ? void 0 : error.response) === null || _b === void 0 ? void 0 : _b.statusCode) === 400) {
392
- break;
393
- }
394
- // Retry on transient errors (timeout, 5xx, network issues)
395
- if (attempt < maxRetries - 1) {
396
- const isTransient = (error === null || error === void 0 ? void 0 : error.name) === 'AbortError' ||
397
- (error === null || error === void 0 ? void 0 : error.code) === 'ETIMEDOUT' ||
398
- ((_c = error === null || error === void 0 ? void 0 : error.response) === null || _c === void 0 ? void 0 : _c.statusCode) >= 500;
399
- if (isTransient) {
400
- // Exponential backoff: 100ms, 200ms, 400ms
401
- await new Promise(resolve => setTimeout(resolve, 100 * Math.pow(2, attempt)));
402
- continue;
403
- }
404
- }
405
- break;
406
- }
407
- }
408
- // Handle final error after retries
409
- const error = lastError;
410
- if ((error === null || error === void 0 ? void 0 : error.name) === 'AbortError' || (error === null || error === void 0 ? void 0 : error.code) === 'ETIMEDOUT') {
411
- throw new n8n_workflow_1.NodeApiError(context.getNode(), error, {
412
- message: `Request timeout after ${timeout}ms (tried ${maxRetries} times)`,
413
- description: `Model: ${model}\nEndpoint: ${ollamaHost}/api/generate`,
414
- });
415
- }
416
- if ((_d = error === null || error === void 0 ? void 0 : error.response) === null || _d === void 0 ? void 0 : _d.body) {
417
- throw new n8n_workflow_1.NodeApiError(context.getNode(), error, {
418
- message: `Ollama API Error (${error.response.statusCode})`,
419
- description: `Endpoint: ${ollamaHost}/api/generate\nModel: ${model}\nResponse: ${JSON.stringify(error.response.body, null, 2)}`,
420
- });
421
- }
422
- throw new n8n_workflow_1.NodeApiError(context.getNode(), error, {
423
- message: 'Ollama reranking request failed',
424
- description: `Endpoint: ${ollamaHost}/api/generate\nModel: ${model}\nError: ${error.message}`,
425
- });
426
- }
427
- /**
428
- * Format prompt based on reranker model type
429
- *
430
- * Different models expect different prompt formats:
431
- * - BGE Reranker: Simple query + document format
432
- * - Qwen3-Reranker: Structured chat format with system/user/assistant tags
433
- */
434
- function formatRerankerPrompt(model, query, documentContent, instruction) {
435
- // Detect model type
436
- const isBGE = model.toLowerCase().includes('bge');
437
- const isQwen = model.toLowerCase().includes('qwen');
438
- if (isBGE) {
439
- // BGE Reranker uses a simple format
440
- // See: https://huggingface.co/BAAI/bge-reranker-v2-m3
441
- return `Instruction: ${instruction}
442
-
443
- Query: ${query}
444
-
445
- Document: ${documentContent}
446
-
447
- Relevance:`;
448
- }
449
- else if (isQwen) {
450
- // Qwen3-Reranker uses structured chat format
451
- // See: https://huggingface.co/dengcao/Qwen3-Reranker-4B
452
- return `<|im_start|>system
453
- Judge whether the Document meets the requirements based on the Query and the Instruct provided. Note that the answer can only be "yes" or "no".<|im_end|>
454
- <|im_start|>user
455
- <Instruct>: ${instruction}
456
- <Query>: ${query}
457
- <Document>: ${documentContent}<|im_end|>
458
- <|im_start|>assistant
459
- <think>`;
460
- }
461
- // Default format for unknown models (similar to BGE)
462
- return `Task: ${instruction}
463
-
464
- Query: ${query}
465
-
466
- Document: ${documentContent}
467
-
468
- Score:`;
469
- }
470
- /**
471
- * Parse BGE model response to extract relevance score
472
- */
473
- function parseBGEScore(output, outputLower) {
474
- // Try to extract floating point number
475
- const scoreRegex = /(\d*\.?\d+)/;
476
- const scoreMatch = scoreRegex.exec(output);
477
- if (scoreMatch) {
478
- const score = parseFloat(scoreMatch[1]);
479
- // BGE returns scores in various ranges, normalize to 0-1
480
- if (score > 1 && score <= 10) {
481
- return score / 10;
482
- }
483
- else if (score > 10) {
484
- return score / 100;
485
- }
486
- return Math.min(Math.max(score, 0), 1); // Clamp to 0-1
487
- }
488
- // Fallback: check for keywords
489
- if (outputLower.includes('high') || outputLower.includes('relevant')) {
490
- return 0.8;
491
- }
492
- if (outputLower.includes('low') || outputLower.includes('irrelevant')) {
493
- return 0.2;
494
- }
495
- return null;
496
- }
497
- /**
498
- * Parse Qwen model response to extract relevance score
499
- */
500
- function parseQwenScore(output, outputLower) {
501
- // Look for explicit yes/no in the response
502
- const yesRegex = /\b(yes|relevant|positive|match)\b/;
503
- const noRegex = /\b(no|irrelevant|negative|not\s+relevant)\b/;
504
- const yesMatch = yesRegex.exec(outputLower);
505
- const noMatch = noRegex.exec(outputLower);
506
- if (yesMatch && !noMatch) {
507
- // Higher confidence for detailed explanations
508
- const hasReasoning = output.length > 100;
509
- const hasMultiplePositives = (output.match(/relevant|yes|match/gi) || []).length > 1;
510
- if (hasReasoning && hasMultiplePositives)
511
- return 0.95;
512
- if (hasReasoning)
513
- return 0.85;
514
- return 0.75;
515
- }
516
- if (noMatch && !yesMatch) {
517
- // Low scores for negative responses
518
- const hasStrongNegative = outputLower.includes('completely') ||
519
- outputLower.includes('totally') ||
520
- outputLower.includes('not at all');
521
- return hasStrongNegative ? 0.05 : 0.15;
522
- }
523
- // Mixed signals - check which appears first
524
- if (yesMatch && noMatch) {
525
- const yesIndex = output.toLowerCase().indexOf(yesMatch[0]);
526
- const noIndex = output.toLowerCase().indexOf(noMatch[0]);
527
- return yesIndex < noIndex ? 0.6 : 0.4;
528
- }
529
- return null;
530
- }
531
- /**
532
- * Parse generic model response with fallback logic
533
- */
534
- function parseGenericScore(output, outputLower) {
535
- // Try numeric extraction first
536
- const numericRegex = /(\d*\.?\d+)/;
537
- const numericMatch = numericRegex.exec(output);
538
- if (numericMatch) {
539
- const score = parseFloat(numericMatch[1]);
540
- if (score >= 0 && score <= 1)
541
- return score;
542
- if (score > 1 && score <= 10)
543
- return score / 10;
544
- if (score > 10 && score <= 100)
545
- return score / 100;
546
- }
547
- // Keyword-based scoring
548
- const positiveKeywords = ['relevant', 'yes', 'high', 'strong', 'good', 'match', 'related'];
549
- const negativeKeywords = ['irrelevant', 'no', 'low', 'weak', 'poor', 'unrelated', 'different'];
550
- const positiveCount = positiveKeywords.filter(kw => outputLower.includes(kw)).length;
551
- const negativeCount = negativeKeywords.filter(kw => outputLower.includes(kw)).length;
552
- if (positiveCount > negativeCount) {
553
- return 0.5 + (positiveCount * 0.1);
554
- }
555
- else if (negativeCount > positiveCount) {
556
- return 0.5 - (negativeCount * 0.1);
557
- }
558
- // Default to neutral if completely ambiguous
559
- return 0.5;
560
- }
561
- /**
562
- * Parse Ollama reranker response to extract relevance score
563
- * Uses model-specific parsing logic for better accuracy
564
- */
565
- function parseRerankerResponse(model, response) {
566
- if (!(response === null || response === void 0 ? void 0 : response.response)) {
567
- return 0.0;
568
- }
569
- const output = response.response;
570
- const outputLower = output.toLowerCase();
571
- const isBGE = model.toLowerCase().includes('bge');
572
- const isQwen = model.toLowerCase().includes('qwen');
573
- // Try model-specific parsers
574
- if (isBGE) {
575
- const score = parseBGEScore(output, outputLower);
576
- if (score !== null)
577
- return score;
578
- }
579
- if (isQwen) {
580
- const score = parseQwenScore(output, outputLower);
581
- if (score !== null)
582
- return score;
583
- }
584
- // Fallback to generic parsing
585
- return parseGenericScore(output, outputLower);
586
- }
@@ -1,4 +1,4 @@
1
- import { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow';
1
+ import { IExecuteFunctions, ILoadOptionsFunctions, INodeExecutionData, INodePropertyOptions, INodeType, INodeTypeDescription } from 'n8n-workflow';
2
2
  /**
3
3
  * Ollama Reranker Workflow Node
4
4
  *
@@ -13,6 +13,15 @@ import { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription
13
13
  */
14
14
  export declare class OllamaRerankerWorkflow implements INodeType {
15
15
  description: INodeTypeDescription;
16
+ methods: {
17
+ loadOptions: {
18
+ /**
19
+ * Load models from Ollama/Custom Rerank API
20
+ * Dynamically fetches available models from /api/tags endpoint
21
+ */
22
+ getModels(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
23
+ };
24
+ };
16
25
  /**
17
26
  * Execute Method (NOT supplyData!)
18
27
  *
@@ -53,53 +53,36 @@ class OllamaRerankerWorkflow {
53
53
  },
54
54
  ],
55
55
  properties: [
56
- // Model selection (same as provider node)
56
+ // Model selection with dynamic loading
57
57
  {
58
58
  displayName: 'Model',
59
59
  name: 'model',
60
60
  type: 'options',
61
+ typeOptions: {
62
+ loadOptionsMethod: 'getModels',
63
+ },
64
+ default: '',
65
+ description: 'The reranker model to use - models are loaded from your configured Ollama/Custom API',
66
+ },
67
+ // API Type selection
68
+ {
69
+ displayName: 'API Type',
70
+ name: 'apiType',
71
+ type: 'options',
61
72
  options: [
62
73
  {
63
- name: 'BGE Reranker v2-M3 (Recommended)',
64
- value: 'bge-reranker-v2-m3',
65
- description: 'Best general-purpose reranker',
66
- },
67
- {
68
- name: 'Qwen3-Reranker-0.6B (Fast)',
69
- value: 'dengcao/Qwen3-Reranker-0.6B:Q5_K_M',
70
- description: 'Fastest option',
71
- },
72
- {
73
- name: 'Qwen3-Reranker-4B (Balanced)',
74
- value: 'dengcao/Qwen3-Reranker-4B:Q5_K_M',
75
- description: 'Best balance',
74
+ name: 'Ollama Generate API',
75
+ value: 'ollama',
76
+ description: 'Standard Ollama /api/generate endpoint (for BGE, Qwen prompt-based rerankers)',
76
77
  },
77
78
  {
78
- name: 'Qwen3-Reranker-8B (Most Accurate)',
79
- value: 'dengcao/Qwen3-Reranker-8B:Q5_K_M',
80
- description: 'Highest accuracy',
81
- },
82
- {
83
- name: 'Custom Model',
79
+ name: 'Custom Rerank API',
84
80
  value: 'custom',
85
- description: 'Specify your own model',
81
+ description: 'Custom /api/rerank endpoint (for deposium-embeddings-turbov2, etc.)',
86
82
  },
87
83
  ],
88
- default: 'bge-reranker-v2-m3',
89
- description: 'Ollama reranker model to use',
90
- },
91
- {
92
- displayName: 'Custom Model Name',
93
- name: 'customModel',
94
- type: 'string',
95
- default: '',
96
- placeholder: 'your-reranker-model:tag',
97
- description: 'Name of your custom Ollama reranker model',
98
- displayOptions: {
99
- show: {
100
- model: ['custom'],
101
- },
102
- },
84
+ default: 'ollama',
85
+ description: 'Which API endpoint to use for reranking',
103
86
  },
104
87
  // Query input (flexible like n8n nodes)
105
88
  {
@@ -269,6 +252,47 @@ class OllamaRerankerWorkflow {
269
252
  },
270
253
  ],
271
254
  };
255
+ this.methods = {
256
+ loadOptions: {
257
+ /**
258
+ * Load models from Ollama/Custom Rerank API
259
+ * Dynamically fetches available models from /api/tags endpoint
260
+ */
261
+ async getModels() {
262
+ const credentials = await this.getCredentials('ollamaApi');
263
+ if (!(credentials === null || credentials === void 0 ? void 0 : credentials.host)) {
264
+ return [];
265
+ }
266
+ const baseUrl = credentials.host.replace(/\/$/, '');
267
+ try {
268
+ const response = await this.helpers.httpRequest({
269
+ method: 'GET',
270
+ url: `${baseUrl}/api/tags`,
271
+ json: true,
272
+ timeout: 5000,
273
+ });
274
+ if (!(response === null || response === void 0 ? void 0 : response.models) || !Array.isArray(response.models)) {
275
+ return [];
276
+ }
277
+ // Sort models alphabetically
278
+ const models = response.models.sort((a, b) => {
279
+ const nameA = a.name || '';
280
+ const nameB = b.name || '';
281
+ return nameA.localeCompare(nameB);
282
+ });
283
+ return models.map((model) => ({
284
+ name: model.name,
285
+ value: model.name,
286
+ description: model.details || `Size: ${Math.round((model.size || 0) / 1024 / 1024)}MB`,
287
+ }));
288
+ }
289
+ catch (error) {
290
+ // If API call fails, return empty array (user can still type model name manually)
291
+ return [];
292
+ }
293
+ },
294
+ },
295
+ };
272
296
  }
273
297
  /**
274
298
  * Execute Method (NOT supplyData!)
@@ -287,13 +311,12 @@ class OllamaRerankerWorkflow {
287
311
  }
288
312
  const ollamaHost = credentials.host.replace(/\/$/, '');
289
313
  // Get model
290
- let model = this.getNodeParameter('model', 0);
291
- if (model === 'custom') {
292
- model = this.getNodeParameter('customModel', 0);
293
- if (!(model === null || model === void 0 ? void 0 : model.trim())) {
294
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Custom model name is required');
295
- }
314
+ const model = this.getNodeParameter('model', 0);
315
+ if (!(model === null || model === void 0 ? void 0 : model.trim())) {
316
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Model selection is required. Please select a model from the dropdown.');
296
317
  }
318
+ // Get API type
319
+ const apiType = this.getNodeParameter('apiType', 0, 'ollama');
297
320
  // Get common parameters
298
321
  const instruction = this.getNodeParameter('instruction', 0);
299
322
  const topK = this.getNodeParameter('topK', 0);
@@ -380,6 +403,7 @@ class OllamaRerankerWorkflow {
380
403
  batchSize,
381
404
  timeout,
382
405
  includeOriginalScores,
406
+ apiType,
383
407
  });
384
408
  // Format output
385
409
  let output;
@@ -11,9 +11,10 @@ export interface RerankConfig {
11
11
  batchSize: number;
12
12
  timeout: number;
13
13
  includeOriginalScores: boolean;
14
+ apiType?: 'ollama' | 'custom';
14
15
  }
15
16
  /**
16
- * Rerank documents using Ollama reranker model
17
+ * Rerank documents using Ollama reranker model or Custom Rerank API
17
18
  */
18
19
  export declare function rerankDocuments(context: RerankerContext, config: RerankConfig): Promise<any[]>;
19
20
  export {};
@@ -3,10 +3,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.rerankDocuments = rerankDocuments;
4
4
  const n8n_workflow_1 = require("n8n-workflow");
5
5
  /**
6
- * Rerank documents using Ollama reranker model
6
+ * Rerank documents using Ollama reranker model or Custom Rerank API
7
7
  */
8
8
  async function rerankDocuments(context, config) {
9
- const { ollamaHost, model, query, documents, instruction, topK, threshold, batchSize, timeout, includeOriginalScores } = config;
9
+ const { ollamaHost, model, query, documents, instruction, topK, threshold, batchSize, timeout, includeOriginalScores, apiType = 'ollama' } = config;
10
+ // Use Custom Rerank API if specified
11
+ if (apiType === 'custom') {
12
+ return await rerankWithCustomAPI(context, config);
13
+ }
14
+ // Otherwise use Ollama Generate API (original logic)
10
15
  const results = [];
11
16
  // Process all documents concurrently with controlled concurrency
12
17
  const promises = [];
@@ -43,6 +48,77 @@ async function rerankDocuments(context, config) {
43
48
  return rerankedDoc;
44
49
  });
45
50
  }
51
+ /**
52
+ * Rerank documents using Custom Rerank API (/api/rerank endpoint)
53
+ * This is for services like deposium-embeddings-turbov2 that implement
54
+ * a custom /api/rerank endpoint with direct cosine similarity scoring
55
+ */
56
+ async function rerankWithCustomAPI(context, config) {
57
+ var _a, _b;
58
+ const { ollamaHost, model, query, documents, topK, threshold, timeout, includeOriginalScores } = config;
59
+ try {
60
+ // Extract document content as strings
61
+ const documentStrings = documents.map(doc => doc.pageContent || JSON.stringify(doc));
62
+ // Call /api/rerank endpoint
63
+ const response = await context.helpers.httpRequest({
64
+ method: 'POST',
65
+ url: `${ollamaHost}/api/rerank`,
66
+ headers: {
67
+ 'Content-Type': 'application/json',
68
+ Accept: 'application/json',
69
+ },
70
+ body: {
71
+ model,
72
+ query,
73
+ documents: documentStrings,
74
+ top_k: topK, // Custom API handles top_k filtering
75
+ },
76
+ json: true,
77
+ timeout,
78
+ });
79
+ // Parse response: { model: "...", results: [{index, document, relevance_score}] }
80
+ if (!(response === null || response === void 0 ? void 0 : response.results) || !Array.isArray(response.results)) {
81
+ throw new n8n_workflow_1.NodeApiError(context.getNode(), response, {
82
+ message: 'Invalid response from Custom Rerank API',
83
+ description: `Expected {results: [...]} but got: ${JSON.stringify(response)}`,
84
+ });
85
+ }
86
+ // Filter by threshold and map to our format
87
+ const filteredResults = response.results
88
+ .filter((r) => r.relevance_score >= threshold)
89
+ .map((result) => {
90
+ const originalDoc = documents[result.index];
91
+ const rerankedDoc = {
92
+ ...originalDoc,
93
+ _rerankScore: result.relevance_score,
94
+ _originalIndex: result.index,
95
+ };
96
+ if (includeOriginalScores && originalDoc._originalScore !== undefined) {
97
+ rerankedDoc._originalScore = originalDoc._originalScore;
98
+ }
99
+ return rerankedDoc;
100
+ });
101
+ return filteredResults;
102
+ }
103
+ catch (error) {
104
+ if (((_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.statusCode) === 404) {
105
+ throw new n8n_workflow_1.NodeApiError(context.getNode(), error, {
106
+ message: 'Custom Rerank API endpoint not found',
107
+ description: `The /api/rerank endpoint was not found at ${ollamaHost}.\nMake sure you're using a service that supports this endpoint (like deposium-embeddings-turbov2).`,
108
+ });
109
+ }
110
+ if ((_b = error === null || error === void 0 ? void 0 : error.response) === null || _b === void 0 ? void 0 : _b.body) {
111
+ throw new n8n_workflow_1.NodeApiError(context.getNode(), error, {
112
+ message: `Custom Rerank API Error (${error.response.statusCode})`,
113
+ description: `Endpoint: ${ollamaHost}/api/rerank\nModel: ${model}\nResponse: ${JSON.stringify(error.response.body, null, 2)}`,
114
+ });
115
+ }
116
+ throw new n8n_workflow_1.NodeApiError(context.getNode(), error, {
117
+ message: 'Custom Rerank API request failed',
118
+ description: `Endpoint: ${ollamaHost}/api/rerank\nModel: ${model}\nError: ${error.message}`,
119
+ });
120
+ }
121
+ }
46
122
  /**
47
123
  * Score a single document against the query using Ollama reranker model with retry logic
48
124
  */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "n8n-nodes-ollama-reranker",
3
- "version": "1.1.0",
4
- "description": "Ollama Reranker for n8n - Vector Store provider + chainable workflow node with AI Agent tool support",
3
+ "version": "1.3.0",
4
+ "description": "Ollama Reranker for n8n - Dynamic model loading + Ollama/Custom API support (Vector Store provider + workflow node)",
5
5
  "main": "index.js",
6
6
  "author": "Gabriel BRUMENT",
7
7
  "license": "MIT",