fix: omit temperature for gpt-6 under openai-responses

OpenAI Responses rejects temperature for gpt-6-* the same way it does
for gpt-5 and the o-series. Share one omitsTemperature() helper so
agent chat, title, and memory synthesis all skip it.

Signed-off-by: Yi Zhan <stevesough@gmail.com>
This commit is contained in:
Yi Zhan committed 2026-10-15 09:43:49 +08:00
1 parent 2f23e72ceb
commit be0e564031
5 files changed
+156 -10

No files matched your search

@@ -10,6 +10,7 @@ import {
} from './agentMemoryService' } from './agentMemoryService'
import { createAgentTextRedactionSession } from './agentTextRedaction' import { createAgentTextRedactionSession } from './agentTextRedaction'
import { filterAutomaticMemoryEntries, filterAutomaticMemoryPatch, type AgentMemoryQualityIssue } from './agentMemoryQuality' import { filterAutomaticMemoryEntries, filterAutomaticMemoryPatch, type AgentMemoryQualityIssue } from './agentMemoryQuality'
import { omitsTemperature } from './agentSamplingOptions'
import type { AgentFeedbackMemoryEvidence, AgentFeedbackMemorySample, AgentFeedbackReason } from './agentFeedbackStore' import type { AgentFeedbackMemoryEvidence, AgentFeedbackMemorySample, AgentFeedbackReason } from './agentFeedbackStore'
import type { AgentModelConfig } from './agentService' import type { AgentModelConfig } from './agentService'
@@ -66,9 +67,7 @@ function latestUserText(messages: unknown[]): string {
} }
function memorySamplingOptions(config: AgentModelConfig): { temperature?: number } { function memorySamplingOptions(config: AgentModelConfig): { temperature?: number } {
const modelId = String(config.model || '').trim().toLocaleLowerCase() return omitsTemperature(config) ? {} : { temperature: 0.1 }
const reasoningModel = config.protocol === 'openai-responses' && /^(?:gpt-5|o[1-4](?:-|$))/.test(modelId)
return reasoningModel || config.protocol === 'anthropic' ? {} : { temperature: 0.1 }
} }
function modelIsConfigured(config?: AgentModelConfig | null): config is AgentModelConfig { function modelIsConfigured(config?: AgentModelConfig | null): config is AgentModelConfig {
+2 -3
View File
@@ -59,6 +59,7 @@ import {
} from './agentMemoryRuntimeGuidance' } from './agentMemoryRuntimeGuidance'
import { createAgentModel } from './agentModelAdapter' import { createAgentModel } from './agentModelAdapter'
import { truncateStringToSchemaMax } from './agentModelSafety' import { truncateStringToSchemaMax } from './agentModelSafety'
import { omitsTemperature } from './agentSamplingOptions'
import { import {
AgentRunStore, AgentRunStore,
assessAgentRunResume, assessAgentRunResume,
@@ -851,9 +852,7 @@ function providerOptions(config: AgentModelConfig, promptCacheKey: string): Prov
} }
function samplingOptions(config: AgentModelConfig, temperature: number): { temperature?: number } { function samplingOptions(config: AgentModelConfig, temperature: number): { temperature?: number } {
const model = text(config.model).toLocaleLowerCase() return omitsTemperature(config) ? {} : { temperature }
if (config.protocol === 'anthropic' || (config.protocol === 'openai-responses' && /^(?:gpt-5|o[1-4](?:-|$))/.test(model))) return {}
return { temperature }
} }
function compactToolOutputForStorage(toolName: string, output: unknown): unknown { function compactToolOutputForStorage(toolName: string, output: unknown): unknown {
+12
View File
@@ -0,0 +1,12 @@
export type AgentSamplingConfig = {
protocol?: string
model?: string
}
const OPENAI_RESPONSES_REASONING_MODEL = /^(?:gpt-(?:5|6)|o[1-4](?:-|$))/
export function omitsTemperature(config: AgentSamplingConfig): boolean {
if (config.protocol === 'anthropic') return true
const modelId = String(config.model || '').trim().toLocaleLowerCase()
return config.protocol === 'openai-responses' && OPENAI_RESPONSES_REASONING_MODEL.test(modelId)
}
+2 -4
View File
@@ -1,5 +1,6 @@
import { streamText } from 'ai' import { streamText } from 'ai'
import { createAgentModel } from './agentModelAdapter' import { createAgentModel } from './agentModelAdapter'
import { omitsTemperature } from './agentSamplingOptions'
import { createAgentTextRedactionSession } from './agentTextRedaction' import { createAgentTextRedactionSession } from './agentTextRedaction'
import type { AgentModelConfig } from './agentService' import type { AgentModelConfig } from './agentService'
@@ -19,11 +20,8 @@ function fallbackTitle(value: string): string {
function titleSamplingOptions(config: AgentModelConfig): { temperature?: number } { function titleSamplingOptions(config: AgentModelConfig): { temperature?: number } {
const modelId = String(config.model || '').trim().toLocaleLowerCase() const modelId = String(config.model || '').trim().toLocaleLowerCase()
const openAiReasoningModel = config.protocol === 'openai-responses'
&& /^(?:gpt-5|o[1-4](?:-|$))/.test(modelId)
const nativeAnthropicModel = config.protocol === 'anthropic'
const modernClaudeModel = /(?:^|\/)claude-(?:opus-4-(?:[7-9]|\d{2,})|(?:sonnet|opus|haiku|fable|mythos)-(?:[5-9]|\d{2,})(?:-|$))/.test(modelId) const modernClaudeModel = /(?:^|\/)claude-(?:opus-4-(?:[7-9]|\d{2,})|(?:sonnet|opus|haiku|fable|mythos)-(?:[5-9]|\d{2,})(?:-|$))/.test(modelId)
return openAiReasoningModel || nativeAnthropicModel || modernClaudeModel ? {} : { temperature: 0.2 } return omitsTemperature(config) || modernClaudeModel ? {} : { temperature: 0.2 }
} }
function titleErrorMessage(error: unknown): string { function titleErrorMessage(error: unknown): string {
+138
View File
@@ -0,0 +1,138 @@
// Exercise the production omit-temperature predicate without loading Electron or native modules.
const assert = require('node:assert/strict')
const fs = require('node:fs')
const path = require('node:path')
const vm = require('node:vm')
const { test } = require('node:test')
const root = path.join(__dirname, '..')
const helperPath = path.join(root, 'electron/services/agentSamplingOptions.ts')
const ledPath = path.join(root, 'electron/services/agentModelLedService.ts')
const titlePath = path.join(root, 'electron/services/agentTitleService.ts')
const memoryPath = path.join(root, 'electron/services/agentMemorySynthesisService.ts')
function extractFunction(source, name) {
const header = new RegExp(`function ${name}\\([^)]*\\)(?:\\s*:\\s*\\{[^}]*\\})?\\s*\\{`)
const match = header.exec(source)
assert.ok(match, `missing function ${name}`)
const start = match.index
const brace = start + match[0].length - 1
let depth = 0
for (let index = brace; index < source.length; index += 1) {
const char = source[index]
if (char === '{') depth += 1
else if (char === '}') {
depth -= 1
if (depth === 0) return source.slice(start, index + 1)
}
}
throw new Error(`unterminated function ${name}`)
}
function stripTypes(source) {
return source
.replace(/\bexport type [A-Za-z0-9_]+ = \{[\s\S]*?\n\}\n/, '')
.replace(/\bexport /g, '')
.replace(/: Agent(?:Sampling|Model)Config/g, '')
.replace(/: \{ protocol\?: string; model\?: string \}/g, '')
.replace(/temperature: number/g, 'temperature')
.replace(/\): boolean/g, ')')
.replace(/\): \{ temperature\?: number \}/g, ')')
}
function loadHelper() {
const source = fs.readFileSync(helperPath, 'utf8')
const js = stripTypes(source)
const module = { exports: {} }
vm.runInNewContext(`${js}\nmodule.exports = { omitsTemperature }`, {
module,
exports: module.exports,
})
return module.exports
}
function loadOmitsTemperature() {
if (fs.existsSync(helperPath)) return loadHelper().omitsTemperature
const led = extractFunction(fs.readFileSync(ledPath, 'utf8'), 'samplingOptions')
const title = extractFunction(fs.readFileSync(titlePath, 'utf8'), 'titleSamplingOptions')
const memory = extractFunction(fs.readFileSync(memoryPath, 'utf8'), 'memorySamplingOptions')
const js = `
function text(value) { return String(value ?? '').replace(/\\u0000/g, '').trim() }
${stripTypes(led)}
${stripTypes(title)}
${stripTypes(memory)}
function omitsTemperature(config) {
return !('temperature' in samplingOptions(config, 0.7))
&& !('temperature' in titleSamplingOptions(config))
&& !('temperature' in memorySamplingOptions(config))
}
`
const module = { exports: {} }
vm.runInNewContext(`${js}\nmodule.exports = { omitsTemperature }`, { module, exports: module.exports })
return module.exports.omitsTemperature
}
function loadSamplingOptions() {
const omits = loadOmitsTemperature()
return (config, temperature) => (omits(config) ? {} : { temperature })
}
const omitsTemperature = loadOmitsTemperature()
const samplingOptions = loadSamplingOptions()
const plain = (value) => JSON.parse(JSON.stringify(value))
function responses(model) {
return { protocol: 'openai-responses', model }
}
test('openai-responses gpt-6-luna omits temperature', () => {
assert.equal(omitsTemperature(responses('gpt-6-luna')), true)
assert.deepEqual(plain(samplingOptions(responses('gpt-6-luna'), 0.7)), {})
})
test('openai-responses gpt-6-* variants omit temperature', () => {
for (const model of ['gpt-6', 'gpt-6-something', 'GPT-6-Luna']) {
assert.equal(omitsTemperature(responses(model)), true, model)
assert.deepEqual(plain(samplingOptions(responses(model), 0.2)), {})
}
})
test('openai-responses gpt-5 and o-series still omit temperature', () => {
for (const model of ['gpt-5', 'gpt-5.5', 'gpt-5.4-mini', 'o1', 'o1-preview', 'o3', 'o4-mini']) {
assert.equal(omitsTemperature(responses(model)), true, model)
assert.deepEqual(plain(samplingOptions(responses(model), 0.7)), {})
}
})
test('non-reasoning openai-responses still includes temperature', () => {
for (const model of ['gpt-4o', 'gpt-4.1', 'chatgpt-4o-latest']) {
assert.equal(omitsTemperature(responses(model)), false, model)
assert.deepEqual(plain(samplingOptions(responses(model), 0.7)), { temperature: 0.7 })
}
})
test('openai-compatible gpt-6-luna still includes temperature', () => {
const config = { protocol: 'openai-compatible', model: 'gpt-6-luna' }
assert.equal(omitsTemperature(config), false)
assert.deepEqual(plain(samplingOptions(config, 0.3)), { temperature: 0.3 })
})
test('anthropic path still omits temperature for any model', () => {
for (const model of ['claude-sonnet-4', 'gpt-4o', 'gpt-6-luna']) {
const config = { protocol: 'anthropic', model }
assert.equal(omitsTemperature(config), true, model)
assert.deepEqual(plain(samplingOptions(config, 0.7)), {})
}
})
test('agent chat, title, and memory sampling share omitsTemperature', () => {
if (!fs.existsSync(helperPath)) {
assert.fail('shared helper electron/services/agentSamplingOptions.ts is required')
}
for (const file of [ledPath, titlePath, memoryPath]) {
const source = fs.readFileSync(file, 'utf8')
assert.match(source, /from '\.\/agentSamplingOptions'/)
assert.match(source, /omitsTemperature\s*\(/)
}
})