mirror of
https://github.com/microsoft/vscode.git
synced 2026-08-15 18:37:00 +01:00
fix: forward reasoning effort on the BYOK Messages API path
The useMessagesApi branch of OpenAIEndpoint.createRequestBody never called _applyReasoningEffort, so a custom endpoint declaring supportsReasoningEffort never received the selected effort. Forward it like the Responses and Chat Completions branches, and add a 'messages' reasoningEffortFormat that writes the Anthropic Messages shape output_config.effort (the default on /messages, overridable via reasoningEffortFormat). The scrub merges into output_config instead of replacing it so sibling fields (e.g. structured output format) survive. Fixes #325121
This commit is contained in:
@@ -1967,9 +1967,10 @@
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"chat-completions",
|
||||
"responses"
|
||||
"responses",
|
||||
"messages"
|
||||
],
|
||||
"markdownDescription": "Body shape used to forward the reasoning effort to the model. `chat-completions` sends a top-level `reasoning_effort` string. `responses` sends a nested `reasoning.effort` object. When unset the format follows the URL: `/responses` → nested, otherwise top-level."
|
||||
"markdownDescription": "Body shape used to forward the reasoning effort to the model. `chat-completions` sends a top-level `reasoning_effort` string. `responses` sends a nested `reasoning.effort` object. `messages` sends the Anthropic Messages `output_config.effort` field. When unset the format follows the URL: `/responses` → nested, `/messages` → `output_config.effort`, otherwise top-level."
|
||||
},
|
||||
"requestHeaders": {
|
||||
"type": "object",
|
||||
@@ -2153,9 +2154,10 @@
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"chat-completions",
|
||||
"responses"
|
||||
"responses",
|
||||
"messages"
|
||||
],
|
||||
"markdownDescription": "Body shape used to forward the reasoning effort to the model. `chat-completions` sends a top-level `reasoning_effort` string. `responses` sends a nested `reasoning.effort` object. When unset the format follows the URL: `/responses` → nested, otherwise top-level."
|
||||
"markdownDescription": "Body shape used to forward the reasoning effort to the model. `chat-completions` sends a top-level `reasoning_effort` string. `responses` sends a nested `reasoning.effort` object. `messages` sends the Anthropic Messages `output_config.effort` field. When unset the format follows the URL: `/responses` → nested, `/messages` → `output_config.effort`, otherwise top-level."
|
||||
},
|
||||
"requestHeaders": {
|
||||
"type": "object",
|
||||
@@ -2306,9 +2308,10 @@
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"chat-completions",
|
||||
"responses"
|
||||
"responses",
|
||||
"messages"
|
||||
],
|
||||
"markdownDescription": "Body shape used to forward the reasoning effort to the model. `chat-completions` sends a top-level `reasoning_effort` string. `responses` sends a nested `reasoning.effort` object. When unset the format follows the URL: `/responses` → nested, otherwise top-level."
|
||||
"markdownDescription": "Body shape used to forward the reasoning effort to the model. `chat-completions` sends a top-level `reasoning_effort` string. `responses` sends a nested `reasoning.effort` object. `messages` sends the Anthropic Messages `output_config.effort` field. When unset the format follows the URL: `/responses` → nested, `/messages` → `output_config.effort`, otherwise top-level."
|
||||
},
|
||||
"requestHeaders": {
|
||||
"type": "object",
|
||||
|
||||
@@ -75,9 +75,10 @@ export interface BYOKModelCapabilities {
|
||||
* Override the body shape used to forward the reasoning effort to the model.
|
||||
* - `'chat-completions'`: top-level `reasoning_effort` (default for `/chat/completions`).
|
||||
* - `'responses'`: nested `reasoning.effort` (default for `/responses`).
|
||||
* If unset the format is inferred from whether the endpoint uses the Responses API.
|
||||
* - `'messages'`: `output_config.effort` (default for `/messages`).
|
||||
* If unset the format is inferred from the API path the endpoint uses.
|
||||
*/
|
||||
reasoningEffortFormat?: 'chat-completions' | 'responses';
|
||||
reasoningEffortFormat?: 'chat-completions' | 'responses' | 'messages';
|
||||
}
|
||||
|
||||
export interface BYOKModelRegistry {
|
||||
|
||||
@@ -268,7 +268,9 @@ export class OpenAIEndpoint extends ChatEndpoint {
|
||||
return this._applyConfiguredModelOptions(body, options);
|
||||
} else if (this.useMessagesApi) {
|
||||
// Delegate to base ChatEndpoint for Messages API dispatch
|
||||
return this._applyConfiguredModelOptions(super.createRequestBody(options), options);
|
||||
const body = super.createRequestBody(options);
|
||||
this._applyReasoningEffort(body, options);
|
||||
return this._applyConfiguredModelOptions(body, options);
|
||||
} else {
|
||||
// Handle Chat Completions: provide callback for thinking data processing
|
||||
const supportsThinking = !!this.modelMetadata.capabilities.supports.thinking;
|
||||
@@ -320,7 +322,8 @@ export class OpenAIEndpoint extends ChatEndpoint {
|
||||
|
||||
/**
|
||||
* Forwards the per-request reasoning effort to the model body in the shape the endpoint expects.
|
||||
* Default shape mirrors the API path (`Responses` \u2192 nested `reasoning.effort`, `Chat Completions` \u2192 top-level `reasoning_effort`).
|
||||
* Default shape mirrors the API path (`Responses` \u2192 nested `reasoning.effort`, `Messages` \u2192 `output_config.effort`,
|
||||
* `Chat Completions` \u2192 top-level `reasoning_effort`).
|
||||
* `IChatModelInformation.reasoningEffortFormat` overrides the default so users hosting OpenAI-compatible servers
|
||||
* with diverging conventions (e.g. nested `reasoning.effort` on `/chat/completions`) can opt in deterministically.
|
||||
*/
|
||||
@@ -330,9 +333,9 @@ export class OpenAIEndpoint extends ChatEndpoint {
|
||||
return;
|
||||
}
|
||||
const format = this.modelMetadata.reasoningEffortFormat
|
||||
?? (this.useResponsesApi ? 'responses' : 'chat-completions');
|
||||
?? (this.useResponsesApi ? 'responses' : this.useMessagesApi ? 'messages' : 'chat-completions');
|
||||
const override = this._configurationService.getConfig(ConfigKey.Advanced.ReasoningEffortOverride);
|
||||
const requested = override || options.modelCapabilities?.reasoningEffort || body.reasoning?.effort || body.reasoning_effort;
|
||||
const requested = override || options.modelCapabilities?.reasoningEffort || body.reasoning?.effort || body.reasoning_effort || body.output_config?.effort;
|
||||
const effort = requested && supports.includes(requested) ? requested : undefined;
|
||||
// Scrub any pre-populated effort first so unsupported values (e.g. the hard-coded `medium` default
|
||||
// from `createResponsesRequestBody`) cannot leak through, then write the resolved value into the
|
||||
@@ -342,9 +345,16 @@ export class OpenAIEndpoint extends ChatEndpoint {
|
||||
body.reasoning = Object.keys(rest).length > 0 ? rest : undefined;
|
||||
}
|
||||
body.reasoning_effort = undefined;
|
||||
if (body.output_config) {
|
||||
// Drop only the effort so other output_config fields (e.g. structured output format) survive
|
||||
const { effort: _drop, ...rest } = body.output_config;
|
||||
body.output_config = Object.keys(rest).length > 0 ? rest : undefined;
|
||||
}
|
||||
if (effort) {
|
||||
if (format === 'responses') {
|
||||
body.reasoning = { ...body.reasoning, effort };
|
||||
} else if (format === 'messages') {
|
||||
body.output_config = { ...body.output_config, effort };
|
||||
} else {
|
||||
body.reasoning_effort = effort;
|
||||
}
|
||||
|
||||
@@ -527,6 +527,48 @@ describe('OpenAIEndpoint - Reasoning Properties', () => {
|
||||
expect(body.reasoning?.effort).toBeUndefined();
|
||||
});
|
||||
|
||||
it('places `output_config.effort` on the Messages API path when the model supports the requested level', () => {
|
||||
const endpoint = instaService.createInstance(OpenAIEndpoint,
|
||||
buildModel({ supported_endpoints: [ModelSupportedEndpoint.Messages] }),
|
||||
'test-api-key',
|
||||
'https://api.anthropic.com/v1/messages');
|
||||
|
||||
const body = endpoint.createRequestBody(buildOptions('high'));
|
||||
|
||||
expect(body.output_config).toEqual({ effort: 'high' });
|
||||
expect(body.reasoning_effort).toBeUndefined();
|
||||
expect(body.reasoning).toBeUndefined();
|
||||
});
|
||||
|
||||
it('emits top-level `reasoning_effort` instead of `output_config.effort` when `reasoningEffortFormat` is `chat-completions` on a Messages URL', () => {
|
||||
const endpoint = instaService.createInstance(OpenAIEndpoint,
|
||||
buildModel({
|
||||
reasoningEffortFormat: 'chat-completions',
|
||||
supported_endpoints: [ModelSupportedEndpoint.Messages],
|
||||
}),
|
||||
'test-api-key',
|
||||
'https://api.anthropic.com/v1/messages');
|
||||
|
||||
const body = endpoint.createRequestBody(buildOptions('high'));
|
||||
|
||||
expect(body.reasoning_effort).toBe('high');
|
||||
expect(body.output_config).toBeUndefined();
|
||||
});
|
||||
|
||||
it('scrubs `output_config.effort` while preserving other `output_config` fields', () => {
|
||||
// `output_config` also carries structured-output fields (e.g. `format`); only the effort may be rewritten
|
||||
const endpoint = instaService.createInstance(OpenAIEndpoint,
|
||||
buildModel({ supported_endpoints: [ModelSupportedEndpoint.Messages] }),
|
||||
'test-api-key',
|
||||
'https://api.anthropic.com/v1/messages');
|
||||
const apply = (endpoint as unknown as { _applyReasoningEffort: (body: IEndpointBody, options: ICreateEndpointBodyOptions) => void })._applyReasoningEffort.bind(endpoint);
|
||||
|
||||
const body: IEndpointBody = { output_config: { effort: 'unsupported-level', format: { type: 'json_schema', schema: {} } } as IEndpointBody['output_config'] };
|
||||
apply(body, buildOptions('high'));
|
||||
|
||||
expect(body.output_config).toEqual({ format: { type: 'json_schema', schema: {} }, effort: 'high' });
|
||||
});
|
||||
|
||||
it('does not emit a reasoning field when the model declares no reasoning support', () => {
|
||||
const endpoint = instaService.createInstance(OpenAIEndpoint,
|
||||
{
|
||||
|
||||
@@ -104,7 +104,7 @@ interface _CustomEndpointModelConfig {
|
||||
modelOptions?: IChatModelRequestOptions;
|
||||
zeroDataRetentionEnabled?: boolean;
|
||||
supportsReasoningEffort?: string[];
|
||||
reasoningEffortFormat?: 'chat-completions' | 'responses';
|
||||
reasoningEffortFormat?: 'chat-completions' | 'responses' | 'messages';
|
||||
}
|
||||
|
||||
export interface CustomEndpointModelConfig extends _CustomEndpointModelConfig {
|
||||
|
||||
@@ -66,7 +66,7 @@ interface _CustomOAIModelConfig {
|
||||
requestHeaders?: Record<string, string>;
|
||||
zeroDataRetentionEnabled?: boolean;
|
||||
supportsReasoningEffort?: string[];
|
||||
reasoningEffortFormat?: 'chat-completions' | 'responses';
|
||||
reasoningEffortFormat?: 'chat-completions' | 'responses' | 'messages';
|
||||
}
|
||||
|
||||
export interface CustomOAIModelConfig extends _CustomOAIModelConfig {
|
||||
|
||||
@@ -138,9 +138,9 @@ export type IChatModelInformation = IModelAPIResponse & {
|
||||
/**
|
||||
* BYOK-only override that forces the body shape used when forwarding the reasoning effort to the model.
|
||||
* Honored by `OpenAIEndpoint`. Unset — the body shape follows the API path (Responses API → nested `reasoning.effort`,
|
||||
* Chat Completions → top-level `reasoning_effort`).
|
||||
* Anthropic Messages API → `output_config.effort`, Chat Completions → top-level `reasoning_effort`).
|
||||
*/
|
||||
reasoningEffortFormat?: 'chat-completions' | 'responses';
|
||||
reasoningEffortFormat?: 'chat-completions' | 'responses' | 'messages';
|
||||
};
|
||||
|
||||
export function isChatModelInformation(model: IModelAPIResponse): model is IChatModelInformation {
|
||||
|
||||
Reference in New Issue
Block a user