diff --git a/src/chrome/src/providers/manager.js b/src/chrome/src/providers/manager.js index 984f1910a..1a8842e1f 100644 --- a/src/chrome/src/providers/manager.js +++ b/src/chrome/src/providers/manager.js @@ -274,6 +274,7 @@ export class ProviderManager { category: 'local', label: 'Ollama (Local)', providerName: 'ollama', + requiresModel: true, baseUrl: 'http://localhost:11434/v1', model: '', contextWindow: 16384, @@ -302,6 +303,7 @@ export class ProviderManager { category: 'local', label: 'Jan (Local)', providerName: 'jan', + requiresModel: true, baseUrl: 'http://localhost:1337/v1', model: '', contextWindow: 16384, @@ -315,6 +317,7 @@ export class ProviderManager { category: 'local', label: 'vLLM (Local)', providerName: 'vllm', + requiresModel: true, baseUrl: 'http://localhost:8000/v1', model: '', contextWindow: 16384, @@ -328,6 +331,7 @@ export class ProviderManager { category: 'local', label: 'SGLang (Local)', providerName: 'sglang', + requiresModel: true, baseUrl: 'http://localhost:30000/v1', model: '', contextWindow: 16384, @@ -341,6 +345,7 @@ export class ProviderManager { category: 'local', label: 'LocalAI (Local)', providerName: 'localai', + requiresModel: true, baseUrl: 'http://localhost:8080/v1', model: '', contextWindow: 16384, @@ -355,6 +360,7 @@ export class ProviderManager { category: 'local', label: 'GPT4All (Local)', providerName: 'gpt4all', + requiresModel: true, baseUrl: 'http://localhost:4891/v1', model: '', contextWindow: 16384, diff --git a/src/chrome/src/providers/openai.js b/src/chrome/src/providers/openai.js index 95804b3ed..72651d20c 100644 --- a/src/chrome/src/providers/openai.js +++ b/src/chrome/src/providers/openai.js @@ -79,10 +79,10 @@ export class OpenAICompatibleProvider extends BaseLLMProvider { get model() { if (this.config.model) return this.config.model; if (this.config.requiresModel) throw new Error(`${this.config.label || this.name} model is required.`); - // Local servers (Ollama, LM Studio, vLLM, …) must never receive a model - // id the user never configured: most 404 on unknown ids and none serves - // a model named after OpenAI's default. Omit the field entirely so the - // server applies its own default. Mirrors LlamaCppProvider. + // Some local servers apply their own default when no model is configured. + // Others carry `requiresModel: true` and throw above. Treat the category as + // the durable boundary so older/custom local entries never inherit a + // fabricated cloud model id merely because their provider name is unknown. if (this.config.category === 'local') return null; return String(this.config.providerName || '').toLowerCase() === 'openai' && this._isOfficialOpenAIBaseUrl() diff --git a/src/firefox/src/providers/manager.js b/src/firefox/src/providers/manager.js index 0bcfdf6a3..73b6e9740 100644 --- a/src/firefox/src/providers/manager.js +++ b/src/firefox/src/providers/manager.js @@ -247,6 +247,7 @@ export class ProviderManager { category: 'local', label: 'Ollama (Local)', providerName: 'ollama', + requiresModel: true, baseUrl: 'http://localhost:11434/v1', model: '', contextWindow: 16384, @@ -275,6 +276,7 @@ export class ProviderManager { category: 'local', label: 'Jan (Local)', providerName: 'jan', + requiresModel: true, baseUrl: 'http://localhost:1337/v1', model: '', contextWindow: 16384, @@ -288,6 +290,7 @@ export class ProviderManager { category: 'local', label: 'vLLM (Local)', providerName: 'vllm', + requiresModel: true, baseUrl: 'http://localhost:8000/v1', model: '', contextWindow: 16384, @@ -301,6 +304,7 @@ export class ProviderManager { category: 'local', label: 'SGLang (Local)', providerName: 'sglang', + requiresModel: true, baseUrl: 'http://localhost:30000/v1', model: '', contextWindow: 16384, @@ -314,6 +318,7 @@ export class ProviderManager { category: 'local', label: 'LocalAI (Local)', providerName: 'localai', + requiresModel: true, baseUrl: 'http://localhost:8080/v1', model: '', contextWindow: 16384, @@ -328,6 +333,7 @@ export class ProviderManager { category: 'local', label: 'GPT4All (Local)', providerName: 'gpt4all', + requiresModel: true, baseUrl: 'http://localhost:4891/v1', model: '', contextWindow: 16384, diff --git a/src/firefox/src/providers/openai.js b/src/firefox/src/providers/openai.js index 26458236d..8fa039933 100644 --- a/src/firefox/src/providers/openai.js +++ b/src/firefox/src/providers/openai.js @@ -79,10 +79,10 @@ export class OpenAICompatibleProvider extends BaseLLMProvider { get model() { if (this.config.model) return this.config.model; if (this.config.requiresModel) throw new Error(`${this.config.label || this.name} model is required.`); - // Local servers (Ollama, LM Studio, vLLM, …) must never receive a model - // id the user never configured: most 404 on unknown ids and none serves - // a model named after OpenAI's default. Omit the field entirely so the - // server applies its own default. Mirrors LlamaCppProvider. + // Some local servers apply their own default when no model is configured. + // Others carry `requiresModel: true` and throw above. Treat the category as + // the durable boundary so older/custom local entries never inherit a + // fabricated cloud model id merely because their provider name is unknown. if (this.config.category === 'local') return null; return String(this.config.providerName || '').toLowerCase() === 'openai' && this._isOfficialOpenAIBaseUrl() diff --git a/test/run.js b/test/run.js index 6894e9e6f..53a10937d 100644 --- a/test/run.js +++ b/test/run.js @@ -49686,6 +49686,66 @@ test('official OpenAI GPT-5.6 and Responses-only GPT-5 Pro variants route to Res } }); +test('local OpenAI-compatible servers that require a model throw a clear error when unset', () => { + for (const Provider of [OpenAIProviderCh, OpenAIProviderFx]) { + for (const providerName of ['ollama', 'jan', 'vllm', 'sglang', 'localai', 'gpt4all', 'local_openai_proxy']) { + const provider = new Provider({ + providerName, + category: 'local', + baseUrl: 'http://localhost:1234/v1', + requiresModel: true, + }); + assert.throws( + () => provider._buildChatCompletionsBody([{ role: 'user', content: 'hello' }], {}), + /model is required/, + `${providerName}: empty model must fail loudly instead of sending a fabricated id`, + ); + } + } +}); + +test('every optional-model local provider omits the model field when unset', () => { + for (const Provider of [OpenAIProviderCh, OpenAIProviderFx]) { + for (const providerName of ['lmstudio', 'privatemode-ai', 'persisted-custom-local']) { + const empty = new Provider({ + providerName, + category: 'local', + baseUrl: 'http://localhost:1234/v1', + }); + assert.equal(empty.model, null, `${providerName}: local providers must not fabricate a model id`); + const chatBody = empty._buildChatCompletionsBody([{ role: 'user', content: 'hello' }], {}); + assert.equal('model' in chatBody, false, `${providerName}: Chat Completions must omit an unset model`); + const responsesBody = empty._responsesBody([{ role: 'user', content: 'hello' }], {}, false); + assert.equal('model' in responsesBody, false, `${providerName}: Responses must omit an unset model`); + } + + const configured = new Provider({ + providerName: 'persisted-custom-local', + category: 'local', + baseUrl: 'http://localhost:1234/v1', + model: 'llama-3.2-3b', + }); + assert.equal( + configured._buildChatCompletionsBody([{ role: 'user', content: 'hello' }], {}).model, + 'llama-3.2-3b', + 'configured local model must be sent', + ); + } +}); + +test('non-local OpenAI-compatible providers keep the legacy model fallback', () => { + for (const Provider of [OpenAIProviderCh, OpenAIProviderFx]) { + assert.equal(new Provider({ providerName: 'openrouter' }).model, 'gpt-4o'); + assert.equal(new Provider({ providerName: 'openai', baseUrl: 'https://proxy.example.test/v1' }).model, 'gpt-4o'); + assert.equal(new Provider({ providerName: 'openai' }).model, 'gpt-5.6-terra'); + const body = new Provider({ providerName: 'openrouter' })._buildChatCompletionsBody( + [{ role: 'user', content: 'hello' }], + {}, + ); + assert.equal(body.model, 'gpt-4o', 'non-local fallback model must stay on the wire'); + } +}); + test('Responses reasoning effort is normalized for GPT-5 Pro model constraints', () => { for (const Provider of [OpenAIProviderCh, OpenAIProviderFx]) { for (const model of ['gpt-5-pro', 'gpt-5-pro-2025-10-06']) {