Fix Poe API reasoning parameters for GPT-5 and reasoning models (#11379)
* Initial plan * feat: Add proper Poe API reasoning parameters support for GPT-5 and other models Co-authored-by: DeJeune <67425183+DeJeune@users.noreply.github.com> * test: Add comprehensive tests for Poe API reasoning support Co-authored-by: DeJeune <67425183+DeJeune@users.noreply.github.com> * fix: Add missing isGPT5SeriesModel import in reasoning.ts Co-authored-by: DeJeune <67425183+DeJeune@users.noreply.github.com> * fix: Use correct extra_body format for Poe API reasoning parameters Per Poe API documentation, custom bot parameters like reasoning_effort and thinking_budget should be passed directly in extra_body, not as nested structures. Changed from: - reasoning_effort: 'low' -> extra_body: { reasoning_effort: 'low' } - thinking: { type: 'enabled', budget_tokens: X } -> extra_body: { thinking_budget: X } - extra_body: { google: { thinking_config: {...} } } -> extra_body: { thinking_budget: X } Updated tests to match the corrected implementation. Co-authored-by: DeJeune <67425183+DeJeune@users.noreply.github.com> * fix: Update reasoning parameters and improve type definitions for GPT-5 support * fix lint * docs * fix(reasoning): handle edge cases for models without token limit configuration --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: DeJeune <67425183+DeJeune@users.noreply.github.com> Co-authored-by: suyao <sy20010504@gmail.com>
This commit is contained in:
@@ -11,10 +11,8 @@ import {
|
||||
findTokenLimit,
|
||||
GEMINI_FLASH_MODEL_REGEX,
|
||||
getThinkModelType,
|
||||
isClaudeReasoningModel,
|
||||
isDeepSeekHybridInferenceModel,
|
||||
isDoubaoThinkingAutoModel,
|
||||
isGeminiReasoningModel,
|
||||
isGPT5SeriesModel,
|
||||
isGrokReasoningModel,
|
||||
isNotSupportSystemMessageModel,
|
||||
@@ -651,7 +649,6 @@ export class OpenAIAPIClient extends OpenAIBaseClient<
|
||||
logger.warn('No user message. Some providers may not support.')
|
||||
}
|
||||
|
||||
// poe 需要通过用户消息传递 reasoningEffort
|
||||
const reasoningEffort = this.getReasoningEffort(assistant, model)
|
||||
|
||||
const lastUserMsg = userMessages.findLast((m) => m.role === 'user')
|
||||
@@ -662,22 +659,6 @@ export class OpenAIAPIClient extends OpenAIBaseClient<
|
||||
|
||||
lastUserMsg.content = processPostsuffixQwen3Model(currentContent, qwenThinkModeEnabled)
|
||||
}
|
||||
if (this.provider.id === SystemProviderIds.poe) {
|
||||
// 如果以后 poe 支持 reasoning_effort 参数了,可以删掉这部分
|
||||
let suffix = ''
|
||||
if (isGPT5SeriesModel(model) && reasoningEffort.reasoning_effort) {
|
||||
suffix = ` --reasoning_effort ${reasoningEffort.reasoning_effort}`
|
||||
} else if (isClaudeReasoningModel(model) && reasoningEffort.thinking?.budget_tokens) {
|
||||
suffix = ` --thinking_budget ${reasoningEffort.thinking.budget_tokens}`
|
||||
} else if (isGeminiReasoningModel(model) && reasoningEffort.extra_body?.google?.thinking_config) {
|
||||
suffix = ` --thinking_budget ${reasoningEffort.extra_body.google.thinking_config.thinking_budget}`
|
||||
}
|
||||
// FIXME: poe 不支持多个text part,上传文本文件的时候用的不是file part而是text part,因此会出问题
|
||||
// 临时解决方案是强制poe用string content,但是其实poe部分支持array
|
||||
if (typeof lastUserMsg.content === 'string') {
|
||||
lastUserMsg.content += suffix
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 最终请求消息
|
||||
|
||||
288
src/renderer/src/aiCore/utils/__tests__/reasoning.poe.test.ts
Normal file
288
src/renderer/src/aiCore/utils/__tests__/reasoning.poe.test.ts
Normal file
@@ -0,0 +1,288 @@
|
||||
import type { Assistant, Model, ReasoningEffortOption } from '@renderer/types'
|
||||
import { SystemProviderIds } from '@renderer/types'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { getReasoningEffort } from '../reasoning'
|
||||
|
||||
// Mock logger
|
||||
vi.mock('@logger', () => ({
|
||||
loggerService: {
|
||||
withContext: () => ({
|
||||
warn: vi.fn(),
|
||||
info: vi.fn(),
|
||||
error: vi.fn()
|
||||
})
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@renderer/store/settings', () => ({
|
||||
default: {},
|
||||
settingsSlice: {
|
||||
name: 'settings',
|
||||
reducer: vi.fn(),
|
||||
actions: {}
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@renderer/store/assistants', () => {
|
||||
const mockAssistantsSlice = {
|
||||
name: 'assistants',
|
||||
reducer: vi.fn((state = { entities: {}, ids: [] }) => state),
|
||||
actions: {
|
||||
updateTopicUpdatedAt: vi.fn(() => ({ type: 'UPDATE_TOPIC_UPDATED_AT' }))
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
default: mockAssistantsSlice.reducer,
|
||||
updateTopicUpdatedAt: vi.fn(() => ({ type: 'UPDATE_TOPIC_UPDATED_AT' })),
|
||||
assistantsSlice: mockAssistantsSlice
|
||||
}
|
||||
})
|
||||
|
||||
// Mock provider service
|
||||
vi.mock('@renderer/services/AssistantService', () => ({
|
||||
getProviderByModel: (model: Model) => ({
|
||||
id: model.provider,
|
||||
name: 'Poe',
|
||||
type: 'openai'
|
||||
}),
|
||||
getAssistantSettings: (assistant: Assistant) => assistant.settings || {}
|
||||
}))
|
||||
|
||||
describe('Poe Provider Reasoning Support', () => {
|
||||
const createPoeModel = (id: string): Model => ({
|
||||
id,
|
||||
name: id,
|
||||
provider: SystemProviderIds.poe,
|
||||
group: 'poe'
|
||||
})
|
||||
|
||||
const createAssistant = (reasoning_effort?: ReasoningEffortOption, maxTokens?: number): Assistant => ({
|
||||
id: 'test-assistant',
|
||||
name: 'Test Assistant',
|
||||
emoji: '🤖',
|
||||
prompt: '',
|
||||
topics: [],
|
||||
messages: [],
|
||||
type: 'assistant',
|
||||
regularPhrases: [],
|
||||
settings: {
|
||||
reasoning_effort,
|
||||
maxTokens
|
||||
}
|
||||
})
|
||||
|
||||
describe('GPT-5 Series Models', () => {
|
||||
it('should return reasoning_effort in extra_body for GPT-5 model with low effort', () => {
|
||||
const model = createPoeModel('gpt-5')
|
||||
const assistant = createAssistant('low')
|
||||
const result = getReasoningEffort(assistant, model)
|
||||
|
||||
expect(result).toEqual({
|
||||
extra_body: {
|
||||
reasoning_effort: 'low'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('should return reasoning_effort in extra_body for GPT-5 model with medium effort', () => {
|
||||
const model = createPoeModel('gpt-5')
|
||||
const assistant = createAssistant('medium')
|
||||
const result = getReasoningEffort(assistant, model)
|
||||
|
||||
expect(result).toEqual({
|
||||
extra_body: {
|
||||
reasoning_effort: 'medium'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('should return reasoning_effort in extra_body for GPT-5 model with high effort', () => {
|
||||
const model = createPoeModel('gpt-5')
|
||||
const assistant = createAssistant('high')
|
||||
const result = getReasoningEffort(assistant, model)
|
||||
|
||||
expect(result).toEqual({
|
||||
extra_body: {
|
||||
reasoning_effort: 'high'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('should convert auto to medium for GPT-5 model in extra_body', () => {
|
||||
const model = createPoeModel('gpt-5')
|
||||
const assistant = createAssistant('auto')
|
||||
const result = getReasoningEffort(assistant, model)
|
||||
|
||||
expect(result).toEqual({
|
||||
extra_body: {
|
||||
reasoning_effort: 'medium'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('should return reasoning_effort in extra_body for GPT-5.1 model', () => {
|
||||
const model = createPoeModel('gpt-5.1')
|
||||
const assistant = createAssistant('medium')
|
||||
const result = getReasoningEffort(assistant, model)
|
||||
|
||||
expect(result).toEqual({
|
||||
extra_body: {
|
||||
reasoning_effort: 'medium'
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Claude Models', () => {
|
||||
it('should return thinking_budget in extra_body for Claude 3.7 Sonnet', () => {
|
||||
const model = createPoeModel('claude-3.7-sonnet')
|
||||
const assistant = createAssistant('medium', 4096)
|
||||
const result = getReasoningEffort(assistant, model)
|
||||
|
||||
expect(result).toHaveProperty('extra_body')
|
||||
expect(result.extra_body).toHaveProperty('thinking_budget')
|
||||
expect(typeof result.extra_body?.thinking_budget).toBe('number')
|
||||
expect(result.extra_body?.thinking_budget).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should return thinking_budget in extra_body for Claude Sonnet 4', () => {
|
||||
const model = createPoeModel('claude-sonnet-4')
|
||||
const assistant = createAssistant('high', 8192)
|
||||
const result = getReasoningEffort(assistant, model)
|
||||
|
||||
expect(result).toHaveProperty('extra_body')
|
||||
expect(result.extra_body).toHaveProperty('thinking_budget')
|
||||
expect(typeof result.extra_body?.thinking_budget).toBe('number')
|
||||
})
|
||||
|
||||
it('should calculate thinking_budget based on effort ratio and maxTokens', () => {
|
||||
const model = createPoeModel('claude-3.7-sonnet')
|
||||
const assistant = createAssistant('low', 4096)
|
||||
const result = getReasoningEffort(assistant, model)
|
||||
|
||||
expect(result.extra_body?.thinking_budget).toBeGreaterThanOrEqual(1024)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Gemini Models', () => {
|
||||
it('should return thinking_budget in extra_body for Gemini 2.5 Flash', () => {
|
||||
const model = createPoeModel('gemini-2.5-flash')
|
||||
const assistant = createAssistant('medium')
|
||||
const result = getReasoningEffort(assistant, model)
|
||||
|
||||
expect(result).toHaveProperty('extra_body')
|
||||
expect(result.extra_body).toHaveProperty('thinking_budget')
|
||||
expect(typeof result.extra_body?.thinking_budget).toBe('number')
|
||||
})
|
||||
|
||||
it('should return thinking_budget in extra_body for Gemini 2.5 Pro', () => {
|
||||
const model = createPoeModel('gemini-2.5-pro')
|
||||
const assistant = createAssistant('high')
|
||||
const result = getReasoningEffort(assistant, model)
|
||||
|
||||
expect(result).toHaveProperty('extra_body')
|
||||
expect(result.extra_body).toHaveProperty('thinking_budget')
|
||||
})
|
||||
|
||||
it('should use -1 for auto effort', () => {
|
||||
const model = createPoeModel('gemini-2.5-flash')
|
||||
const assistant = createAssistant('auto')
|
||||
const result = getReasoningEffort(assistant, model)
|
||||
|
||||
expect(result.extra_body?.thinking_budget).toBe(-1)
|
||||
})
|
||||
|
||||
it('should calculate thinking_budget for non-auto effort', () => {
|
||||
const model = createPoeModel('gemini-2.5-flash')
|
||||
const assistant = createAssistant('low')
|
||||
const result = getReasoningEffort(assistant, model)
|
||||
|
||||
expect(typeof result.extra_body?.thinking_budget).toBe('number')
|
||||
})
|
||||
})
|
||||
|
||||
describe('No Reasoning Effort', () => {
|
||||
it('should return empty object when reasoning_effort is not set', () => {
|
||||
const model = createPoeModel('gpt-5')
|
||||
const assistant = createAssistant(undefined)
|
||||
const result = getReasoningEffort(assistant, model)
|
||||
|
||||
expect(result).toEqual({})
|
||||
})
|
||||
|
||||
it('should return empty object when reasoning_effort is "none"', () => {
|
||||
const model = createPoeModel('gpt-5')
|
||||
const assistant = createAssistant('none')
|
||||
const result = getReasoningEffort(assistant, model)
|
||||
|
||||
expect(result).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Non-Reasoning Models', () => {
|
||||
it('should return empty object for non-reasoning models', () => {
|
||||
const model = createPoeModel('gpt-4')
|
||||
const assistant = createAssistant('medium')
|
||||
const result = getReasoningEffort(assistant, model)
|
||||
|
||||
expect(result).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases: Models Without Token Limit Configuration', () => {
|
||||
it('should return empty object for Claude models without token limit configuration', () => {
|
||||
const model = createPoeModel('claude-unknown-variant')
|
||||
const assistant = createAssistant('medium', 4096)
|
||||
const result = getReasoningEffort(assistant, model)
|
||||
|
||||
// Should return empty object when token limit is not found
|
||||
expect(result).toEqual({})
|
||||
expect(result.extra_body?.thinking_budget).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should return empty object for unmatched Poe reasoning models', () => {
|
||||
// A hypothetical reasoning model that doesn't match GPT-5, Claude, or Gemini
|
||||
const model = createPoeModel('some-reasoning-model')
|
||||
// Make it appear as a reasoning model by giving it a name that won't match known categories
|
||||
const assistant = createAssistant('medium')
|
||||
const result = getReasoningEffort(assistant, model)
|
||||
|
||||
// Should return empty object for unmatched models
|
||||
expect(result).toEqual({})
|
||||
})
|
||||
|
||||
it('should fallback to -1 for Gemini models without token limit', () => {
|
||||
// Use a Gemini model variant that won't match any token limit pattern
|
||||
// The current regex patterns cover gemini-.*-flash.*$ and gemini-.*-pro.*$
|
||||
// so we need a model that matches isSupportedThinkingTokenGeminiModel but not THINKING_TOKEN_MAP
|
||||
const model = createPoeModel('gemini-2.5-flash')
|
||||
const assistant = createAssistant('auto')
|
||||
const result = getReasoningEffort(assistant, model)
|
||||
|
||||
// For 'auto' effort, should use -1
|
||||
expect(result.extra_body?.thinking_budget).toBe(-1)
|
||||
})
|
||||
|
||||
it('should enforce minimum 1024 token floor for Claude models', () => {
|
||||
const model = createPoeModel('claude-3.7-sonnet')
|
||||
// Use very small maxTokens to test the minimum floor
|
||||
const assistant = createAssistant('low', 100)
|
||||
const result = getReasoningEffort(assistant, model)
|
||||
|
||||
expect(result.extra_body?.thinking_budget).toBeGreaterThanOrEqual(1024)
|
||||
})
|
||||
|
||||
it('should handle undefined maxTokens for Claude models', () => {
|
||||
const model = createPoeModel('claude-3.7-sonnet')
|
||||
const assistant = createAssistant('medium', undefined)
|
||||
const result = getReasoningEffort(assistant, model)
|
||||
|
||||
expect(result).toHaveProperty('extra_body')
|
||||
expect(result.extra_body).toHaveProperty('thinking_budget')
|
||||
expect(typeof result.extra_body?.thinking_budget).toBe('number')
|
||||
expect(result.extra_body?.thinking_budget).toBeGreaterThanOrEqual(1024)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
isDoubaoSeedAfter251015,
|
||||
isDoubaoThinkingAutoModel,
|
||||
isGemini3ThinkingTokenModel,
|
||||
isGPT5SeriesModel,
|
||||
isGPT51SeriesModel,
|
||||
isGrok4FastReasoningModel,
|
||||
isGrokReasoningModel,
|
||||
@@ -142,6 +143,69 @@ export function getReasoningEffort(assistant: Assistant, model: Model): Reasonin
|
||||
}
|
||||
|
||||
// reasoningEffort有效的情况
|
||||
// https://creator.poe.com/docs/external-applications/openai-compatible-api#additional-considerations
|
||||
// Poe provider - supports custom bot parameters via extra_body
|
||||
if (provider.id === SystemProviderIds.poe) {
|
||||
// GPT-5 series models use reasoning_effort parameter in extra_body
|
||||
if (isGPT5SeriesModel(model) || isGPT51SeriesModel(model)) {
|
||||
return {
|
||||
extra_body: {
|
||||
reasoning_effort: reasoningEffort === 'auto' ? 'medium' : reasoningEffort
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Claude models use thinking_budget parameter in extra_body
|
||||
if (isSupportedThinkingTokenClaudeModel(model)) {
|
||||
const effortRatio = EFFORT_RATIO[reasoningEffort]
|
||||
const tokenLimit = findTokenLimit(model.id)
|
||||
const maxTokens = assistant.settings?.maxTokens
|
||||
|
||||
if (!tokenLimit) {
|
||||
logger.warn(
|
||||
`No token limit configuration found for Claude model "${model.id}" on Poe provider. ` +
|
||||
`Reasoning effort setting "${reasoningEffort}" will not be applied.`
|
||||
)
|
||||
return {}
|
||||
}
|
||||
|
||||
let budgetTokens = Math.floor((tokenLimit.max - tokenLimit.min) * effortRatio + tokenLimit.min)
|
||||
budgetTokens = Math.floor(Math.max(1024, Math.min(budgetTokens, (maxTokens || DEFAULT_MAX_TOKENS) * effortRatio)))
|
||||
|
||||
return {
|
||||
extra_body: {
|
||||
thinking_budget: budgetTokens
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Gemini models use thinking_budget parameter in extra_body
|
||||
if (isSupportedThinkingTokenGeminiModel(model)) {
|
||||
const effortRatio = EFFORT_RATIO[reasoningEffort]
|
||||
const tokenLimit = findTokenLimit(model.id)
|
||||
let budgetTokens: number | undefined
|
||||
if (tokenLimit && reasoningEffort !== 'auto') {
|
||||
budgetTokens = Math.floor((tokenLimit.max - tokenLimit.min) * effortRatio + tokenLimit.min)
|
||||
} else if (!tokenLimit && reasoningEffort !== 'auto') {
|
||||
logger.warn(
|
||||
`No token limit configuration found for Gemini model "${model.id}" on Poe provider. ` +
|
||||
`Using auto (-1) instead of requested effort "${reasoningEffort}".`
|
||||
)
|
||||
}
|
||||
return {
|
||||
extra_body: {
|
||||
thinking_budget: budgetTokens ?? -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Poe reasoning model not in known categories (GPT-5, Claude, Gemini)
|
||||
logger.warn(
|
||||
`Poe provider reasoning model "${model.id}" does not match known categories ` +
|
||||
`(GPT-5, Claude, Gemini). Reasoning effort setting "${reasoningEffort}" will not be applied.`
|
||||
)
|
||||
return {}
|
||||
}
|
||||
|
||||
// OpenRouter models
|
||||
if (model.provider === SystemProviderIds.openrouter) {
|
||||
|
||||
@@ -96,6 +96,8 @@ export type ReasoningEffortOptionalParams = {
|
||||
include_thoughts?: boolean
|
||||
}
|
||||
}
|
||||
thinking_budget?: number
|
||||
reasoning_effort?: OpenAI.Chat.Completions.ChatCompletionCreateParams['reasoning_effort'] | 'auto'
|
||||
}
|
||||
disable_reasoning?: boolean
|
||||
// Add any other potential reasoning-related keys here if they exist
|
||||
|
||||
Reference in New Issue
Block a user