Skip to content

Commit b7e094d

Browse files
authored
Guard reasoning effort parameter against unsupported models (#5010)
When a subagent inherits the parent agent's reasoningEffort but is routed to a model that doesn't support the effort parameter (e.g. claude-haiku-4.5 via search subagent), the API returns a 400 error. Fix: only include the effort parameter in the request body when the endpoint declares supportsReasoningEffort with at least one value. Affects both Messages API (Anthropic) and Responses API (OpenAI). Includes unit tests for the Messages API effort guard.
1 parent 1dad896 commit b7e094d

3 files changed

Lines changed: 175 additions & 6 deletions

File tree

src/platform/endpoint/node/messagesApi.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -168,10 +168,8 @@ export function createMessagesRequestBody(accessor: ServicesAccessor, options: I
168168
}
169169

170170
const thinkingEnabled = !!thinkingConfig;
171-
172-
// Build output config with effort level for thinking, validating reasoningEffort
173171
let effort: 'low' | 'medium' | 'high' | undefined;
174-
if (thinkingConfig) {
172+
if (thinkingConfig && endpoint.supportsReasoningEffort?.length) {
175173
const candidateEffort = configurationService.getConfig(ConfigKey.TeamInternal.AnthropicThinkingEffort) ?? reasoningEffort;
176174
if (candidateEffort === 'low' || candidateEffort === 'medium' || candidateEffort === 'high') {
177175
effort = candidateEffort;

src/platform/endpoint/node/responsesApi.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,9 @@ export function createResponsesRequestBody(accessor: ServicesAccessor, options:
7474
const summaryConfig = configService.getExperimentBasedConfig(ConfigKey.ResponsesApiReasoningSummary, expService);
7575
const shouldDisableReasoningSummary = endpoint.family === 'gpt-5.3-codex-spark-preview';
7676
const effortFromSetting = configService.getConfig(ConfigKey.TeamInternal.ResponsesApiReasoningEffort);
77-
const effort = effortFromSetting || options.reasoningEffort || 'medium';
77+
const effort = endpoint.supportsReasoningEffort?.length
78+
? (effortFromSetting || options.reasoningEffort || 'medium')
79+
: undefined;
7880
const summary = summaryConfig === 'off' || shouldDisableReasoningSummary ? undefined : summaryConfig;
7981
if (effort || summary) {
8082
body.reasoning = {

src/platform/endpoint/test/node/messagesApi.spec.ts

Lines changed: 171 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,17 @@
55

66
import type { ContentBlockParam, DocumentBlockParam, ImageBlockParam, MessageParam, TextBlockParam, ToolReferenceBlockParam, ToolResultBlockParam } from '@anthropic-ai/sdk/resources';
77
import { Raw } from '@vscode/prompt-tsx';
8-
import { expect, suite, test } from 'vitest';
8+
import { beforeEach, describe, expect, suite, test } from 'vitest';
9+
import { DisposableStore } from '../../../../util/vs/base/common/lifecycle';
10+
import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation';
11+
import { ChatLocation } from '../../../chat/common/commonTypes';
12+
import { ConfigKey, IConfigurationService } from '../../../configuration/common/configurationService';
13+
import { InMemoryConfigurationService } from '../../../configuration/test/common/inMemoryConfigurationService';
914
import { AnthropicMessagesTool, CUSTOM_TOOL_SEARCH_NAME } from '../../../networking/common/anthropic';
10-
import { addToolsAndSystemCacheControl, buildToolInputSchema, rawMessagesToMessagesAPI } from '../../node/messagesApi';
15+
import { IChatEndpoint, ICreateEndpointBodyOptions } from '../../../networking/common/networking';
16+
import { IToolDeferralService } from '../../../networking/common/toolDeferralService';
17+
import { createPlatformServices } from '../../../test/node/services';
18+
import { addToolsAndSystemCacheControl, buildToolInputSchema, createMessagesRequestBody, rawMessagesToMessagesAPI } from '../../node/messagesApi';
1119

1220
function assertContentArray(content: MessageParam['content']): ContentBlockParam[] {
1321
expect(Array.isArray(content)).toBe(true);
@@ -656,3 +664,164 @@ suite('buildToolInputSchema', function () {
656664
expect(result).toEqual(schema);
657665
});
658666
});
667+
668+
describe('createMessagesRequestBody reasoning effort', () => {
669+
let disposables: DisposableStore;
670+
let instantiationService: IInstantiationService;
671+
let mockConfig: InMemoryConfigurationService;
672+
673+
function createMockEndpoint(overrides: Partial<IChatEndpoint> = {}): IChatEndpoint {
674+
return {
675+
model: 'claude-sonnet-4.5',
676+
family: 'claude-sonnet-4.5',
677+
modelProvider: 'Anthropic',
678+
maxOutputTokens: 8192,
679+
modelMaxPromptTokens: 200000,
680+
supportsToolCalls: true,
681+
supportsVision: true,
682+
supportsPrediction: false,
683+
showInModelPicker: true,
684+
isFallback: false,
685+
name: 'test',
686+
version: '1.0',
687+
policy: 'enabled',
688+
urlOrRequestMetadata: 'https://test.com',
689+
tokenizer: 0,
690+
isDefault: false,
691+
processResponseFromChatEndpoint: () => { throw new Error('not implemented'); },
692+
acceptChatPolicy: () => { throw new Error('not implemented'); },
693+
makeChatRequest2: () => { throw new Error('not implemented'); },
694+
createRequestBody: () => { throw new Error('not implemented'); },
695+
cloneWithTokenOverride: () => { throw new Error('not implemented'); },
696+
interceptBody: () => { },
697+
getExtraHeaders: () => ({}),
698+
...overrides,
699+
} as IChatEndpoint;
700+
}
701+
702+
function createMinimalOptions(overrides: Partial<ICreateEndpointBodyOptions> = {}): ICreateEndpointBodyOptions {
703+
return {
704+
debugName: 'test',
705+
requestId: 'test-request-id',
706+
finishedCb: undefined,
707+
messages: [{
708+
role: Raw.ChatRole.User,
709+
content: [{ type: Raw.ChatCompletionContentPartKind.Text, text: 'Hello' }],
710+
}],
711+
postOptions: { max_tokens: 8192 },
712+
location: ChatLocation.Panel,
713+
...overrides,
714+
};
715+
}
716+
717+
beforeEach(() => {
718+
disposables = new DisposableStore();
719+
const services = disposables.add(createPlatformServices(disposables));
720+
services.define(IToolDeferralService, {
721+
_serviceBrand: undefined,
722+
isNonDeferredTool: () => true,
723+
});
724+
const accessor = services.createTestingAccessor();
725+
instantiationService = accessor.get(IInstantiationService);
726+
mockConfig = accessor.get(IConfigurationService) as InMemoryConfigurationService;
727+
});
728+
729+
test('includes effort in output_config when model supports reasoning effort and thinking is adaptive', () => {
730+
const endpoint = createMockEndpoint({
731+
supportsAdaptiveThinking: true,
732+
supportsReasoningEffort: ['low', 'medium', 'high'],
733+
});
734+
const options = createMinimalOptions({
735+
enableThinking: true,
736+
reasoningEffort: 'high',
737+
});
738+
739+
const body = instantiationService.invokeFunction(createMessagesRequestBody, options, endpoint.model, endpoint);
740+
741+
expect(body.thinking).toEqual({ type: 'adaptive' });
742+
expect(body.output_config).toEqual({ effort: 'high' });
743+
});
744+
745+
test('omits effort when model does not declare supportsReasoningEffort', () => {
746+
const endpoint = createMockEndpoint({
747+
supportsAdaptiveThinking: true,
748+
// supportsReasoningEffort is undefined
749+
});
750+
const options = createMinimalOptions({
751+
enableThinking: true,
752+
reasoningEffort: 'high',
753+
});
754+
755+
const body = instantiationService.invokeFunction(createMessagesRequestBody, options, endpoint.model, endpoint);
756+
757+
expect(body.thinking).toEqual({ type: 'adaptive' });
758+
expect(body.output_config).toBeUndefined();
759+
});
760+
761+
test('omits effort when supportsReasoningEffort is an empty array', () => {
762+
const endpoint = createMockEndpoint({
763+
supportsAdaptiveThinking: true,
764+
supportsReasoningEffort: [],
765+
});
766+
const options = createMinimalOptions({
767+
enableThinking: true,
768+
reasoningEffort: 'medium',
769+
});
770+
771+
const body = instantiationService.invokeFunction(createMessagesRequestBody, options, endpoint.model, endpoint);
772+
773+
expect(body.thinking).toEqual({ type: 'adaptive' });
774+
expect(body.output_config).toBeUndefined();
775+
});
776+
777+
test('omits effort when thinking is not enabled', () => {
778+
const endpoint = createMockEndpoint({
779+
supportsAdaptiveThinking: true,
780+
supportsReasoningEffort: ['low', 'medium', 'high'],
781+
});
782+
const options = createMinimalOptions({
783+
enableThinking: false,
784+
reasoningEffort: 'high',
785+
});
786+
787+
const body = instantiationService.invokeFunction(createMessagesRequestBody, options, endpoint.model, endpoint);
788+
789+
expect(body.thinking).toBeUndefined();
790+
expect(body.output_config).toBeUndefined();
791+
});
792+
793+
test('omits effort when reasoningEffort is an invalid value', () => {
794+
const endpoint = createMockEndpoint({
795+
supportsAdaptiveThinking: true,
796+
supportsReasoningEffort: ['low', 'medium', 'high'],
797+
});
798+
const options = createMinimalOptions({
799+
enableThinking: true,
800+
reasoningEffort: 'xhigh' as any,
801+
});
802+
803+
const body = instantiationService.invokeFunction(createMessagesRequestBody, options, endpoint.model, endpoint);
804+
805+
expect(body.thinking).toEqual({ type: 'adaptive' });
806+
expect(body.output_config).toBeUndefined();
807+
});
808+
809+
test('uses budget_tokens thinking when model has maxThinkingBudget but not adaptive', () => {
810+
const endpoint = createMockEndpoint({
811+
supportsAdaptiveThinking: false,
812+
maxThinkingBudget: 32000,
813+
minThinkingBudget: 1024,
814+
supportsReasoningEffort: ['low', 'medium', 'high'],
815+
});
816+
mockConfig.setConfig(ConfigKey.AnthropicThinkingBudget, 10000);
817+
const options = createMinimalOptions({
818+
enableThinking: true,
819+
reasoningEffort: 'low',
820+
});
821+
822+
const body = instantiationService.invokeFunction(createMessagesRequestBody, options, endpoint.model, endpoint);
823+
824+
expect(body.thinking).toEqual({ type: 'enabled', budget_tokens: 8191 });
825+
expect(body.output_config).toEqual({ effort: 'low' });
826+
});
827+
});

0 commit comments

Comments
 (0)