diff --git a/extensions/copilot/src/extension/byok/common/geminiFunctionDeclarationConverter.ts b/extensions/copilot/src/extension/byok/common/geminiFunctionDeclarationConverter.ts index f25e50d3ee5..990537ed9a0 100644 --- a/extensions/copilot/src/extension/byok/common/geminiFunctionDeclarationConverter.ts +++ b/extensions/copilot/src/extension/byok/common/geminiFunctionDeclarationConverter.ts @@ -11,7 +11,7 @@ export type ToolJsonSchema = { properties?: Record; 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) { diff --git a/extensions/copilot/src/extension/byok/common/geminiMessageConverter.ts b/extensions/copilot/src/extension/byok/common/geminiMessageConverter.ts index 2a8179c1a95..56b0db40d46 100644 --- a/extensions/copilot/src/extension/byok/common/geminiMessageConverter.ts +++ b/extensions/copilot/src/extension/byok/common/geminiMessageConverter.ts @@ -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): { 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()) { diff --git a/extensions/copilot/src/extension/byok/common/test/geminiFunctionDeclarationConverter.spec.ts b/extensions/copilot/src/extension/byok/common/test/geminiFunctionDeclarationConverter.spec.ts index 986c6a51749..80b64f6d4d2 100644 --- a/extensions/copilot/src/extension/byok/common/test/geminiFunctionDeclarationConverter.spec.ts +++ b/extensions/copilot/src/extension/byok/common/test/geminiFunctionDeclarationConverter.spec.ts @@ -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', diff --git a/extensions/copilot/src/extension/byok/common/test/geminiMessageConverter.spec.ts b/extensions/copilot/src/extension/byok/common/test/geminiMessageConverter.spec.ts index 68e36160eb4..1d02c638f2e 100644 --- a/extensions/copilot/src/extension/byok/common/test/geminiMessageConverter.spec.ts +++ b/extensions/copilot/src/extension/byok/common/test/geminiMessageConverter.spec.ts @@ -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 = [{ + 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"}')]);