diff --git a/extensions/copilot/package.json b/extensions/copilot/package.json index 2c3bb392eeb..3bc9fcfb8f7 100644 --- a/extensions/copilot/package.json +++ b/extensions/copilot/package.json @@ -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", diff --git a/extensions/copilot/src/extension/byok/common/byokProvider.ts b/extensions/copilot/src/extension/byok/common/byokProvider.ts index a47b6d00637..adbfe439046 100644 --- a/extensions/copilot/src/extension/byok/common/byokProvider.ts +++ b/extensions/copilot/src/extension/byok/common/byokProvider.ts @@ -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 { diff --git a/extensions/copilot/src/extension/byok/node/openAIEndpoint.ts b/extensions/copilot/src/extension/byok/node/openAIEndpoint.ts index aed5d1b64dd..ba73c73c9e3 100644 --- a/extensions/copilot/src/extension/byok/node/openAIEndpoint.ts +++ b/extensions/copilot/src/extension/byok/node/openAIEndpoint.ts @@ -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; } diff --git a/extensions/copilot/src/extension/byok/node/test/openAIEndpoint.spec.ts b/extensions/copilot/src/extension/byok/node/test/openAIEndpoint.spec.ts index e8716894eff..44bca53a257 100644 --- a/extensions/copilot/src/extension/byok/node/test/openAIEndpoint.spec.ts +++ b/extensions/copilot/src/extension/byok/node/test/openAIEndpoint.spec.ts @@ -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, { diff --git a/extensions/copilot/src/extension/byok/vscode-node/customEndpointProvider.ts b/extensions/copilot/src/extension/byok/vscode-node/customEndpointProvider.ts index 565e6c05a8a..6bf6c10d170 100644 --- a/extensions/copilot/src/extension/byok/vscode-node/customEndpointProvider.ts +++ b/extensions/copilot/src/extension/byok/vscode-node/customEndpointProvider.ts @@ -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 { diff --git a/extensions/copilot/src/extension/byok/vscode-node/customOAIProvider.ts b/extensions/copilot/src/extension/byok/vscode-node/customOAIProvider.ts index 318f14dedba..02da71ab718 100644 --- a/extensions/copilot/src/extension/byok/vscode-node/customOAIProvider.ts +++ b/extensions/copilot/src/extension/byok/vscode-node/customOAIProvider.ts @@ -66,7 +66,7 @@ interface _CustomOAIModelConfig { requestHeaders?: Record; zeroDataRetentionEnabled?: boolean; supportsReasoningEffort?: string[]; - reasoningEffortFormat?: 'chat-completions' | 'responses'; + reasoningEffortFormat?: 'chat-completions' | 'responses' | 'messages'; } export interface CustomOAIModelConfig extends _CustomOAIModelConfig { diff --git a/extensions/copilot/src/platform/endpoint/common/endpointProvider.ts b/extensions/copilot/src/platform/endpoint/common/endpointProvider.ts index 28d7f2283de..d53f9bb744a 100644 --- a/extensions/copilot/src/platform/endpoint/common/endpointProvider.ts +++ b/extensions/copilot/src/platform/endpoint/common/endpointProvider.ts @@ -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 {