fix(provider): pass reasoning effort for proxies (#905)

Tested:
- bun test src/services/api/claudeEffort.test.ts src/server/__tests__/proxy-transform.test.ts src/utils/__tests__/thinking.test.ts
- bun run check:server
- git diff --check

Not-tested:
- bun run verify / coverage

Confidence: high
Scope-risk: narrow
This commit is contained in:
程序员阿江(Relakkes) 2026-07-02 21:01:09 +08:00
parent 1d20434746
commit 2e52133a64
7 changed files with 177 additions and 3 deletions

View File

@ -184,6 +184,31 @@ describe('anthropicToOpenaiChat', () => {
expect(anthropicToOpenaiChat(req, { passThinkingToggle: true }).thinking).toEqual({ type: 'disabled' })
})
test('maps output_config effort to reasoning_effort for OpenAI-compatible chat providers', () => {
const req: AnthropicRequest = {
model: 'longcat',
max_tokens: 100,
messages: [{ role: 'user', content: 'Hi' }],
thinking: { type: 'adaptive' },
output_config: { effort: 'high' },
}
const result = anthropicToOpenaiChat(req)
expect(result.reasoning_effort).toBe('high')
})
test('clamps max output_config effort to high for OpenAI-compatible chat providers', () => {
const req: AnthropicRequest = {
model: 'longcat',
max_tokens: 100,
messages: [{ role: 'user', content: 'Hi' }],
output_config: { effort: 'max' },
}
const result = anthropicToOpenaiChat(req)
expect(result.reasoning_effort).toBe('high')
})
test('assistant message with tool_use', () => {
const req: AnthropicRequest = {
model: 'gpt-4',
@ -536,6 +561,31 @@ describe('anthropicToOpenaiResponses', () => {
expect(result.reasoning).toEqual({ effort: 'high' })
})
test('output_config effort → reasoning effort', () => {
const req: AnthropicRequest = {
model: 'gpt-5.5',
max_tokens: 100,
messages: [{ role: 'user', content: 'Hi' }],
thinking: { type: 'adaptive' },
output_config: { effort: 'high' },
}
const result = anthropicToOpenaiResponses(req)
expect(result.reasoning).toEqual({ effort: 'high' })
})
test('clamps max output_config effort for Responses API', () => {
const req: AnthropicRequest = {
model: 'gpt-5.5',
max_tokens: 100,
messages: [{ role: 'user', content: 'Hi' }],
output_config: { effort: 'max' },
}
const result = anthropicToOpenaiResponses(req)
expect(result.reasoning).toEqual({ effort: 'high' })
})
test('stop_sequences dropped', () => {
const req: AnthropicRequest = {
model: 'gpt-4o',

View File

@ -15,6 +15,7 @@ import type {
OpenAITool,
} from './types.js'
import { stripLeadingBillingHeader } from './billingHeader.js'
import { normalizeOpenAIReasoningEffort } from './effort.js'
type OpenAIChatImageContentMode = 'vision' | 'text_only'
@ -114,6 +115,10 @@ export function anthropicToOpenaiChat(
result.thinking = { type: body.thinking.type }
}
}
const outputConfigEffort = normalizeOpenAIReasoningEffort(body.output_config?.effort)
if (outputConfigEffort !== undefined) {
result.reasoning_effort = outputConfigEffort
}
return result
}

View File

@ -13,6 +13,7 @@ import type {
OpenAIChatContentPart,
} from './types.js'
import { stripLeadingBillingHeader } from './billingHeader.js'
import { normalizeOpenAIReasoningEffort } from './effort.js'
export type OpenAIResponsesTransformOptions = {
/** Stable cache routing key, forwarded as `prompt_cache_key`. */
@ -95,6 +96,10 @@ export function anthropicToOpenaiResponses(
result.reasoning = { effort: 'high' }
}
}
const outputConfigEffort = normalizeOpenAIReasoningEffort(body.output_config?.effort)
if (outputConfigEffort !== undefined) {
result.reasoning = { ...(result.reasoning ?? {}), effort: outputConfigEffort }
}
// stop_sequences not supported in Responses API, dropped

View File

@ -0,0 +1,13 @@
import type { OpenAIReasoningEffort } from './types.js'
export function normalizeOpenAIReasoningEffort(
effort: unknown,
): OpenAIReasoningEffort | undefined {
if (effort === 'low' || effort === 'medium' || effort === 'high') {
return effort
}
if (effort === 'max') {
return 'high'
}
return undefined
}

View File

@ -6,6 +6,8 @@
// ─── OpenAI Chat Completions ────────────────────────────────
export type OpenAIReasoningEffort = 'low' | 'medium' | 'high'
export type OpenAIChatMessage = {
role: 'system' | 'user' | 'assistant' | 'tool'
content?: string | OpenAIChatContentPart[] | null
@ -49,7 +51,7 @@ export type OpenAIChatRequest = {
stream_options?: { include_usage: boolean }
tools?: OpenAITool[]
tool_choice?: unknown
reasoning_effort?: 'low' | 'medium' | 'high'
reasoning_effort?: OpenAIReasoningEffort
thinking?: { type: string }
}
@ -136,7 +138,7 @@ export type OpenAIResponsesRequest = {
parameters?: Record<string, unknown>
}>
tool_choice?: unknown
reasoning?: { effort?: 'low' | 'medium' | 'high' }
reasoning?: { effort?: OpenAIReasoningEffort }
prompt_cache_key?: string
}
@ -190,6 +192,10 @@ export type AnthropicRequest = {
type: string
budget_tokens?: number
}
output_config?: {
effort?: unknown
[key: string]: unknown
}
}
export type AnthropicResponse = {

View File

@ -446,7 +446,7 @@ function should1hCacheTTL(querySource?: QuerySource): boolean {
* Configure effort parameters for API request.
*
*/
function configureEffortParams(
export function configureEffortParams(
effortValue: EffortValue | undefined,
outputConfig: BetaOutputConfig,
extraBodyParams: Record<string, unknown>,
@ -458,6 +458,7 @@ function configureEffortParams(
}
if (effortValue === undefined) {
outputConfig.effort = "high";
betas.push(EFFORT_BETA_HEADER);
} else if (typeof effortValue === "string") {
// Send string effort level as is

View File

@ -0,0 +1,94 @@
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
import { EFFORT_BETA_HEADER } from '../../constants/betas.js'
import { get3PModelCapabilityOverride } from '../../utils/model/modelSupportOverrides.js'
import { configureEffortParams } from './claude.js'
describe('configureEffortParams', () => {
let originalBaseUrl: string | undefined
let originalSonnetModel: string | undefined
let originalSonnetCapabilities: string | undefined
let originalBedrock: string | undefined
let originalVertex: string | undefined
let originalFoundry: string | undefined
beforeEach(() => {
originalBaseUrl = process.env.ANTHROPIC_BASE_URL
originalSonnetModel = process.env.ANTHROPIC_DEFAULT_SONNET_MODEL
originalSonnetCapabilities = process.env.ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES
originalBedrock = process.env.CLAUDE_CODE_USE_BEDROCK
originalVertex = process.env.CLAUDE_CODE_USE_VERTEX
originalFoundry = process.env.CLAUDE_CODE_USE_FOUNDRY
process.env.ANTHROPIC_BASE_URL = 'https://ark.cn-beijing.volces.com/api/coding'
process.env.ANTHROPIC_DEFAULT_SONNET_MODEL = 'glm-5.2'
process.env.ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES =
'thinking,effort,adaptive_thinking,max_effort'
delete process.env.CLAUDE_CODE_USE_BEDROCK
delete process.env.CLAUDE_CODE_USE_VERTEX
delete process.env.CLAUDE_CODE_USE_FOUNDRY
clearCapabilityCache()
})
afterEach(() => {
restoreEnv('ANTHROPIC_BASE_URL', originalBaseUrl)
restoreEnv('ANTHROPIC_DEFAULT_SONNET_MODEL', originalSonnetModel)
restoreEnv('ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES', originalSonnetCapabilities)
restoreEnv('CLAUDE_CODE_USE_BEDROCK', originalBedrock)
restoreEnv('CLAUDE_CODE_USE_VERTEX', originalVertex)
restoreEnv('CLAUDE_CODE_USE_FOUNDRY', originalFoundry)
clearCapabilityCache()
})
test('sends explicit high effort for effort-capable third-party models when unset', () => {
const outputConfig: Record<string, unknown> = {}
const extraBodyParams: Record<string, unknown> = {}
const betas: string[] = []
configureEffortParams(
undefined,
outputConfig,
extraBodyParams,
betas,
'glm-5.2',
)
expect(outputConfig).toEqual({ effort: 'high' })
expect(extraBodyParams).toEqual({})
expect(betas).toContain(EFFORT_BETA_HEADER)
})
test('does not send effort when provider capabilities do not opt in', () => {
process.env.ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES = 'thinking'
clearCapabilityCache()
const outputConfig: Record<string, unknown> = {}
const extraBodyParams: Record<string, unknown> = {}
const betas: string[] = []
configureEffortParams(
undefined,
outputConfig,
extraBodyParams,
betas,
'glm-5.2',
)
expect(outputConfig).toEqual({})
expect(extraBodyParams).toEqual({})
expect(betas).not.toContain(EFFORT_BETA_HEADER)
})
})
function restoreEnv(key: string, value: string | undefined) {
if (value === undefined) {
delete process.env[key]
} else {
process.env[key] = value
}
}
function clearCapabilityCache() {
;(get3PModelCapabilityOverride as typeof get3PModelCapabilityOverride & {
cache?: { clear?: () => void }
}).cache?.clear?.()
}