From 5bf075251269a7abc3475d9943cdcca25c811602 Mon Sep 17 00:00:00 2001 From: Emre Sokullu Date: Tue, 22 Sep 2026 14:41:45 -0700 Subject: [PATCH 1/2] Add GPT-6 Luna Pro compatibility --- src/chrome/src/providers/context-windows.js | 1 + src/chrome/src/providers/openai.js | 12 +++-- .../src/providers/provider-compatibility.js | 16 ++++++ src/chrome/src/ui/settings.js | 1 + src/firefox/src/providers/context-windows.js | 1 + src/firefox/src/providers/openai.js | 12 +++-- .../src/providers/provider-compatibility.js | 16 ++++++ src/firefox/src/ui/settings.js | 1 + test/run.js | 49 +++++++++++++++++-- 9 files changed, 100 insertions(+), 9 deletions(-) diff --git a/src/chrome/src/providers/context-windows.js b/src/chrome/src/providers/context-windows.js index 1599ce99f..061db1f2c 100644 --- a/src/chrome/src/providers/context-windows.js +++ b/src/chrome/src/providers/context-windows.js @@ -279,6 +279,7 @@ export function inferContextWindow(config = {}) { if (!model) return DEFAULT_CLOUD_CONTEXT_WINDOW; // OpenAI + if (/^gpt-6-luna-pro(?:[.\-]|$)/.test(model) || model.includes('/gpt-6-luna-pro')) return 1050000; if (/^gpt-5\.6(?:[.\-]|$)/.test(model) || model.includes('/gpt-5.6')) return 1050000; if (model.includes('gpt-5.5-pro')) return 1050000; if (/^gpt-5(?:[.\-]|$)/.test(model) || model.includes('/gpt-5')) return 400000; diff --git a/src/chrome/src/providers/openai.js b/src/chrome/src/providers/openai.js index d0dd66d13..cfec22950 100644 --- a/src/chrome/src/providers/openai.js +++ b/src/chrome/src/providers/openai.js @@ -4,6 +4,7 @@ import { isNewOpenAIContractConfig, isOfficialOpenAIConfig, isOpenCodeZenConfig, + requiresOpenAIDefaultTemperature, shouldUseOpenAIResponsesApi, supportsOpenAIAskStreaming, applyOpenRouterRoutingVariant, @@ -293,11 +294,16 @@ export class OpenAICompatibleProvider extends BaseLLMProvider { } _addTemperature(body, options) { - // GPT-5 / o-series only accept the default temperature (1). Sending - // anything else returns 400. Provider configs can impose + // GPT-5, GPT-6 Luna Pro, and o-series models only accept the default + // temperature. Provider configs can impose // the same omission for fixed-temperature models such as Kimi K2.5/K3. // In both cases, let the API apply its required default. - if (this._isNewOpenAIContract() || this.config.omitTemperature) return; + if (requiresOpenAIDefaultTemperature({ + ...this.config, + providerName: this.config.providerName || this.name, + baseUrl: this.baseUrl, + model: this.model, + }) || this.config.omitTemperature) return; body.temperature = options.temperature ?? 0.7; } diff --git a/src/chrome/src/providers/provider-compatibility.js b/src/chrome/src/providers/provider-compatibility.js index 7d0de62b6..def6a94f0 100644 --- a/src/chrome/src/providers/provider-compatibility.js +++ b/src/chrome/src/providers/provider-compatibility.js @@ -264,6 +264,21 @@ export function isNewOpenAIContractConfig(config = {}) { return isNewOpenAIContractModel(config.model); } +/** + * GPT-6 Luna Pro uses Chat Completions with `max_tokens`, but rejects an + * explicit temperature. Keep this separate from the GPT-5/o-series contract, + * whose token-field migration is different. + */ +export function requiresOpenAIDefaultTemperature(config = {}) { + if (isNewOpenAIContractConfig(config)) return true; + const providerName = clean(config.providerName); + const model = clean(config.model); + if (providerName === 'openrouter') { + return /(?:^|\/)openai\/gpt-6-luna-pro(?:$|[-_.\/:])/.test(model); + } + return isOfficialOpenAIConfig(config) && /^gpt-6-luna-pro(?:$|[-_.:])/.test(model); +} + export function supportsOpenAIAskStreaming(config = {}) { if (!isOfficialOpenAIConfig(config)) return false; @@ -274,6 +289,7 @@ export function supportsOpenAIAskStreaming(config = {}) { if (shouldUseOpenAIResponsesApi(config)) return true; return [ + /^gpt-6-luna-pro(?:$|[-_.:])/, /^gpt-5\.5(?:$|-\d{4}-\d{2}-\d{2}$)/, /^gpt-5\.4(?:$|-\d{4}-\d{2}-\d{2}$|-(?:mini|nano)(?:$|-\d{4}-\d{2}-\d{2}$))/, /^gpt-5\.(?:1|2)(?:$|-\d{4}-\d{2}-\d{2}$)/, diff --git a/src/chrome/src/ui/settings.js b/src/chrome/src/ui/settings.js index c2086941e..49d10e1d4 100644 --- a/src/chrome/src/ui/settings.js +++ b/src/chrome/src/ui/settings.js @@ -3241,6 +3241,7 @@ function renderProviders() { { key: 'apiKey', labelKey: 'st.provider.field.api_key', type: 'password', placeholder: 'sk-...' }, { key: 'model', labelKey: 'st.provider.field.model', type: 'text', placeholder: 'gpt-5.6-terra', suggestions: [ + 'gpt-6-luna-pro', 'gpt-5.6-terra', 'gpt-5.6-sol', 'gpt-5.6-luna', diff --git a/src/firefox/src/providers/context-windows.js b/src/firefox/src/providers/context-windows.js index 1599ce99f..061db1f2c 100644 --- a/src/firefox/src/providers/context-windows.js +++ b/src/firefox/src/providers/context-windows.js @@ -279,6 +279,7 @@ export function inferContextWindow(config = {}) { if (!model) return DEFAULT_CLOUD_CONTEXT_WINDOW; // OpenAI + if (/^gpt-6-luna-pro(?:[.\-]|$)/.test(model) || model.includes('/gpt-6-luna-pro')) return 1050000; if (/^gpt-5\.6(?:[.\-]|$)/.test(model) || model.includes('/gpt-5.6')) return 1050000; if (model.includes('gpt-5.5-pro')) return 1050000; if (/^gpt-5(?:[.\-]|$)/.test(model) || model.includes('/gpt-5')) return 400000; diff --git a/src/firefox/src/providers/openai.js b/src/firefox/src/providers/openai.js index 4b0501164..c346cf5bf 100644 --- a/src/firefox/src/providers/openai.js +++ b/src/firefox/src/providers/openai.js @@ -4,6 +4,7 @@ import { isNewOpenAIContractConfig, isOfficialOpenAIConfig, isOpenCodeZenConfig, + requiresOpenAIDefaultTemperature, shouldUseOpenAIResponsesApi, supportsOpenAIAskStreaming, applyOpenRouterRoutingVariant, @@ -303,11 +304,16 @@ export class OpenAICompatibleProvider extends BaseLLMProvider { } _addTemperature(body, options) { - // GPT-5 / o-series only accept the default temperature (1). Sending - // anything else returns 400. Provider configs can impose + // GPT-5, GPT-6 Luna Pro, and o-series models only accept the default + // temperature. Provider configs can impose // the same omission for fixed-temperature models such as Kimi K2.5/K3. // In both cases, let the API apply its required default. - if (this._isNewOpenAIContract() || this.config.omitTemperature) return; + if (requiresOpenAIDefaultTemperature({ + ...this.config, + providerName: this.config.providerName || this.name, + baseUrl: this.baseUrl, + model: this.model, + }) || this.config.omitTemperature) return; body.temperature = options.temperature ?? 0.7; } diff --git a/src/firefox/src/providers/provider-compatibility.js b/src/firefox/src/providers/provider-compatibility.js index 7d0de62b6..def6a94f0 100644 --- a/src/firefox/src/providers/provider-compatibility.js +++ b/src/firefox/src/providers/provider-compatibility.js @@ -264,6 +264,21 @@ export function isNewOpenAIContractConfig(config = {}) { return isNewOpenAIContractModel(config.model); } +/** + * GPT-6 Luna Pro uses Chat Completions with `max_tokens`, but rejects an + * explicit temperature. Keep this separate from the GPT-5/o-series contract, + * whose token-field migration is different. + */ +export function requiresOpenAIDefaultTemperature(config = {}) { + if (isNewOpenAIContractConfig(config)) return true; + const providerName = clean(config.providerName); + const model = clean(config.model); + if (providerName === 'openrouter') { + return /(?:^|\/)openai\/gpt-6-luna-pro(?:$|[-_.\/:])/.test(model); + } + return isOfficialOpenAIConfig(config) && /^gpt-6-luna-pro(?:$|[-_.:])/.test(model); +} + export function supportsOpenAIAskStreaming(config = {}) { if (!isOfficialOpenAIConfig(config)) return false; @@ -274,6 +289,7 @@ export function supportsOpenAIAskStreaming(config = {}) { if (shouldUseOpenAIResponsesApi(config)) return true; return [ + /^gpt-6-luna-pro(?:$|[-_.:])/, /^gpt-5\.5(?:$|-\d{4}-\d{2}-\d{2}$)/, /^gpt-5\.4(?:$|-\d{4}-\d{2}-\d{2}$|-(?:mini|nano)(?:$|-\d{4}-\d{2}-\d{2}$))/, /^gpt-5\.(?:1|2)(?:$|-\d{4}-\d{2}-\d{2}$)/, diff --git a/src/firefox/src/ui/settings.js b/src/firefox/src/ui/settings.js index ba2c78714..9499132f9 100644 --- a/src/firefox/src/ui/settings.js +++ b/src/firefox/src/ui/settings.js @@ -2626,6 +2626,7 @@ function renderProviders() { { key: 'apiKey', labelKey: 'st.provider.field.api_key', type: 'password', placeholder: 'sk-...' }, { key: 'model', labelKey: 'st.provider.field.model', type: 'text', placeholder: 'gpt-5.6-terra', suggestions: [ + 'gpt-6-luna-pro', 'gpt-5.6-terra', 'gpt-5.6-sol', 'gpt-5.6-luna', diff --git a/test/run.js b/test/run.js index 14cf64a86..28ae9d6ff 100644 --- a/test/run.js +++ b/test/run.js @@ -67176,7 +67176,7 @@ test('inferContextWindow: model-aware cloud/router defaults and local 16k fallba for (const providerName of ['lmstudio', 'jan', 'vllm', 'sglang', 'localai', 'gpt4all', 'local-openai-proxy']) { assert.equal(infer({ category: 'local', providerName, model: 'qwen3.7-plus' }), 16384); } - for (const model of ['gpt-5.6', 'gpt-5.6-sol', 'gpt-5.6-terra', 'gpt-5.6-luna']) { + for (const model of ['gpt-6-luna-pro', 'gpt-5.6', 'gpt-5.6-sol', 'gpt-5.6-terra', 'gpt-5.6-luna']) { assert.equal(infer({ category: 'cloud', providerName: 'openai', model }), 1050000); } assert.equal(infer({ category: 'cloud', providerName: 'openai', model: 'gpt-5.5-pro' }), 1050000); @@ -71308,8 +71308,9 @@ test('built-in catalog defaults opt into vision when the model name is multimoda } }); -test('OpenAI settings list only the GPT-5.6 family and current dated models', () => { +test('OpenAI settings list GPT-6 Luna Pro, the GPT-5.6 family, and current dated models', () => { const expectedModels = [ + 'gpt-6-luna-pro', 'gpt-5.6-terra', 'gpt-5.6-sol', 'gpt-5.6-luna', @@ -71750,6 +71751,7 @@ test('Responses reasoning effort is normalized for GPT-5 Pro model constraints', test('official OpenAI Ask streaming follows the documented model capability', () => { const supportedModels = [ + 'gpt-6-luna-pro', 'gpt-5.6-terra', 'gpt-5.5', 'gpt-5.5-2026-04-23', @@ -73843,7 +73845,7 @@ test('OpenAI-compatible local providers always use legacy request token fields', } }); -test('router-prefixed OpenAI reasoning ids use the advertised Chat Completions contract', () => { +test('OpenAI reasoning and GPT-6 ids use the advertised Chat Completions contract', () => { const messages = [{ role: 'user', content: 'hello' }]; const newContractModels = ['openai/gpt-5.6-terra', 'openai/gpt-5.6-terra:batch', 'openai/gpt-5.6-terra:image']; const legacyContractModels = [ @@ -73869,6 +73871,25 @@ test('router-prefixed OpenAI reasoning ids use the advertised Chat Completions c for (const model of legacyContractModels) { assert.equal(compatibility.isNewOpenAIContractConfig({ providerName: 'openrouter', model }), false, `${model} should keep the legacy contract`); } + assert.equal( + compatibility.requiresOpenAIDefaultTemperature({ providerName: 'openrouter', model: 'openai/gpt-6-luna-pro' }), + true, + 'OpenRouter GPT-6 Luna Pro should omit temperature', + ); + assert.equal( + compatibility.requiresOpenAIDefaultTemperature({ + providerName: 'openai', + baseUrl: 'https://api.openai.com/v1', + model: 'gpt-6-luna-pro', + }), + true, + 'official GPT-6 Luna Pro should omit temperature', + ); + assert.equal( + compatibility.requiresOpenAIDefaultTemperature({ providerName: 'custom-proxy', model: 'openai/gpt-6-luna-pro' }), + false, + 'a custom proxy must not inherit GPT-6 Luna Pro temperature behavior', + ); } for (const Provider of [OpenAIProviderCh, OpenAIProviderFx]) { @@ -73898,6 +73919,28 @@ test('router-prefixed OpenAI reasoning ids use the advertised Chat Completions c assert.equal(body.temperature, 0.7, `${model} should keep the default temperature`); } + for (const config of [ + { + label: 'OpenRouter GPT-6 Luna Pro', + providerName: 'openrouter', + baseUrl: 'https://openrouter.ai/api/v1', + model: 'openai/gpt-6-luna-pro', + }, + { + label: 'official GPT-6 Luna Pro', + providerName: 'openai', + baseUrl: 'https://api.openai.com/v1', + model: 'gpt-6-luna-pro', + }, + ]) { + const provider = new Provider(config); + assert.equal(provider._isNewOpenAIContract(), false, `${config.label} should keep the Chat Completions token contract`); + const body = provider._buildChatCompletionsBody(messages, { maxTokens: 123, temperature: 0.2 }, false); + assert.equal(body.max_tokens, 123, `${config.label} should use max_tokens`); + assert.equal(body.max_completion_tokens, undefined, `${config.label} must not send max_completion_tokens`); + assert.equal(body.temperature, undefined, `${config.label} must omit temperature`); + } + // gpt-4.1 accepts both parameter sets; it must stay legacy so explicit // temperatures (deterministic planner/compaction paths) are not dropped. const gpt41 = new Provider({ From 0dcf807d7d2ddac3f0c9b625f6bc74f06c8ba9c0 Mon Sep 17 00:00:00 2001 From: Emre Sokullu Date: Tue, 22 Sep 2026 16:33:26 -0700 Subject: [PATCH 2/2] Mark GPT-6 Luna Pro vision-capable --- src/chrome/src/providers/openai.js | 2 +- src/firefox/src/providers/openai.js | 2 +- test/run.js | 16 ++++++++++++++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/chrome/src/providers/openai.js b/src/chrome/src/providers/openai.js index cfec22950..2ba5beb5a 100644 --- a/src/chrome/src/providers/openai.js +++ b/src/chrome/src/providers/openai.js @@ -150,7 +150,7 @@ export class OpenAICompatibleProvider extends BaseLLMProvider { * `supportsVision` getter above. */ _modelNameSniffedVision(model) { - return /gpt-4o|gpt-4\.1|gpt-4-turbo|gpt-5|claude|gemini|grok|minimax-m3|kimi-k(?:-?3|2\.[5-9])|llava|qwen.*vl|qwen2.*vl|qwen3.*vl|qwen3\.[5-9]|qwen3p8-27b|pixtral|llama.*vision|gemma.*vision|gemma-?[34]|step-3/.test(String(model || '')); + return /gpt-4o|gpt-4\.1|gpt-4-turbo|gpt-5|gpt-6-luna-pro(?:$|[-_.:/])|claude|gemini|grok|minimax-m3|kimi-k(?:-?3|2\.[5-9])|llava|qwen.*vl|qwen2.*vl|qwen3.*vl|qwen3\.[5-9]|qwen3p8-27b|pixtral|llama.*vision|gemma.*vision|gemma-?[34]|step-3/.test(String(model || '')); } get useCompactPrompt() { diff --git a/src/firefox/src/providers/openai.js b/src/firefox/src/providers/openai.js index c346cf5bf..af3c740ea 100644 --- a/src/firefox/src/providers/openai.js +++ b/src/firefox/src/providers/openai.js @@ -151,7 +151,7 @@ export class OpenAICompatibleProvider extends BaseLLMProvider { * `supportsVision` getter above. */ _modelNameSniffedVision(model) { - return /gpt-4o|gpt-4\.1|gpt-4-turbo|gpt-5|claude|gemini|grok|minimax-m3|kimi-k(?:-?3|2\.[5-9])|llava|qwen.*vl|qwen2.*vl|qwen3.*vl|qwen3\.[5-9]|qwen3p8-27b|pixtral|llama.*vision|gemma.*vision|gemma-?[34]|step-3/.test(String(model || '')); + return /gpt-4o|gpt-4\.1|gpt-4-turbo|gpt-5|gpt-6-luna-pro(?:$|[-_.:/])|claude|gemini|grok|minimax-m3|kimi-k(?:-?3|2\.[5-9])|llava|qwen.*vl|qwen2.*vl|qwen3.*vl|qwen3\.[5-9]|qwen3p8-27b|pixtral|llama.*vision|gemma.*vision|gemma-?[34]|step-3/.test(String(model || '')); } get useCompactPrompt() { diff --git a/test/run.js b/test/run.js index 28ae9d6ff..bafa50ab3 100644 --- a/test/run.js +++ b/test/run.js @@ -71308,6 +71308,22 @@ test('built-in catalog defaults opt into vision when the model name is multimoda } }); +test('GPT-6 Luna Pro vision capability is mirrored for direct and routed OpenAI models', () => { + for (const Provider of [OpenAIProviderCh, OpenAIProviderFx]) { + for (const config of [ + { providerName: 'openai', baseUrl: 'https://api.openai.com/v1', model: 'gpt-6-luna-pro' }, + { providerName: 'openrouter', baseUrl: 'https://openrouter.ai/api/v1', model: 'openai/gpt-6-luna-pro' }, + ]) { + assert.equal(new Provider(config).supportsVision, true, `${config.model} should receive screenshots`); + } + assert.equal( + new Provider({ providerName: 'openai', baseUrl: 'https://api.openai.com/v1', model: 'gpt-6-luna' }).supportsVision, + false, + 'unlisted GPT-6 variants must not inherit Luna Pro vision capability', + ); + } +}); + test('OpenAI settings list GPT-6 Luna Pro, the GPT-5.6 family, and current dated models', () => { const expectedModels = [ 'gpt-6-luna-pro',