diff --git a/agent-code-review/src/providerDetector.ts b/agent-code-review/src/providerDetector.ts index 7cd3672..eedbf8e 100644 --- a/agent-code-review/src/providerDetector.ts +++ b/agent-code-review/src/providerDetector.ts @@ -27,6 +27,14 @@ const DEFAULT_MODELS: Record = { const OLLAMA_DEFAULT_URL = 'http://localhost:11434'; +const EMBEDDING_MODEL_PATTERN = + /embed|minilm|mxbai|bge[-_]|gte[-_]|nomic|snowflake-arctic|e5[-_]|clip$/i; + +export function pickOllamaChatModel(models: string[]): string | null { + const chatModel = models.find((m) => !EMBEDDING_MODEL_PATTERN.test(m)); + return chatModel ?? null; +} + export async function detectProvider(options: DetectOptions = {}): Promise { const env = options.env ?? process.env; const ollamaBaseUrl = options.ollamaBaseUrl ?? OLLAMA_DEFAULT_URL; @@ -69,14 +77,17 @@ export async function detectProvider(options: DetectOptions = {}): Promise 0) { - const model = ollamaModels[0]; - return { - provider: 'ollama', - model, - baseUrl: ollamaBaseUrl, - autoDetected: true, - detectedAt: new Date().toISOString(), - }; + const model = pickOllamaChatModel(ollamaModels); + if (model) { + return { + provider: 'ollama', + model, + baseUrl: ollamaBaseUrl, + autoDetected: true, + detectedAt: new Date().toISOString(), + }; + } + return null; } if (ollamaModels === null) { diff --git a/agent-code-review/tests/providerDetector.test.ts b/agent-code-review/tests/providerDetector.test.ts index 95ecf63..e9a8fe3 100644 --- a/agent-code-review/tests/providerDetector.test.ts +++ b/agent-code-review/tests/providerDetector.test.ts @@ -5,6 +5,7 @@ import os from 'os'; import { detectProvider, detectOllama, + pickOllamaChatModel, loadConfig, saveConfig, getDefaultModel, @@ -60,6 +61,37 @@ describe('providerDetector', () => { } }); + it('skips embedding models and picks a chat-capable Ollama model', async () => { + const originalFetch = globalThis.fetch; + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ models: [{ name: 'nomic-embed-text:latest' }, { name: 'llama3' }] }), + } as any); + try { + const result = await detectProvider({ env: {} }); + expect(result).not.toBeNull(); + expect(result!.provider).toBe('ollama'); + expect(result!.model).toBe('llama3'); + expect(result!.baseUrl).toBe('http://localhost:11434'); + } finally { + globalThis.fetch = originalFetch; + } + }); + + it('returns null when Ollama only has embedding models', async () => { + const originalFetch = globalThis.fetch; + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ models: [{ name: 'nomic-embed-text:latest' }] }), + } as any); + try { + const result = await detectProvider({ env: {} }); + expect(result).toBeNull(); + } finally { + globalThis.fetch = originalFetch; + } + }); + it('returns null when nothing is available', async () => { const originalFetch = globalThis.fetch; globalThis.fetch = vi.fn().mockRejectedValue(new Error('Connection refused')); @@ -144,6 +176,24 @@ describe('providerDetector', () => { }); }); + describe('pickOllamaChatModel', () => { + it('returns null for an empty list', () => { + expect(pickOllamaChatModel([])).toBeNull(); + }); + + it('skips embedding models and picks the first chat model', () => { + expect(pickOllamaChatModel(['nomic-embed-text:latest', 'llama3'])).toBe('llama3'); + }); + + it('returns null when all models are embeddings', () => { + expect(pickOllamaChatModel(['nomic-embed-text:latest', 'mxbai-embed-large'])).toBeNull(); + }); + + it('returns the first non-embedding model', () => { + expect(pickOllamaChatModel(['qwen2.5-coder:1.5b', 'llama3'])).toBe('qwen2.5-coder:1.5b'); + }); + }); + describe('loadConfig', () => { let tmpDir: string; diff --git a/agent-doc-generator/src/providerDetector.ts b/agent-doc-generator/src/providerDetector.ts index 7cd3672..eedbf8e 100644 --- a/agent-doc-generator/src/providerDetector.ts +++ b/agent-doc-generator/src/providerDetector.ts @@ -27,6 +27,14 @@ const DEFAULT_MODELS: Record = { const OLLAMA_DEFAULT_URL = 'http://localhost:11434'; +const EMBEDDING_MODEL_PATTERN = + /embed|minilm|mxbai|bge[-_]|gte[-_]|nomic|snowflake-arctic|e5[-_]|clip$/i; + +export function pickOllamaChatModel(models: string[]): string | null { + const chatModel = models.find((m) => !EMBEDDING_MODEL_PATTERN.test(m)); + return chatModel ?? null; +} + export async function detectProvider(options: DetectOptions = {}): Promise { const env = options.env ?? process.env; const ollamaBaseUrl = options.ollamaBaseUrl ?? OLLAMA_DEFAULT_URL; @@ -69,14 +77,17 @@ export async function detectProvider(options: DetectOptions = {}): Promise 0) { - const model = ollamaModels[0]; - return { - provider: 'ollama', - model, - baseUrl: ollamaBaseUrl, - autoDetected: true, - detectedAt: new Date().toISOString(), - }; + const model = pickOllamaChatModel(ollamaModels); + if (model) { + return { + provider: 'ollama', + model, + baseUrl: ollamaBaseUrl, + autoDetected: true, + detectedAt: new Date().toISOString(), + }; + } + return null; } if (ollamaModels === null) { diff --git a/agent-doc-generator/tests/providerDetector.test.ts b/agent-doc-generator/tests/providerDetector.test.ts index 95ecf63..e9a8fe3 100644 --- a/agent-doc-generator/tests/providerDetector.test.ts +++ b/agent-doc-generator/tests/providerDetector.test.ts @@ -5,6 +5,7 @@ import os from 'os'; import { detectProvider, detectOllama, + pickOllamaChatModel, loadConfig, saveConfig, getDefaultModel, @@ -60,6 +61,37 @@ describe('providerDetector', () => { } }); + it('skips embedding models and picks a chat-capable Ollama model', async () => { + const originalFetch = globalThis.fetch; + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ models: [{ name: 'nomic-embed-text:latest' }, { name: 'llama3' }] }), + } as any); + try { + const result = await detectProvider({ env: {} }); + expect(result).not.toBeNull(); + expect(result!.provider).toBe('ollama'); + expect(result!.model).toBe('llama3'); + expect(result!.baseUrl).toBe('http://localhost:11434'); + } finally { + globalThis.fetch = originalFetch; + } + }); + + it('returns null when Ollama only has embedding models', async () => { + const originalFetch = globalThis.fetch; + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ models: [{ name: 'nomic-embed-text:latest' }] }), + } as any); + try { + const result = await detectProvider({ env: {} }); + expect(result).toBeNull(); + } finally { + globalThis.fetch = originalFetch; + } + }); + it('returns null when nothing is available', async () => { const originalFetch = globalThis.fetch; globalThis.fetch = vi.fn().mockRejectedValue(new Error('Connection refused')); @@ -144,6 +176,24 @@ describe('providerDetector', () => { }); }); + describe('pickOllamaChatModel', () => { + it('returns null for an empty list', () => { + expect(pickOllamaChatModel([])).toBeNull(); + }); + + it('skips embedding models and picks the first chat model', () => { + expect(pickOllamaChatModel(['nomic-embed-text:latest', 'llama3'])).toBe('llama3'); + }); + + it('returns null when all models are embeddings', () => { + expect(pickOllamaChatModel(['nomic-embed-text:latest', 'mxbai-embed-large'])).toBeNull(); + }); + + it('returns the first non-embedding model', () => { + expect(pickOllamaChatModel(['qwen2.5-coder:1.5b', 'llama3'])).toBe('qwen2.5-coder:1.5b'); + }); + }); + describe('loadConfig', () => { let tmpDir: string; diff --git a/agent-refactor/src/providerDetector.ts b/agent-refactor/src/providerDetector.ts index 7cd3672..eedbf8e 100644 --- a/agent-refactor/src/providerDetector.ts +++ b/agent-refactor/src/providerDetector.ts @@ -27,6 +27,14 @@ const DEFAULT_MODELS: Record = { const OLLAMA_DEFAULT_URL = 'http://localhost:11434'; +const EMBEDDING_MODEL_PATTERN = + /embed|minilm|mxbai|bge[-_]|gte[-_]|nomic|snowflake-arctic|e5[-_]|clip$/i; + +export function pickOllamaChatModel(models: string[]): string | null { + const chatModel = models.find((m) => !EMBEDDING_MODEL_PATTERN.test(m)); + return chatModel ?? null; +} + export async function detectProvider(options: DetectOptions = {}): Promise { const env = options.env ?? process.env; const ollamaBaseUrl = options.ollamaBaseUrl ?? OLLAMA_DEFAULT_URL; @@ -69,14 +77,17 @@ export async function detectProvider(options: DetectOptions = {}): Promise 0) { - const model = ollamaModels[0]; - return { - provider: 'ollama', - model, - baseUrl: ollamaBaseUrl, - autoDetected: true, - detectedAt: new Date().toISOString(), - }; + const model = pickOllamaChatModel(ollamaModels); + if (model) { + return { + provider: 'ollama', + model, + baseUrl: ollamaBaseUrl, + autoDetected: true, + detectedAt: new Date().toISOString(), + }; + } + return null; } if (ollamaModels === null) { diff --git a/agent-refactor/tests/providerDetector.test.ts b/agent-refactor/tests/providerDetector.test.ts index 95ecf63..e9a8fe3 100644 --- a/agent-refactor/tests/providerDetector.test.ts +++ b/agent-refactor/tests/providerDetector.test.ts @@ -5,6 +5,7 @@ import os from 'os'; import { detectProvider, detectOllama, + pickOllamaChatModel, loadConfig, saveConfig, getDefaultModel, @@ -60,6 +61,37 @@ describe('providerDetector', () => { } }); + it('skips embedding models and picks a chat-capable Ollama model', async () => { + const originalFetch = globalThis.fetch; + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ models: [{ name: 'nomic-embed-text:latest' }, { name: 'llama3' }] }), + } as any); + try { + const result = await detectProvider({ env: {} }); + expect(result).not.toBeNull(); + expect(result!.provider).toBe('ollama'); + expect(result!.model).toBe('llama3'); + expect(result!.baseUrl).toBe('http://localhost:11434'); + } finally { + globalThis.fetch = originalFetch; + } + }); + + it('returns null when Ollama only has embedding models', async () => { + const originalFetch = globalThis.fetch; + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ models: [{ name: 'nomic-embed-text:latest' }] }), + } as any); + try { + const result = await detectProvider({ env: {} }); + expect(result).toBeNull(); + } finally { + globalThis.fetch = originalFetch; + } + }); + it('returns null when nothing is available', async () => { const originalFetch = globalThis.fetch; globalThis.fetch = vi.fn().mockRejectedValue(new Error('Connection refused')); @@ -144,6 +176,24 @@ describe('providerDetector', () => { }); }); + describe('pickOllamaChatModel', () => { + it('returns null for an empty list', () => { + expect(pickOllamaChatModel([])).toBeNull(); + }); + + it('skips embedding models and picks the first chat model', () => { + expect(pickOllamaChatModel(['nomic-embed-text:latest', 'llama3'])).toBe('llama3'); + }); + + it('returns null when all models are embeddings', () => { + expect(pickOllamaChatModel(['nomic-embed-text:latest', 'mxbai-embed-large'])).toBeNull(); + }); + + it('returns the first non-embedding model', () => { + expect(pickOllamaChatModel(['qwen2.5-coder:1.5b', 'llama3'])).toBe('qwen2.5-coder:1.5b'); + }); + }); + describe('loadConfig', () => { let tmpDir: string; diff --git a/agent-security-audit/src/providerDetector.ts b/agent-security-audit/src/providerDetector.ts index 7cd3672..eedbf8e 100644 --- a/agent-security-audit/src/providerDetector.ts +++ b/agent-security-audit/src/providerDetector.ts @@ -27,6 +27,14 @@ const DEFAULT_MODELS: Record = { const OLLAMA_DEFAULT_URL = 'http://localhost:11434'; +const EMBEDDING_MODEL_PATTERN = + /embed|minilm|mxbai|bge[-_]|gte[-_]|nomic|snowflake-arctic|e5[-_]|clip$/i; + +export function pickOllamaChatModel(models: string[]): string | null { + const chatModel = models.find((m) => !EMBEDDING_MODEL_PATTERN.test(m)); + return chatModel ?? null; +} + export async function detectProvider(options: DetectOptions = {}): Promise { const env = options.env ?? process.env; const ollamaBaseUrl = options.ollamaBaseUrl ?? OLLAMA_DEFAULT_URL; @@ -69,14 +77,17 @@ export async function detectProvider(options: DetectOptions = {}): Promise 0) { - const model = ollamaModels[0]; - return { - provider: 'ollama', - model, - baseUrl: ollamaBaseUrl, - autoDetected: true, - detectedAt: new Date().toISOString(), - }; + const model = pickOllamaChatModel(ollamaModels); + if (model) { + return { + provider: 'ollama', + model, + baseUrl: ollamaBaseUrl, + autoDetected: true, + detectedAt: new Date().toISOString(), + }; + } + return null; } if (ollamaModels === null) { diff --git a/agent-security-audit/tests/providerDetector.test.ts b/agent-security-audit/tests/providerDetector.test.ts index 95ecf63..e9a8fe3 100644 --- a/agent-security-audit/tests/providerDetector.test.ts +++ b/agent-security-audit/tests/providerDetector.test.ts @@ -5,6 +5,7 @@ import os from 'os'; import { detectProvider, detectOllama, + pickOllamaChatModel, loadConfig, saveConfig, getDefaultModel, @@ -60,6 +61,37 @@ describe('providerDetector', () => { } }); + it('skips embedding models and picks a chat-capable Ollama model', async () => { + const originalFetch = globalThis.fetch; + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ models: [{ name: 'nomic-embed-text:latest' }, { name: 'llama3' }] }), + } as any); + try { + const result = await detectProvider({ env: {} }); + expect(result).not.toBeNull(); + expect(result!.provider).toBe('ollama'); + expect(result!.model).toBe('llama3'); + expect(result!.baseUrl).toBe('http://localhost:11434'); + } finally { + globalThis.fetch = originalFetch; + } + }); + + it('returns null when Ollama only has embedding models', async () => { + const originalFetch = globalThis.fetch; + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ models: [{ name: 'nomic-embed-text:latest' }] }), + } as any); + try { + const result = await detectProvider({ env: {} }); + expect(result).toBeNull(); + } finally { + globalThis.fetch = originalFetch; + } + }); + it('returns null when nothing is available', async () => { const originalFetch = globalThis.fetch; globalThis.fetch = vi.fn().mockRejectedValue(new Error('Connection refused')); @@ -144,6 +176,24 @@ describe('providerDetector', () => { }); }); + describe('pickOllamaChatModel', () => { + it('returns null for an empty list', () => { + expect(pickOllamaChatModel([])).toBeNull(); + }); + + it('skips embedding models and picks the first chat model', () => { + expect(pickOllamaChatModel(['nomic-embed-text:latest', 'llama3'])).toBe('llama3'); + }); + + it('returns null when all models are embeddings', () => { + expect(pickOllamaChatModel(['nomic-embed-text:latest', 'mxbai-embed-large'])).toBeNull(); + }); + + it('returns the first non-embedding model', () => { + expect(pickOllamaChatModel(['qwen2.5-coder:1.5b', 'llama3'])).toBe('qwen2.5-coder:1.5b'); + }); + }); + describe('loadConfig', () => { let tmpDir: string; diff --git a/agent-test-generator/src/providerDetector.ts b/agent-test-generator/src/providerDetector.ts index 7cd3672..eedbf8e 100644 --- a/agent-test-generator/src/providerDetector.ts +++ b/agent-test-generator/src/providerDetector.ts @@ -27,6 +27,14 @@ const DEFAULT_MODELS: Record = { const OLLAMA_DEFAULT_URL = 'http://localhost:11434'; +const EMBEDDING_MODEL_PATTERN = + /embed|minilm|mxbai|bge[-_]|gte[-_]|nomic|snowflake-arctic|e5[-_]|clip$/i; + +export function pickOllamaChatModel(models: string[]): string | null { + const chatModel = models.find((m) => !EMBEDDING_MODEL_PATTERN.test(m)); + return chatModel ?? null; +} + export async function detectProvider(options: DetectOptions = {}): Promise { const env = options.env ?? process.env; const ollamaBaseUrl = options.ollamaBaseUrl ?? OLLAMA_DEFAULT_URL; @@ -69,14 +77,17 @@ export async function detectProvider(options: DetectOptions = {}): Promise 0) { - const model = ollamaModels[0]; - return { - provider: 'ollama', - model, - baseUrl: ollamaBaseUrl, - autoDetected: true, - detectedAt: new Date().toISOString(), - }; + const model = pickOllamaChatModel(ollamaModels); + if (model) { + return { + provider: 'ollama', + model, + baseUrl: ollamaBaseUrl, + autoDetected: true, + detectedAt: new Date().toISOString(), + }; + } + return null; } if (ollamaModels === null) { diff --git a/agent-test-generator/tests/providerDetector.test.ts b/agent-test-generator/tests/providerDetector.test.ts index 95ecf63..e9a8fe3 100644 --- a/agent-test-generator/tests/providerDetector.test.ts +++ b/agent-test-generator/tests/providerDetector.test.ts @@ -5,6 +5,7 @@ import os from 'os'; import { detectProvider, detectOllama, + pickOllamaChatModel, loadConfig, saveConfig, getDefaultModel, @@ -60,6 +61,37 @@ describe('providerDetector', () => { } }); + it('skips embedding models and picks a chat-capable Ollama model', async () => { + const originalFetch = globalThis.fetch; + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ models: [{ name: 'nomic-embed-text:latest' }, { name: 'llama3' }] }), + } as any); + try { + const result = await detectProvider({ env: {} }); + expect(result).not.toBeNull(); + expect(result!.provider).toBe('ollama'); + expect(result!.model).toBe('llama3'); + expect(result!.baseUrl).toBe('http://localhost:11434'); + } finally { + globalThis.fetch = originalFetch; + } + }); + + it('returns null when Ollama only has embedding models', async () => { + const originalFetch = globalThis.fetch; + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ models: [{ name: 'nomic-embed-text:latest' }] }), + } as any); + try { + const result = await detectProvider({ env: {} }); + expect(result).toBeNull(); + } finally { + globalThis.fetch = originalFetch; + } + }); + it('returns null when nothing is available', async () => { const originalFetch = globalThis.fetch; globalThis.fetch = vi.fn().mockRejectedValue(new Error('Connection refused')); @@ -144,6 +176,24 @@ describe('providerDetector', () => { }); }); + describe('pickOllamaChatModel', () => { + it('returns null for an empty list', () => { + expect(pickOllamaChatModel([])).toBeNull(); + }); + + it('skips embedding models and picks the first chat model', () => { + expect(pickOllamaChatModel(['nomic-embed-text:latest', 'llama3'])).toBe('llama3'); + }); + + it('returns null when all models are embeddings', () => { + expect(pickOllamaChatModel(['nomic-embed-text:latest', 'mxbai-embed-large'])).toBeNull(); + }); + + it('returns the first non-embedding model', () => { + expect(pickOllamaChatModel(['qwen2.5-coder:1.5b', 'llama3'])).toBe('qwen2.5-coder:1.5b'); + }); + }); + describe('loadConfig', () => { let tmpDir: string;