mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-26 15:03:34 +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' })
|
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', () => {
|
test('assistant message with tool_use', () => {
|
||||||
const req: AnthropicRequest = {
|
const req: AnthropicRequest = {
|
||||||
model: 'gpt-4',
|
model: 'gpt-4',
|
||||||
@ -536,6 +561,31 @@ describe('anthropicToOpenaiResponses', () => {
|
|||||||
expect(result.reasoning).toEqual({ effort: 'high' })
|
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', () => {
|
test('stop_sequences dropped', () => {
|
||||||
const req: AnthropicRequest = {
|
const req: AnthropicRequest = {
|
||||||
model: 'gpt-4o',
|
model: 'gpt-4o',
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import type {
|
|||||||
OpenAITool,
|
OpenAITool,
|
||||||
} from './types.js'
|
} from './types.js'
|
||||||
import { stripLeadingBillingHeader } from './billingHeader.js'
|
import { stripLeadingBillingHeader } from './billingHeader.js'
|
||||||
|
import { normalizeOpenAIReasoningEffort } from './effort.js'
|
||||||
|
|
||||||
type OpenAIChatImageContentMode = 'vision' | 'text_only'
|
type OpenAIChatImageContentMode = 'vision' | 'text_only'
|
||||||
|
|
||||||
@ -114,6 +115,10 @@ export function anthropicToOpenaiChat(
|
|||||||
result.thinking = { type: body.thinking.type }
|
result.thinking = { type: body.thinking.type }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const outputConfigEffort = normalizeOpenAIReasoningEffort(body.output_config?.effort)
|
||||||
|
if (outputConfigEffort !== undefined) {
|
||||||
|
result.reasoning_effort = outputConfigEffort
|
||||||
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import type {
|
|||||||
OpenAIChatContentPart,
|
OpenAIChatContentPart,
|
||||||
} from './types.js'
|
} from './types.js'
|
||||||
import { stripLeadingBillingHeader } from './billingHeader.js'
|
import { stripLeadingBillingHeader } from './billingHeader.js'
|
||||||
|
import { normalizeOpenAIReasoningEffort } from './effort.js'
|
||||||
|
|
||||||
export type OpenAIResponsesTransformOptions = {
|
export type OpenAIResponsesTransformOptions = {
|
||||||
/** Stable cache routing key, forwarded as `prompt_cache_key`. */
|
/** Stable cache routing key, forwarded as `prompt_cache_key`. */
|
||||||
@ -95,6 +96,10 @@ export function anthropicToOpenaiResponses(
|
|||||||
result.reasoning = { effort: 'high' }
|
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
|
// 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 ────────────────────────────────
|
// ─── OpenAI Chat Completions ────────────────────────────────
|
||||||
|
|
||||||
|
export type OpenAIReasoningEffort = 'low' | 'medium' | 'high'
|
||||||
|
|
||||||
export type OpenAIChatMessage = {
|
export type OpenAIChatMessage = {
|
||||||
role: 'system' | 'user' | 'assistant' | 'tool'
|
role: 'system' | 'user' | 'assistant' | 'tool'
|
||||||
content?: string | OpenAIChatContentPart[] | null
|
content?: string | OpenAIChatContentPart[] | null
|
||||||
@ -49,7 +51,7 @@ export type OpenAIChatRequest = {
|
|||||||
stream_options?: { include_usage: boolean }
|
stream_options?: { include_usage: boolean }
|
||||||
tools?: OpenAITool[]
|
tools?: OpenAITool[]
|
||||||
tool_choice?: unknown
|
tool_choice?: unknown
|
||||||
reasoning_effort?: 'low' | 'medium' | 'high'
|
reasoning_effort?: OpenAIReasoningEffort
|
||||||
thinking?: { type: string }
|
thinking?: { type: string }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,7 +138,7 @@ export type OpenAIResponsesRequest = {
|
|||||||
parameters?: Record<string, unknown>
|
parameters?: Record<string, unknown>
|
||||||
}>
|
}>
|
||||||
tool_choice?: unknown
|
tool_choice?: unknown
|
||||||
reasoning?: { effort?: 'low' | 'medium' | 'high' }
|
reasoning?: { effort?: OpenAIReasoningEffort }
|
||||||
prompt_cache_key?: string
|
prompt_cache_key?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -190,6 +192,10 @@ export type AnthropicRequest = {
|
|||||||
type: string
|
type: string
|
||||||
budget_tokens?: number
|
budget_tokens?: number
|
||||||
}
|
}
|
||||||
|
output_config?: {
|
||||||
|
effort?: unknown
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AnthropicResponse = {
|
export type AnthropicResponse = {
|
||||||
|
|||||||
@ -446,7 +446,7 @@ function should1hCacheTTL(querySource?: QuerySource): boolean {
|
|||||||
* Configure effort parameters for API request.
|
* Configure effort parameters for API request.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
function configureEffortParams(
|
export function configureEffortParams(
|
||||||
effortValue: EffortValue | undefined,
|
effortValue: EffortValue | undefined,
|
||||||
outputConfig: BetaOutputConfig,
|
outputConfig: BetaOutputConfig,
|
||||||
extraBodyParams: Record<string, unknown>,
|
extraBodyParams: Record<string, unknown>,
|
||||||
@ -458,6 +458,7 @@ function configureEffortParams(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (effortValue === undefined) {
|
if (effortValue === undefined) {
|
||||||
|
outputConfig.effort = "high";
|
||||||
betas.push(EFFORT_BETA_HEADER);
|
betas.push(EFFORT_BETA_HEADER);
|
||||||
} else if (typeof effortValue === "string") {
|
} else if (typeof effortValue === "string") {
|
||||||
// Send string effort level as is
|
// 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