diff --git a/extensions/copilot/src/extension/byok/common/byokProvider.ts b/extensions/copilot/src/extension/byok/common/byokProvider.ts index b729c6144e55a6..5929d56a360a94 100644 --- a/extensions/copilot/src/extension/byok/common/byokProvider.ts +++ b/extensions/copilot/src/extension/byok/common/byokProvider.ts @@ -68,6 +68,7 @@ export interface BYOKModelCapabilities { editTools?: EndpointEditToolName[]; requestHeaders?: Record; modelOptions?: IChatModelRequestOptions; + extraBody?: Record, supportedEndpoints?: ModelSupportedEndpoint[]; zeroDataRetentionEnabled?: boolean; supportsReasoningEffort?: string[]; @@ -172,6 +173,7 @@ export function resolveModelInfo(modelId: string, providerName: string, knownMod supported_endpoints: knownModelInfo?.supportedEndpoints, zeroDataRetentionEnabled: knownModelInfo?.zeroDataRetentionEnabled, modelOptions: knownModelInfo?.modelOptions, + extraBody: knownModelInfo?.extraBody, reasoningEffortFormat: knownModelInfo?.reasoningEffortFormat }; if (knownModelInfo?.requestHeaders && Object.keys(knownModelInfo.requestHeaders).length > 0) { diff --git a/extensions/copilot/src/extension/byok/common/test/byokProvider.spec.ts b/extensions/copilot/src/extension/byok/common/test/byokProvider.spec.ts index cd34a9381644d9..6e44fc0fe47a0e 100644 --- a/extensions/copilot/src/extension/byok/common/test/byokProvider.spec.ts +++ b/extensions/copilot/src/extension/byok/common/test/byokProvider.spec.ts @@ -102,6 +102,23 @@ describe('resolveModelInfo', () => { }); }); + it('propagates configured extra body properties into chat-endpoint inputs', () => { + const info = resolveModelInfo('m1', 'TestProvider', undefined, { + ...baseCapabilities, + extraBody: { + chat_template_kwargs: { + enable_thinking: false, + }, + }, + }); + + expect(info.extraBody).toEqual({ + chat_template_kwargs: { + enable_thinking: false, + }, + }); + }); + it('honors an explicit contextWindow as the source of truth for the context window', () => { // A model documented as: Context Length 1M, Max Output 384K. The user can now // declare the real capability directly instead of back-computing maxInputTokens. diff --git a/extensions/copilot/src/extension/byok/node/openAIEndpoint.ts b/extensions/copilot/src/extension/byok/node/openAIEndpoint.ts index 7193675b8be904..1e7fa044d0c2b6 100644 --- a/extensions/copilot/src/extension/byok/node/openAIEndpoint.ts +++ b/extensions/copilot/src/extension/byok/node/openAIEndpoint.ts @@ -281,11 +281,13 @@ export class OpenAIEndpoint extends ChatEndpoint { body.previous_response_id = undefined; } this._applyReasoningEffort(body, options); + this._applyExtraBody(body, options); return this._applyConfiguredModelOptions(body, options); } else if (this.useMessagesApi) { // Delegate to base ChatEndpoint for Messages API dispatch const body = super.createRequestBody(options); this._applyReasoningEffort(body, options); + this._applyExtraBody(body, options); return this._applyConfiguredModelOptions(body, options); } else { const body = createCapiRequestBody(options, this.model, this.getCompletionsCallback()); @@ -293,6 +295,7 @@ export class OpenAIEndpoint extends ChatEndpoint { body.messages = normalizeKimiToolCallIds(body.messages); } this._applyReasoningEffort(body, options); + this._applyExtraBody(body, options); return this._applyConfiguredModelOptions(body, options); } } @@ -362,6 +365,24 @@ export class OpenAIEndpoint extends ChatEndpoint { } } + /** + * Forwards optional JSON properties, similar to OpenAI's 'extra_body' API. + * Typical parameters include: 'enable_thinking' when using QwenCloud, 'top_k' against a vLLM backend or + * 'chat_template_kwargs' for SGLang, etc. Those properties are fully user provided and don't follow + * any predefined model. + */ + private _applyExtraBody(body: IEndpointBody, options: ICreateEndpointBodyOptions): void { + const extraBody = this.modelMetadata.extraBody; + if (!extraBody) { + return; + } + + for (const [key, value] of Object.entries(extraBody)) { + // The options can be arbitrary objects, e.g. 'chat_template_kwargs' + (body as Record)[key] = value; + } + } + override interceptBody(body: IEndpointBody | undefined): void { super.interceptBody(body); // TODO @lramos15 - We should do this for all models and not just here diff --git a/extensions/copilot/src/extension/byok/vscode-node/azureProvider.ts b/extensions/copilot/src/extension/byok/vscode-node/azureProvider.ts index 0d42778c40c40e..4569b9c520d304 100644 --- a/extensions/copilot/src/extension/byok/vscode-node/azureProvider.ts +++ b/extensions/copilot/src/extension/byok/vscode-node/azureProvider.ts @@ -139,6 +139,7 @@ export class AzureBYOKModelProvider extends AbstractCustomOAIBYOKModelProvider { thinking: modelConfiguration?.thinking, streaming: modelConfiguration?.streaming, requestHeaders: modelConfiguration?.requestHeaders, + extraBody: modelConfiguration?.extraBody, editTools: model.capabilities?.editTools?.filter(isEndpointEditToolName), zeroDataRetentionEnabled: modelConfiguration?.zeroDataRetentionEnabled }; diff --git a/extensions/copilot/src/extension/byok/vscode-node/customEndpointProvider.ts b/extensions/copilot/src/extension/byok/vscode-node/customEndpointProvider.ts index 6bf6c10d170b90..8f1f806f72b6d9 100644 --- a/extensions/copilot/src/extension/byok/vscode-node/customEndpointProvider.ts +++ b/extensions/copilot/src/extension/byok/vscode-node/customEndpointProvider.ts @@ -102,6 +102,7 @@ interface _CustomEndpointModelConfig { editTools?: EndpointEditToolName[]; requestHeaders?: Record; modelOptions?: IChatModelRequestOptions; + extraBody?: Record, zeroDataRetentionEnabled?: boolean; supportsReasoningEffort?: string[]; reasoningEffortFormat?: 'chat-completions' | 'responses' | 'messages'; @@ -165,6 +166,7 @@ export class CustomEndpointBYOKModelProvider extends AbstractOpenAICompatibleLMP streaming: modelConfiguration?.streaming, requestHeaders: modelConfiguration?.requestHeaders, modelOptions: modelConfiguration?.modelOptions, + extraBody: modelConfiguration?.extraBody, zeroDataRetentionEnabled: modelConfiguration?.zeroDataRetentionEnabled, supportsReasoningEffort: modelConfiguration?.supportsReasoningEffort, reasoningEffortFormat: modelConfiguration?.reasoningEffortFormat diff --git a/extensions/copilot/src/extension/byok/vscode-node/customOAIProvider.ts b/extensions/copilot/src/extension/byok/vscode-node/customOAIProvider.ts index 02da71ab718eb7..79caba0f9a8630 100644 --- a/extensions/copilot/src/extension/byok/vscode-node/customOAIProvider.ts +++ b/extensions/copilot/src/extension/byok/vscode-node/customOAIProvider.ts @@ -64,6 +64,8 @@ interface _CustomOAIModelConfig { streaming?: boolean; editTools?: EndpointEditToolName[]; requestHeaders?: Record; + /** This is called 'extra_body' in OpenAI api */ + extraBody?: Record, zeroDataRetentionEnabled?: boolean; supportsReasoningEffort?: string[]; reasoningEffortFormat?: 'chat-completions' | 'responses' | 'messages'; @@ -149,6 +151,7 @@ export abstract class AbstractCustomOAIBYOKModelProvider extends AbstractOpenAIC thinking: modelConfiguration?.thinking ?? false, streaming: modelConfiguration?.streaming, requestHeaders: modelConfiguration?.requestHeaders, + extraBody: modelConfiguration?.extraBody, zeroDataRetentionEnabled: modelConfiguration?.zeroDataRetentionEnabled, supportsReasoningEffort: modelConfiguration?.supportsReasoningEffort, reasoningEffortFormat: modelConfiguration?.reasoningEffortFormat diff --git a/extensions/copilot/src/extension/byok/vscode-node/test/customEndpointProvider.spec.ts b/extensions/copilot/src/extension/byok/vscode-node/test/customEndpointProvider.spec.ts index e9e5903be69c94..e2cba79306797b 100644 --- a/extensions/copilot/src/extension/byok/vscode-node/test/customEndpointProvider.spec.ts +++ b/extensions/copilot/src/extension/byok/vscode-node/test/customEndpointProvider.spec.ts @@ -453,6 +453,66 @@ describe('CustomEndpointBYOKModelProvider', () => { ]); }); + it('applies configured extra body parameters to Responses and Messages API bodies', () => { + const results = [ + { + supportedEndpoints: [ModelSupportedEndpoint.Responses], + url: 'https://api.example.com/v1/responses', + }, + { + supportedEndpoints: [ModelSupportedEndpoint.Messages], + url: 'https://api.example.com/v1/messages', + }, + ].map(({ supportedEndpoints, url }) => { + const metadata: IChatModelInformation = { + ...makeMetadata(supportedEndpoints), + extraBody: { + chat_template_kwargs: { + enable_thinking: false + } + }, + }; + const endpoint = instaService.createInstance(CustomEndpointOAIEndpoint, + metadata, + 'test-api-key', + url); + const body = endpoint.createRequestBody({ + debugName: 'test', + messages: [{ + role: Raw.ChatRole.User, + content: [{ type: Raw.ChatCompletionContentPartKind.Text, text: 'Hello' }] + }], + requestId: `test-req-${endpoint.apiType}-model-options`, + postOptions: { + stream: true, + }, + finishedCb: undefined, + location: ChatLocation.Other, + }); + + return { + apiType: endpoint.apiType, + chat_template_kwargs: (body as any).chat_template_kwargs, + }; + }); + + expect(results).toEqual([ + { + apiType: 'responses', + chat_template_kwargs: { + enable_thinking: false + } + }, + { + apiType: 'messages', + chat_template_kwargs: { + enable_thinking: false + } + }, + ]); + }); + + it('replaces default Bearer with user-supplied Authorization header on Chat Completions endpoints', () => { const metadata = makeMetadata(undefined); metadata.requestHeaders = { 'Authorization': 'Bearer user-token' }; diff --git a/extensions/copilot/src/extension/prompt/vscode-node/requestLoggerImpl.ts b/extensions/copilot/src/extension/prompt/vscode-node/requestLoggerImpl.ts index 233540ca5c1dac..d07fe3487e4688 100644 --- a/extensions/copilot/src/extension/prompt/vscode-node/requestLoggerImpl.ts +++ b/extensions/copilot/src/extension/prompt/vscode-node/requestLoggerImpl.ts @@ -15,7 +15,6 @@ import { ILogService } from '../../../platform/log/common/logService'; import { messageToMarkdown } from '../../../platform/log/common/messageStringify'; import { ContextManagementResponse } from '../../../platform/networking/common/anthropic'; import { IResponseDelta, isOpenAiFunctionTool } from '../../../platform/networking/common/fetch'; -import { IEndpointBody } from '../../../platform/networking/common/networking'; import { CapturingToken } from '../../../platform/requestLogger/common/capturingToken'; import { ChatRequestScheme, ILoggedElementInfo, ILoggedRequestInfo, ILoggedToolCall, LoggedInfo, LoggedInfoKind, LoggedRequest, LoggedRequestKind, resolveMarkdownContent } from '../../../platform/requestLogger/common/requestLogger'; import { AbstractRequestLogger } from '../../../platform/requestLogger/node/requestLogger'; @@ -602,12 +601,26 @@ export class RequestLogger extends AbstractRequestLogger { result.push(`# ${entry.debugName} - ${id}`); result.push(``); - // Just some other options to track - // TODO Probably we should just extract every item on the body and format it as below, instead of doing this one-by-one - const otherOptions: Record = {}; - for (const opt of ['temperature', 'stream', 'store', 'reasoning_effort'] satisfies (keyof IEndpointBody)[]) { - if (entry.chatParams.body?.[opt] !== undefined) { - otherOptions[opt] = entry.chatParams.body[opt]; + // Options that aren't already shown all end up in 'otherOptions' for debug purposes. + // This includes 'temperature', 'stream', 'store', 'reasoning_effort', everything that is in 'extraBody', etc. + const otherOptions: Record = {}; + if (entry.chatParams.body) { + for (const [optKey, optValue] of Object.entries(entry.chatParams.body)) { + if ([ + 'input', + 'max_completion_tokens', + 'max_output_tokens', + 'max_tokens', + 'messages', + 'model', + 'prediction', + 'reasoning', + 'tools', + ].includes(optKey)) { + continue; + } + + otherOptions[optKey] = optValue; } } diff --git a/extensions/copilot/src/platform/endpoint/common/endpointProvider.ts b/extensions/copilot/src/platform/endpoint/common/endpointProvider.ts index 7b8dbf0e901e68..68bdf2be06fa8c 100644 --- a/extensions/copilot/src/platform/endpoint/common/endpointProvider.ts +++ b/extensions/copilot/src/platform/endpoint/common/endpointProvider.ts @@ -143,6 +143,7 @@ export type IChatModelInformation = IModelAPIResponse & { urlOrRequestMetadata?: string | RequestMetadata; requestHeaders?: Readonly>; modelOptions?: Readonly; + extraBody?: Readonly>; zeroDataRetentionEnabled?: boolean; /** * BYOK-only override that forces the body shape used when forwarding the reasoning effort to the model.