Просмотр исходного кода

fix(openai): remove convertToSimpleMessages to fix tool calling for OpenAI-compatible providers (#10575)

Daniel 2 недель назад
Родитель
Сommit
907b94bc40

+ 0 - 1
packages/types/src/provider-settings.ts

@@ -242,7 +242,6 @@ const vertexSchema = apiModelIdProviderModelSchema.extend({
 const openAiSchema = baseProviderSettingsSchema.extend({
 	openAiBaseUrl: z.string().optional(),
 	openAiApiKey: z.string().optional(),
-	openAiLegacyFormat: z.boolean().optional(),
 	openAiR1FormatEnabled: z.boolean().optional(),
 	openAiModelId: z.string().optional(),
 	openAiCustomModelInfo: modelInfoSchema.nullish(),

+ 1 - 8
src/api/providers/openai.ts

@@ -17,7 +17,6 @@ import { XmlMatcher } from "../../utils/xml-matcher"
 
 import { convertToOpenAiMessages } from "../transform/openai-format"
 import { convertToR1Format } from "../transform/r1-format"
-import { convertToSimpleMessages } from "../transform/simple-format"
 import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
 import { getModelParams } from "../transform/model-params"
 
@@ -90,10 +89,8 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
 		const modelUrl = this.options.openAiBaseUrl ?? ""
 		const modelId = this.options.openAiModelId ?? ""
 		const enabledR1Format = this.options.openAiR1FormatEnabled ?? false
-		const enabledLegacyFormat = this.options.openAiLegacyFormat ?? false
 		const isAzureAiInference = this._isAzureAiInference(modelUrl)
 		const deepseekReasoner = modelId.includes("deepseek-reasoner") || enabledR1Format
-		const ark = modelUrl.includes(".volces.com")
 
 		if (modelId.includes("o1") || modelId.includes("o3") || modelId.includes("o4")) {
 			yield* this.handleO3FamilyMessage(modelId, systemPrompt, messages, metadata)
@@ -110,8 +107,6 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
 
 			if (deepseekReasoner) {
 				convertedMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
-			} else if (ark || enabledLegacyFormat) {
-				convertedMessages = [systemMessage, ...convertToSimpleMessages(messages)]
 			} else {
 				if (modelInfo.supportsPromptCache) {
 					systemMessage = {
@@ -233,9 +228,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
 				model: modelId,
 				messages: deepseekReasoner
 					? convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
-					: enabledLegacyFormat
-						? [systemMessage, ...convertToSimpleMessages(messages)]
-						: [systemMessage, ...convertToOpenAiMessages(messages)],
+					: [systemMessage, ...convertToOpenAiMessages(messages)],
 				...(metadata?.tools && { tools: this.convertToolsForOpenAI(metadata.tools) }),
 				...(metadata?.tool_choice && { tool_choice: metadata.tool_choice }),
 				...(metadata?.toolProtocol === "native" && {

+ 0 - 140
src/api/transform/__tests__/simple-format.spec.ts

@@ -1,140 +0,0 @@
-// npx vitest run src/api/transform/__tests__/simple-format.spec.ts
-
-import { Anthropic } from "@anthropic-ai/sdk"
-import { convertToSimpleContent, convertToSimpleMessages } from "../simple-format"
-
-describe("simple-format", () => {
-	describe("convertToSimpleContent", () => {
-		it("returns string content as-is", () => {
-			const content = "Hello world"
-			expect(convertToSimpleContent(content)).toBe("Hello world")
-		})
-
-		it("extracts text from text blocks", () => {
-			const content = [
-				{ type: "text", text: "Hello" },
-				{ type: "text", text: "world" },
-			] as Anthropic.Messages.TextBlockParam[]
-			expect(convertToSimpleContent(content)).toBe("Hello\nworld")
-		})
-
-		it("converts image blocks to descriptive text", () => {
-			const content = [
-				{ type: "text", text: "Here's an image:" },
-				{
-					type: "image",
-					source: {
-						type: "base64",
-						media_type: "image/png",
-						data: "base64data",
-					},
-				},
-			] as Array<Anthropic.Messages.TextBlockParam | Anthropic.Messages.ImageBlockParam>
-			expect(convertToSimpleContent(content)).toBe("Here's an image:\n[Image: image/png]")
-		})
-
-		it("converts tool use blocks to descriptive text", () => {
-			const content = [
-				{ type: "text", text: "Using a tool:" },
-				{
-					type: "tool_use",
-					id: "tool-1",
-					name: "read_file",
-					input: { path: "test.txt" },
-				},
-			] as Array<Anthropic.Messages.TextBlockParam | Anthropic.Messages.ToolUseBlockParam>
-			expect(convertToSimpleContent(content)).toBe("Using a tool:\n[Tool Use: read_file]")
-		})
-
-		it("handles string tool result content", () => {
-			const content = [
-				{ type: "text", text: "Tool result:" },
-				{
-					type: "tool_result",
-					tool_use_id: "tool-1",
-					content: "Result text",
-				},
-			] as Array<Anthropic.Messages.TextBlockParam | Anthropic.Messages.ToolResultBlockParam>
-			expect(convertToSimpleContent(content)).toBe("Tool result:\nResult text")
-		})
-
-		it("handles array tool result content with text and images", () => {
-			const content = [
-				{
-					type: "tool_result",
-					tool_use_id: "tool-1",
-					content: [
-						{ type: "text", text: "Result 1" },
-						{
-							type: "image",
-							source: {
-								type: "base64",
-								media_type: "image/jpeg",
-								data: "base64data",
-							},
-						},
-						{ type: "text", text: "Result 2" },
-					],
-				},
-			] as Anthropic.Messages.ToolResultBlockParam[]
-			expect(convertToSimpleContent(content)).toBe("Result 1\n[Image: image/jpeg]\nResult 2")
-		})
-
-		it("filters out empty strings", () => {
-			const content = [
-				{ type: "text", text: "Hello" },
-				{ type: "text", text: "" },
-				{ type: "text", text: "world" },
-			] as Anthropic.Messages.TextBlockParam[]
-			expect(convertToSimpleContent(content)).toBe("Hello\nworld")
-		})
-	})
-
-	describe("convertToSimpleMessages", () => {
-		it("converts messages with string content", () => {
-			const messages = [
-				{ role: "user", content: "Hello" },
-				{ role: "assistant", content: "Hi there" },
-			] as Anthropic.Messages.MessageParam[]
-			expect(convertToSimpleMessages(messages)).toEqual([
-				{ role: "user", content: "Hello" },
-				{ role: "assistant", content: "Hi there" },
-			])
-		})
-
-		it("converts messages with complex content", () => {
-			const messages = [
-				{
-					role: "user",
-					content: [
-						{ type: "text", text: "Look at this:" },
-						{
-							type: "image",
-							source: {
-								type: "base64",
-								media_type: "image/png",
-								data: "base64data",
-							},
-						},
-					],
-				},
-				{
-					role: "assistant",
-					content: [
-						{ type: "text", text: "I see the image" },
-						{
-							type: "tool_use",
-							id: "tool-1",
-							name: "analyze_image",
-							input: { data: "base64data" },
-						},
-					],
-				},
-			] as Anthropic.Messages.MessageParam[]
-			expect(convertToSimpleMessages(messages)).toEqual([
-				{ role: "user", content: "Look at this:\n[Image: image/png]" },
-				{ role: "assistant", content: "I see the image\n[Tool Use: analyze_image]" },
-			])
-		})
-	})
-})

+ 0 - 58
src/api/transform/simple-format.ts

@@ -1,58 +0,0 @@
-import { Anthropic } from "@anthropic-ai/sdk"
-
-/**
- * Convert complex content blocks to simple string content
- */
-export function convertToSimpleContent(content: Anthropic.Messages.MessageParam["content"]): string {
-	if (typeof content === "string") {
-		return content
-	}
-
-	// Extract text from content blocks
-	return content
-		.map((block) => {
-			if (block.type === "text") {
-				return block.text
-			}
-			if (block.type === "image") {
-				return `[Image: ${block.source.media_type}]`
-			}
-			if (block.type === "tool_use") {
-				return `[Tool Use: ${block.name}]`
-			}
-			if (block.type === "tool_result") {
-				if (typeof block.content === "string") {
-					return block.content
-				}
-				if (Array.isArray(block.content)) {
-					return block.content
-						.map((part) => {
-							if (part.type === "text") {
-								return part.text
-							}
-							if (part.type === "image") {
-								return `[Image: ${part.source.media_type}]`
-							}
-							return ""
-						})
-						.join("\n")
-				}
-				return ""
-			}
-			return ""
-		})
-		.filter(Boolean)
-		.join("\n")
-}
-
-/**
- * Convert Anthropic messages to simple format with string content
- */
-export function convertToSimpleMessages(
-	messages: Anthropic.Messages.MessageParam[],
-): Array<{ role: "user" | "assistant"; content: string }> {
-	return messages.map((message) => ({
-		role: message.role,
-		content: convertToSimpleContent(message.content),
-	}))
-}

+ 0 - 11
webview-ui/src/components/settings/providers/OpenAICompatible.tsx

@@ -41,7 +41,6 @@ export const OpenAICompatible = ({
 	const { t } = useAppTranslation()
 
 	const [azureApiVersionSelected, setAzureApiVersionSelected] = useState(!!apiConfiguration?.azureApiVersion)
-	const [openAiLegacyFormatSelected, setOpenAiLegacyFormatSelected] = useState(!!apiConfiguration?.openAiLegacyFormat)
 
 	const [openAiModels, setOpenAiModels] = useState<Record<string, ModelInfo> | null>(null)
 
@@ -155,16 +154,6 @@ export const OpenAICompatible = ({
 				onChange={handleInputChange("openAiR1FormatEnabled", noTransform)}
 				openAiR1FormatEnabled={apiConfiguration?.openAiR1FormatEnabled ?? false}
 			/>
-			<div>
-				<Checkbox
-					checked={openAiLegacyFormatSelected}
-					onChange={(checked: boolean) => {
-						setOpenAiLegacyFormatSelected(checked)
-						setApiConfigurationField("openAiLegacyFormat", checked)
-					}}>
-					{t("settings:providers.useLegacyFormat")}
-				</Checkbox>
-			</div>
 			<Checkbox
 				checked={apiConfiguration?.openAiStreamingEnabled ?? true}
 				onChange={handleInputChange("openAiStreamingEnabled", noTransform)}>

+ 0 - 1
webview-ui/src/i18n/locales/ca/settings.json

@@ -278,7 +278,6 @@
 		"useCustomBaseUrl": "Utilitzar URL base personalitzada",
 		"useReasoning": "Activar raonament",
 		"useHostHeader": "Utilitzar capçalera Host personalitzada",
-		"useLegacyFormat": "Utilitzar el format d'API OpenAI antic",
 		"customHeaders": "Capçaleres personalitzades",
 		"headerName": "Nom de la capçalera",
 		"headerValue": "Valor de la capçalera",

+ 0 - 1
webview-ui/src/i18n/locales/de/settings.json

@@ -280,7 +280,6 @@
 		"useCustomBaseUrl": "Benutzerdefinierte Basis-URL verwenden",
 		"useReasoning": "Reasoning aktivieren",
 		"useHostHeader": "Benutzerdefinierten Host-Header verwenden",
-		"useLegacyFormat": "Altes OpenAI API-Format verwenden",
 		"customHeaders": "Benutzerdefinierte Headers",
 		"headerName": "Header-Name",
 		"headerValue": "Header-Wert",

+ 0 - 1
webview-ui/src/i18n/locales/en/settings.json

@@ -287,7 +287,6 @@
 		"useCustomBaseUrl": "Use custom base URL",
 		"useReasoning": "Enable reasoning",
 		"useHostHeader": "Use custom Host header",
-		"useLegacyFormat": "Use legacy OpenAI API format",
 		"customHeaders": "Custom Headers",
 		"headerName": "Header name",
 		"headerValue": "Header value",

+ 0 - 1
webview-ui/src/i18n/locales/es/settings.json

@@ -278,7 +278,6 @@
 		"useCustomBaseUrl": "Usar URL base personalizada",
 		"useReasoning": "Habilitar razonamiento",
 		"useHostHeader": "Usar encabezado Host personalizado",
-		"useLegacyFormat": "Usar formato API de OpenAI heredado",
 		"customHeaders": "Encabezados personalizados",
 		"headerName": "Nombre del encabezado",
 		"headerValue": "Valor del encabezado",

+ 0 - 1
webview-ui/src/i18n/locales/fr/settings.json

@@ -278,7 +278,6 @@
 		"useCustomBaseUrl": "Utiliser une URL de base personnalisée",
 		"useReasoning": "Activer le raisonnement",
 		"useHostHeader": "Utiliser un en-tête Host personnalisé",
-		"useLegacyFormat": "Utiliser le format API OpenAI hérité",
 		"customHeaders": "En-têtes personnalisés",
 		"headerName": "Nom de l'en-tête",
 		"headerValue": "Valeur de l'en-tête",

+ 0 - 1
webview-ui/src/i18n/locales/hi/settings.json

@@ -278,7 +278,6 @@
 		"useCustomBaseUrl": "कस्टम बेस URL का उपयोग करें",
 		"useReasoning": "तर्क सक्षम करें",
 		"useHostHeader": "कस्टम होस्ट हेडर का उपयोग करें",
-		"useLegacyFormat": "पुराने OpenAI API प्रारूप का उपयोग करें",
 		"customHeaders": "कस्टम हेडर्स",
 		"headerName": "हेडर नाम",
 		"headerValue": "हेडर मूल्य",

+ 0 - 1
webview-ui/src/i18n/locales/id/settings.json

@@ -282,7 +282,6 @@
 		"useCustomBaseUrl": "Gunakan base URL kustom",
 		"useReasoning": "Aktifkan reasoning",
 		"useHostHeader": "Gunakan Host header kustom",
-		"useLegacyFormat": "Gunakan format API OpenAI legacy",
 		"customHeaders": "Header Kustom",
 		"headerName": "Nama header",
 		"headerValue": "Nilai header",

+ 0 - 1
webview-ui/src/i18n/locales/it/settings.json

@@ -278,7 +278,6 @@
 		"useCustomBaseUrl": "Usa URL base personalizzato",
 		"useReasoning": "Abilita ragionamento",
 		"useHostHeader": "Usa intestazione Host personalizzata",
-		"useLegacyFormat": "Usa formato API OpenAI legacy",
 		"customHeaders": "Intestazioni personalizzate",
 		"headerName": "Nome intestazione",
 		"headerValue": "Valore intestazione",

+ 0 - 1
webview-ui/src/i18n/locales/ja/settings.json

@@ -278,7 +278,6 @@
 		"useCustomBaseUrl": "カスタムベースURLを使用",
 		"useReasoning": "推論を有効化",
 		"useHostHeader": "カスタムHostヘッダーを使用",
-		"useLegacyFormat": "レガシーOpenAI API形式を使用",
 		"customHeaders": "カスタムヘッダー",
 		"headerName": "ヘッダー名",
 		"headerValue": "ヘッダー値",

+ 0 - 1
webview-ui/src/i18n/locales/ko/settings.json

@@ -278,7 +278,6 @@
 		"useCustomBaseUrl": "사용자 정의 기본 URL 사용",
 		"useReasoning": "추론 활성화",
 		"useHostHeader": "사용자 정의 Host 헤더 사용",
-		"useLegacyFormat": "레거시 OpenAI API 형식 사용",
 		"customHeaders": "사용자 정의 헤더",
 		"headerName": "헤더 이름",
 		"headerValue": "헤더 값",

+ 0 - 1
webview-ui/src/i18n/locales/nl/settings.json

@@ -278,7 +278,6 @@
 		"useCustomBaseUrl": "Aangepaste basis-URL gebruiken",
 		"useReasoning": "Redenering inschakelen",
 		"useHostHeader": "Aangepaste Host-header gebruiken",
-		"useLegacyFormat": "Verouderd OpenAI API-formaat gebruiken",
 		"customHeaders": "Aangepaste headers",
 		"headerName": "Headernaam",
 		"headerValue": "Headerwaarde",

+ 0 - 1
webview-ui/src/i18n/locales/pl/settings.json

@@ -278,7 +278,6 @@
 		"useCustomBaseUrl": "Użyj niestandardowego URL bazowego",
 		"useReasoning": "Włącz rozumowanie",
 		"useHostHeader": "Użyj niestandardowego nagłówka Host",
-		"useLegacyFormat": "Użyj starszego formatu API OpenAI",
 		"customHeaders": "Niestandardowe nagłówki",
 		"headerName": "Nazwa nagłówka",
 		"headerValue": "Wartość nagłówka",

+ 0 - 1
webview-ui/src/i18n/locales/pt-BR/settings.json

@@ -278,7 +278,6 @@
 		"useCustomBaseUrl": "Usar URL base personalizado",
 		"useReasoning": "Habilitar raciocínio",
 		"useHostHeader": "Usar cabeçalho Host personalizado",
-		"useLegacyFormat": "Usar formato de API OpenAI legado",
 		"customHeaders": "Cabeçalhos personalizados",
 		"headerName": "Nome do cabeçalho",
 		"headerValue": "Valor do cabeçalho",

+ 0 - 1
webview-ui/src/i18n/locales/ru/settings.json

@@ -278,7 +278,6 @@
 		"useCustomBaseUrl": "Использовать пользовательский базовый URL",
 		"useReasoning": "Включить рассуждения",
 		"useHostHeader": "Использовать пользовательский Host-заголовок",
-		"useLegacyFormat": "Использовать устаревший формат OpenAI API",
 		"customHeaders": "Пользовательские заголовки",
 		"headerName": "Имя заголовка",
 		"headerValue": "Значение заголовка",

+ 0 - 1
webview-ui/src/i18n/locales/tr/settings.json

@@ -278,7 +278,6 @@
 		"useCustomBaseUrl": "Özel temel URL kullan",
 		"useReasoning": "Akıl yürütmeyi etkinleştir",
 		"useHostHeader": "Özel Host başlığı kullan",
-		"useLegacyFormat": "Eski OpenAI API formatını kullan",
 		"customHeaders": "Özel Başlıklar",
 		"headerName": "Başlık adı",
 		"headerValue": "Başlık değeri",

+ 0 - 1
webview-ui/src/i18n/locales/vi/settings.json

@@ -278,7 +278,6 @@
 		"useCustomBaseUrl": "Sử dụng URL cơ sở tùy chỉnh",
 		"useReasoning": "Bật lý luận",
 		"useHostHeader": "Sử dụng tiêu đề Host tùy chỉnh",
-		"useLegacyFormat": "Sử dụng định dạng API OpenAI cũ",
 		"customHeaders": "Tiêu đề tùy chỉnh",
 		"headerName": "Tên tiêu đề",
 		"headerValue": "Giá trị tiêu đề",

+ 0 - 1
webview-ui/src/i18n/locales/zh-CN/settings.json

@@ -278,7 +278,6 @@
 		"useCustomBaseUrl": "使用自定义基础 URL",
 		"useReasoning": "启用推理",
 		"useHostHeader": "使用自定义 Host 标头",
-		"useLegacyFormat": "使用传统 OpenAI API 格式",
 		"customHeaders": "自定义标头",
 		"headerName": "标头名称",
 		"headerValue": "标头值",

+ 0 - 1
webview-ui/src/i18n/locales/zh-TW/settings.json

@@ -278,7 +278,6 @@
 		"useCustomBaseUrl": "使用自訂基礎 URL",
 		"useReasoning": "啟用推理",
 		"useHostHeader": "使用自訂 Host 標頭",
-		"useLegacyFormat": "使用舊版 OpenAI API 格式",
 		"customHeaders": "自訂標頭",
 		"headerName": "標頭名稱",
 		"headerValue": "標頭值",