Merge pull request #328147 from microsoft/agents/gemini-agent-host-byok-fixes

copilot: preserve Gemini BYOK tool fidelity
This commit is contained in:
Vritant Bhardwaj
2026-07-31 05:57:31 -07:00
committed by GitHub
4 changed files with 67 additions and 9 deletions
@@ -11,7 +11,7 @@ export type ToolJsonSchema = {
properties?: Record<string, ToolJsonSchema>;
items?: ToolJsonSchema;
required?: string[];
enum?: string[];
enum?: unknown[];
// Add support for JSON Schema composition keywords
anyOf?: ToolJsonSchema[];
@@ -104,8 +104,11 @@ function transformConcrete(schema: ToolJsonSchema): Schema {
transformed.description = schema.description;
}
if (schema.enum) {
transformed.enum = schema.enum;
if (type === 'string' && schema.enum) {
const values = schema.enum.filter((value): value is string => typeof value === 'string');
if (values.length > 0) {
transformed.enum = values;
}
}
if (type === 'object' && schema.properties) {
@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import type { Content, FunctionCall, FunctionResponse, Part } from '@google/genai';
import { Raw } from '@vscode/prompt-tsx';
import type { LanguageModelChatMessage } from 'vscode';
import type { LanguageModelChatMessage, LanguageModelChatMessage2 } from 'vscode';
import { CustomDataPartMimeTypes } from '../../../platform/endpoint/common/endpointTypes';
import { LanguageModelChatMessageRole, LanguageModelDataPart, LanguageModelTextPart, LanguageModelThinkingPart, LanguageModelToolCallPart, LanguageModelToolResultPart, LanguageModelToolResultPart2 } from '../../../vscodeTypes';
@@ -120,7 +120,7 @@ function apiContentToGeminiContent(content: (LanguageModelTextPart | LanguageMod
return convertedContent;
}
export function apiMessageToGeminiMessage(messages: LanguageModelChatMessage[]): { contents: Content[]; systemInstruction?: Content } {
export function apiMessageToGeminiMessage(messages: Array<LanguageModelChatMessage | LanguageModelChatMessage2>): { contents: Content[]; systemInstruction?: Content } {
const contents: Content[] = [];
let systemInstruction: Content | undefined;
@@ -131,8 +131,7 @@ export function apiMessageToGeminiMessage(messages: LanguageModelChatMessage[]):
if (message.role === LanguageModelChatMessageRole.System) {
// Gemini uses system instruction separately
const systemText = message.content
.filter((p): p is LanguageModelTextPart => p instanceof LanguageModelTextPart)
.map(p => p.value)
.map(part => part instanceof LanguageModelTextPart ? part.value : '')
.join('');
if (systemText.trim()) {
@@ -179,6 +179,41 @@ describe('GeminiFunctionDeclarationConverter', () => {
});
});
it('should omit non-string enums from nested schemas', () => {
const result = toGeminiFunction('nestedEnumFunction', 'Function with nested non-string enums', {
type: 'object',
properties: {
values: {
type: 'array',
items: {
type: 'object',
properties: {
enabled: {
type: 'boolean',
enum: [true]
},
count: {
type: 'integer',
enum: [1, 2]
}
}
}
}
}
});
expect(result.parameters!.properties!['values']).toEqual({
type: Type.ARRAY,
items: {
type: Type.OBJECT,
properties: {
enabled: { type: Type.BOOLEAN },
count: { type: Type.INTEGER }
}
}
});
});
it('should handle nullable anyOf schemas', () => {
const result = toGeminiFunction('nullableAnyOfFunction', 'Function with nullable anyOf', {
type: 'object',
@@ -5,9 +5,9 @@
import { Raw } from '@vscode/prompt-tsx';
import { describe, expect, it } from 'vitest';
import type { LanguageModelChatMessage } from 'vscode';
import type { LanguageModelChatMessage, LanguageModelChatMessage2 } from 'vscode';
import { CustomDataPartMimeTypes } from '../../../../platform/endpoint/common/endpointTypes';
import { LanguageModelChatMessageRole, LanguageModelDataPart, LanguageModelTextPart, LanguageModelToolResultPart, LanguageModelTextPart as LMText } from '../../../../vscodeTypes';
import { LanguageModelChatMessageRole, LanguageModelDataPart, LanguageModelTextPart, LanguageModelThinkingPart, LanguageModelToolCallPart, LanguageModelToolResultPart, LanguageModelTextPart as LMText } from '../../../../vscodeTypes';
import { apiMessageToGeminiMessage } from '../geminiMessageConverter';
describe('GeminiMessageConverter', () => {
@@ -80,6 +80,27 @@ describe('GeminiMessageConverter', () => {
expect(result.contents[0].parts![1].text).toBe('Hello!');
});
it('should attach a thought signature to the following function call', () => {
const messages: Array<LanguageModelChatMessage | LanguageModelChatMessage2> = [{
role: LanguageModelChatMessageRole.Assistant,
content: [
new LanguageModelThinkingPart('', undefined, { signature: 'thought-signature' }),
new LanguageModelToolCallPart('call-1', 'default_api:view', { path: 'README.md' }),
],
name: undefined,
}];
const result = apiMessageToGeminiMessage(messages);
expect(result.contents[0].parts).toEqual([{
functionCall: {
name: 'default_api:view',
args: { path: 'README.md' },
},
thoughtSignature: 'thought-signature',
}]);
});
it('should extract functionResponse parts from model message into subsequent user message and prune empty model', () => {
// Simulate a model message that (incorrectly) contains only a tool result part
const toolResult = new LanguageModelToolResultPart('myTool_12345', [new LanguageModelTextPart('{"foo":"bar"}')]);