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:
Copilot
2025-11-26 19:56:31 +08:00
committed by GitHub
parent 79f75843a7
commit 82ef4a32eb
4 changed files with 354 additions and 19 deletions

View File

@@ -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. 最终请求消息

View 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)
})
})
})

View File

@@ -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) {

View File

@@ -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