From 2e52133a640f4598e6cebff932aa5e4544dd72f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Thu, 2 Jul 2026 21:01:09 +0800 Subject: [PATCH] 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 --- src/server/__tests__/proxy-transform.test.ts | 50 ++++++++++ .../proxy/transform/anthropicToOpenaiChat.ts | 5 + .../transform/anthropicToOpenaiResponses.ts | 5 + src/server/proxy/transform/effort.ts | 13 +++ src/server/proxy/transform/types.ts | 10 +- src/services/api/claude.ts | 3 +- src/services/api/claudeEffort.test.ts | 94 +++++++++++++++++++ 7 files changed, 177 insertions(+), 3 deletions(-) create mode 100644 src/server/proxy/transform/effort.ts create mode 100644 src/services/api/claudeEffort.test.ts diff --git a/src/server/__tests__/proxy-transform.test.ts b/src/server/__tests__/proxy-transform.test.ts index a4580af3..8f5c5b53 100644 --- a/src/server/__tests__/proxy-transform.test.ts +++ b/src/server/__tests__/proxy-transform.test.ts @@ -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', diff --git a/src/server/proxy/transform/anthropicToOpenaiChat.ts b/src/server/proxy/transform/anthropicToOpenaiChat.ts index 76c96658..00e575c1 100644 --- a/src/server/proxy/transform/anthropicToOpenaiChat.ts +++ b/src/server/proxy/transform/anthropicToOpenaiChat.ts @@ -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 } diff --git a/src/server/proxy/transform/anthropicToOpenaiResponses.ts b/src/server/proxy/transform/anthropicToOpenaiResponses.ts index a8544adb..12648fc6 100644 --- a/src/server/proxy/transform/anthropicToOpenaiResponses.ts +++ b/src/server/proxy/transform/anthropicToOpenaiResponses.ts @@ -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 diff --git a/src/server/proxy/transform/effort.ts b/src/server/proxy/transform/effort.ts new file mode 100644 index 00000000..780a5c44 --- /dev/null +++ b/src/server/proxy/transform/effort.ts @@ -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 +} diff --git a/src/server/proxy/transform/types.ts b/src/server/proxy/transform/types.ts index d5a5d6a8..d007cb37 100644 --- a/src/server/proxy/transform/types.ts +++ b/src/server/proxy/transform/types.ts @@ -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 }> 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 = { diff --git a/src/services/api/claude.ts b/src/services/api/claude.ts index cdc27c3f..981e5ecb 100644 --- a/src/services/api/claude.ts +++ b/src/services/api/claude.ts @@ -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, @@ -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 diff --git a/src/services/api/claudeEffort.test.ts b/src/services/api/claudeEffort.test.ts new file mode 100644 index 00000000..97df89e5 --- /dev/null +++ b/src/services/api/claudeEffort.test.ts @@ -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 = {} + const extraBodyParams: Record = {} + 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 = {} + const extraBodyParams: Record = {} + 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?.() +}