mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-10-25 17:36:32 +08:00
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:
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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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*\(/)
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in new issue
Block a user