Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions extensions/copilot/src/extension/byok/common/byokProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export interface BYOKModelCapabilities {
editTools?: EndpointEditToolName[];
requestHeaders?: Record<string, string>;
modelOptions?: IChatModelRequestOptions;
extraBody?: Record<string, Object>,
supportedEndpoints?: ModelSupportedEndpoint[];
zeroDataRetentionEnabled?: boolean;
supportsReasoningEffort?: string[];
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,23 @@ describe('resolveModelInfo', () => {
});
});

it('propagates configured extra body properties into chat-endpoint inputs', () => {
Comment thread
gpotter2 marked this conversation as resolved.
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.
Expand Down
21 changes: 21 additions & 0 deletions extensions/copilot/src/extension/byok/node/openAIEndpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,18 +281,21 @@ 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());
if (body.messages && isKimiFamily(this)) {
body.messages = normalizeKimiToolCallIds(body.messages);
}
this._applyReasoningEffort(body, options);
this._applyExtraBody(body, options);
return this._applyConfiguredModelOptions(body, options);
}
}
Expand Down Expand Up @@ -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<string, Object>)[key] = value;
}
}

override interceptBody(body: IEndpointBody | undefined): void {
super.interceptBody(body);
// TODO @lramos15 - We should do this for all models and not just here
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ interface _CustomEndpointModelConfig {
editTools?: EndpointEditToolName[];
requestHeaders?: Record<string, string>;
modelOptions?: IChatModelRequestOptions;
extraBody?: Record<string, Object>,
zeroDataRetentionEnabled?: boolean;
supportsReasoningEffort?: string[];
reasoningEffortFormat?: 'chat-completions' | 'responses' | 'messages';
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ interface _CustomOAIModelConfig {
streaming?: boolean;
editTools?: EndpointEditToolName[];
requestHeaders?: Record<string, string>;
/** This is called 'extra_body' in OpenAI api */
extraBody?: Record<string, Object>,
zeroDataRetentionEnabled?: boolean;
supportsReasoningEffort?: string[];
reasoningEffortFormat?: 'chat-completions' | 'responses' | 'messages';
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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' };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<string, string | number | boolean> = {};
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<string, string | number | boolean | object> = {};
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;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ export type IChatModelInformation = IModelAPIResponse & {
urlOrRequestMetadata?: string | RequestMetadata;
requestHeaders?: Readonly<Record<string, string>>;
modelOptions?: Readonly<IChatModelRequestOptions>;
extraBody?: Readonly<Record<string, Object>>;
Comment thread
gpotter2 marked this conversation as resolved.
zeroDataRetentionEnabled?: boolean;
/**
* BYOK-only override that forces the body shape used when forwarding the reasoning effort to the model.
Expand Down