From 8cc67261caef94a0054af36c2cc12a5e7251e24e Mon Sep 17 00:00:00 2001 From: alectimison-maker Date: Sun, 16 Aug 2026 20:48:24 +0800 Subject: [PATCH 1/2] fix(providers): require a model for local servers that need one, omit for LM Studio Local OpenAI-compatible servers reject an empty model field, so sending a fabricated fallback id (previously gpt-4o) or omitting the field both fail. Mark ollama, vLLM, SGLang, LocalAI, Jan, and GPT4All as requiresModel so an empty Model field fails with a clear error instead of an opaque server 400. LM Studio (like llama.cpp) applies its own default, so it omits the field when unset. Non-local providers keep their fallback. --- src/chrome/src/providers/manager.js | 6 +++ src/chrome/src/providers/openai.js | 13 ++++++- src/firefox/src/providers/manager.js | 6 +++ src/firefox/src/providers/openai.js | 13 ++++++- test/run.js | 56 ++++++++++++++++++++++++++++ 5 files changed, 90 insertions(+), 4 deletions(-) diff --git a/src/chrome/src/providers/manager.js b/src/chrome/src/providers/manager.js index f9392bb32..b04da33ac 100644 --- a/src/chrome/src/providers/manager.js +++ b/src/chrome/src/providers/manager.js @@ -239,6 +239,7 @@ export class ProviderManager { category: 'local', label: 'Ollama (Local)', providerName: 'ollama', + requiresModel: true, baseUrl: 'http://localhost:11434/v1', model: '', contextWindow: 16384, @@ -267,6 +268,7 @@ export class ProviderManager { category: 'local', label: 'Jan (Local)', providerName: 'jan', + requiresModel: true, baseUrl: 'http://localhost:1337/v1', model: '', contextWindow: 16384, @@ -280,6 +282,7 @@ export class ProviderManager { category: 'local', label: 'vLLM (Local)', providerName: 'vllm', + requiresModel: true, baseUrl: 'http://localhost:8000/v1', model: '', contextWindow: 16384, @@ -293,6 +296,7 @@ export class ProviderManager { category: 'local', label: 'SGLang (Local)', providerName: 'sglang', + requiresModel: true, baseUrl: 'http://localhost:30000/v1', model: '', contextWindow: 16384, @@ -306,6 +310,7 @@ export class ProviderManager { category: 'local', label: 'LocalAI (Local)', providerName: 'localai', + requiresModel: true, baseUrl: 'http://localhost:8080/v1', model: '', contextWindow: 16384, @@ -320,6 +325,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 56dab64e4..76fae2108 100644 --- a/src/chrome/src/providers/openai.js +++ b/src/chrome/src/providers/openai.js @@ -79,6 +79,15 @@ 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.`); + // LM Studio applies the server's own default model when none is + // configured (as does llama.cpp, handled by its own provider class). + // Local servers that reject an empty model instead carry + // `requiresModel: true` in the catalog and throw above, so no local + // server ever receives a fabricated model id. + if (this.config.category === 'local' + && String(this.config.providerName || '').toLowerCase() === 'lmstudio') { + return null; + } return String(this.config.providerName || '').toLowerCase() === 'openai' && this._isOfficialOpenAIBaseUrl() ? 'gpt-5.6-terra' @@ -376,10 +385,10 @@ export class OpenAICompatibleProvider extends BaseLLMProvider { */ _buildChatCompletionsBody(messages, options = {}, stream = false) { let body = { - model: this.model, messages: this._chatMessages(messages, options), stream, }; + if (this.model) body.model = this.model; this._addTemperature(body, options); this._addMaxTokens(body, options); if (this._shouldSendTools(messages, options)) { @@ -499,7 +508,6 @@ export class OpenAICompatibleProvider extends BaseLLMProvider { _responsesBody(messages, options, stream) { let body = { - model: this.model, input: this._responsesInput(messages), stream, store: false, @@ -514,6 +522,7 @@ export class OpenAICompatibleProvider extends BaseLLMProvider { if (body.reasoning.effort === 'auto' || body.reasoning.effort === 'off') { body.reasoning.effort = body.reasoning.effort === 'off' ? 'none' : 'medium'; } + if (this.model) body.model = this.model; if (this._shouldSendTools(messages, options)) { body.tools = this._responsesTools(options.tools); diff --git a/src/firefox/src/providers/manager.js b/src/firefox/src/providers/manager.js index 98cf49d6e..bd2b27437 100644 --- a/src/firefox/src/providers/manager.js +++ b/src/firefox/src/providers/manager.js @@ -212,6 +212,7 @@ export class ProviderManager { category: 'local', label: 'Ollama (Local)', providerName: 'ollama', + requiresModel: true, baseUrl: 'http://localhost:11434/v1', model: '', contextWindow: 16384, @@ -240,6 +241,7 @@ export class ProviderManager { category: 'local', label: 'Jan (Local)', providerName: 'jan', + requiresModel: true, baseUrl: 'http://localhost:1337/v1', model: '', contextWindow: 16384, @@ -253,6 +255,7 @@ export class ProviderManager { category: 'local', label: 'vLLM (Local)', providerName: 'vllm', + requiresModel: true, baseUrl: 'http://localhost:8000/v1', model: '', contextWindow: 16384, @@ -266,6 +269,7 @@ export class ProviderManager { category: 'local', label: 'SGLang (Local)', providerName: 'sglang', + requiresModel: true, baseUrl: 'http://localhost:30000/v1', model: '', contextWindow: 16384, @@ -279,6 +283,7 @@ export class ProviderManager { category: 'local', label: 'LocalAI (Local)', providerName: 'localai', + requiresModel: true, baseUrl: 'http://localhost:8080/v1', model: '', contextWindow: 16384, @@ -293,6 +298,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 95cb8dca6..55129d997 100644 --- a/src/firefox/src/providers/openai.js +++ b/src/firefox/src/providers/openai.js @@ -79,6 +79,15 @@ 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.`); + // LM Studio applies the server's own default model when none is + // configured (as does llama.cpp, handled by its own provider class). + // Local servers that reject an empty model instead carry + // `requiresModel: true` in the catalog and throw above, so no local + // server ever receives a fabricated model id. + if (this.config.category === 'local' + && String(this.config.providerName || '').toLowerCase() === 'lmstudio') { + return null; + } return String(this.config.providerName || '').toLowerCase() === 'openai' && this._isOfficialOpenAIBaseUrl() ? 'gpt-5.6-terra' @@ -376,10 +385,10 @@ export class OpenAICompatibleProvider extends BaseLLMProvider { */ _buildChatCompletionsBody(messages, options = {}, stream = false) { let body = { - model: this.model, messages: this._chatMessages(messages, options), stream, }; + if (this.model) body.model = this.model; this._addTemperature(body, options); this._addMaxTokens(body, options); if (this._shouldSendTools(messages, options)) { @@ -499,7 +508,6 @@ export class OpenAICompatibleProvider extends BaseLLMProvider { _responsesBody(messages, options, stream) { let body = { - model: this.model, input: this._responsesInput(messages), stream, store: false, @@ -514,6 +522,7 @@ export class OpenAICompatibleProvider extends BaseLLMProvider { if (body.reasoning.effort === 'auto' || body.reasoning.effort === 'off') { body.reasoning.effort = body.reasoning.effort === 'off' ? 'none' : 'medium'; } + if (this.model) body.model = this.model; if (this._shouldSendTools(messages, options)) { body.tools = this._responsesTools(options.tools); diff --git a/test/run.js b/test/run.js index cbab07c8a..706827119 100644 --- a/test/run.js +++ b/test/run.js @@ -48208,6 +48208,62 @@ 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('LM Studio omits the model field when unset and sends it when configured', () => { + for (const Provider of [OpenAIProviderCh, OpenAIProviderFx]) { + const empty = new Provider({ + providerName: 'lmstudio', + category: 'local', + baseUrl: 'http://localhost:1234/v1', + }); + assert.equal(empty.model, null, 'lmstudio must not fabricate a model id'); + const body = empty._buildChatCompletionsBody([{ role: 'user', content: 'hello' }], {}); + assert.equal('model' in body, false, 'lmstudio request body must omit model when unset'); + + const configured = new Provider({ + providerName: 'lmstudio', + 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']) { From 534b04def63883e4f4925dfff18af631d06dec16 Mon Sep 17 00:00:00 2001 From: Barack Sokullu Date: Mon, 17 Aug 2026 03:47:31 +0300 Subject: [PATCH 2/2] fix(providers): omit unset models for every local config --- src/chrome/src/providers/openai.js | 14 +++++--------- src/firefox/src/providers/openai.js | 14 +++++--------- test/run.js | 24 ++++++++++++++---------- 3 files changed, 24 insertions(+), 28 deletions(-) diff --git a/src/chrome/src/providers/openai.js b/src/chrome/src/providers/openai.js index 76fae2108..840d0ecd4 100644 --- a/src/chrome/src/providers/openai.js +++ b/src/chrome/src/providers/openai.js @@ -79,15 +79,11 @@ 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.`); - // LM Studio applies the server's own default model when none is - // configured (as does llama.cpp, handled by its own provider class). - // Local servers that reject an empty model instead carry - // `requiresModel: true` in the catalog and throw above, so no local - // server ever receives a fabricated model id. - if (this.config.category === 'local' - && String(this.config.providerName || '').toLowerCase() === 'lmstudio') { - return null; - } + // 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() ? 'gpt-5.6-terra' diff --git a/src/firefox/src/providers/openai.js b/src/firefox/src/providers/openai.js index 55129d997..16b9b67e3 100644 --- a/src/firefox/src/providers/openai.js +++ b/src/firefox/src/providers/openai.js @@ -79,15 +79,11 @@ 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.`); - // LM Studio applies the server's own default model when none is - // configured (as does llama.cpp, handled by its own provider class). - // Local servers that reject an empty model instead carry - // `requiresModel: true` in the catalog and throw above, so no local - // server ever receives a fabricated model id. - if (this.config.category === 'local' - && String(this.config.providerName || '').toLowerCase() === 'lmstudio') { - return null; - } + // 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() ? 'gpt-5.6-terra' diff --git a/test/run.js b/test/run.js index 706827119..103776465 100644 --- a/test/run.js +++ b/test/run.js @@ -48226,19 +48226,23 @@ test('local OpenAI-compatible servers that require a model throw a clear error w } }); -test('LM Studio omits the model field when unset and sends it when configured', () => { +test('every optional-model local provider omits the model field when unset', () => { for (const Provider of [OpenAIProviderCh, OpenAIProviderFx]) { - const empty = new Provider({ - providerName: 'lmstudio', - category: 'local', - baseUrl: 'http://localhost:1234/v1', - }); - assert.equal(empty.model, null, 'lmstudio must not fabricate a model id'); - const body = empty._buildChatCompletionsBody([{ role: 'user', content: 'hello' }], {}); - assert.equal('model' in body, false, 'lmstudio request body must omit model when unset'); + 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: 'lmstudio', + providerName: 'persisted-custom-local', category: 'local', baseUrl: 'http://localhost:1234/v1', model: 'llama-3.2-3b',