From f61529bfbcfda9d0f88fcb5815aafb72bf84d864 Mon Sep 17 00:00:00 2001
From: Peter Dave Hello <3691490+PeterDaveHello@users.noreply.github.com>
Date: Sat, 15 Aug 2026 17:22:47 +0800
Subject: [PATCH] Make temperature overrides opt-in
---
src/_locales/de/main.json | 3 +
src/_locales/en/main.json | 3 +
src/_locales/es/main.json | 3 +
src/_locales/fr/main.json | 3 +
src/_locales/in/main.json | 3 +
src/_locales/it/main.json | 3 +
src/_locales/ja/main.json | 3 +
src/_locales/ko/main.json | 3 +
src/_locales/pt/main.json | 3 +
src/_locales/ru/main.json | 3 +
src/_locales/tr/main.json | 3 +
src/_locales/zh-hans/main.json | 3 +
src/_locales/zh-hant/main.json | 3 +
src/config/index.mjs | 1 +
src/popup/sections/AdvancedPart.jsx | 49 +++++--
src/services/apis/azure-openai-api.mjs | 10 +-
src/services/apis/claude-api.mjs | 14 +-
src/services/apis/openai-api.mjs | 1 +
src/services/apis/openai-compatible-core.mjs | 10 +-
src/services/apis/temperature-params.mjs | 55 ++++++++
tests/unit/config/user-config.test.mjs | 21 +++
.../services/apis/azure-openai-api.test.mjs | 31 +++++
.../apis/azure-openai-temperature.test.mjs | 40 ++++++
tests/unit/services/apis/claude-api.test.mjs | 34 +++++
tests/unit/services/apis/custom-api.test.mjs | 1 +
.../services/apis/openai-api-compat.test.mjs | 38 ++++++
.../openai-compatible-temperature.test.mjs | 122 ++++++++++++++++++
.../services/apis/temperature-params.test.mjs | 99 ++++++++++++++
28 files changed, 537 insertions(+), 28 deletions(-)
create mode 100644 src/services/apis/temperature-params.mjs
create mode 100644 tests/unit/services/apis/azure-openai-temperature.test.mjs
create mode 100644 tests/unit/services/apis/openai-compatible-temperature.test.mjs
create mode 100644 tests/unit/services/apis/temperature-params.test.mjs
diff --git a/src/_locales/de/main.json b/src/_locales/de/main.json
index 350a5a10d..8b0d204bc 100644
--- a/src/_locales/de/main.json
+++ b/src/_locales/de/main.json
@@ -114,6 +114,9 @@
"Pin Tab": "Tab anheften",
"Modules": "Module",
"API Params": "API-Parameter",
+ "Override provider temperature": "Anbieter-Temperatur überschreiben",
+ "The temperature parameter is not sent. The provider or model default is used.": "Der Temperaturparameter wird nicht gesendet. Es wird der Standardwert des Anbieters oder Modells verwendet.",
+ "The current model does not accept a custom temperature. The parameter will not be sent.": "Das aktuelle Modell akzeptiert keine benutzerdefinierte Temperatur. Der Parameter wird nicht gesendet.",
"API Url": "API-URL",
"Provider": "Anbieter",
"Others": "Andere",
diff --git a/src/_locales/en/main.json b/src/_locales/en/main.json
index 10f8caeeb..e558fc898 100644
--- a/src/_locales/en/main.json
+++ b/src/_locales/en/main.json
@@ -121,6 +121,9 @@
"Pin Tab": "Pin Tab",
"Modules": "Modules",
"API Params": "API Params",
+ "Override provider temperature": "Override provider temperature",
+ "The temperature parameter is not sent. The provider or model default is used.": "The temperature parameter is not sent. The provider or model default is used.",
+ "The current model does not accept a custom temperature. The parameter will not be sent.": "The current model does not accept a custom temperature. The parameter will not be sent.",
"API Url": "API Url",
"Provider": "Provider",
"Others": "Others",
diff --git a/src/_locales/es/main.json b/src/_locales/es/main.json
index 14953fb28..c9f5b5a11 100644
--- a/src/_locales/es/main.json
+++ b/src/_locales/es/main.json
@@ -112,6 +112,9 @@
"Pin Tab": "Fijar pestaña",
"Modules": "Módulos",
"API Params": "Parámetros de la API",
+ "Override provider temperature": "Sobrescribir la temperatura del proveedor",
+ "The temperature parameter is not sent. The provider or model default is used.": "El parámetro de temperatura no se envía. Se utiliza el valor predeterminado del proveedor o del modelo.",
+ "The current model does not accept a custom temperature. The parameter will not be sent.": "El modelo actual no acepta una temperatura personalizada. El parámetro no se enviará.",
"API Url": "URL de la API",
"Provider": "Proveedor",
"Others": "Otros",
diff --git a/src/_locales/fr/main.json b/src/_locales/fr/main.json
index 3b0546ca1..1fa86e8a3 100644
--- a/src/_locales/fr/main.json
+++ b/src/_locales/fr/main.json
@@ -112,6 +112,9 @@
"Pin Tab": "Épingler l'onglet",
"Modules": "Modules",
"API Params": "Paramètres de l'API",
+ "Override provider temperature": "Remplacer la température du fournisseur",
+ "The temperature parameter is not sent. The provider or model default is used.": "Le paramètre de température n’est pas envoyé. La valeur par défaut du fournisseur ou du modèle est utilisée.",
+ "The current model does not accept a custom temperature. The parameter will not be sent.": "Le modèle actuel n’accepte pas de température personnalisée. Le paramètre ne sera pas envoyé.",
"API Url": "URL de l'API",
"Provider": "Fournisseur",
"Others": "Autres",
diff --git a/src/_locales/in/main.json b/src/_locales/in/main.json
index f38e645fc..5267098d2 100644
--- a/src/_locales/in/main.json
+++ b/src/_locales/in/main.json
@@ -112,6 +112,9 @@
"Pin Tab": "Sematkan Tab",
"Modules": "Modul",
"API Params": "Parameter API",
+ "Override provider temperature": "Timpa suhu penyedia",
+ "The temperature parameter is not sent. The provider or model default is used.": "Parameter suhu tidak dikirim. Nilai bawaan penyedia atau model akan digunakan.",
+ "The current model does not accept a custom temperature. The parameter will not be sent.": "Model saat ini tidak menerima suhu khusus. Parameter tidak akan dikirim.",
"API Url": "URL API",
"Provider": "Penyedia",
"Others": "Lainnya",
diff --git a/src/_locales/it/main.json b/src/_locales/it/main.json
index 8149c7b5c..4a592ff01 100644
--- a/src/_locales/it/main.json
+++ b/src/_locales/it/main.json
@@ -112,6 +112,9 @@
"Pin Tab": "Fissa scheda",
"Modules": "Moduli",
"API Params": "Parametri API",
+ "Override provider temperature": "Sovrascrivi la temperatura del provider",
+ "The temperature parameter is not sent. The provider or model default is used.": "Il parametro della temperatura non viene inviato. Viene usato il valore predefinito del provider o del modello.",
+ "The current model does not accept a custom temperature. The parameter will not be sent.": "Il modello attuale non accetta una temperatura personalizzata. Il parametro non verrà inviato.",
"API Url": "URL API",
"Provider": "Provider",
"Others": "Altri",
diff --git a/src/_locales/ja/main.json b/src/_locales/ja/main.json
index b95cf7eab..91c960949 100644
--- a/src/_locales/ja/main.json
+++ b/src/_locales/ja/main.json
@@ -112,6 +112,9 @@
"Pin Tab": "タブをピン留め",
"Modules": "モジュール",
"API Params": "APIパラメータ",
+ "Override provider temperature": "プロバイダーの温度を上書き",
+ "The temperature parameter is not sent. The provider or model default is used.": "温度パラメーターは送信されません。プロバイダーまたはモデルの既定値が使用されます。",
+ "The current model does not accept a custom temperature. The parameter will not be sent.": "現在のモデルはカスタム温度を受け付けません。このパラメーターは送信されません。",
"API Url": "API URL",
"Provider": "プロバイダー",
"Others": "その他",
diff --git a/src/_locales/ko/main.json b/src/_locales/ko/main.json
index 7ab91041d..252ed15f8 100644
--- a/src/_locales/ko/main.json
+++ b/src/_locales/ko/main.json
@@ -112,6 +112,9 @@
"Pin Tab": "탭 고정",
"Modules": "모듈",
"API Params": "API 매개변수",
+ "Override provider temperature": "공급자 온도 재정의",
+ "The temperature parameter is not sent. The provider or model default is used.": "온도 매개변수는 전송되지 않습니다. 공급자 또는 모델의 기본값이 사용됩니다.",
+ "The current model does not accept a custom temperature. The parameter will not be sent.": "현재 모델은 사용자 지정 온도를 허용하지 않습니다. 이 매개변수는 전송되지 않습니다.",
"API Url": "API 주소",
"Provider": "공급자",
"Others": "기타",
diff --git a/src/_locales/pt/main.json b/src/_locales/pt/main.json
index 36045eb86..b3379c159 100644
--- a/src/_locales/pt/main.json
+++ b/src/_locales/pt/main.json
@@ -112,6 +112,9 @@
"Pin Tab": "Fixar Tab",
"Modules": "Módulos",
"API Params": "Parâmetros da API",
+ "Override provider temperature": "Substituir a temperatura do provedor",
+ "The temperature parameter is not sent. The provider or model default is used.": "O parâmetro de temperatura não é enviado. O valor padrão do provedor ou do modelo é utilizado.",
+ "The current model does not accept a custom temperature. The parameter will not be sent.": "O modelo atual não aceita uma temperatura personalizada. O parâmetro não será enviado.",
"API Url": "URL da API",
"Provider": "Provedor",
"Others": "Outros",
diff --git a/src/_locales/ru/main.json b/src/_locales/ru/main.json
index aedd70ce9..d857f3f62 100644
--- a/src/_locales/ru/main.json
+++ b/src/_locales/ru/main.json
@@ -120,6 +120,9 @@
"Pin Tab": "Закрепить вкладку",
"Modules": "Модули",
"API Params": "Параметры API",
+ "Override provider temperature": "Переопределить температуру провайдера",
+ "The temperature parameter is not sent. The provider or model default is used.": "Параметр температуры не отправляется. Используется значение по умолчанию провайдера или модели.",
+ "The current model does not accept a custom temperature. The parameter will not be sent.": "Текущая модель не принимает пользовательскую температуру. Параметр не будет отправлен.",
"API Url": "URL API",
"Provider": "Провайдер",
"Others": "Другие",
diff --git a/src/_locales/tr/main.json b/src/_locales/tr/main.json
index e2a6bb556..1b2391cf4 100644
--- a/src/_locales/tr/main.json
+++ b/src/_locales/tr/main.json
@@ -112,6 +112,9 @@
"Pin Tab": "Sekmeyi Sabitle",
"Modules": "Modüller",
"API Params": "API Parametreleri",
+ "Override provider temperature": "Sağlayıcı sıcaklığını geçersiz kıl",
+ "The temperature parameter is not sent. The provider or model default is used.": "Sıcaklık parametresi gönderilmez. Sağlayıcının veya modelin varsayılan değeri kullanılır.",
+ "The current model does not accept a custom temperature. The parameter will not be sent.": "Geçerli model özel bir sıcaklık kabul etmiyor. Parametre gönderilmeyecek.",
"API Url": "API Url'si",
"Provider": "Sağlayıcı",
"Others": "Diğerleri",
diff --git a/src/_locales/zh-hans/main.json b/src/_locales/zh-hans/main.json
index d15d0c393..cfad6d2d1 100644
--- a/src/_locales/zh-hans/main.json
+++ b/src/_locales/zh-hans/main.json
@@ -115,6 +115,9 @@
"Pin Tab": "固定页面",
"Modules": "模块",
"API Params": "API参数",
+ "Override provider temperature": "覆盖提供商的温度参数",
+ "The temperature parameter is not sent. The provider or model default is used.": "不会发送温度参数,将使用提供商或模型的默认值。",
+ "The current model does not accept a custom temperature. The parameter will not be sent.": "当前模型不接受自定义温度参数,因此不会发送该参数。",
"API Url": "API地址",
"Provider": "提供商",
"Others": "其他",
diff --git a/src/_locales/zh-hant/main.json b/src/_locales/zh-hant/main.json
index bc200d682..3dd20a229 100644
--- a/src/_locales/zh-hant/main.json
+++ b/src/_locales/zh-hant/main.json
@@ -115,6 +115,9 @@
"Pin Tab": "固定頁面",
"Modules": "模組",
"API Params": "API 參數",
+ "Override provider temperature": "覆寫供應商的溫度參數",
+ "The temperature parameter is not sent. The provider or model default is used.": "不會傳送溫度參數,將使用供應商或模型的預設值。",
+ "The current model does not accept a custom temperature. The parameter will not be sent.": "目前的模型不接受自訂溫度參數,因此不會傳送這個參數。",
"API Url": "API 網址",
"Provider": "供應商",
"Others": "其他",
diff --git a/src/config/index.mjs b/src/config/index.mjs
index 3d37b3452..0e081e4dc 100644
--- a/src/config/index.mjs
+++ b/src/config/index.mjs
@@ -800,6 +800,7 @@ export const defaultConfig = {
maxResponseTokenLength: 2000,
maxConversationContextLength: 9,
+ temperatureOverrideEnabled: false,
temperature: 1,
customChatGptWebApiUrl: 'https://chatgpt.com',
customChatGptWebApiPath: '/backend-api/conversation',
diff --git a/src/popup/sections/AdvancedPart.jsx b/src/popup/sections/AdvancedPart.jsx
index 74989dc1f..017b9191f 100644
--- a/src/popup/sections/AdvancedPart.jsx
+++ b/src/popup/sections/AdvancedPart.jsx
@@ -1,5 +1,8 @@
import { useTranslation } from 'react-i18next'
import { parseFloatWithClamp, parseIntWithClamp } from '../../utils/index.mjs'
+import { getModelValue } from '../../utils/model-name-convert.mjs'
+import { isUsingAzureOpenAiApiModel } from '../../config/index.mjs'
+import { canApplyTemperatureOverride } from '../../services/apis/temperature-params.mjs'
import PropTypes from 'prop-types'
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs'
import Browser from 'webextension-polyfill'
@@ -11,6 +14,14 @@ ApiParams.propTypes = {
function ApiParams({ config, updateConfig }) {
const { t } = useTranslation()
+ const temperatureOverrideEnabled = config.temperatureOverrideEnabled === true
+ // Azure deployment names are opaque aliases, not canonical model identifiers.
+ const selectedModel = isUsingAzureOpenAiApiModel(config)
+ ? null
+ : config.modelName === 'customModel' && !config.apiMode
+ ? config.customModelName
+ : getModelValue(config)
+ const temperatureOverrideAvailable = canApplyTemperatureOverride(selectedModel)
return (
<>
@@ -43,19 +54,41 @@ function ApiParams({ config, updateConfig }) {
/>
+ {!temperatureOverrideEnabled ? (
+
+ {t('The temperature parameter is not sent. The provider or model default is used.')}
+
+ ) : !temperatureOverrideAvailable ? (
+
+ {t(
+ 'The current model does not accept a custom temperature. The parameter will not be sent.',
+ )}
+
+ ) : (
+
+ )}
>
)
}
diff --git a/src/services/apis/azure-openai-api.mjs b/src/services/apis/azure-openai-api.mjs
index a8e8eaf80..f5e1b4b6d 100644
--- a/src/services/apis/azure-openai-api.mjs
+++ b/src/services/apis/azure-openai-api.mjs
@@ -4,6 +4,7 @@ import { getConversationPairs } from '../../utils/get-conversation-pairs.mjs'
import { fetchSSE } from '../../utils/fetch-sse.mjs'
import { isEmpty } from 'lodash-es'
import { getModelValue } from '../../utils/model-name-convert.mjs'
+import { getTemperatureParams } from './temperature-params.mjs'
/**
* @param {Runtime.Port} port
@@ -13,8 +14,8 @@ import { getModelValue } from '../../utils/model-name-convert.mjs'
export async function generateAnswersWithAzureOpenaiApi(port, question, session) {
const { controller, messageListener, disconnectListener } = setAbortController(port)
const config = await getUserConfig()
- let model = getModelValue(session)
- if (!model) model = config.azureDeploymentName
+ let deploymentName = getModelValue(session)
+ if (!deploymentName) deploymentName = config.azureDeploymentName
const prompt = getConversationPairs(
session.conversationRecords.slice(-config.maxConversationContextLength),
@@ -27,7 +28,7 @@ export async function generateAnswersWithAzureOpenaiApi(port, question, session)
`${config.azureEndpoint.replace(
/\/$/,
'',
- )}/openai/deployments/${model}/chat/completions?api-version=2024-02-01`,
+ )}/openai/deployments/${deploymentName}/chat/completions?api-version=2024-02-01`,
{
method: 'POST',
signal: controller.signal,
@@ -39,7 +40,8 @@ export async function generateAnswersWithAzureOpenaiApi(port, question, session)
messages: prompt,
stream: true,
max_tokens: config.maxResponseTokenLength,
- temperature: config.temperature,
+ // Azure deployment names are opaque aliases, not canonical model identifiers.
+ ...getTemperatureParams(config),
}),
onMessage(message) {
console.debug('sse message', message)
diff --git a/src/services/apis/claude-api.mjs b/src/services/apis/claude-api.mjs
index c9b4f9caf..d589fc894 100644
--- a/src/services/apis/claude-api.mjs
+++ b/src/services/apis/claude-api.mjs
@@ -4,15 +4,7 @@ import { FETCH_RESPONSE_STREAM_FAILED, fetchSSE } from '../../utils/fetch-sse.mj
import { isEmpty } from 'lodash-es'
import { getConversationPairs } from '../../utils/get-conversation-pairs.mjs'
import { getModelValue } from '../../utils/model-name-convert.mjs'
-
-function shouldOmitTemperature(model) {
- return (
- model === 'claude-opus-4-7' ||
- model === 'claude-opus-4-8' ||
- model === 'claude-sonnet-5' ||
- model === 'claude-opus-5'
- )
-}
+import { getTemperatureParams } from './temperature-params.mjs'
function shouldDisableDefaultThinking(model) {
return model === 'claude-sonnet-5'
@@ -40,13 +32,11 @@ export async function generateAnswersWithClaudeApi(port, question, session) {
messages: prompt,
stream: true,
max_tokens: config.maxResponseTokenLength,
+ ...getTemperatureParams(config, model),
}
if (shouldDisableDefaultThinking(model)) {
body.thinking = { type: 'disabled' }
}
- if (!shouldOmitTemperature(model)) {
- body.temperature = config.temperature
- }
let answer = ''
let stopReason = ''
diff --git a/src/services/apis/openai-api.mjs b/src/services/apis/openai-api.mjs
index 9023db2ba..2a3f04b4c 100644
--- a/src/services/apis/openai-api.mjs
+++ b/src/services/apis/openai-api.mjs
@@ -33,6 +33,7 @@ function resolveModelName(session, config) {
const OPENAI_COMPATIBLE_RUNTIME_CONFIG_KEYS = [
'maxConversationContextLength',
'maxResponseTokenLength',
+ 'temperatureOverrideEnabled',
'temperature',
]
diff --git a/src/services/apis/openai-compatible-core.mjs b/src/services/apis/openai-compatible-core.mjs
index f65142667..02dfe4a81 100644
--- a/src/services/apis/openai-compatible-core.mjs
+++ b/src/services/apis/openai-compatible-core.mjs
@@ -3,6 +3,7 @@ import { getConversationPairs } from '../../utils/get-conversation-pairs.mjs'
import { isEmpty } from 'lodash-es'
import { getCompletionPromptBase, pushRecord, setAbortController } from './shared.mjs'
import { getChatCompletionsTokenParams } from './openai-token-params.mjs'
+import { getTemperatureParams } from './temperature-params.mjs'
function buildHeaders(apiKey, extraHeaders = {}) {
const headers = {
@@ -73,6 +74,8 @@ export async function generateAnswersWithOpenAICompatible({
? session.conversationRecords
: []
session.conversationRecords = conversationRecords
+ const safeExtraBody = { ...extraBody }
+ delete safeExtraBody.temperature
if (endpointType === 'completion') {
const prompt =
(await getCompletionPromptBase()) +
@@ -83,9 +86,9 @@ export async function generateAnswersWithOpenAICompatible({
model,
stream: true,
max_tokens: config.maxResponseTokenLength,
- temperature: config.temperature,
+ ...getTemperatureParams(config, model),
stop: '\nHuman',
- ...extraBody,
+ ...safeExtraBody,
}
} else {
const messages = getConversationPairs(
@@ -100,14 +103,13 @@ export async function generateAnswersWithOpenAICompatible({
)
const conflictingTokenParamKey =
'max_completion_tokens' in tokenParams ? 'max_tokens' : 'max_completion_tokens'
- const safeExtraBody = { ...extraBody }
delete safeExtraBody[conflictingTokenParamKey]
requestBody = {
messages,
model,
stream: true,
...tokenParams,
- temperature: config.temperature,
+ ...getTemperatureParams(config, model),
...safeExtraBody,
}
}
diff --git a/src/services/apis/temperature-params.mjs b/src/services/apis/temperature-params.mjs
new file mode 100644
index 000000000..cb065c115
--- /dev/null
+++ b/src/services/apis/temperature-params.mjs
@@ -0,0 +1,55 @@
+const MODELS_WITHOUT_CUSTOM_TEMPERATURE = new Set([
+ 'claude-opus-4-7',
+ 'claude-opus-4-8',
+ 'claude-sonnet-5',
+ 'claude-opus-5',
+])
+
+function normalizeModelId(model) {
+ return String(model || '')
+ .trim()
+ .toLowerCase()
+ .replace(/^(?:anthropic|google)\//, '')
+ .replace(/\./g, '-')
+}
+
+function matchesModelFamily(model, baseModel) {
+ return (
+ model === baseModel || model.startsWith(`${baseModel}-`) || model.startsWith(`${baseModel}:`)
+ )
+}
+
+function isKnownModelWithoutCustomTemperature(model) {
+ return [...MODELS_WITHOUT_CUSTOM_TEMPERATURE].some((baseModel) =>
+ matchesModelFamily(model, baseModel),
+ )
+}
+
+function isGeminiWithoutCustomTemperature(model) {
+ const match = /^gemini-(\d+)(?:-(\d+))?(?=$|[-:])/.exec(model)
+ if (!match) return false
+
+ const major = Number(match[1])
+ const minor = Number(match[2] || 0)
+ if (major > 3) return true
+ if (major !== 3) return false
+
+ const isGemini35FlashLite =
+ minor === 5 && matchesModelFamily(model, 'gemini-3-5-flash-lite')
+ return minor >= 6 || isGemini35FlashLite
+}
+
+export function canApplyTemperatureOverride(model) {
+ const normalizedModel = normalizeModelId(model)
+ return (
+ !isKnownModelWithoutCustomTemperature(normalizedModel) &&
+ !isGeminiWithoutCustomTemperature(normalizedModel)
+ )
+}
+
+export function getTemperatureParams(config, model) {
+ if (config?.temperatureOverrideEnabled !== true) return {}
+ if (!Number.isFinite(config?.temperature)) return {}
+ if (!canApplyTemperatureOverride(model)) return {}
+ return { temperature: config.temperature }
+}
diff --git a/tests/unit/config/user-config.test.mjs b/tests/unit/config/user-config.test.mjs
index 4e9709152..14dbe1582 100644
--- a/tests/unit/config/user-config.test.mjs
+++ b/tests/unit/config/user-config.test.mjs
@@ -263,3 +263,24 @@ test('clearOldAccessToken keeps token when exactly 30 days old', async (t) => {
assert.equal(storage.accessToken, 'boundary-token')
assert.equal(storage.tokenSavedOn, savedOn)
})
+
+test('getUserConfig keeps a stored temperature but leaves overrides disabled by default', async () => {
+ globalThis.__TEST_BROWSER_SHIM__.replaceStorage({ temperature: 0.7 })
+
+ const config = await getUserConfig()
+
+ assert.equal(config.temperature, 0.7)
+ assert.equal(config.temperatureOverrideEnabled, false)
+})
+
+test('getUserConfig preserves an explicitly enabled temperature override', async () => {
+ globalThis.__TEST_BROWSER_SHIM__.replaceStorage({
+ temperatureOverrideEnabled: true,
+ temperature: 0.7,
+ })
+
+ const config = await getUserConfig()
+
+ assert.equal(config.temperature, 0.7)
+ assert.equal(config.temperatureOverrideEnabled, true)
+})
diff --git a/tests/unit/services/apis/azure-openai-api.test.mjs b/tests/unit/services/apis/azure-openai-api.test.mjs
index 66b5aabe3..1576fc8a0 100644
--- a/tests/unit/services/apis/azure-openai-api.test.mjs
+++ b/tests/unit/services/apis/azure-openai-api.test.mjs
@@ -125,6 +125,7 @@ test('azure-openai: sends max_tokens and temperature in body', async (t) => {
azureDeploymentName: 'gpt-4o',
maxConversationContextLength: 3,
maxResponseTokenLength: 1024,
+ temperatureOverrideEnabled: true,
temperature: 0.9,
})
@@ -151,6 +152,36 @@ test('azure-openai: sends max_tokens and temperature in body', async (t) => {
assert.equal(body.stream, true)
})
+test('azure-openai: uses the provider temperature default', async (t) => {
+ t.mock.method(console, 'debug', () => {})
+ setStorage({
+ azureEndpoint: 'https://myinstance.openai.azure.com',
+ azureApiKey: 'az-key',
+ azureDeploymentName: 'gpt-4o',
+ maxConversationContextLength: 3,
+ maxResponseTokenLength: 1024,
+ })
+
+ const session = {
+ modelName: 'azureOpenAi',
+ conversationRecords: [],
+ isRetry: false,
+ }
+ const port = createFakePort()
+ let capturedInit
+ t.mock.method(globalThis, 'fetch', async (_input, init) => {
+ capturedInit = init
+ return createMockSseResponse([
+ 'data: {"choices":[{"delta":{"content":"OK"},"finish_reason":"stop"}]}\n\n',
+ ])
+ })
+
+ await generateAnswersWithAzureOpenaiApi(port, 'Q', session)
+
+ const body = JSON.parse(capturedInit.body)
+ assert.equal(Object.hasOwn(body, 'temperature'), false)
+})
+
test('azure-openai: aggregates SSE deltas and pushes record on finish', async (t) => {
t.mock.method(console, 'debug', () => {})
setStorage({
diff --git a/tests/unit/services/apis/azure-openai-temperature.test.mjs b/tests/unit/services/apis/azure-openai-temperature.test.mjs
new file mode 100644
index 000000000..00bf3e5ec
--- /dev/null
+++ b/tests/unit/services/apis/azure-openai-temperature.test.mjs
@@ -0,0 +1,40 @@
+import assert from 'node:assert/strict'
+import { beforeEach, test } from 'node:test'
+import { generateAnswersWithAzureOpenaiApi } from '../../../../src/services/apis/azure-openai-api.mjs'
+import { createFakePort } from '../../helpers/port.mjs'
+import { createMockSseResponse } from '../../helpers/sse-response.mjs'
+
+beforeEach(() => {
+ globalThis.__TEST_BROWSER_SHIM__.clearStorage()
+})
+
+test('Azure temperature override does not treat deployment aliases as canonical model IDs', async (t) => {
+ t.mock.method(console, 'debug', () => {})
+ globalThis.__TEST_BROWSER_SHIM__.replaceStorage({
+ azureEndpoint: 'https://myinstance.openai.azure.com',
+ azureApiKey: 'az-key',
+ azureDeploymentName: 'gemini-4-flash',
+ maxConversationContextLength: 3,
+ maxResponseTokenLength: 128,
+ temperatureOverrideEnabled: true,
+ temperature: 0.9,
+ })
+
+ const session = {
+ modelName: 'azureOpenAi',
+ conversationRecords: [],
+ isRetry: false,
+ }
+ let capturedInit
+ t.mock.method(globalThis, 'fetch', async (_input, init) => {
+ capturedInit = init
+ return createMockSseResponse([
+ 'data: {"choices":[{"delta":{"content":"OK"},"finish_reason":"stop"}]}\n\n',
+ ])
+ })
+
+ await generateAnswersWithAzureOpenaiApi(createFakePort(), 'Q', session)
+
+ const body = JSON.parse(capturedInit.body)
+ assert.equal(body.temperature, 0.9)
+})
diff --git a/tests/unit/services/apis/claude-api.test.mjs b/tests/unit/services/apis/claude-api.test.mjs
index 322fe19c4..9e51ccb0e 100644
--- a/tests/unit/services/apis/claude-api.test.mjs
+++ b/tests/unit/services/apis/claude-api.test.mjs
@@ -76,6 +76,7 @@ test('claude-api: sends model, max_tokens, temperature in body', async (t) => {
claudeApiKey: 'sk-ant-test',
maxConversationContextLength: 3,
maxResponseTokenLength: 1024,
+ temperatureOverrideEnabled: true,
temperature: 0.9,
})
@@ -105,6 +106,37 @@ test('claude-api: sends model, max_tokens, temperature in body', async (t) => {
assert.equal(body.stream, true)
})
+test('claude-api: uses the provider temperature default', async (t) => {
+ t.mock.method(console, 'debug', () => {})
+ setStorage({
+ customClaudeApiUrl: 'https://api.anthropic.com',
+ claudeApiKey: 'sk-ant-test',
+ maxConversationContextLength: 3,
+ maxResponseTokenLength: 1024,
+ })
+
+ const session = {
+ modelName: 'claudeSonnet46Api',
+ conversationRecords: [],
+ isRetry: false,
+ }
+ const port = createFakePort()
+ let capturedInit
+ t.mock.method(globalThis, 'fetch', async (_input, init) => {
+ capturedInit = init
+ return createMockSseResponse([
+ 'data: {"type":"content_block_delta","delta":{"type":"text_delta","text":"OK"}}\n\n',
+ 'data: {"type":"message_delta","delta":{"stop_reason":"end_turn"}}\n\n',
+ 'data: {"type":"message_stop"}\n\n',
+ ])
+ })
+
+ await generateAnswersWithClaudeApi(port, 'Q', session)
+
+ const body = JSON.parse(capturedInit.body)
+ assert.equal(Object.hasOwn(body, 'temperature'), false)
+})
+
test('claude-api: keeps temperature for Opus 4.6', async (t) => {
t.mock.method(console, 'debug', () => {})
setStorage({
@@ -112,6 +144,7 @@ test('claude-api: keeps temperature for Opus 4.6', async (t) => {
claudeApiKey: 'sk-ant-test',
maxConversationContextLength: 3,
maxResponseTokenLength: 1024,
+ temperatureOverrideEnabled: true,
temperature: 0.9,
})
@@ -156,6 +189,7 @@ test('claude-api: omits temperature for models that reject custom sampling', asy
claudeApiKey: 'sk-ant-test',
maxConversationContextLength: 3,
maxResponseTokenLength: 1024,
+ temperatureOverrideEnabled: true,
temperature: 0.9,
})
diff --git a/tests/unit/services/apis/custom-api.test.mjs b/tests/unit/services/apis/custom-api.test.mjs
index c5151ed0b..57a6f7fde 100644
--- a/tests/unit/services/apis/custom-api.test.mjs
+++ b/tests/unit/services/apis/custom-api.test.mjs
@@ -18,6 +18,7 @@ test('aggregates delta.content SSE chunks and finishes on finish_reason', async
setStorage({
maxConversationContextLength: 3,
maxResponseTokenLength: 256,
+ temperatureOverrideEnabled: true,
temperature: 0.5,
})
diff --git a/tests/unit/services/apis/openai-api-compat.test.mjs b/tests/unit/services/apis/openai-api-compat.test.mjs
index c11a3ebb4..26559c480 100644
--- a/tests/unit/services/apis/openai-api-compat.test.mjs
+++ b/tests/unit/services/apis/openai-api-compat.test.mjs
@@ -69,6 +69,7 @@ test('generateAnswersWithOpenAiApiCompat sends expected request and aggregates S
setStorage({
maxConversationContextLength: 3,
maxResponseTokenLength: 256,
+ temperatureOverrideEnabled: true,
temperature: 0.25,
})
@@ -128,6 +129,39 @@ test('generateAnswersWithOpenAiApiCompat sends expected request and aggregates S
assert.deepEqual(session.conversationRecords.at(-1), { question: 'CurrentQ', answer: 'Hello' })
})
+test('generateAnswersWithOpenAiApiCompat uses the provider temperature default', async (t) => {
+ t.mock.method(console, 'debug', () => {})
+ setStorage({
+ maxConversationContextLength: 3,
+ maxResponseTokenLength: 256,
+ })
+
+ const session = {
+ modelName: 'chatgptApi4oMini',
+ conversationRecords: [],
+ isRetry: false,
+ }
+ const port = createFakePort()
+ let capturedInit
+ t.mock.method(globalThis, 'fetch', async (_input, init) => {
+ capturedInit = init
+ return createMockSseResponse([
+ 'data: {"choices":[{"delta":{"content":"OK"},"finish_reason":"stop"}]}\n\n',
+ ])
+ })
+
+ await generateAnswersWithOpenAiApiCompat(
+ 'https://api.example.com/v1',
+ port,
+ 'CurrentQ',
+ session,
+ 'sk-test',
+ )
+
+ const body = JSON.parse(capturedInit.body)
+ assert.equal(Object.hasOwn(body, 'temperature'), false)
+})
+
test('generateAnswersWithOpenAiApiCompat emits fallback done message when stream ends without finish reason', async (t) => {
t.mock.method(console, 'debug', () => {})
setStorage({
@@ -818,13 +852,16 @@ test('generateAnswersWithOpenAICompatibleApi uses caller config snapshot for req
setStorage({
maxConversationContextLength: 1,
maxResponseTokenLength: 111,
+ temperatureOverrideEnabled: true,
temperature: 0.1,
})
const config = {
maxConversationContextLength: 2,
maxResponseTokenLength: 777,
+ temperatureOverrideEnabled: true,
temperature: 0.7,
+
customOpenAIProviders: [
{
id: 'snapshot-provider',
@@ -2184,6 +2221,7 @@ test('generateAnswersWithGptCompletionApi builds completion prompt and appends a
customOpenAiApiUrl: 'https://api.example.com',
maxConversationContextLength: 5,
maxResponseTokenLength: 300,
+ temperatureOverrideEnabled: true,
temperature: 0.5,
})
diff --git a/tests/unit/services/apis/openai-compatible-temperature.test.mjs b/tests/unit/services/apis/openai-compatible-temperature.test.mjs
new file mode 100644
index 000000000..d5d73c87f
--- /dev/null
+++ b/tests/unit/services/apis/openai-compatible-temperature.test.mjs
@@ -0,0 +1,122 @@
+import assert from 'node:assert/strict'
+import { test } from 'node:test'
+import { generateAnswersWithOpenAICompatible } from '../../../../src/services/apis/openai-compatible-core.mjs'
+import { createFakePort } from '../../helpers/port.mjs'
+import { createMockSseResponse } from '../../helpers/sse-response.mjs'
+
+function createSession() {
+ return {
+ conversationRecords: [],
+ isRetry: false,
+ }
+}
+
+function createConfig(overrides = {}) {
+ return {
+ maxConversationContextLength: 3,
+ maxResponseTokenLength: 128,
+ temperatureOverrideEnabled: false,
+ temperature: 0.7,
+ ...overrides,
+ }
+}
+
+test('OpenAI-compatible extra body cannot enable temperature while override is disabled', async (t) => {
+ t.mock.method(console, 'debug', () => {})
+ let capturedInit
+ t.mock.method(globalThis, 'fetch', async (_input, init) => {
+ capturedInit = init
+ return createMockSseResponse([
+ 'data: {"choices":[{"delta":{"content":"OK"},"finish_reason":"stop"}]}\n\n',
+ ])
+ })
+
+ await generateAnswersWithOpenAICompatible({
+ port: createFakePort(),
+ question: 'Q',
+ session: createSession(),
+ endpointType: 'chat',
+ requestUrl: 'https://api.example.com/v1/chat/completions',
+ model: 'gpt-4.1',
+ apiKey: 'test-key',
+ config: createConfig(),
+ extraBody: { temperature: 0.2 },
+ })
+
+ const body = JSON.parse(capturedInit.body)
+ assert.equal(Object.hasOwn(body, 'temperature'), false)
+})
+
+test('OpenAI-compatible configured override takes precedence over extra-body temperature', async (t) => {
+ t.mock.method(console, 'debug', () => {})
+ let capturedInit
+ t.mock.method(globalThis, 'fetch', async (_input, init) => {
+ capturedInit = init
+ return createMockSseResponse([
+ 'data: {"choices":[{"delta":{"content":"OK"},"finish_reason":"stop"}]}\n\n',
+ ])
+ })
+
+ await generateAnswersWithOpenAICompatible({
+ port: createFakePort(),
+ question: 'Q',
+ session: createSession(),
+ endpointType: 'chat',
+ requestUrl: 'https://api.example.com/v1/chat/completions',
+ model: 'gpt-4.1',
+ apiKey: 'test-key',
+ config: createConfig({ temperatureOverrideEnabled: true }),
+ extraBody: { temperature: 0.2 },
+ })
+
+ const body = JSON.parse(capturedInit.body)
+ assert.equal(body.temperature, 0.7)
+})
+
+test('OpenAI-compatible completion extra body cannot enable temperature while override is disabled', async (t) => {
+ t.mock.method(console, 'debug', () => {})
+ let capturedInit
+ t.mock.method(globalThis, 'fetch', async (_input, init) => {
+ capturedInit = init
+ return createMockSseResponse(['data: {"choices":[{"text":"OK","finish_reason":"stop"}]}\n\n'])
+ })
+
+ await generateAnswersWithOpenAICompatible({
+ port: createFakePort(),
+ question: 'Q',
+ session: createSession(),
+ endpointType: 'completion',
+ requestUrl: 'https://api.example.com/v1/completions',
+ model: 'gpt-3.5-turbo-instruct',
+ apiKey: 'test-key',
+ config: createConfig(),
+ extraBody: { temperature: 0.2 },
+ })
+
+ const body = JSON.parse(capturedInit.body)
+ assert.equal(Object.hasOwn(body, 'temperature'), false)
+})
+
+test('OpenAI-compatible completion override takes precedence over extra-body temperature', async (t) => {
+ t.mock.method(console, 'debug', () => {})
+ let capturedInit
+ t.mock.method(globalThis, 'fetch', async (_input, init) => {
+ capturedInit = init
+ return createMockSseResponse(['data: {"choices":[{"text":"OK","finish_reason":"stop"}]}\n\n'])
+ })
+
+ await generateAnswersWithOpenAICompatible({
+ port: createFakePort(),
+ question: 'Q',
+ session: createSession(),
+ endpointType: 'completion',
+ requestUrl: 'https://api.example.com/v1/completions',
+ model: 'gpt-3.5-turbo-instruct',
+ apiKey: 'test-key',
+ config: createConfig({ temperatureOverrideEnabled: true }),
+ extraBody: { temperature: 0.2 },
+ })
+
+ const body = JSON.parse(capturedInit.body)
+ assert.equal(body.temperature, 0.7)
+})
diff --git a/tests/unit/services/apis/temperature-params.test.mjs b/tests/unit/services/apis/temperature-params.test.mjs
new file mode 100644
index 000000000..9358611e8
--- /dev/null
+++ b/tests/unit/services/apis/temperature-params.test.mjs
@@ -0,0 +1,99 @@
+import assert from 'node:assert/strict'
+import { test } from 'node:test'
+import {
+ canApplyTemperatureOverride,
+ getTemperatureParams,
+} from '../../../../src/services/apis/temperature-params.mjs'
+
+test('temperature params require an explicit finite override', () => {
+ assert.deepEqual(getTemperatureParams({}, 'gpt-4.1'), {})
+ assert.deepEqual(
+ getTemperatureParams({ temperatureOverrideEnabled: false, temperature: 0.7 }, 'gpt-4.1'),
+ {},
+ )
+ assert.deepEqual(
+ getTemperatureParams({ temperatureOverrideEnabled: true, temperature: Number.NaN }, 'gpt-4.1'),
+ {},
+ )
+ assert.deepEqual(
+ getTemperatureParams({ temperatureOverrideEnabled: true, temperature: 0 }, 'gpt-4.1'),
+ { temperature: 0 },
+ )
+ assert.deepEqual(
+ getTemperatureParams({ temperatureOverrideEnabled: true, temperature: 0.7 }, 'gpt-4.1'),
+ { temperature: 0.7 },
+ )
+})
+
+test('temperature overrides omit known Anthropic models across provider ID formats', () => {
+ for (const model of [
+ 'claude-opus-4-7',
+ 'claude-opus-4-8',
+ 'anthropic/claude-opus-4.8',
+ 'anthropic/claude-opus-4-8',
+ 'anthropic/claude-opus-4.8:free',
+ 'claude-opus-4-8-20260801',
+ 'claude-sonnet-5',
+ 'claude-opus-5',
+ ]) {
+ assert.equal(canApplyTemperatureOverride(model), false, model)
+ assert.deepEqual(
+ getTemperatureParams({ temperatureOverrideEnabled: true, temperature: 0.7 }, model),
+ {},
+ model,
+ )
+ }
+ assert.equal(canApplyTemperatureOverride('claude-opus-4-6'), true)
+})
+
+test('temperature overrides omit Gemini models with deprecated sampling parameters', () => {
+ for (const model of [
+ 'gemini-3.5-flash-lite',
+ 'google/gemini-3.5-flash-lite',
+ 'google/gemini-3-5-flash-lite',
+ 'google/gemini-3.5-flash-lite:free',
+ 'gemini-3-5-flash-lite-preview',
+ 'gemini-3.6-flash',
+ 'google/gemini-3.6-flash',
+ 'google/gemini-3-6-flash',
+ 'gemini-3.7-flash',
+ 'gemini-4-flash',
+ 'gemini-4:free',
+ ]) {
+ assert.equal(canApplyTemperatureOverride(model), false, model)
+ assert.deepEqual(
+ getTemperatureParams({ temperatureOverrideEnabled: true, temperature: 0.7 }, model),
+ {},
+ model,
+ )
+ }
+})
+
+test('Gemini version checks do not classify arbitrary numeric model names', () => {
+ for (const model of ['gemini-4o', 'gemini-35b', 'gemini-3-6flash']) {
+ assert.equal(canApplyTemperatureOverride(model), true, model)
+ assert.deepEqual(
+ getTemperatureParams({ temperatureOverrideEnabled: true, temperature: 0.7 }, model),
+ { temperature: 0.7 },
+ model,
+ )
+ }
+})
+
+test('temperature overrides remain available for earlier Gemini models', () => {
+ for (const model of [
+ 'gemini-2.5-flash',
+ 'gemini-3-flash-preview',
+ 'gemini-3.1-pro-preview',
+ 'gemini-3.5-flash',
+ 'google/gemini-3.5-flash',
+ 'google/gemini-3-5-flash',
+ ]) {
+ assert.equal(canApplyTemperatureOverride(model), true, model)
+ assert.deepEqual(
+ getTemperatureParams({ temperatureOverrideEnabled: true, temperature: 0.7 }, model),
+ { temperature: 0.7 },
+ model,
+ )
+ }
+})