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

This commit is contained in:
Daniel 2026-01-09 12:03:46 -05:00 committed by GitHub
parent b7bd859473
commit 907b94bc40
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 1 additions and 236 deletions

View file

@ -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(),

View file

@ -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" && {

View file

@ -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]" },
])
})
})
})

View file

@ -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),
}))
}

View file

@ -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)}>

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

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

View file

@ -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",

View file

@ -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",

View file

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

View file

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

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

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

View file

@ -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",

View file

@ -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 đề",

View file

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

View file

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