mirror of
https://github.com/Gitlawb/openclaude.git
synced 2026-08-24 02:34:15 -05:00
fix(effort): preserve known model exclusions when force-enabled (#2148)
* fix(effort): preserve known model exclusions when force-enabled * fix(effort): address review feedback * test(effort): cover provider-scoped capability overrides * fix(effort): preserve route-specific reasoning controls * fix(effort): honor scoped routing environment
This commit is contained in:
@@ -22,6 +22,7 @@ import { asSystemPrompt } from '../../utils/systemPromptType.js'
|
||||
import {
|
||||
executeNonStreamingRequest,
|
||||
type Options,
|
||||
queryHaiku,
|
||||
queryModelWithStreaming,
|
||||
} from './claude.js'
|
||||
import { EMPTY_USAGE } from './emptyUsage.js'
|
||||
@@ -32,6 +33,8 @@ const envKeys = [
|
||||
'ANTHROPIC_API_KEY',
|
||||
'ANTHROPIC_BASE_URL',
|
||||
'ANTHROPIC_MODEL',
|
||||
'ANTHROPIC_SMALL_FAST_MODEL',
|
||||
'CLAUDE_CODE_ALWAYS_ENABLE_EFFORT',
|
||||
'CLAUDE_CODE_TEST_FIXTURES_ROOT',
|
||||
'CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED',
|
||||
'CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID',
|
||||
@@ -166,6 +169,50 @@ function makeOpenAIStreamingResponse(): Response {
|
||||
)
|
||||
}
|
||||
|
||||
function makeAnthropicStreamingResponse(): Response {
|
||||
const events = [
|
||||
{
|
||||
event: 'message_start',
|
||||
data: { type: 'message_start', message: makeBetaMessage() },
|
||||
},
|
||||
{
|
||||
event: 'content_block_start',
|
||||
data: {
|
||||
type: 'content_block_start',
|
||||
index: 0,
|
||||
content_block: { type: 'text', text: '' },
|
||||
},
|
||||
},
|
||||
{
|
||||
event: 'content_block_delta',
|
||||
data: {
|
||||
type: 'content_block_delta',
|
||||
index: 0,
|
||||
delta: { type: 'text_delta', text: 'ok' },
|
||||
},
|
||||
},
|
||||
{
|
||||
event: 'content_block_stop',
|
||||
data: { type: 'content_block_stop', index: 0 },
|
||||
},
|
||||
{
|
||||
event: 'message_delta',
|
||||
data: {
|
||||
type: 'message_delta',
|
||||
delta: { stop_reason: 'end_turn', stop_sequence: null },
|
||||
usage: { output_tokens: 1 },
|
||||
},
|
||||
},
|
||||
{ event: 'message_stop', data: { type: 'message_stop' } },
|
||||
]
|
||||
const body = events
|
||||
.map(event => `event: ${event.event}\ndata: ${JSON.stringify(event.data)}\n\n`)
|
||||
.join('')
|
||||
return new Response(body, {
|
||||
headers: { 'content-type': 'text/event-stream' },
|
||||
})
|
||||
}
|
||||
|
||||
function makeStallingOpenAIStreamResponse(
|
||||
onCancel?: (reason: unknown) => void,
|
||||
): Response {
|
||||
@@ -362,6 +409,43 @@ afterEach(() => {
|
||||
})
|
||||
|
||||
describe('Claude API lifecycle tracking', () => {
|
||||
test('Haiku side queries omit effort from native Anthropic requests when force enabled', async () => {
|
||||
setClientTestEnv()
|
||||
process.env.CLAUDE_CODE_ALWAYS_ENABLE_EFFORT = '1'
|
||||
process.env.ANTHROPIC_SMALL_FAST_MODEL = 'claude-haiku-4-5-20251001'
|
||||
process.env.OPENCLAUDE_MAX_RETRIES = '0'
|
||||
const queryLifecycle = new QueryLifecycleOperationTracker()
|
||||
let requestBody: Record<string, unknown> | undefined
|
||||
let requestHeaders: Headers | undefined
|
||||
const fetchOverride: FetchOverride = async (_input, init) => {
|
||||
requestBody = parseRequestBody(init)
|
||||
requestHeaders = new Headers(init?.headers)
|
||||
return makeAnthropicStreamingResponse()
|
||||
}
|
||||
|
||||
await queryHaiku({
|
||||
userPrompt: 'summarize this turn',
|
||||
systemPrompt: asSystemPrompt([]),
|
||||
signal: new AbortController().signal,
|
||||
options: {
|
||||
isNonInteractiveSession: false,
|
||||
querySource: 'sdk',
|
||||
agents: [],
|
||||
hasAppendSystemPrompt: false,
|
||||
mcpTools: [],
|
||||
queryLifecycle,
|
||||
fetchOverride,
|
||||
effortValue: 'medium',
|
||||
},
|
||||
})
|
||||
|
||||
expect(requestBody?.model).toBe('claude-haiku-4-5-20251001')
|
||||
expect(requestBody).not.toHaveProperty('output_config.effort')
|
||||
expect(requestBody).not.toHaveProperty('reasoning_effort')
|
||||
expect(requestBody).not.toHaveProperty('effort')
|
||||
expect(requestHeaders?.get('anthropic-beta')).not.toContain('effort')
|
||||
})
|
||||
|
||||
test('uses the original codexplan selection for custom-gateway defaults', async () => {
|
||||
setClientTestEnv()
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||
|
||||
@@ -22,6 +22,21 @@ const realProviders = {
|
||||
}
|
||||
mock.module('../../utils/model/providers.js', () => realProviders)
|
||||
mock.module('src/utils/model/providers.js', () => realProviders)
|
||||
const _realModelSupportOverridesModule = await import(
|
||||
`../../utils/model/modelSupportOverrides.js?real=${Date.now()}-${Math.random()}`,
|
||||
)
|
||||
const realModelSupportOverrides = {
|
||||
get3PModelCapabilityOverride:
|
||||
_realModelSupportOverridesModule.get3PModelCapabilityOverride,
|
||||
}
|
||||
mock.module(
|
||||
'../../utils/model/modelSupportOverrides.js',
|
||||
() => realModelSupportOverrides,
|
||||
)
|
||||
mock.module(
|
||||
'src/utils/model/modelSupportOverrides.js',
|
||||
() => realModelSupportOverrides,
|
||||
)
|
||||
const { getAnthropicClient } = await import(
|
||||
`./client.js?real=${Date.now()}-${Math.random()}`,
|
||||
)
|
||||
@@ -39,6 +54,8 @@ type ShimClient = {
|
||||
const originalFetch = globalThis.fetch
|
||||
const originalMacro = (globalThis as Record<string, unknown>).MACRO
|
||||
const originalEnv = {
|
||||
CLAUDE_CODE_ALWAYS_ENABLE_EFFORT:
|
||||
process.env.CLAUDE_CODE_ALWAYS_ENABLE_EFFORT,
|
||||
CLAUDE_CODE_USE_OPENAI: process.env.CLAUDE_CODE_USE_OPENAI,
|
||||
CLAUDE_CODE_USE_BEDROCK: process.env.CLAUDE_CODE_USE_BEDROCK,
|
||||
CLAUDE_CODE_SKIP_BEDROCK_AUTH: process.env.CLAUDE_CODE_SKIP_BEDROCK_AUTH,
|
||||
@@ -79,6 +96,18 @@ const originalEnv = {
|
||||
ANTHROPIC_BASE_URL: process.env.ANTHROPIC_BASE_URL,
|
||||
ANTHROPIC_MODEL: process.env.ANTHROPIC_MODEL,
|
||||
ANTHROPIC_CUSTOM_HEADERS: process.env.ANTHROPIC_CUSTOM_HEADERS,
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL:
|
||||
process.env.ANTHROPIC_DEFAULT_OPUS_MODEL,
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES:
|
||||
process.env.ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES,
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL:
|
||||
process.env.ANTHROPIC_DEFAULT_SONNET_MODEL,
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES:
|
||||
process.env.ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES,
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL:
|
||||
process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL,
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES:
|
||||
process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES,
|
||||
USER_TYPE: process.env.USER_TYPE,
|
||||
USE_STAGING_OAUTH: process.env.USE_STAGING_OAUTH,
|
||||
CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED:
|
||||
@@ -133,6 +162,12 @@ function clearEnvForMiniMaxOnlyTest(): void {
|
||||
delete process.env.ANTHROPIC_BASE_URL
|
||||
delete process.env.ANTHROPIC_MODEL
|
||||
delete process.env.ANTHROPIC_CUSTOM_HEADERS
|
||||
delete process.env.ANTHROPIC_DEFAULT_OPUS_MODEL
|
||||
delete process.env.ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES
|
||||
delete process.env.ANTHROPIC_DEFAULT_SONNET_MODEL
|
||||
delete process.env.ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES
|
||||
delete process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL
|
||||
delete process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES
|
||||
delete process.env.USER_TYPE
|
||||
delete process.env.USE_STAGING_OAUTH
|
||||
}
|
||||
@@ -146,6 +181,7 @@ beforeEach(async () => {
|
||||
process.env.GEMINI_BASE_URL = 'https://gemini.example/v1beta/openai'
|
||||
process.env.GEMINI_AUTH_MODE = 'api-key'
|
||||
|
||||
delete process.env.CLAUDE_CODE_ALWAYS_ENABLE_EFFORT
|
||||
delete process.env.CLAUDE_CODE_USE_OPENAI
|
||||
delete process.env.CLAUDE_CODE_USE_BEDROCK
|
||||
delete process.env.CLAUDE_CODE_SKIP_BEDROCK_AUTH
|
||||
@@ -181,6 +217,12 @@ beforeEach(async () => {
|
||||
delete process.env.ANTHROPIC_BASE_URL
|
||||
delete process.env.ANTHROPIC_MODEL
|
||||
delete process.env.ANTHROPIC_CUSTOM_HEADERS
|
||||
delete process.env.ANTHROPIC_DEFAULT_OPUS_MODEL
|
||||
delete process.env.ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES
|
||||
delete process.env.ANTHROPIC_DEFAULT_SONNET_MODEL
|
||||
delete process.env.ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES
|
||||
delete process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL
|
||||
delete process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES
|
||||
delete process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED
|
||||
delete process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID
|
||||
})
|
||||
@@ -188,6 +230,10 @@ beforeEach(async () => {
|
||||
afterEach(() => {
|
||||
try {
|
||||
;(globalThis as Record<string, unknown>).MACRO = originalMacro
|
||||
restoreEnv(
|
||||
'CLAUDE_CODE_ALWAYS_ENABLE_EFFORT',
|
||||
originalEnv.CLAUDE_CODE_ALWAYS_ENABLE_EFFORT,
|
||||
)
|
||||
restoreEnv('CLAUDE_CODE_USE_OPENAI', originalEnv.CLAUDE_CODE_USE_OPENAI)
|
||||
restoreEnv('CLAUDE_CODE_USE_BEDROCK', originalEnv.CLAUDE_CODE_USE_BEDROCK)
|
||||
restoreEnv(
|
||||
@@ -231,6 +277,30 @@ afterEach(() => {
|
||||
restoreEnv('ANTHROPIC_BASE_URL', originalEnv.ANTHROPIC_BASE_URL)
|
||||
restoreEnv('ANTHROPIC_MODEL', originalEnv.ANTHROPIC_MODEL)
|
||||
restoreEnv('ANTHROPIC_CUSTOM_HEADERS', originalEnv.ANTHROPIC_CUSTOM_HEADERS)
|
||||
restoreEnv(
|
||||
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
||||
originalEnv.ANTHROPIC_DEFAULT_OPUS_MODEL,
|
||||
)
|
||||
restoreEnv(
|
||||
'ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES',
|
||||
originalEnv.ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES,
|
||||
)
|
||||
restoreEnv(
|
||||
'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
||||
originalEnv.ANTHROPIC_DEFAULT_SONNET_MODEL,
|
||||
)
|
||||
restoreEnv(
|
||||
'ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES',
|
||||
originalEnv.ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES,
|
||||
)
|
||||
restoreEnv(
|
||||
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
||||
originalEnv.ANTHROPIC_DEFAULT_HAIKU_MODEL,
|
||||
)
|
||||
restoreEnv(
|
||||
'ANTHROPIC_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES',
|
||||
originalEnv.ANTHROPIC_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES,
|
||||
)
|
||||
restoreEnv('USER_TYPE', originalEnv.USER_TYPE)
|
||||
restoreEnv('USE_STAGING_OAUTH', originalEnv.USE_STAGING_OAUTH)
|
||||
restoreEnv(
|
||||
@@ -1854,9 +1924,9 @@ test('auto-routed Azure gpt-5.4 and gpt-5.5 requests preserve selected effort',
|
||||
])
|
||||
})
|
||||
|
||||
test('OPENAI_API_BASE gateway does not inherit first-party GPT-5.6 effort metadata', async () => {
|
||||
test('OPENAI_API_BASE gateway omits the GPT-5.6 default but preserves explicit effort', async () => {
|
||||
let requestUrl = ''
|
||||
let requestBody: Record<string, unknown> | undefined
|
||||
const requestBodies: Record<string, unknown>[] = []
|
||||
delete process.env.CLAUDE_CODE_USE_GEMINI
|
||||
delete process.env.GEMINI_API_KEY
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||
@@ -1865,7 +1935,7 @@ test('OPENAI_API_BASE gateway does not inherit first-party GPT-5.6 effort metada
|
||||
|
||||
globalThis.fetch = (async (input, init) => {
|
||||
requestUrl = String(input)
|
||||
requestBody = JSON.parse(String(init?.body))
|
||||
requestBodies.push(JSON.parse(String(init?.body)))
|
||||
return new Response(JSON.stringify({
|
||||
id: 'chatcmpl-gateway',
|
||||
choices: [{ message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }],
|
||||
@@ -1873,15 +1943,22 @@ test('OPENAI_API_BASE gateway does not inherit first-party GPT-5.6 effort metada
|
||||
}), { headers: { 'Content-Type': 'application/json' } })
|
||||
}) as FetchType
|
||||
|
||||
const client = (await getAnthropicClient({
|
||||
const defaultClient = (await getAnthropicClient({
|
||||
maxRetries: 0,
|
||||
model: 'gpt-5.6-sol',
|
||||
})) as unknown as ShimClient
|
||||
await defaultClient.beta.messages.create({ model: 'gpt-5.6-sol', messages: [{ role: 'user', content: 'hello' }], max_tokens: 64, stream: false })
|
||||
|
||||
const explicitClient = (await getAnthropicClient({
|
||||
maxRetries: 0,
|
||||
model: 'gpt-5.6-sol',
|
||||
effortValue: 'xhigh',
|
||||
})) as unknown as ShimClient
|
||||
await client.beta.messages.create({ model: 'gpt-5.6-sol', messages: [{ role: 'user', content: 'hello' }], max_tokens: 64, stream: false })
|
||||
await explicitClient.beta.messages.create({ model: 'gpt-5.6-sol', messages: [{ role: 'user', content: 'hello' }], max_tokens: 64, stream: false })
|
||||
|
||||
expect(requestUrl).toBe('https://gateway.example/v1/chat/completions')
|
||||
expect(requestBody?.reasoning_effort).toBeUndefined()
|
||||
expect(requestBodies[0]?.reasoning_effort).toBeUndefined()
|
||||
expect(requestBodies[1]?.reasoning_effort).toBe('xhigh')
|
||||
})
|
||||
|
||||
test('providerOverride Azure gpt effort uses the override base for catalog metadata', async () => {
|
||||
@@ -2009,7 +2086,7 @@ test('providerOverride custom OpenAI-compatible gpt effort uses legacy support',
|
||||
|
||||
const client = (await getAnthropicClient({
|
||||
maxRetries: 0,
|
||||
effortValue: 'high',
|
||||
effortValue: 'medium',
|
||||
providerOverride: {
|
||||
model: 'gpt-5.4',
|
||||
baseURL: 'https://custom-openai-compatible.example.test/v1',
|
||||
@@ -2025,8 +2102,372 @@ test('providerOverride custom OpenAI-compatible gpt effort uses legacy support',
|
||||
stream: false,
|
||||
})
|
||||
|
||||
expect(requestBody?.reasoning_effort).toBe('high')
|
||||
expect(requestBody?.reasoning_effort).toBe('medium')
|
||||
})
|
||||
|
||||
test('providerOverride custom shims do not infer native Claude or Gemini effort', async () => {
|
||||
const requestBodies: Record<string, unknown>[] = []
|
||||
|
||||
globalThis.fetch = (async (_input, init) => {
|
||||
const requestBody = JSON.parse(String(init?.body))
|
||||
requestBodies.push(requestBody)
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
id: 'chatcmpl-provider-override-native-name',
|
||||
model: requestBody.model,
|
||||
choices: [
|
||||
{
|
||||
message: { role: 'assistant', content: 'ok' },
|
||||
finish_reason: 'stop',
|
||||
},
|
||||
],
|
||||
usage: {
|
||||
prompt_tokens: 8,
|
||||
completion_tokens: 3,
|
||||
total_tokens: 11,
|
||||
},
|
||||
}),
|
||||
{ headers: { 'Content-Type': 'application/json' } },
|
||||
)
|
||||
}) as FetchType
|
||||
|
||||
for (const model of ['claude-opus-4-5', 'gemini-3-pro']) {
|
||||
const client = (await getAnthropicClient({
|
||||
maxRetries: 0,
|
||||
effortValue: 'medium',
|
||||
providerOverride: {
|
||||
model,
|
||||
baseURL: 'https://custom-openai-compatible.example.test/v1',
|
||||
apiKey: 'provider-test-key',
|
||||
},
|
||||
})) as unknown as ShimClient
|
||||
|
||||
await client.beta.messages.create({
|
||||
model: 'unused',
|
||||
messages: [{ role: 'user', content: 'hello' }],
|
||||
max_tokens: 64,
|
||||
stream: false,
|
||||
})
|
||||
}
|
||||
|
||||
expect(requestBodies.map(body => body.model)).toEqual([
|
||||
'claude-opus-4-5',
|
||||
'gemini-3-pro',
|
||||
])
|
||||
for (const requestBody of requestBodies) {
|
||||
expect(requestBody.reasoning_effort).toBeUndefined()
|
||||
}
|
||||
|
||||
process.env.CLAUDE_CODE_ALWAYS_ENABLE_EFFORT = '1'
|
||||
const forceEnabledGemini = (await getAnthropicClient({
|
||||
maxRetries: 0,
|
||||
effortValue: 'medium',
|
||||
providerOverride: {
|
||||
model: 'gemini-3-pro',
|
||||
baseURL: 'https://custom-openai-compatible.example.test/v1',
|
||||
apiKey: 'provider-test-key',
|
||||
},
|
||||
})) as unknown as ShimClient
|
||||
await forceEnabledGemini.beta.messages.create({
|
||||
model: 'unused',
|
||||
messages: [{ role: 'user', content: 'hello' }],
|
||||
max_tokens: 64,
|
||||
stream: false,
|
||||
})
|
||||
expect(requestBodies).toHaveLength(3)
|
||||
expect(requestBodies[2]!.reasoning_effort).toBeUndefined()
|
||||
})
|
||||
|
||||
test('NVIDIA NIM does not infer native Claude effort from the model name', async () => {
|
||||
let requestBody: Record<string, unknown> | undefined
|
||||
delete process.env.CLAUDE_CODE_USE_GEMINI
|
||||
delete process.env.GEMINI_API_KEY
|
||||
process.env.NVIDIA_NIM = '1'
|
||||
process.env.NVIDIA_API_KEY = 'nvidia-test-key'
|
||||
process.env.OPENAI_BASE_URL = 'https://integrate.api.nvidia.com/v1'
|
||||
process.env.OPENAI_MODEL = 'claude-opus-4-5'
|
||||
|
||||
globalThis.fetch = (async (_input, init) => {
|
||||
requestBody = JSON.parse(String(init?.body))
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
id: 'chatcmpl-nvidia-native-name',
|
||||
model: 'claude-opus-4-5',
|
||||
choices: [
|
||||
{
|
||||
message: { role: 'assistant', content: 'ok' },
|
||||
finish_reason: 'stop',
|
||||
},
|
||||
],
|
||||
usage: {
|
||||
prompt_tokens: 8,
|
||||
completion_tokens: 3,
|
||||
total_tokens: 11,
|
||||
},
|
||||
}),
|
||||
{ headers: { 'Content-Type': 'application/json' } },
|
||||
)
|
||||
}) as FetchType
|
||||
|
||||
const client = (await getAnthropicClient({
|
||||
maxRetries: 0,
|
||||
model: 'claude-opus-4-5',
|
||||
effortValue: 'medium',
|
||||
})) as unknown as ShimClient
|
||||
await client.beta.messages.create({
|
||||
model: 'claude-opus-4-5',
|
||||
messages: [{ role: 'user', content: 'hello' }],
|
||||
max_tokens: 64,
|
||||
stream: false,
|
||||
})
|
||||
|
||||
expect(requestBody?.reasoning_effort).toBeUndefined()
|
||||
})
|
||||
|
||||
test('OpenCode native endpoints preserve authorized Claude and Gemini effort', async () => {
|
||||
const requests: Array<{
|
||||
url: string
|
||||
body: Record<string, unknown>
|
||||
}> = []
|
||||
delete process.env.CLAUDE_CODE_USE_GEMINI
|
||||
delete process.env.GEMINI_API_KEY
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||
process.env.OPENAI_API_KEY = 'opencode-test-key'
|
||||
process.env.OPENAI_BASE_URL = 'https://opencode.ai/zen/v1'
|
||||
|
||||
globalThis.fetch = (async (input, init) => {
|
||||
const url = String(input)
|
||||
requests.push({ url, body: JSON.parse(String(init?.body)) })
|
||||
if (url.includes('/messages')) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
id: 'msg_opencode_effort',
|
||||
type: 'message',
|
||||
role: 'assistant',
|
||||
model: 'claude-opus-4-5',
|
||||
content: [{ type: 'text', text: 'ok' }],
|
||||
stop_reason: 'end_turn',
|
||||
stop_sequence: null,
|
||||
usage: { input_tokens: 8, output_tokens: 3 },
|
||||
}),
|
||||
{ headers: { 'Content-Type': 'application/json' } },
|
||||
)
|
||||
}
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
candidates: [
|
||||
{
|
||||
content: { role: 'model', parts: [{ text: 'ok' }] },
|
||||
finishReason: 'STOP',
|
||||
},
|
||||
],
|
||||
usageMetadata: {
|
||||
promptTokenCount: 8,
|
||||
candidatesTokenCount: 3,
|
||||
totalTokenCount: 11,
|
||||
},
|
||||
}),
|
||||
{ headers: { 'Content-Type': 'application/json' } },
|
||||
)
|
||||
}) as FetchType
|
||||
|
||||
for (const model of ['claude-opus-4-5', 'gemini-3.1-pro']) {
|
||||
process.env.OPENAI_MODEL = model
|
||||
const client = (await getAnthropicClient({
|
||||
maxRetries: 0,
|
||||
model,
|
||||
effortValue: 'medium',
|
||||
})) as unknown as ShimClient
|
||||
await client.beta.messages.create({
|
||||
model,
|
||||
messages: [{ role: 'user', content: 'hello' }],
|
||||
max_tokens: 64,
|
||||
stream: false,
|
||||
})
|
||||
}
|
||||
|
||||
expect(requests[0]?.url).toEndWith('/messages')
|
||||
expect(requests[0]?.body.effort).toBe('medium')
|
||||
expect(requests[1]?.url).toContain('/models/gemini-3.1-pro')
|
||||
expect(requests[1]?.body.generationConfig).toMatchObject({
|
||||
thinkingConfig: {
|
||||
includeThoughts: true,
|
||||
thinkingLevel: 'medium',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('force enable adds effort only for an otherwise unresolved custom shim model', async () => {
|
||||
const requestBodies: Record<string, unknown>[] = []
|
||||
|
||||
globalThis.fetch = (async (_input, init) => {
|
||||
requestBodies.push(JSON.parse(String(init?.body)))
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
id: 'chatcmpl-provider-override-force-effort',
|
||||
model: 'gateway-custom-model',
|
||||
choices: [
|
||||
{
|
||||
message: { role: 'assistant', content: 'ok' },
|
||||
finish_reason: 'stop',
|
||||
},
|
||||
],
|
||||
usage: {
|
||||
prompt_tokens: 8,
|
||||
completion_tokens: 3,
|
||||
total_tokens: 11,
|
||||
},
|
||||
}),
|
||||
{ headers: { 'Content-Type': 'application/json' } },
|
||||
)
|
||||
}) as FetchType
|
||||
|
||||
for (const forceEnabled of [false, true]) {
|
||||
if (forceEnabled) {
|
||||
process.env.CLAUDE_CODE_ALWAYS_ENABLE_EFFORT = '1'
|
||||
} else {
|
||||
delete process.env.CLAUDE_CODE_ALWAYS_ENABLE_EFFORT
|
||||
}
|
||||
const client = (await getAnthropicClient({
|
||||
maxRetries: 0,
|
||||
effortValue: 'medium',
|
||||
providerOverride: {
|
||||
model: 'gateway-custom-model',
|
||||
baseURL: 'https://custom-openai-compatible.example.test/v1',
|
||||
apiKey: 'provider-test-key',
|
||||
},
|
||||
})) as unknown as ShimClient
|
||||
|
||||
await client.beta.messages.create({
|
||||
model: 'unused',
|
||||
messages: [{ role: 'user', content: 'hello' }],
|
||||
max_tokens: 64,
|
||||
stream: false,
|
||||
})
|
||||
}
|
||||
|
||||
expect(requestBodies[0]?.reasoning_effort).toBeUndefined()
|
||||
expect(requestBodies[1]?.reasoning_effort).toBe('medium')
|
||||
})
|
||||
|
||||
test('providerOverride third-party false beats force enable under a first-party parent', async () => {
|
||||
let requestBody: Record<string, unknown> | undefined
|
||||
delete process.env.CLAUDE_CODE_USE_GEMINI
|
||||
process.env.CLAUDE_CODE_ALWAYS_ENABLE_EFFORT = '1'
|
||||
process.env.ANTHROPIC_DEFAULT_SONNET_MODEL = 'gateway-custom-model'
|
||||
process.env.ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES = ''
|
||||
|
||||
globalThis.fetch = (async (_input, init) => {
|
||||
requestBody = JSON.parse(String(init?.body))
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
id: 'chatcmpl-provider-override-force-false',
|
||||
model: 'gateway-custom-model',
|
||||
choices: [
|
||||
{
|
||||
message: { role: 'assistant', content: 'ok' },
|
||||
finish_reason: 'stop',
|
||||
},
|
||||
],
|
||||
usage: {
|
||||
prompt_tokens: 8,
|
||||
completion_tokens: 3,
|
||||
total_tokens: 11,
|
||||
},
|
||||
}),
|
||||
{ headers: { 'Content-Type': 'application/json' } },
|
||||
)
|
||||
}) as FetchType
|
||||
|
||||
const client = (await getAnthropicClient({
|
||||
maxRetries: 0,
|
||||
effortValue: 'medium',
|
||||
providerOverride: {
|
||||
model: 'gateway-custom-model',
|
||||
baseURL: 'https://custom-openai-compatible.example.test/v1',
|
||||
apiKey: 'provider-test-key',
|
||||
},
|
||||
})) as unknown as ShimClient
|
||||
|
||||
await client.beta.messages.create({
|
||||
model: 'unused',
|
||||
messages: [{ role: 'user', content: 'hello' }],
|
||||
max_tokens: 64,
|
||||
stream: false,
|
||||
})
|
||||
|
||||
expect(requestBody?.reasoning_effort).toBeUndefined()
|
||||
})
|
||||
|
||||
test('force enable respects a shim non-effort contract before request serialization', async () => {
|
||||
let requestBody: Record<string, unknown> | undefined
|
||||
process.env.CLAUDE_CODE_ALWAYS_ENABLE_EFFORT = '1'
|
||||
|
||||
globalThis.fetch = (async (_input, init) => {
|
||||
requestBody = JSON.parse(String(init?.body))
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
id: 'chatcmpl-provider-override-force-veto',
|
||||
model: 'gateway-custom-model',
|
||||
choices: [
|
||||
{
|
||||
message: { role: 'assistant', content: 'ok' },
|
||||
finish_reason: 'stop',
|
||||
},
|
||||
],
|
||||
usage: {
|
||||
prompt_tokens: 8,
|
||||
completion_tokens: 3,
|
||||
total_tokens: 11,
|
||||
},
|
||||
}),
|
||||
{ headers: { 'Content-Type': 'application/json' } },
|
||||
)
|
||||
}) as FetchType
|
||||
|
||||
_clearRegistryForTesting()
|
||||
try {
|
||||
registerGateway({
|
||||
id: 'non-effort-transport-test',
|
||||
label: 'Non-effort Transport Test',
|
||||
defaultBaseUrl: 'https://non-effort-transport.example.test/v1',
|
||||
setup: { requiresAuth: true, authMode: 'api-key' },
|
||||
transportConfig: {
|
||||
kind: 'openai-compatible',
|
||||
openaiShim: { thinkingRequestFormat: 'none' },
|
||||
},
|
||||
catalog: { source: 'static', models: [] },
|
||||
})
|
||||
|
||||
const client = (await getAnthropicClient({
|
||||
maxRetries: 0,
|
||||
effortValue: 'medium',
|
||||
providerOverride: {
|
||||
model: 'gateway-custom-model',
|
||||
baseURL: 'https://non-effort-transport.example.test/v1',
|
||||
apiKey: 'provider-test-key',
|
||||
},
|
||||
})) as unknown as ShimClient
|
||||
|
||||
// Remove the synthetic route before serialization so no later request-body
|
||||
// cleanup can hide an incorrect reasoningEffort value captured by the client.
|
||||
_clearRegistryForTesting()
|
||||
ensureIntegrationsLoaded()
|
||||
|
||||
await client.beta.messages.create({
|
||||
model: 'unused',
|
||||
messages: [{ role: 'user', content: 'hello' }],
|
||||
max_tokens: 64,
|
||||
stream: false,
|
||||
})
|
||||
} finally {
|
||||
_clearRegistryForTesting()
|
||||
ensureIntegrationsLoaded()
|
||||
}
|
||||
|
||||
expect(requestBody?.reasoning_effort).toBeUndefined()
|
||||
})
|
||||
|
||||
test('providerOverride clamps stale effort against metadata levels', async () => {
|
||||
let requestBody: Record<string, unknown> | undefined
|
||||
|
||||
|
||||
@@ -504,11 +504,9 @@ export async function getAnthropicClient({
|
||||
openaiShimConfig: effortShimConfig,
|
||||
baseUrl: effortBaseUrl,
|
||||
processEnv: effortProcessEnv,
|
||||
apiProvider: effortRuntimeContext.routeId === 'openai'
|
||||
? 'openai' as const
|
||||
: effortRuntimeContext.routeId === 'codex'
|
||||
? 'codex' as const
|
||||
: undefined,
|
||||
// Per-agent overrides always use the OpenAI-compatible shim, regardless
|
||||
// of the parent session's provider.
|
||||
apiProvider: providerOverride ? 'openai' as const : getAPIProvider(),
|
||||
}
|
||||
: undefined
|
||||
const supportsShimReasoningEffort = effortModel
|
||||
@@ -519,7 +517,7 @@ export async function getAnthropicClient({
|
||||
effortShimConfig.removeBodyFields,
|
||||
effortContext,
|
||||
)
|
||||
: modelSupportsWireEffort(effortModel)
|
||||
: modelSupportsWireEffort(effortModel, effortContext)
|
||||
: false
|
||||
const reasoningControl = effortModel
|
||||
? resolveModelReasoningControl(effortModel, effortContext)
|
||||
|
||||
+329
-10
@@ -13,6 +13,35 @@ import * as actualAuth from './auth.js'
|
||||
import * as actualThinking from './thinking.js'
|
||||
import * as actualGrowthbook from 'src/services/analytics/growthbook.js'
|
||||
import * as actualModelSupportOverrides from './model/modelSupportOverrides.js'
|
||||
import type { APIProvider } from './model/providers.js'
|
||||
|
||||
type MockedThirdPartyCapability = 'effort' | 'max_effort' | 'xhigh_effort'
|
||||
|
||||
const originalEnv = { ...process.env }
|
||||
const routingEnvKeys = [
|
||||
'ANTHROPIC_BASE_URL',
|
||||
'CLAUDE_CODE_ALWAYS_ENABLE_EFFORT',
|
||||
'CLAUDE_CODE_EFFORT_LEVEL',
|
||||
'CLAUDE_CODE_USE_BEDROCK',
|
||||
'CLAUDE_CODE_USE_FOUNDRY',
|
||||
'CLAUDE_CODE_USE_GEMINI',
|
||||
'CLAUDE_CODE_USE_GITHUB',
|
||||
'CLAUDE_CODE_USE_MISTRAL',
|
||||
'CLAUDE_CODE_USE_OPENAI',
|
||||
'CLAUDE_CODE_USE_VERTEX',
|
||||
'GEMINI_API_KEY',
|
||||
'MIMO_API_KEY',
|
||||
'MINIMAX_API_KEY',
|
||||
'NVIDIA_API_KEY',
|
||||
'NVIDIA_NIM',
|
||||
'OPENAI_API_BASE',
|
||||
'OPENAI_API_KEY',
|
||||
'OPENAI_BASE_URL',
|
||||
'OPENAI_MODEL',
|
||||
'XAI_API_KEY',
|
||||
'ZAI_API_KEY',
|
||||
'USER_TYPE',
|
||||
] as const
|
||||
|
||||
function restoreMockedModulesToActual(): void {
|
||||
mock.module('./model/modelSupportOverrides.js', () => actualModelSupportOverrides)
|
||||
@@ -21,31 +50,57 @@ function restoreMockedModulesToActual(): void {
|
||||
mock.module('src/services/analytics/growthbook.js', () => actualGrowthbook)
|
||||
}
|
||||
|
||||
function restoreProcessEnv(): void {
|
||||
for (const key of Object.keys(process.env)) {
|
||||
if (!Object.hasOwn(originalEnv, key)) delete process.env[key]
|
||||
}
|
||||
for (const [key, value] of Object.entries(originalEnv)) {
|
||||
if (value === undefined) delete process.env[key]
|
||||
else process.env[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('utils/effort.codex.test.ts')
|
||||
for (const key of routingEnvKeys) {
|
||||
delete process.env[key]
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
try {
|
||||
mock.restore()
|
||||
restoreMockedModulesToActual()
|
||||
restoreProcessEnv()
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
async function importFreshEffortModule(options: {
|
||||
provider: 'codex' | 'openai'
|
||||
provider: APIProvider
|
||||
supportsCodexReasoningEffort: boolean
|
||||
routeId?: string
|
||||
catalogEntries?: any[]
|
||||
modelDescriptors?: Record<string, any>
|
||||
openaiShimConfig?: any
|
||||
thirdPartyCapabilityOverrides?: {
|
||||
apiProvider: APIProvider
|
||||
capabilities: Partial<Record<MockedThirdPartyCapability, boolean>>
|
||||
}
|
||||
useRuntimeFallback?: boolean
|
||||
}) {
|
||||
mock.module('./model/modelSupportOverrides.js', () => ({
|
||||
...actualModelSupportOverrides,
|
||||
get3PModelCapabilityOverride: () => undefined,
|
||||
get3PModelCapabilityOverride: (
|
||||
_model: string,
|
||||
capability: MockedThirdPartyCapability,
|
||||
apiProvider?: APIProvider,
|
||||
) => {
|
||||
const override = options.thirdPartyCapabilityOverrides
|
||||
if (!override || apiProvider !== override.apiProvider) return undefined
|
||||
return override.capabilities[capability]
|
||||
},
|
||||
}))
|
||||
mock.module('./auth.js', () => ({
|
||||
...actualAuth,
|
||||
@@ -70,7 +125,8 @@ async function importFreshEffortModule(options: {
|
||||
options.routeId !== undefined ||
|
||||
options.catalogEntries !== undefined ||
|
||||
options.modelDescriptors !== undefined ||
|
||||
options.openaiShimConfig !== undefined
|
||||
options.openaiShimConfig !== undefined ||
|
||||
options.useRuntimeFallback !== undefined
|
||||
)
|
||||
? {
|
||||
apiProvider: options.provider,
|
||||
@@ -79,6 +135,7 @@ async function importFreshEffortModule(options: {
|
||||
catalogEntries: options.catalogEntries,
|
||||
modelDescriptors: options.modelDescriptors,
|
||||
openaiShimConfig: options.openaiShimConfig,
|
||||
useRuntimeFallback: options.useRuntimeFallback,
|
||||
}
|
||||
: undefined
|
||||
|
||||
@@ -86,6 +143,17 @@ async function importFreshEffortModule(options: {
|
||||
...effort,
|
||||
resolveModelReasoningControl: (model: string) =>
|
||||
effort.resolveModelReasoningControl(model, reasoningContext),
|
||||
resolveModelReasoningControlWithCompatibility: (
|
||||
model: string,
|
||||
compatibilityOverrides: {
|
||||
thinkingRequestFormat?: 'none' | 'deepseek-compatible' | 'zai-compatible'
|
||||
removeBodyFields?: string[]
|
||||
},
|
||||
) => effort.resolveModelReasoningControl(
|
||||
model,
|
||||
reasoningContext,
|
||||
compatibilityOverrides,
|
||||
),
|
||||
modelSupportsEffort: (model: string) =>
|
||||
effort.modelSupportsEffort(model, reasoningContext),
|
||||
modelSupportsWireEffort: (model: string) =>
|
||||
@@ -319,7 +387,7 @@ test('e2e: xhigh → persisted xhigh → resolveAppliedEffort → wire xhigh on
|
||||
|
||||
test('e2e: max on non-Opus Anthropic model still clamps to high', async () => {
|
||||
const { resolveAppliedEffort } = await importFreshEffortModule({
|
||||
provider: 'firstParty' as unknown as 'openai',
|
||||
provider: 'firstParty',
|
||||
supportsCodexReasoningEffort: false,
|
||||
})
|
||||
|
||||
@@ -328,7 +396,7 @@ test('e2e: max on non-Opus Anthropic model still clamps to high', async () => {
|
||||
|
||||
test('modelSupportsXHighEffort: opus-4-7 and opus-4-8 are allowed; other Claude models are not', async () => {
|
||||
const { modelSupportsXHighEffort } = await importFreshEffortModule({
|
||||
provider: 'firstParty' as unknown as 'openai',
|
||||
provider: 'firstParty',
|
||||
supportsCodexReasoningEffort: false,
|
||||
})
|
||||
|
||||
@@ -344,7 +412,7 @@ test('modelSupportsXHighEffort: opus-4-7 and opus-4-8 are allowed; other Claude
|
||||
|
||||
test('xhigh does not appear in available levels for non-supporting models', async () => {
|
||||
const { getAvailableEffortLevels } = await importFreshEffortModule({
|
||||
provider: 'firstParty' as unknown as 'openai',
|
||||
provider: 'firstParty',
|
||||
supportsCodexReasoningEffort: false,
|
||||
})
|
||||
|
||||
@@ -368,7 +436,7 @@ test('effort allowlist is narrowed to the shim isAdaptive||isOpus45 set', async
|
||||
// effort for them would silently drop low/medium on the wire.
|
||||
const { modelSupportsEffort, getAvailableEffortLevels } =
|
||||
await importFreshEffortModule({
|
||||
provider: 'firstParty' as unknown as 'openai',
|
||||
provider: 'firstParty',
|
||||
supportsCodexReasoningEffort: false,
|
||||
})
|
||||
|
||||
@@ -398,7 +466,7 @@ test('effort allowlist is narrowed to the shim isAdaptive||isOpus45 set', async
|
||||
|
||||
test('xhigh clamps to high on non-supporting models so stale settings.json values do not produce API errors', async () => {
|
||||
const { resolveAppliedEffort } = await importFreshEffortModule({
|
||||
provider: 'firstParty' as unknown as 'openai',
|
||||
provider: 'firstParty',
|
||||
supportsCodexReasoningEffort: false,
|
||||
})
|
||||
|
||||
@@ -412,6 +480,9 @@ test('clampUltracodeEffort: clamps to xhigh on non-firstParty xhigh-capable mode
|
||||
const { clampUltracodeEffort, resolveAppliedEffort } = await importFreshEffortModule({
|
||||
provider: 'openai',
|
||||
supportsCodexReasoningEffort: true,
|
||||
routeId: 'opencode',
|
||||
useRuntimeFallback: false,
|
||||
openaiShimConfig: { endpointPath: '/messages' },
|
||||
})
|
||||
|
||||
// ultracode isn't selectable off firstParty, so it clamps — but to xhigh
|
||||
@@ -428,7 +499,7 @@ test('clampUltracodeEffort: clamps to xhigh on non-firstParty xhigh-capable mode
|
||||
|
||||
test('clampUltracodeEffort: clamps to high on firstParty non-xhigh model', async () => {
|
||||
const { clampUltracodeEffort, resolveAppliedEffort } = await importFreshEffortModule({
|
||||
provider: 'firstParty' as unknown as 'openai',
|
||||
provider: 'firstParty',
|
||||
supportsCodexReasoningEffort: false,
|
||||
})
|
||||
|
||||
@@ -1014,6 +1085,253 @@ test('explicit non-controllable metadata opts out even when the model matches le
|
||||
expect(resolveAppliedEffort('gpt-5.4', 'high')).toBeUndefined()
|
||||
})
|
||||
|
||||
test('force enable cannot override non-effort metadata or transport contracts', async () => {
|
||||
process.env.CLAUDE_CODE_ALWAYS_ENABLE_EFFORT = '1'
|
||||
const metadata = await importFreshEffortModule({
|
||||
provider: 'openai',
|
||||
supportsCodexReasoningEffort: true,
|
||||
routeId: 'custom-gateway',
|
||||
useRuntimeFallback: false,
|
||||
thirdPartyCapabilityOverrides: {
|
||||
apiProvider: 'openai',
|
||||
capabilities: { effort: true },
|
||||
},
|
||||
catalogEntries: [
|
||||
{
|
||||
id: 'metadata-no-effort',
|
||||
apiName: 'metadata-no-effort',
|
||||
capabilities: { supportsReasoning: true },
|
||||
reasoning: {
|
||||
mode: 'always-on',
|
||||
wireFormat: 'none',
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
expect(metadata.modelSupportsEffort('metadata-no-effort')).toBe(false)
|
||||
expect(metadata.modelSupportsShimReasoningEffort('metadata-no-effort')).toBe(false)
|
||||
expect(metadata.modelSupportsWireEffort('metadata-no-effort')).toBe(false)
|
||||
expect(metadata.resolveAppliedEffort('metadata-no-effort', 'high')).toBeUndefined()
|
||||
|
||||
const transport = await importFreshEffortModule({
|
||||
provider: 'openai',
|
||||
supportsCodexReasoningEffort: true,
|
||||
routeId: 'custom',
|
||||
useRuntimeFallback: false,
|
||||
thirdPartyCapabilityOverrides: {
|
||||
apiProvider: 'openai',
|
||||
capabilities: { effort: true },
|
||||
},
|
||||
openaiShimConfig: { thinkingRequestFormat: 'none' },
|
||||
})
|
||||
|
||||
expect(transport.modelSupportsEffort('transport-no-effort')).toBe(false)
|
||||
expect(transport.modelSupportsShimReasoningEffort('transport-no-effort')).toBe(false)
|
||||
expect(transport.modelSupportsWireEffort('transport-no-effort')).toBe(false)
|
||||
expect(transport.resolveAppliedEffort('transport-no-effort', 'high')).toBeUndefined()
|
||||
|
||||
const metadataWithTransportVeto = await importFreshEffortModule({
|
||||
provider: 'openai',
|
||||
supportsCodexReasoningEffort: true,
|
||||
routeId: 'custom-gateway',
|
||||
useRuntimeFallback: false,
|
||||
thirdPartyCapabilityOverrides: {
|
||||
apiProvider: 'openai',
|
||||
capabilities: { effort: true },
|
||||
},
|
||||
openaiShimConfig: { thinkingRequestFormat: 'none' },
|
||||
catalogEntries: [
|
||||
{
|
||||
id: 'metadata-with-transport-veto',
|
||||
apiName: 'metadata-with-transport-veto',
|
||||
capabilities: { supportsReasoning: true },
|
||||
reasoning: {
|
||||
mode: 'levels',
|
||||
levels: ['low', 'medium', 'high'],
|
||||
wireFormat: 'reasoning_effort',
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
expect(
|
||||
metadataWithTransportVeto.modelSupportsEffort(
|
||||
'metadata-with-transport-veto',
|
||||
),
|
||||
).toBe(false)
|
||||
expect(
|
||||
metadataWithTransportVeto.modelSupportsShimReasoningEffort(
|
||||
'metadata-with-transport-veto',
|
||||
),
|
||||
).toBe(false)
|
||||
expect(
|
||||
metadataWithTransportVeto.modelSupportsWireEffort(
|
||||
'metadata-with-transport-veto',
|
||||
),
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
test('resolver and shim effort predicate accept explicit transport vetoes', async () => {
|
||||
process.env.CLAUDE_CODE_ALWAYS_ENABLE_EFFORT = '1'
|
||||
const {
|
||||
modelSupportsShimReasoningEffort,
|
||||
resolveModelReasoningControlWithCompatibility,
|
||||
} = await importFreshEffortModule({
|
||||
provider: 'openai',
|
||||
supportsCodexReasoningEffort: false,
|
||||
routeId: 'custom',
|
||||
useRuntimeFallback: false,
|
||||
})
|
||||
|
||||
expect(
|
||||
resolveModelReasoningControlWithCompatibility(
|
||||
'transport-no-effort',
|
||||
{ thinkingRequestFormat: 'none' },
|
||||
),
|
||||
).toMatchObject({
|
||||
supportsReasoning: false,
|
||||
controllable: false,
|
||||
source: 'compat',
|
||||
})
|
||||
|
||||
expect(
|
||||
modelSupportsShimReasoningEffort('transport-no-effort', 'none'),
|
||||
).toBe(false)
|
||||
expect(
|
||||
modelSupportsShimReasoningEffort(
|
||||
'transport-no-effort',
|
||||
undefined,
|
||||
['reasoning_effort'],
|
||||
),
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
test('third-party false beats force enable for unresolved models', async () => {
|
||||
process.env.CLAUDE_CODE_ALWAYS_ENABLE_EFFORT = '1'
|
||||
const {
|
||||
modelSupportsEffort,
|
||||
modelSupportsShimReasoningEffort,
|
||||
modelSupportsWireEffort,
|
||||
resolveAppliedEffort,
|
||||
} = await importFreshEffortModule({
|
||||
provider: 'openai',
|
||||
supportsCodexReasoningEffort: false,
|
||||
routeId: 'custom',
|
||||
useRuntimeFallback: false,
|
||||
thirdPartyCapabilityOverrides: {
|
||||
apiProvider: 'openai',
|
||||
capabilities: { effort: false },
|
||||
},
|
||||
})
|
||||
|
||||
expect(modelSupportsEffort('third-party-custom-model')).toBe(false)
|
||||
expect(modelSupportsShimReasoningEffort('third-party-custom-model')).toBe(false)
|
||||
expect(modelSupportsWireEffort('third-party-custom-model')).toBe(false)
|
||||
expect(resolveAppliedEffort('third-party-custom-model', 'high')).toBeUndefined()
|
||||
})
|
||||
|
||||
test('third-party effort overrides require the matching API provider', async () => {
|
||||
const matchingProvider = await importFreshEffortModule({
|
||||
provider: 'bedrock',
|
||||
supportsCodexReasoningEffort: false,
|
||||
routeId: 'bedrock',
|
||||
useRuntimeFallback: false,
|
||||
thirdPartyCapabilityOverrides: {
|
||||
apiProvider: 'bedrock',
|
||||
capabilities: {
|
||||
effort: true,
|
||||
max_effort: true,
|
||||
xhigh_effort: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(
|
||||
matchingProvider.modelSupportsEffort('provider-scoped-model'),
|
||||
).toBe(true)
|
||||
expect(
|
||||
matchingProvider.getAvailableEffortLevels('provider-scoped-model'),
|
||||
).toEqual(['low', 'medium', 'high', 'max'])
|
||||
|
||||
const xhighProvider = await importFreshEffortModule({
|
||||
provider: 'foundry',
|
||||
supportsCodexReasoningEffort: false,
|
||||
routeId: 'foundry',
|
||||
useRuntimeFallback: false,
|
||||
thirdPartyCapabilityOverrides: {
|
||||
apiProvider: 'foundry',
|
||||
capabilities: {
|
||||
effort: true,
|
||||
max_effort: false,
|
||||
xhigh_effort: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(
|
||||
xhighProvider.getAvailableEffortLevels('provider-scoped-model'),
|
||||
).toEqual(['low', 'medium', 'high', 'xhigh'])
|
||||
|
||||
const differentProvider = await importFreshEffortModule({
|
||||
provider: 'vertex',
|
||||
supportsCodexReasoningEffort: false,
|
||||
routeId: 'vertex',
|
||||
useRuntimeFallback: false,
|
||||
thirdPartyCapabilityOverrides: {
|
||||
apiProvider: 'bedrock',
|
||||
capabilities: {
|
||||
effort: true,
|
||||
max_effort: true,
|
||||
xhigh_effort: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(
|
||||
differentProvider.modelSupportsEffort('provider-scoped-model'),
|
||||
).toBe(false)
|
||||
expect(
|
||||
differentProvider.getAvailableEffortLevels('provider-scoped-model'),
|
||||
).toEqual([])
|
||||
})
|
||||
|
||||
test('explicit effort metadata beats a third-party false override', async () => {
|
||||
process.env.CLAUDE_CODE_ALWAYS_ENABLE_EFFORT = '1'
|
||||
const {
|
||||
modelSupportsEffort,
|
||||
modelSupportsShimReasoningEffort,
|
||||
modelSupportsWireEffort,
|
||||
resolveAppliedEffort,
|
||||
} = await importFreshEffortModule({
|
||||
provider: 'openai',
|
||||
supportsCodexReasoningEffort: false,
|
||||
routeId: 'custom-gateway',
|
||||
useRuntimeFallback: false,
|
||||
thirdPartyCapabilityOverrides: {
|
||||
apiProvider: 'openai',
|
||||
capabilities: { effort: false },
|
||||
},
|
||||
catalogEntries: [
|
||||
{
|
||||
id: 'metadata-effort-model',
|
||||
apiName: 'metadata-effort-model',
|
||||
capabilities: { supportsReasoning: true },
|
||||
reasoning: {
|
||||
mode: 'levels',
|
||||
levels: ['low', 'medium', 'high'],
|
||||
wireFormat: 'reasoning_effort',
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
expect(modelSupportsEffort('metadata-effort-model')).toBe(true)
|
||||
expect(modelSupportsShimReasoningEffort('metadata-effort-model')).toBe(true)
|
||||
expect(modelSupportsWireEffort('metadata-effort-model')).toBe(true)
|
||||
expect(resolveAppliedEffort('metadata-effort-model', 'high')).toBe('high')
|
||||
})
|
||||
|
||||
test('toggle reasoning metadata stays non-controllable until toggle serialization exists', async () => {
|
||||
const {
|
||||
getAvailableEffortLevels,
|
||||
@@ -1108,7 +1426,8 @@ test('compat DeepSeek routes stay non-controllable when the runtime shim strips
|
||||
expect(resolveModelReasoningControl('deepseek-r1-distill-llama-70b')).toMatchObject({
|
||||
supportsReasoning: false,
|
||||
controllable: false,
|
||||
source: 'none',
|
||||
source: 'compat',
|
||||
levels: [],
|
||||
})
|
||||
expect(modelSupportsEffort('deepseek-r1-distill-llama-70b')).toBe(false)
|
||||
expect(modelSupportsWireEffort('deepseek-r1-distill-llama-70b')).toBe(false)
|
||||
|
||||
+387
-2
@@ -3,17 +3,75 @@ import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../test/sharedMutationLock.js'
|
||||
import {
|
||||
ensureIntegrationsLoaded,
|
||||
getCatalogEntriesForRoute,
|
||||
} from '../integrations/index.js'
|
||||
import * as realAuth from './auth.js'
|
||||
import * as realThinking from './thinking.js'
|
||||
const realModelSupportOverridesModule = await import(
|
||||
`./model/modelSupportOverrides.js?real=${Date.now()}-${Math.random()}`,
|
||||
)
|
||||
const realModelSupportOverrides = {
|
||||
get3PModelCapabilityOverride:
|
||||
realModelSupportOverridesModule.get3PModelCapabilityOverride,
|
||||
}
|
||||
|
||||
const originalEnv = { ...process.env }
|
||||
const routingEnvKeys = [
|
||||
'ANTHROPIC_BASE_URL',
|
||||
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
||||
'ANTHROPIC_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES',
|
||||
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
||||
'ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES',
|
||||
'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
||||
'ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES',
|
||||
'CLAUDE_CODE_ALWAYS_ENABLE_EFFORT',
|
||||
'CLAUDE_CODE_EFFORT_LEVEL',
|
||||
'CLAUDE_CODE_USE_BEDROCK',
|
||||
'CLAUDE_CODE_USE_FOUNDRY',
|
||||
'CLAUDE_CODE_USE_GEMINI',
|
||||
'CLAUDE_CODE_USE_GITHUB',
|
||||
'CLAUDE_CODE_USE_MISTRAL',
|
||||
'CLAUDE_CODE_USE_OPENAI',
|
||||
'CLAUDE_CODE_USE_VERTEX',
|
||||
'GEMINI_API_KEY',
|
||||
'MIMO_API_KEY',
|
||||
'MINIMAX_API_KEY',
|
||||
'NVIDIA_API_KEY',
|
||||
'NVIDIA_NIM',
|
||||
'OPENAI_API_BASE',
|
||||
'OPENAI_API_KEY',
|
||||
'OPENAI_BASE_URL',
|
||||
'OPENAI_MODEL',
|
||||
'XAI_API_KEY',
|
||||
'ZAI_API_KEY',
|
||||
'USER_TYPE',
|
||||
] as const
|
||||
|
||||
async function importFreshEffortModule() {
|
||||
return import(`./effort.ts?ts=${Date.now()}-${Math.random()}`)
|
||||
return import(`./effort.js?ts=${Date.now()}-${Math.random()}`)
|
||||
}
|
||||
|
||||
function restoreProcessEnv(): void {
|
||||
for (const key of Object.keys(process.env)) {
|
||||
if (!Object.hasOwn(originalEnv, key)) delete process.env[key]
|
||||
}
|
||||
for (const [key, value] of Object.entries(originalEnv)) {
|
||||
if (value === undefined) delete process.env[key]
|
||||
else process.env[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock('utils/effort.test.ts')
|
||||
mock.module(
|
||||
'./model/modelSupportOverrides.js',
|
||||
() => realModelSupportOverrides,
|
||||
)
|
||||
for (const key of routingEnvKeys) {
|
||||
delete process.env[key]
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
@@ -21,7 +79,11 @@ afterEach(() => {
|
||||
mock.restore()
|
||||
mock.module('./auth.js', () => realAuth)
|
||||
mock.module('./thinking.js', () => realThinking)
|
||||
process.env = { ...originalEnv }
|
||||
mock.module(
|
||||
'./model/modelSupportOverrides.js',
|
||||
() => realModelSupportOverrides,
|
||||
)
|
||||
restoreProcessEnv()
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
@@ -53,3 +115,326 @@ describe('getDefaultEffortForModel — default-Opus effort gate (#1769)', () =>
|
||||
expect(getDefaultEffortForModel('claude-opus-4-1')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('CLAUDE_CODE_ALWAYS_ENABLE_EFFORT precedence', () => {
|
||||
test('keeps API-rejecting Claude models excluded across every public effort predicate', async () => {
|
||||
process.env.CLAUDE_CODE_ALWAYS_ENABLE_EFFORT = '1'
|
||||
|
||||
const {
|
||||
modelSupportsEffort,
|
||||
modelSupportsShimReasoningEffort,
|
||||
modelSupportsWireEffort,
|
||||
resolveAppliedEffort,
|
||||
resolveModelReasoningControl,
|
||||
} = await importFreshEffortModule()
|
||||
const context = {
|
||||
apiProvider: 'firstParty' as const,
|
||||
routeId: 'anthropic',
|
||||
useRuntimeFallback: false,
|
||||
}
|
||||
const rejectedModels = [
|
||||
'claude-3-7-sonnet-20250219',
|
||||
'claude-3-5-haiku-20241022',
|
||||
'claude-sonnet-4-20250514',
|
||||
'claude-sonnet-4-5-20250929',
|
||||
'claude-opus-4-20250514',
|
||||
'claude-opus-4-1-20250805',
|
||||
'claude-haiku-4-5-20251001',
|
||||
]
|
||||
|
||||
for (const model of rejectedModels) {
|
||||
expect(resolveModelReasoningControl(model, context)).toMatchObject({
|
||||
supportsReasoning: false,
|
||||
controllable: false,
|
||||
source: 'none',
|
||||
})
|
||||
expect(modelSupportsEffort(model, context)).toBe(false)
|
||||
expect(
|
||||
modelSupportsShimReasoningEffort(
|
||||
model,
|
||||
undefined,
|
||||
undefined,
|
||||
context,
|
||||
),
|
||||
).toBe(false)
|
||||
expect(modelSupportsWireEffort(model, context)).toBe(false)
|
||||
expect(resolveAppliedEffort(model, 'medium', context)).toBeUndefined()
|
||||
}
|
||||
})
|
||||
|
||||
test('preserves supported Claude controls and selected wire values', async () => {
|
||||
process.env.CLAUDE_CODE_ALWAYS_ENABLE_EFFORT = '1'
|
||||
|
||||
const {
|
||||
modelSupportsEffort,
|
||||
modelSupportsShimReasoningEffort,
|
||||
modelSupportsWireEffort,
|
||||
resolveAppliedEffort,
|
||||
} = await importFreshEffortModule()
|
||||
const context = {
|
||||
apiProvider: 'firstParty' as const,
|
||||
routeId: 'anthropic',
|
||||
useRuntimeFallback: false,
|
||||
}
|
||||
const supportedModels = [
|
||||
['claude-opus-4-5-20251101', 'low', 'low'],
|
||||
['claude-opus-4-6', 'max', 'max'],
|
||||
['claude-opus-4-8', 'xhigh', 'xhigh'],
|
||||
['claude-sonnet-4-6', 'max', 'high'],
|
||||
] as const
|
||||
|
||||
for (const [model, selected, expected] of supportedModels) {
|
||||
expect(modelSupportsEffort(model, context)).toBe(true)
|
||||
expect(
|
||||
modelSupportsShimReasoningEffort(
|
||||
model,
|
||||
undefined,
|
||||
undefined,
|
||||
context,
|
||||
),
|
||||
).toBe(true)
|
||||
expect(modelSupportsWireEffort(model, context)).toBe(true)
|
||||
expect(resolveAppliedEffort(model, selected, context)).toBe(expected)
|
||||
}
|
||||
})
|
||||
|
||||
test('only force-enables unresolved custom models beyond their provider fallback', async () => {
|
||||
const {
|
||||
modelSupportsEffort,
|
||||
modelSupportsShimReasoningEffort,
|
||||
modelSupportsWireEffort,
|
||||
resolveAppliedEffort,
|
||||
} = await importFreshEffortModule()
|
||||
const model = 'gateway-custom-model'
|
||||
const context = {
|
||||
apiProvider: 'openai' as const,
|
||||
routeId: 'custom',
|
||||
useRuntimeFallback: false,
|
||||
}
|
||||
const support = () => [
|
||||
modelSupportsEffort(model, context),
|
||||
modelSupportsShimReasoningEffort(
|
||||
model,
|
||||
undefined,
|
||||
undefined,
|
||||
context,
|
||||
),
|
||||
modelSupportsWireEffort(model, context),
|
||||
]
|
||||
|
||||
delete process.env.CLAUDE_CODE_ALWAYS_ENABLE_EFFORT
|
||||
expect(support()).toEqual([false, false, false])
|
||||
expect(resolveAppliedEffort(model, 'medium', context)).toBeUndefined()
|
||||
|
||||
process.env.CLAUDE_CODE_ALWAYS_ENABLE_EFFORT = '1'
|
||||
expect(support()).toEqual([true, true, true])
|
||||
expect(resolveAppliedEffort(model, 'medium', context)).toBe('medium')
|
||||
})
|
||||
|
||||
test('uses the scoped routing environment for force enable', async () => {
|
||||
const { modelSupportsEffort, resolveAppliedEffort } =
|
||||
await importFreshEffortModule()
|
||||
const model = 'gateway-custom-model'
|
||||
const context = {
|
||||
apiProvider: 'openai' as const,
|
||||
routeId: 'custom',
|
||||
useRuntimeFallback: false,
|
||||
processEnv: {
|
||||
CLAUDE_CODE_ALWAYS_ENABLE_EFFORT: '1',
|
||||
} as NodeJS.ProcessEnv,
|
||||
}
|
||||
|
||||
delete process.env.CLAUDE_CODE_ALWAYS_ENABLE_EFFORT
|
||||
expect(modelSupportsEffort(model, context)).toBe(true)
|
||||
expect(resolveAppliedEffort(model, 'medium', context)).toBe('medium')
|
||||
|
||||
process.env.CLAUDE_CODE_ALWAYS_ENABLE_EFFORT = '1'
|
||||
context.processEnv = {}
|
||||
expect(modelSupportsEffort(model, context)).toBe(false)
|
||||
expect(resolveAppliedEffort(model, 'medium', context)).toBeUndefined()
|
||||
})
|
||||
|
||||
test('uses the scoped environment for compatibility and catalog route fallbacks', async () => {
|
||||
const { resolveModelReasoningControl } = await importFreshEffortModule()
|
||||
const scopedOpenAIEnv = {
|
||||
CLAUDE_CODE_USE_OPENAI: '1',
|
||||
OPENAI_API_KEY: 'scoped-key',
|
||||
OPENAI_BASE_URL: 'https://api.openai.com/v1',
|
||||
} as NodeJS.ProcessEnv
|
||||
const context = {
|
||||
apiProvider: 'openai' as const,
|
||||
processEnv: scopedOpenAIEnv,
|
||||
}
|
||||
const noControl = {
|
||||
supportsReasoning: false,
|
||||
controllable: false,
|
||||
source: 'none',
|
||||
}
|
||||
|
||||
process.env.LONGCAT_API_KEY = 'ambient-key'
|
||||
expect(resolveModelReasoningControl('gateway-custom-model', context)).toMatchObject(
|
||||
noControl,
|
||||
)
|
||||
|
||||
delete process.env.LONGCAT_API_KEY
|
||||
process.env.ZAI_API_KEY = 'ambient-key'
|
||||
expect(
|
||||
resolveModelReasoningControl('glm-5.2', {
|
||||
...context,
|
||||
openaiShimConfig: {},
|
||||
}),
|
||||
).toMatchObject(noControl)
|
||||
|
||||
delete process.env.ZAI_API_KEY
|
||||
process.env.XAI_API_KEY = 'ambient-key'
|
||||
expect(resolveModelReasoningControl('grok-4.6', context)).toMatchObject(
|
||||
noControl,
|
||||
)
|
||||
|
||||
delete process.env.XAI_API_KEY
|
||||
process.env.OPENAI_BASE_URL = 'https://api.openai.com/v1'
|
||||
expect(
|
||||
resolveModelReasoningControl('gpt-5.6', {
|
||||
apiProvider: 'openai',
|
||||
processEnv: {
|
||||
CLAUDE_CODE_USE_OPENAI: '1',
|
||||
OPENAI_API_KEY: 'scoped-key',
|
||||
OPENAI_API_BASE: 'https://gateway.example.test/v1',
|
||||
},
|
||||
supportsCodexReasoningEffort: false,
|
||||
}),
|
||||
).toMatchObject(noControl)
|
||||
})
|
||||
|
||||
test('ambient custom-route predicates do not advertise native transport effort', async () => {
|
||||
delete process.env.CLAUDE_CODE_USE_GEMINI
|
||||
delete process.env.GEMINI_API_KEY
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||
process.env.OPENAI_API_KEY = 'test-key'
|
||||
process.env.OPENAI_BASE_URL = 'https://gateway.example.test/v1'
|
||||
process.env.OPENAI_MODEL = 'claude-opus-4-5'
|
||||
|
||||
const { modelSupportsEffort, resolveAppliedEffort } =
|
||||
await importFreshEffortModule()
|
||||
|
||||
for (const model of ['claude-opus-4-5', 'gemini-3-pro']) {
|
||||
expect(modelSupportsEffort(model)).toBe(false)
|
||||
expect(resolveAppliedEffort(model, 'medium')).toBeUndefined()
|
||||
}
|
||||
expect(modelSupportsEffort('gpt-5.4')).toBe(true)
|
||||
expect(resolveAppliedEffort('gpt-5.4', 'medium')).toBe('medium')
|
||||
})
|
||||
})
|
||||
|
||||
describe('configured third-party effort precedence', () => {
|
||||
test('supersedes descriptive LongCat catalog capabilities without bypassing route contracts', async () => {
|
||||
ensureIntegrationsLoaded()
|
||||
const longCatEntry = getCatalogEntriesForRoute('longcat').find(
|
||||
entry => entry.apiName === 'LongCat-2.0',
|
||||
)!
|
||||
const {
|
||||
getAvailableEffortLevels,
|
||||
modelSupportsEffort,
|
||||
modelSupportsWireEffort,
|
||||
resolveAppliedEffort,
|
||||
resolveModelReasoningControl,
|
||||
} = await importFreshEffortModule()
|
||||
const context = {
|
||||
apiProvider: 'openai' as const,
|
||||
routeId: 'longcat',
|
||||
useRuntimeFallback: false,
|
||||
}
|
||||
|
||||
expect(resolveModelReasoningControl('LongCat-2.0', context)).toMatchObject({
|
||||
supportsReasoning: true,
|
||||
controllable: false,
|
||||
source: 'capability',
|
||||
levels: [],
|
||||
})
|
||||
|
||||
process.env.ANTHROPIC_DEFAULT_SONNET_MODEL = 'LongCat-2.0'
|
||||
process.env.ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES =
|
||||
'effort,max_effort,xhigh_effort'
|
||||
|
||||
expect(resolveModelReasoningControl('LongCat-2.0', context)).toMatchObject({
|
||||
supportsReasoning: true,
|
||||
controllable: true,
|
||||
source: 'capability',
|
||||
levels: ['low', 'medium', 'high', 'xhigh', 'max'],
|
||||
wireFormat: 'reasoning_effort',
|
||||
})
|
||||
expect(modelSupportsEffort('LongCat-2.0', context)).toBe(true)
|
||||
expect(modelSupportsWireEffort('LongCat-2.0', context)).toBe(true)
|
||||
expect(getAvailableEffortLevels('LongCat-2.0', context)).toEqual([
|
||||
'low',
|
||||
'medium',
|
||||
'high',
|
||||
'xhigh',
|
||||
'max',
|
||||
])
|
||||
expect(resolveAppliedEffort('LongCat-2.0', 'max', context)).toBe('max')
|
||||
|
||||
const transportVetoContext = {
|
||||
...context,
|
||||
openaiShimConfig: { removeBodyFields: ['reasoning_effort'] },
|
||||
}
|
||||
expect(
|
||||
resolveModelReasoningControl('LongCat-2.0', transportVetoContext),
|
||||
).toMatchObject({
|
||||
controllable: false,
|
||||
source: 'compat',
|
||||
})
|
||||
expect(
|
||||
resolveAppliedEffort('LongCat-2.0', 'max', transportVetoContext),
|
||||
).toBeUndefined()
|
||||
|
||||
const explicitMetadataContext = {
|
||||
...context,
|
||||
catalogEntries: [
|
||||
{
|
||||
...longCatEntry,
|
||||
reasoning: {
|
||||
mode: 'always-on' as const,
|
||||
wireFormat: 'none' as const,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
expect(
|
||||
resolveModelReasoningControl('LongCat-2.0', explicitMetadataContext),
|
||||
).toMatchObject({
|
||||
supportsReasoning: true,
|
||||
controllable: false,
|
||||
source: 'metadata',
|
||||
wireFormat: 'none',
|
||||
})
|
||||
})
|
||||
|
||||
test('preserves the legacy default for a configured override', async () => {
|
||||
process.env.ANTHROPIC_DEFAULT_OPUS_MODEL = 'claude-opus-4-6'
|
||||
process.env.ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES = 'effort'
|
||||
mock.module('./auth.js', () => ({
|
||||
...realAuth,
|
||||
isProSubscriber: () => true,
|
||||
isMaxSubscriber: () => false,
|
||||
isTeamSubscriber: () => false,
|
||||
}))
|
||||
mock.module('./thinking.js', () => ({
|
||||
...realThinking,
|
||||
isUltrathinkEnabled: () => false,
|
||||
}))
|
||||
|
||||
const { getDefaultEffortForModel, resolveAppliedEffort } =
|
||||
await importFreshEffortModule()
|
||||
const context = {
|
||||
apiProvider: 'openai' as const,
|
||||
routeId: 'custom',
|
||||
useRuntimeFallback: false,
|
||||
}
|
||||
|
||||
expect(getDefaultEffortForModel('claude-opus-4-6', context)).toBe('medium')
|
||||
expect(
|
||||
resolveAppliedEffort('claude-opus-4-6', undefined, context),
|
||||
).toBe('medium')
|
||||
})
|
||||
})
|
||||
|
||||
+178
-84
@@ -75,6 +75,11 @@ type OpenAIShimReasoningSupportContext = {
|
||||
useRuntimeFallback?: boolean
|
||||
}
|
||||
|
||||
type ReasoningCompatibilityOverrides = {
|
||||
thinkingRequestFormat?: OpenAIShimThinkingRequestFormat
|
||||
removeBodyFields?: string[]
|
||||
}
|
||||
|
||||
export type ReasoningControlContext = OpenAIShimReasoningSupportContext & {
|
||||
apiProvider?: ReturnType<typeof getAPIProvider>
|
||||
supportsCodexReasoningEffort?: boolean | ((model: string) => boolean)
|
||||
@@ -219,6 +224,7 @@ function resolveCompatibilityWireFormat(
|
||||
thinkingRequestFormat?: OpenAIShimThinkingRequestFormat,
|
||||
routeIdOverride?: string | null,
|
||||
useRuntimeFallback = true,
|
||||
processEnv: NodeJS.ProcessEnv = process.env,
|
||||
): ReasoningWireFormat | undefined {
|
||||
if (thinkingRequestFormat === 'deepseek-compatible') {
|
||||
return 'deepseek_compatible'
|
||||
@@ -233,7 +239,7 @@ function resolveCompatibilityWireFormat(
|
||||
const routeId = routeIdOverride !== undefined
|
||||
? routeIdOverride
|
||||
: useRuntimeFallback
|
||||
? resolveActiveRouteIdFromEnv(process.env)
|
||||
? resolveActiveRouteIdFromEnv(processEnv)
|
||||
: undefined
|
||||
if (!routeId || routeId === 'anthropic' || routeId === 'openai') {
|
||||
return undefined
|
||||
@@ -254,9 +260,10 @@ function resolveCompatibilityReasoningControl(
|
||||
context?: ReasoningControlContext,
|
||||
): ReasoningControlResolution | undefined {
|
||||
const useRuntimeFallback = context?.useRuntimeFallback ?? true
|
||||
const processEnv = context?.processEnv ?? process.env
|
||||
const runtimeShimConfig = context?.openaiShimConfig ?? (useRuntimeFallback && thinkingRequestFormat === undefined && removeBodyFields === undefined
|
||||
? resolveOpenAIShimRuntimeContext({
|
||||
processEnv: process.env,
|
||||
processEnv,
|
||||
model,
|
||||
}).openaiShimConfig
|
||||
: undefined)
|
||||
@@ -264,20 +271,29 @@ function resolveCompatibilityReasoningControl(
|
||||
thinkingRequestFormat ?? runtimeShimConfig?.thinkingRequestFormat
|
||||
const resolvedRemoveBodyFields =
|
||||
removeBodyFields ?? runtimeShimConfig?.removeBodyFields
|
||||
if (
|
||||
resolvedThinkingRequestFormat === 'none' ||
|
||||
resolvedRemoveBodyFields?.includes('reasoning_effort')
|
||||
) {
|
||||
return {
|
||||
supportsReasoning: false,
|
||||
controllable: false,
|
||||
levels: [],
|
||||
source: 'compat',
|
||||
}
|
||||
}
|
||||
const wireFormat = resolveCompatibilityWireFormat(
|
||||
model,
|
||||
resolvedThinkingRequestFormat,
|
||||
context?.routeId,
|
||||
useRuntimeFallback,
|
||||
processEnv,
|
||||
)
|
||||
if (!wireFormat) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (wireFormat === 'deepseek_compatible') {
|
||||
if (resolvedRemoveBodyFields?.includes('reasoning_effort')) {
|
||||
return undefined
|
||||
}
|
||||
return {
|
||||
supportsReasoning: true,
|
||||
controllable: true,
|
||||
@@ -290,9 +306,7 @@ function resolveCompatibilityReasoningControl(
|
||||
}
|
||||
|
||||
if (wireFormat === 'zai_compatible') {
|
||||
const reasoningEffortStripped =
|
||||
resolvedRemoveBodyFields?.includes('reasoning_effort') === true
|
||||
const levels: EffortLevel[] = supportsZaiReasoningEffort(model) && !reasoningEffortStripped
|
||||
const levels: EffortLevel[] = supportsZaiReasoningEffort(model)
|
||||
? ['high', 'xhigh']
|
||||
: ['high']
|
||||
return {
|
||||
@@ -316,11 +330,12 @@ function resolveCatalogReasoningMetadata(
|
||||
capabilities?: CapabilityFlags
|
||||
reasoning?: ReasoningControlMetadata
|
||||
} | undefined {
|
||||
const processEnv = context?.processEnv ?? process.env
|
||||
const routeId = context?.routeId !== undefined
|
||||
? context.routeId
|
||||
: context?.useRuntimeFallback === false
|
||||
? undefined
|
||||
: resolveActiveRouteIdFromEnv(process.env)
|
||||
: resolveActiveRouteIdFromEnv(processEnv)
|
||||
if (!routeId || routeId === 'anthropic') {
|
||||
return undefined
|
||||
}
|
||||
@@ -337,7 +352,7 @@ function resolveCatalogReasoningMetadata(
|
||||
const entries = context?.catalogEntries ?? getCatalogEntriesForRoute(routeId)
|
||||
let entry = entries.find(matchesModel)
|
||||
const fallbackBaseUrl =
|
||||
context?.baseUrl ?? context?.processEnv?.OPENAI_BASE_URL ?? process.env.OPENAI_BASE_URL ?? process.env.OPENAI_API_BASE
|
||||
context?.baseUrl ?? processEnv.OPENAI_BASE_URL ?? processEnv.OPENAI_API_BASE
|
||||
if (
|
||||
!entry &&
|
||||
routeId === 'custom' &&
|
||||
@@ -416,21 +431,103 @@ function resolveMetadataReasoningControl(
|
||||
}
|
||||
}
|
||||
|
||||
function resolveConfigured3PReasoningControl(
|
||||
model: string,
|
||||
context?: ReasoningControlContext,
|
||||
): ReasoningControlResolution | undefined {
|
||||
const apiProvider = getReasoningApiProvider(context)
|
||||
if (get3PModelCapabilityOverride(model, 'effort', apiProvider) !== true) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const levels: EffortLevel[] = ['low', 'medium', 'high']
|
||||
if (
|
||||
get3PModelCapabilityOverride(model, 'xhigh_effort', apiProvider) === true
|
||||
) {
|
||||
levels.push('xhigh')
|
||||
}
|
||||
if (
|
||||
get3PModelCapabilityOverride(model, 'max_effort', apiProvider) === true
|
||||
) {
|
||||
levels.push('max')
|
||||
}
|
||||
|
||||
return {
|
||||
supportsReasoning: true,
|
||||
controllable: true,
|
||||
mode: 'levels',
|
||||
levels,
|
||||
defaultLevel: getLegacyDefaultEffortForModel(model, context),
|
||||
wireFormat: 'reasoning_effort',
|
||||
source: 'capability',
|
||||
}
|
||||
}
|
||||
|
||||
type NativeLegacyEffortTransport = 'anthropic' | 'gemini'
|
||||
|
||||
function resolveNativeLegacyEffortTransport(
|
||||
model: string,
|
||||
context?: ReasoningControlContext,
|
||||
): NativeLegacyEffortTransport | undefined {
|
||||
const useRuntimeFallback = context?.useRuntimeFallback ?? true
|
||||
const runtimeShimConfig = context?.openaiShimConfig ?? (
|
||||
useRuntimeFallback
|
||||
? resolveOpenAIShimRuntimeContext({
|
||||
processEnv: context?.processEnv ?? process.env,
|
||||
baseUrl: context?.baseUrl,
|
||||
model,
|
||||
}).openaiShimConfig
|
||||
: undefined
|
||||
)
|
||||
const endpointPath = runtimeShimConfig?.endpointPath
|
||||
if (endpointPath === '/messages') return 'anthropic'
|
||||
if (endpointPath?.startsWith('/models/gemini-')) return 'gemini'
|
||||
|
||||
const apiProvider = getReasoningApiProvider(context)
|
||||
if (
|
||||
apiProvider === 'firstParty' ||
|
||||
apiProvider === 'bedrock' ||
|
||||
apiProvider === 'vertex' ||
|
||||
apiProvider === 'foundry' ||
|
||||
apiProvider === 'github'
|
||||
) {
|
||||
return 'anthropic'
|
||||
}
|
||||
return apiProvider === 'gemini' ? 'gemini' : undefined
|
||||
}
|
||||
|
||||
function modelMatchesNativeLegacyTransport(
|
||||
model: string,
|
||||
transport: NativeLegacyEffortTransport | undefined,
|
||||
): boolean {
|
||||
const normalized = model.toLowerCase()
|
||||
return transport === 'anthropic'
|
||||
? normalized.includes('haiku') ||
|
||||
normalized.includes('sonnet') ||
|
||||
normalized.includes('opus')
|
||||
: transport === 'gemini' && normalized.includes('gemini-')
|
||||
}
|
||||
|
||||
function legacyModelSupportsEffort(
|
||||
model: string,
|
||||
context?: ReasoningControlContext,
|
||||
): boolean {
|
||||
const m = model.toLowerCase()
|
||||
if (isEnvTruthy(process.env.CLAUDE_CODE_ALWAYS_ENABLE_EFFORT)) {
|
||||
return true
|
||||
}
|
||||
const supported3P = get3PModelCapabilityOverride(model, 'effort')
|
||||
const supported3P = get3PModelCapabilityOverride(
|
||||
model,
|
||||
'effort',
|
||||
getReasoningApiProvider(context),
|
||||
)
|
||||
if (supported3P !== undefined) {
|
||||
return supported3P
|
||||
}
|
||||
if (modelUsesOpenAIEffort(model, context) && modelSupportsCodexReasoningEffort(model, context)) {
|
||||
if (
|
||||
modelUsesOpenAIEffort(model, context) &&
|
||||
modelSupportsCodexReasoningEffort(model, context)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
const nativeTransport = resolveNativeLegacyEffortTransport(model, context)
|
||||
// Claude 4 models that support effort. Mirrors the Anthropic /messages
|
||||
// shim's isAdaptive || isOpus45 set (openaiShim.ts:2292-2297) — only
|
||||
// these models serialize low/medium as anthropicBody.effort. Older
|
||||
@@ -438,19 +535,35 @@ function legacyModelSupportsEffort(
|
||||
// high/max, so advertising effort for them would silently drop
|
||||
// low/medium on the wire. The substring match also covers prefix
|
||||
// variations (e.g. `claude-opus-4-7`, `opencode-claude-opus-4-8`).
|
||||
if (m.includes('opus-4-5') || m.includes('opus-4-6') ||
|
||||
if (
|
||||
nativeTransport === 'anthropic' &&
|
||||
(m.includes('opus-4-5') || m.includes('opus-4-6') ||
|
||||
m.includes('opus-4-7') || m.includes('opus-4-8') ||
|
||||
m.includes('sonnet-4-6')) {
|
||||
m.includes('sonnet-4-6'))
|
||||
) {
|
||||
return true
|
||||
}
|
||||
// OpenCode Gemini models that support thinking via /models/gemini-* endpoint
|
||||
if (m.includes('gemini-3')) {
|
||||
if (nativeTransport === 'gemini' && m.includes('gemini-3')) {
|
||||
return true
|
||||
}
|
||||
// Exclude any other known legacy models (haiku, older opus/sonnet variants)
|
||||
if (m.includes('haiku') || m.includes('sonnet') || m.includes('opus')) {
|
||||
// Native model names need an authorized native transport before force-enable
|
||||
// or provider defaults may add an incompatible field to a generic shim.
|
||||
if (
|
||||
m.includes('haiku') ||
|
||||
m.includes('sonnet') ||
|
||||
m.includes('opus') ||
|
||||
m.includes('gemini-')
|
||||
) {
|
||||
return false
|
||||
}
|
||||
if (
|
||||
isEnvTruthy(
|
||||
(context?.processEnv ?? process.env).CLAUDE_CODE_ALWAYS_ENABLE_EFFORT,
|
||||
)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
// IMPORTANT: Do not change the default effort support without notifying
|
||||
// the model launch DRI and research. This is a sensitive setting that can
|
||||
@@ -489,17 +602,42 @@ function resolveLegacyReasoningControl(
|
||||
export function resolveModelReasoningControl(
|
||||
model: string,
|
||||
context?: ReasoningControlContext,
|
||||
compatibilityOverrides?: ReasoningCompatibilityOverrides,
|
||||
): ReasoningControlResolution {
|
||||
const metadata = resolveMetadataReasoningControl(model, context)
|
||||
if (metadata?.source === 'metadata') {
|
||||
const compatibility = resolveCompatibilityReasoningControl(
|
||||
model,
|
||||
compatibilityOverrides?.thinkingRequestFormat,
|
||||
compatibilityOverrides?.removeBodyFields,
|
||||
context,
|
||||
)
|
||||
if (compatibility && !compatibility.controllable) {
|
||||
return compatibility
|
||||
}
|
||||
if (metadata?.source === 'metadata' || metadata?.supportsReasoning === false) {
|
||||
return metadata
|
||||
}
|
||||
|
||||
const compatibility = resolveCompatibilityReasoningControl(model, undefined, undefined, context)
|
||||
if (compatibility) {
|
||||
return compatibility
|
||||
}
|
||||
|
||||
const configured3P = resolveConfigured3PReasoningControl(model, context)
|
||||
if (configured3P) {
|
||||
return configured3P
|
||||
}
|
||||
|
||||
const nativeTransport = resolveNativeLegacyEffortTransport(model, context)
|
||||
if (
|
||||
metadata?.source === 'capability' &&
|
||||
modelMatchesNativeLegacyTransport(model, nativeTransport)
|
||||
) {
|
||||
const nativeLegacy = resolveLegacyReasoningControl(model, context)
|
||||
if (nativeLegacy.controllable) {
|
||||
return nativeLegacy
|
||||
}
|
||||
}
|
||||
|
||||
if (metadata) {
|
||||
return metadata
|
||||
}
|
||||
@@ -509,13 +647,6 @@ export function resolveModelReasoningControl(
|
||||
|
||||
// @[MODEL LAUNCH]: Add the new model to the allowlist if it supports the effort parameter.
|
||||
export function modelSupportsEffort(model: string, context?: ReasoningControlContext): boolean {
|
||||
if (isEnvTruthy(process.env.CLAUDE_CODE_ALWAYS_ENABLE_EFFORT)) {
|
||||
return true
|
||||
}
|
||||
const supported3P = get3PModelCapabilityOverride(model, 'effort')
|
||||
if (supported3P !== undefined) {
|
||||
return supported3P
|
||||
}
|
||||
return resolveModelReasoningControl(model, context).controllable
|
||||
}
|
||||
|
||||
@@ -525,63 +656,15 @@ export function modelSupportsShimReasoningEffort(
|
||||
removeBodyFields?: string[],
|
||||
context?: ReasoningControlContext,
|
||||
): boolean {
|
||||
if (isEnvTruthy(process.env.CLAUDE_CODE_ALWAYS_ENABLE_EFFORT)) {
|
||||
return true
|
||||
}
|
||||
const supported3P = get3PModelCapabilityOverride(model, 'effort')
|
||||
if (supported3P !== undefined) {
|
||||
return supported3P
|
||||
}
|
||||
|
||||
const metadata = resolveMetadataReasoningControl(
|
||||
const control = resolveModelReasoningControl(
|
||||
model,
|
||||
context,
|
||||
{ thinkingRequestFormat, removeBodyFields },
|
||||
)
|
||||
if (metadata?.source === 'metadata') {
|
||||
return Boolean(metadata.controllable && metadataWireFormatSupportsEffort(metadata.wireFormat))
|
||||
}
|
||||
|
||||
const compatibility = resolveCompatibilityReasoningControl(
|
||||
model,
|
||||
thinkingRequestFormat,
|
||||
removeBodyFields,
|
||||
context,
|
||||
)
|
||||
if (compatibility) {
|
||||
return compatibility.controllable
|
||||
}
|
||||
|
||||
if (
|
||||
context?.routeId &&
|
||||
(context.routeId === 'openai' || context.routeId === 'codex') &&
|
||||
!removeBodyFields?.includes('reasoning_effort')
|
||||
) {
|
||||
return modelSupportsCodexReasoningEffort(model, context)
|
||||
}
|
||||
|
||||
if (context?.useRuntimeFallback === false) {
|
||||
if (
|
||||
context.routeId == null &&
|
||||
thinkingRequestFormat === undefined &&
|
||||
!removeBodyFields?.includes('reasoning_effort')
|
||||
) {
|
||||
return resolveLegacyReasoningControl(model, context).controllable
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const control = metadata ?? resolveLegacyReasoningControl(model, context)
|
||||
return Boolean(control.controllable && metadataWireFormatSupportsEffort(control.wireFormat))
|
||||
}
|
||||
|
||||
export function modelSupportsWireEffort(model: string, context?: ReasoningControlContext): boolean {
|
||||
if (isEnvTruthy(process.env.CLAUDE_CODE_ALWAYS_ENABLE_EFFORT)) {
|
||||
return true
|
||||
}
|
||||
const supported3P = get3PModelCapabilityOverride(model, 'effort')
|
||||
if (supported3P !== undefined) {
|
||||
return supported3P
|
||||
}
|
||||
return modelSupportsShimReasoningEffort(model, undefined, undefined, context)
|
||||
}
|
||||
|
||||
@@ -701,8 +784,15 @@ export function resolveOpenAIShimReasoningRequestPlan(options: {
|
||||
// @[MODEL LAUNCH]: Add the new model to the allowlist if it supports 'max' effort.
|
||||
// Per API docs, 'max' is supported on the recent Opus models (4.8/4.7/4.6) for
|
||||
// public models — other models return an error.
|
||||
function legacyModelSupportsMaxEffort(model: string): boolean {
|
||||
const supported3P = get3PModelCapabilityOverride(model, 'max_effort')
|
||||
function legacyModelSupportsMaxEffort(
|
||||
model: string,
|
||||
context?: ReasoningControlContext,
|
||||
): boolean {
|
||||
const supported3P = get3PModelCapabilityOverride(
|
||||
model,
|
||||
'max_effort',
|
||||
getReasoningApiProvider(context),
|
||||
)
|
||||
if (supported3P !== undefined) {
|
||||
return supported3P
|
||||
}
|
||||
@@ -725,7 +815,11 @@ function legacyModelSupportsXHighEffort(
|
||||
if (!legacyModelSupportsEffort(model, context)) {
|
||||
return false
|
||||
}
|
||||
const supported3P = get3PModelCapabilityOverride(model, 'xhigh_effort')
|
||||
const supported3P = get3PModelCapabilityOverride(
|
||||
model,
|
||||
'xhigh_effort',
|
||||
getReasoningApiProvider(context),
|
||||
)
|
||||
if (supported3P !== undefined) {
|
||||
return supported3P
|
||||
}
|
||||
@@ -787,7 +881,7 @@ function getLegacyAvailableEffortLevels(
|
||||
if (legacyModelSupportsXHighEffort(model, context)) {
|
||||
levels.push('xhigh')
|
||||
}
|
||||
if (legacyModelSupportsMaxEffort(model)) {
|
||||
if (legacyModelSupportsMaxEffort(model, context)) {
|
||||
levels.push('max')
|
||||
}
|
||||
if (
|
||||
@@ -818,7 +912,7 @@ export function modelSupportsMaxEffort(model: string, context?: ReasoningControl
|
||||
if (control.source === 'metadata' || control.source === 'capability' || control.source === 'compat') {
|
||||
return control.levels.includes('max')
|
||||
}
|
||||
return legacyModelSupportsMaxEffort(model)
|
||||
return legacyModelSupportsMaxEffort(model, context)
|
||||
}
|
||||
|
||||
export function modelSupportsXHighEffort(model: string, context?: ReasoningControlContext): boolean {
|
||||
|
||||
@@ -27,7 +27,9 @@ const TIERS = [
|
||||
function buildCapabilityOverrideCacheKey(
|
||||
model: string,
|
||||
capability: ModelCapabilityOverride,
|
||||
apiProvider?: ReturnType<typeof getAPIProvider>,
|
||||
): string {
|
||||
const resolvedApiProvider = apiProvider ?? getAPIProvider()
|
||||
const envParts = TIERS.flatMap(tier => [
|
||||
process.env[tier.modelEnvVar] ?? '',
|
||||
process.env[tier.capabilitiesEnvVar] ?? '',
|
||||
@@ -36,7 +38,7 @@ function buildCapabilityOverrideCacheKey(
|
||||
return [
|
||||
model.toLowerCase(),
|
||||
capability,
|
||||
getAPIProvider(),
|
||||
resolvedApiProvider,
|
||||
process.env.ANTHROPIC_BASE_URL ?? '',
|
||||
process.env.USER_TYPE ?? '',
|
||||
...envParts,
|
||||
@@ -48,9 +50,14 @@ function buildCapabilityOverrideCacheKey(
|
||||
* the pinned ANTHROPIC_DEFAULT_*_MODEL env vars.
|
||||
*/
|
||||
export const get3PModelCapabilityOverride = memoize(
|
||||
(model: string, capability: ModelCapabilityOverride): boolean | undefined => {
|
||||
(
|
||||
model: string,
|
||||
capability: ModelCapabilityOverride,
|
||||
apiProvider?: ReturnType<typeof getAPIProvider>,
|
||||
): boolean | undefined => {
|
||||
const resolvedApiProvider = apiProvider ?? getAPIProvider()
|
||||
if (
|
||||
getAPIProvider() === 'firstParty' &&
|
||||
resolvedApiProvider === 'firstParty' &&
|
||||
isFirstPartyAnthropicBaseUrl()
|
||||
) {
|
||||
return undefined
|
||||
|
||||
Reference in New Issue
Block a user