mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-18 13:23:33 +08:00
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:
parent
1d20434746
commit
2e52133a64
@ -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',
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
13
src/server/proxy/transform/effort.ts
Normal file
13
src/server/proxy/transform/effort.ts
Normal 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
|
||||
}
|
||||
@ -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 = {
|
||||
|
||||
@ -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
|
||||
|
||||
94
src/services/api/claudeEffort.test.ts
Normal file
94
src/services/api/claudeEffort.test.ts
Normal 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?.()
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user