From 83802d800e1f05d8823f9e0a85cfaa580986d086 Mon Sep 17 00:00:00 2001 From: Milosz Jankiewicz <25470423+Jaaneek@users.noreply.github.com> Date: Tue, 6 Oct 2026 17:11:21 +0100 Subject: [PATCH] fix(opencode): bump @ai-sdk/xai to 3.0.139 so tool-result images reach xAI (#53549) --- bun.lock | 26 ++- package.json | 2 +- packages/core/package.json | 2 +- packages/opencode/package.json | 2 +- packages/opencode/src/session/message-v2.ts | 11 +- .../opencode/test/session/message-v2.test.ts | 192 +++++++++++++++++- ....102.patch => @ai-sdk%2Fxai@3.0.139.patch} | 178 ++++++++-------- 7 files changed, 319 insertions(+), 94 deletions(-) rename patches/{@ai-sdk%2Fxai@3.0.102.patch => @ai-sdk%2Fxai@3.0.139.patch} (62%) diff --git a/bun.lock b/bun.lock index b7eeb1b510..7cfb128028 100644 --- a/bun.lock +++ b/bun.lock @@ -313,7 +313,7 @@ "@ai-sdk/provider-utils": "4.0.51", "@ai-sdk/togetherai": "2.0.68", "@ai-sdk/vercel": "2.0.39", - "@ai-sdk/xai": "3.0.102", + "@ai-sdk/xai": "3.0.139", "@aws-sdk/credential-providers": "3.1057.0", "@effect/opentelemetry": "catalog:", "@effect/platform-node": "catalog:", @@ -588,7 +588,7 @@ "@ai-sdk/provider": "3.0.16", "@ai-sdk/togetherai": "2.0.68", "@ai-sdk/vercel": "2.0.39", - "@ai-sdk/xai": "3.0.102", + "@ai-sdk/xai": "3.0.139", "@aws-sdk/credential-providers": "3.1057.0", "@clack/prompts": "1.0.0-alpha.1", "@effect/opentelemetry": "catalog:", @@ -1058,7 +1058,6 @@ "patchedDependencies": { "@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch", "@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch", - "@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch", "@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch", "gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch", "@ai-sdk/openai@3.0.88": "patches/@ai-sdk%2Fopenai@3.0.88.patch", @@ -1071,6 +1070,7 @@ "@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch", "@ai-sdk/amazon-bedrock@4.0.166": "patches/@ai-sdk%2Famazon-bedrock@4.0.166.patch", "@ai-sdk/groq@3.0.31": "patches/@ai-sdk%2Fgroq@3.0.31.patch", + "@ai-sdk/xai@3.0.139": "patches/@ai-sdk%2Fxai@3.0.139.patch", "@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch", "pacote@21.5.0": "patches/pacote@21.5.0.patch", "@dnd-kit/dom@0.5.0": "patches/@dnd-kit%2Fdom@0.5.0.patch", @@ -1214,7 +1214,7 @@ "@ai-sdk/vercel": ["@ai-sdk/vercel@2.0.39", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.37", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8eu3ljJpkCTP4ppcyYB+NcBrkcBoSOFthCSgk5VnjaxnDaOJFaxnPwfddM7wx3RwMk2CiK1O61Px/LlqNc7QkQ=="], - "@ai-sdk/xai": ["@ai-sdk/xai@3.0.102", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.56", "@ai-sdk/provider": "3.0.13", "@ai-sdk/provider-utils": "4.0.35" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-NeQyOR7OCqDMgaLS4uNX/ep/HrwUzzFYLzXQSRoqLy2jsnqxAJhsgltRwAwf+ADjyPBIAKEOestWnIQA+LrLrQ=="], + "@ai-sdk/xai": ["@ai-sdk/xai@3.0.139", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.81", "@ai-sdk/provider": "3.0.18", "@ai-sdk/provider-utils": "4.0.57" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-epoKIfuCcmQh6uVn90OESeWmr7mqPJ3YtXNXStdpRvXLVqPmo47pnDUW/AH1vyvZ+6gsKg9TbSGVvyGeFQyHlg=="], "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], @@ -5710,11 +5710,11 @@ "@ai-sdk/vercel/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], - "@ai-sdk/xai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.56", "", { "dependencies": { "@ai-sdk/provider": "3.0.13", "@ai-sdk/provider-utils": "4.0.35" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cQrN6OUn/jvsY3OdsU6Wn+ss7vp1iwIcakZKSlSRMnYqShBfyT7Qht+eqmgxs7w9ttrw6FAG6o11AiBs+iEsTA=="], + "@ai-sdk/xai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.81", "", { "dependencies": { "@ai-sdk/provider": "3.0.18", "@ai-sdk/provider-utils": "4.0.57" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-L14Jd0lAFKNM42l9TxlpwkNiyMlSvSvGySLc9GN4FB0qOo4/WmYOeLyn/CPhghIr0K4O6vH1y0nChxGljq2ccg=="], - "@ai-sdk/xai/@ai-sdk/provider": ["@ai-sdk/provider@3.0.13", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-ZPtVYt5QIJzOta1kdUiDuCx4HhFkvNPv/rvmZ2b1iXwybYjJsCnNYR4PAw4kW7rgVfDARvHXcU64efWuqNp6bw=="], + "@ai-sdk/xai/@ai-sdk/provider": ["@ai-sdk/provider@3.0.18", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-IpefF5ssZVOZCD4Wu7zxt6PwZoGT2HLVjxpJ10Bjx6EAmRfLYJPjyrfxb0RYDlwvXnDb9nkQ7Do/oJVDuRFzew=="], - "@ai-sdk/xai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.35", "", { "dependencies": { "@ai-sdk/provider": "3.0.13", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bjYld/2KGPLt78kpqbya+fD4LYS7BqVQJyUjE3qAHrYB0FR2Q90BaWEVIBZaguTWXf/A8L6uG1zO1v9TxVlGWg=="], + "@ai-sdk/xai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.57", "", { "dependencies": { "@ai-sdk/provider": "3.0.18", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8", "undici": "^6.28.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-89a7sPvZqXsP3PigcAQLWntu/duIVEcN/RyChAB+j6p3C7NwW767sVttvL8r0+BetTg63uAXQoYHCZZueQKBSw=="], "@astrojs/check/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], @@ -6176,6 +6176,8 @@ "ai-gateway-provider/@ai-sdk/perplexity": ["@ai-sdk/perplexity@3.0.53", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@ai-sdk/provider-utils": "4.0.45" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bL3SWrPltTuxVNg/bZ5APbJLVe0+BIU+BYAbT8Yo0eSAZ5eMTImN2murQHZa08Wi8lUMm8MNolaYBQkb0JDbFw=="], + "ai-gateway-provider/@ai-sdk/xai": ["@ai-sdk/xai@3.0.102", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.56", "@ai-sdk/provider": "3.0.13", "@ai-sdk/provider-utils": "4.0.35" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-NeQyOR7OCqDMgaLS4uNX/ep/HrwUzzFYLzXQSRoqLy2jsnqxAJhsgltRwAwf+ADjyPBIAKEOestWnIQA+LrLrQ=="], + "ai-gateway-provider/@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.10.0", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-FMsAEjLUt5pWuRE2LDC/LCvVrFjLlrEzUITH5+5SZtfq7KZ2wrOHjQVxzz92sju8S9ltpzW87CLW8/b0oBXVCw=="], "ajv-keywords/ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], @@ -6618,6 +6620,8 @@ "@ai-sdk/xai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@ai-sdk/xai/@ai-sdk/provider-utils/undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="], + "@astrojs/check/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], "@astrojs/check/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -7014,6 +7018,12 @@ "ai-gateway-provider/@ai-sdk/perplexity/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.45", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8", "undici": "^5.29.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-7u5B/E2uZmU65SlJhhQGFHZwRCN0xOz4HHtFc4sEGV9PHbX3fGiEiZBpc/SABay1dGeJgK3VD60rvLGoWdWPXA=="], + "ai-gateway-provider/@ai-sdk/xai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.56", "", { "dependencies": { "@ai-sdk/provider": "3.0.13", "@ai-sdk/provider-utils": "4.0.35" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cQrN6OUn/jvsY3OdsU6Wn+ss7vp1iwIcakZKSlSRMnYqShBfyT7Qht+eqmgxs7w9ttrw6FAG6o11AiBs+iEsTA=="], + + "ai-gateway-provider/@ai-sdk/xai/@ai-sdk/provider": ["@ai-sdk/provider@3.0.13", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-ZPtVYt5QIJzOta1kdUiDuCx4HhFkvNPv/rvmZ2b1iXwybYjJsCnNYR4PAw4kW7rgVfDARvHXcU64efWuqNp6bw=="], + + "ai-gateway-provider/@ai-sdk/xai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.35", "", { "dependencies": { "@ai-sdk/provider": "3.0.13", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bjYld/2KGPLt78kpqbya+fD4LYS7BqVQJyUjE3qAHrYB0FR2Q90BaWEVIBZaguTWXf/A8L6uG1zO1v9TxVlGWg=="], + "ai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "ajv-keywords/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], @@ -7462,6 +7472,8 @@ "ai-gateway-provider/@ai-sdk/perplexity/@ai-sdk/provider-utils/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], + "ai-gateway-provider/@ai-sdk/xai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "app-builder-lib/@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], diff --git a/package.json b/package.json index 59bf0501f0..003e39a89f 100644 --- a/package.json +++ b/package.json @@ -151,7 +151,7 @@ "@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch", "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", "solid-js@1.9.10": "patches/solid-js@1.9.10.patch", - "@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch", + "@ai-sdk/xai@3.0.139": "patches/@ai-sdk%2Fxai@3.0.139.patch", "@ai-sdk/mistral@3.0.51": "patches/@ai-sdk%2Fmistral@3.0.51.patch", "gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch", "pacote@21.5.0": "patches/pacote@21.5.0.patch", diff --git a/packages/core/package.json b/packages/core/package.json index 4f7ff417c9..c5c9304044 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -80,7 +80,7 @@ "@ai-sdk/provider-utils": "4.0.51", "@ai-sdk/togetherai": "2.0.68", "@ai-sdk/vercel": "2.0.39", - "@ai-sdk/xai": "3.0.102", + "@ai-sdk/xai": "3.0.139", "@aws-sdk/credential-providers": "3.1057.0", "@effect/opentelemetry": "catalog:", "@effect/platform-node": "catalog:", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 5f6fd83e1d..ad48df8628 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -73,7 +73,7 @@ "@ai-sdk/provider": "3.0.16", "@ai-sdk/togetherai": "2.0.68", "@ai-sdk/vercel": "2.0.39", - "@ai-sdk/xai": "3.0.102", + "@ai-sdk/xai": "3.0.139", "@aws-sdk/credential-providers": "3.1057.0", "@clack/prompts": "1.0.0-alpha.1", "@effect/opentelemetry": "catalog:", diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 2bd0e51715..75f2d89379 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -162,6 +162,12 @@ export const toModelMessagesEffect = Effect.fnUntraced(function* ( return false } + // xAI rejects any other image format (e.g. GIF) with invalid_image, failing the whole request. + const rejectedByProvider = (attachment: { mime: string }) => + model.api.npm === "@ai-sdk/xai" && + attachment.mime.startsWith("image/") && + !["image/png", "image/jpeg", "image/webp"].includes(attachment.mime) + const toModelOutput = (options: { toolCallId: string; input: unknown; output: unknown }) => { const output = options.output if (typeof output === "string") { @@ -297,7 +303,10 @@ export const toModelMessagesEffect = Effect.fnUntraced(function* ( const outputText = part.state.time.compacted ? "[Old tool result content cleared]" : truncateToolOutput(part.state.output, options?.toolOutputMaxChars) - const attachments = part.state.time.compacted || options?.stripMedia ? [] : (part.state.attachments ?? []) + const attachments = + part.state.time.compacted || options?.stripMedia + ? [] + : (part.state.attachments ?? []).filter((a) => !rejectedByProvider(a)) // For providers that don't support media in tool results, extract media files // (images, PDFs) to be sent as a separate user message diff --git a/packages/opencode/test/session/message-v2.test.ts b/packages/opencode/test/session/message-v2.test.ts index 734a30e424..5ee876210e 100644 --- a/packages/opencode/test/session/message-v2.test.ts +++ b/packages/opencode/test/session/message-v2.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from "bun:test" import { SessionV1 } from "@opencode-ai/core/v1/session" -import { APICallError } from "ai" +import { createXai } from "@ai-sdk/xai" +import { APICallError, generateText } from "ai" import { MessageV2 } from "../../src/session/message-v2" import { ProviderTransform } from "@/provider/transform" import type { Provider } from "@/provider/provider" @@ -494,6 +495,195 @@ describe("session.message-v2.toModelMessage", () => { }) }) + test("sends tool-result images to xai responses as input_image", async () => { + const xaiModel: Provider.Model = { + ...model, + id: ModelV2.ID.make("grok-4.7"), + providerID: ProviderV2.ID.make("xai"), + api: { + id: "grok-4.7", + url: "https://api.x.ai/v1", + npm: "@ai-sdk/xai", + }, + capabilities: { + ...model.capabilities, + attachment: true, + input: { + ...model.capabilities.input, + image: true, + }, + }, + } + const png = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]).toString("base64") + const userID = "m-user-xai" + const assistantID = "m-assistant-xai" + const input: SessionV1.WithParts[] = [ + { + info: userInfo(userID), + parts: [ + { + ...basePart(userID, "u1-xai"), + type: "text", + text: "describe the diagram", + }, + ] as SessionV1.Part[], + }, + { + info: assistantInfo(assistantID, userID), + parts: [ + { + ...basePart(assistantID, "a1-xai"), + type: "tool", + callID: "call-xai-1", + tool: "read", + state: { + status: "completed", + input: { filePath: "/tmp/diagram.png" }, + output: "Image read successfully", + title: "Read", + metadata: {}, + time: { start: 0, end: 1 }, + attachments: [ + { + ...basePart(assistantID, "file-xai-1"), + type: "file", + mime: "image/png", + filename: "diagram.png", + url: `data:image/png;base64,${png}`, + }, + ], + }, + }, + ] as SessionV1.Part[], + }, + ] + + const bodies: Record[] = [] + const xai = createXai({ + apiKey: "test", + fetch: Object.assign( + async (url: RequestInfo | URL, init?: RequestInit) => { + bodies.push(await new Request(url, init).json()) + return new Response("{}", { status: 400 }) + }, + { preconnect: globalThis.fetch.preconnect }, + ), + }) + await generateText({ + model: xai.responses(xaiModel.api.id), + messages: ProviderTransform.message(await MessageV2.toModelMessages(input, xaiModel), xaiModel, {}), + providerOptions: { xai: { promptCacheKey: "session", reasoningEffort: "xhigh" } }, + maxRetries: 0, + }).catch(() => undefined) + + expect(bodies).toHaveLength(1) + expect(bodies[0].prompt_cache_key).toBe("session") + expect(bodies[0].reasoning).toEqual({ effort: "xhigh" }) + expect(bodies[0].input).toContainEqual({ + type: "function_call_output", + call_id: "call-xai-1", + output: [ + { type: "input_text", text: "Image read successfully" }, + { type: "input_image", image_url: `data:image/png;base64,${png}` }, + ], + }) + }) + + test("drops tool-result images xai rejects but keeps png and webp", async () => { + const xaiModel: Provider.Model = { + ...model, + id: ModelV2.ID.make("grok-4.7"), + providerID: ProviderV2.ID.make("xai"), + api: { + id: "grok-4.7", + url: "https://api.x.ai/v1", + npm: "@ai-sdk/xai", + }, + capabilities: { + ...model.capabilities, + attachment: true, + input: { + ...model.capabilities.input, + image: true, + }, + }, + } + const images = [ + { mime: "image/png", data: Buffer.from([0x89, 0x50, 0x4e, 0x47]).toString("base64") }, + { mime: "image/gif", data: Buffer.from("GIF89a").toString("base64") }, + { mime: "image/webp", data: Buffer.from("RIFFWEBP").toString("base64") }, + ] + const userID = "m-user-xai-gif" + const assistantID = "m-assistant-xai-gif" + const input: SessionV1.WithParts[] = [ + { + info: userInfo(userID), + parts: [ + { + ...basePart(userID, "u1-xai-gif"), + type: "text", + text: "describe the images", + }, + ] as SessionV1.Part[], + }, + { + info: assistantInfo(assistantID, userID), + parts: [ + { + ...basePart(assistantID, "a1-xai-gif"), + type: "tool", + callID: "call-xai-gif", + tool: "read", + state: { + status: "completed", + input: { filePath: "/tmp/images" }, + output: "Images read successfully", + title: "Read", + metadata: {}, + time: { start: 0, end: 1 }, + attachments: images.map((image, index) => ({ + ...basePart(assistantID, `file-xai-gif-${index}`), + type: "file" as const, + mime: image.mime, + filename: `image-${index}`, + url: `data:${image.mime};base64,${image.data}`, + })), + }, + }, + ] as SessionV1.Part[], + }, + ] + + const bodies: Record[] = [] + const xai = createXai({ + apiKey: "test", + fetch: Object.assign( + async (url: RequestInfo | URL, init?: RequestInit) => { + bodies.push(await new Request(url, init).json()) + return new Response("{}", { status: 400 }) + }, + { preconnect: globalThis.fetch.preconnect }, + ), + }) + await generateText({ + model: xai.responses(xaiModel.api.id), + messages: ProviderTransform.message(await MessageV2.toModelMessages(input, xaiModel), xaiModel, {}), + maxRetries: 0, + }).catch(() => undefined) + + expect(bodies).toHaveLength(1) + expect(bodies[0].input).toContainEqual({ + type: "function_call_output", + call_id: "call-xai-gif", + output: [ + { type: "input_text", text: "Images read successfully" }, + { type: "input_image", image_url: `data:image/png;base64,${images[0].data}` }, + { type: "input_image", image_url: `data:image/webp;base64,${images[2].data}` }, + ], + }) + expect(JSON.stringify(bodies[0])).not.toContain("image/gif") + }) + test("moves bedrock pdf tool-result media into a separate user message", async () => { const bedrockModel: Provider.Model = { ...model, diff --git a/patches/@ai-sdk%2Fxai@3.0.102.patch b/patches/@ai-sdk%2Fxai@3.0.139.patch similarity index 62% rename from patches/@ai-sdk%2Fxai@3.0.102.patch rename to patches/@ai-sdk%2Fxai@3.0.139.patch index 1ea20de9cd..adc19cf33e 100644 --- a/patches/@ai-sdk%2Fxai@3.0.102.patch +++ b/patches/@ai-sdk%2Fxai@3.0.139.patch @@ -1,22 +1,23 @@ diff --git a/dist/index.d.mts b/dist/index.d.mts -index 266c5ffdd9ee74ff95908ce90858ee4369d4e4ae..6ac6e2873b7681ac632c903694aa38c7b09773fa 100644 +index e9c9916..b2cefd4 100644 --- a/dist/index.d.mts +++ b/dist/index.d.mts -@@ -5,12 +5,7 @@ import { FetchFunction } from '@ai-sdk/provider-utils'; +@@ -5,13 +5,7 @@ import { FetchFunction } from '@ai-sdk/provider-utils'; - type XaiChatModelId = 'grok-4.3' | 'grok-4.20-0309-reasoning' | 'grok-4.20-0309-non-reasoning' | 'grok-4.20-multi-agent-0309' | 'grok-build-0.1' | (string & {}); + type XaiChatModelId = 'grok-4.7' | 'grok-4.6' | 'grok-4.5' | 'grok-4.3' | 'grok-4.20-0309-reasoning' | 'grok-4.20-0309-non-reasoning' | 'grok-4.20-multi-agent-0309' | 'grok-build-0.1' | (string & {}); declare const xaiLanguageModelChatOptions: z.ZodObject<{ - reasoningEffort: z.ZodOptional>; + reasoningEffort: z.ZodOptional; logprobs: z.ZodOptional; topLogprobs: z.ZodOptional; - parallel_function_calling: z.ZodOptional; -@@ -68,16 +63,12 @@ type XaiResponsesModelId = 'grok-4.3' | 'grok-4.20-0309-reasoning' | 'grok-4.20- + serviceTier: z.ZodOptional>; + reasoningEffort: z.ZodOptional; logprobs: z.ZodOptional; topLogprobs: z.ZodOptional; + serviceTier: z.ZodOptional>; store: z.ZodOptional; previousResponseId: z.ZodOptional; + promptCacheKey: z.ZodOptional; @@ -36,24 +41,25 @@ index 266c5ffdd9ee74ff95908ce90858ee4369d4e4ae..6ac6e2873b7681ac632c903694aa38c7 "file_search_call.results": "file_search_call.results"; }>>>>; diff --git a/dist/index.d.ts b/dist/index.d.ts -index 266c5ffdd9ee74ff95908ce90858ee4369d4e4ae..6ac6e2873b7681ac632c903694aa38c7b09773fa 100644 +index e9c9916..b2cefd4 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts -@@ -5,12 +5,7 @@ import { FetchFunction } from '@ai-sdk/provider-utils'; +@@ -5,13 +5,7 @@ import { FetchFunction } from '@ai-sdk/provider-utils'; - type XaiChatModelId = 'grok-4.3' | 'grok-4.20-0309-reasoning' | 'grok-4.20-0309-non-reasoning' | 'grok-4.20-multi-agent-0309' | 'grok-build-0.1' | (string & {}); + type XaiChatModelId = 'grok-4.7' | 'grok-4.6' | 'grok-4.5' | 'grok-4.3' | 'grok-4.20-0309-reasoning' | 'grok-4.20-0309-non-reasoning' | 'grok-4.20-multi-agent-0309' | 'grok-build-0.1' | (string & {}); declare const xaiLanguageModelChatOptions: z.ZodObject<{ - reasoningEffort: z.ZodOptional>; + reasoningEffort: z.ZodOptional; logprobs: z.ZodOptional; topLogprobs: z.ZodOptional; - parallel_function_calling: z.ZodOptional; -@@ -68,16 +63,12 @@ type XaiResponsesModelId = 'grok-4.3' | 'grok-4.20-0309-reasoning' | 'grok-4.20- + serviceTier: z.ZodOptional>; + reasoningEffort: z.ZodOptional; logprobs: z.ZodOptional; topLogprobs: z.ZodOptional; + serviceTier: z.ZodOptional>; store: z.ZodOptional; previousResponseId: z.ZodOptional; + promptCacheKey: z.ZodOptional; @@ -73,19 +83,19 @@ index 266c5ffdd9ee74ff95908ce90858ee4369d4e4ae..6ac6e2873b7681ac632c903694aa38c7 "file_search_call.results": "file_search_call.results"; }>>>>; diff --git a/dist/index.js b/dist/index.js -index 717b74538f5c8f0d6ab1475ebb2a84a47ccd3950..0fd8f0d1cae951cd24401034a9c1dba762d9fd84 100644 +index d5eb865..13ad9ca 100644 --- a/dist/index.js +++ b/dist/index.js -@@ -246,7 +246,7 @@ var searchSourceSchema = import_v4.z.discriminatedUnion("type", [ +@@ -274,7 +274,7 @@ var searchSourceSchema = import_v42.z.discriminatedUnion("type", [ rssSourceSchema ]); - var xaiLanguageModelChatOptions = import_v4.z.object({ -- reasoningEffort: import_v4.z.enum(["none", "low", "medium", "high"]).optional(), -+ reasoningEffort: import_v4.z.string().optional(), - logprobs: import_v4.z.boolean().optional(), - topLogprobs: import_v4.z.number().int().min(0).max(8).optional(), - /** -@@ -1119,6 +1119,14 @@ async function convertToXaiResponsesInput({ + var xaiLanguageModelChatOptions = import_v42.z.object({ +- reasoningEffort: import_v42.z.enum(["none", "low", "medium", "high", "xhigh"]).optional(), ++ reasoningEffort: import_v42.z.string().optional(), + logprobs: import_v42.z.boolean().optional(), + topLogprobs: import_v42.z.number().int().min(0).max(8).optional(), + serviceTier: import_v42.z.enum(["default", "priority"]).optional(), +@@ -1162,6 +1162,14 @@ async function convertToXaiResponsesInput({ type: "input_file", file_url: block.data.toString() }); @@ -100,50 +110,50 @@ index 717b74538f5c8f0d6ab1475ebb2a84a47ccd3950..0fd8f0d1cae951cd24401034a9c1dba7 } else { throw new import_provider4.UnsupportedFunctionalityError({ functionality: `file part media type ${block.mediaType} as inline data (xAI Responses requires a URL or a Files API reference for non-image files)` -@@ -1746,7 +1754,7 @@ var xaiLanguageModelResponsesOptions = import_v47.z.object({ - * tokens), `medium` and `high` (uses more reasoning tokens). Not all models - * support reasoning effort; see xAI's docs for the values each model accepts. +@@ -1846,7 +1854,7 @@ var xaiLanguageModelResponsesOptions = import_v48.z.object({ + * models support reasoning effort; see xAI's docs for the values each model + * accepts. `xhigh` is currently only supported by `grok-4.6`. */ -- reasoningEffort: import_v47.z.enum(["none", "low", "medium", "high"]).optional(), -+ reasoningEffort: import_v47.z.string().optional(), - logprobs: import_v47.z.boolean().optional(), - topLogprobs: import_v47.z.number().int().min(0).max(8).optional(), - /** -@@ -1760,6 +1768,10 @@ var xaiLanguageModelResponsesOptions = import_v47.z.object({ +- reasoningEffort: import_v48.z.enum(["none", "low", "medium", "high", "xhigh"]).optional(), ++ reasoningEffort: import_v48.z.string().optional(), + logprobs: import_v48.z.boolean().optional(), + topLogprobs: import_v48.z.number().int().min(0).max(8).optional(), + serviceTier: import_v48.z.enum(["default", "priority"]).optional(), +@@ -1861,6 +1869,10 @@ var xaiLanguageModelResponsesOptions = import_v48.z.object({ * The ID of the previous response from the model. */ - previousResponseId: import_v47.z.string().optional(), + previousResponseId: import_v48.z.string().optional(), + /** + * A stable key used by xAI to improve prompt cache routing. + */ -+ promptCacheKey: import_v47.z.string().optional(), ++ promptCacheKey: import_v48.z.string().optional(), /** * Specify additional output data to include in the model response. * Example values: 'file_search_call.results'. -@@ -2165,6 +2177,9 @@ var XaiResponsesLanguageModel = class { - }, +@@ -2288,6 +2300,9 @@ var XaiResponsesLanguageModel = class { ...options.previousResponseId != null && { previous_response_id: options.previousResponseId -+ }, + }, + ...options.promptCacheKey != null && { + prompt_cache_key: options.promptCacheKey ++ }, + ...options.serviceTier != null && { + service_tier: options.serviceTier } - }; - if (xaiTools2 && xaiTools2.length > 0) { diff --git a/dist/index.mjs b/dist/index.mjs -index a26af109585fc2bd3053b320142aa869c06d36f4..5faca56477b4e55a87f6f57850731c7d3e1721a5 100644 +index 1b24818..afb67cc 100644 --- a/dist/index.mjs +++ b/dist/index.mjs -@@ -230,7 +230,7 @@ var searchSourceSchema = z.discriminatedUnion("type", [ +@@ -258,7 +258,7 @@ var searchSourceSchema = z2.discriminatedUnion("type", [ rssSourceSchema ]); - var xaiLanguageModelChatOptions = z.object({ -- reasoningEffort: z.enum(["none", "low", "medium", "high"]).optional(), -+ reasoningEffort: z.string().optional(), - logprobs: z.boolean().optional(), - topLogprobs: z.number().int().min(0).max(8).optional(), - /** -@@ -1122,6 +1122,14 @@ async function convertToXaiResponsesInput({ + var xaiLanguageModelChatOptions = z2.object({ +- reasoningEffort: z2.enum(["none", "low", "medium", "high", "xhigh"]).optional(), ++ reasoningEffort: z2.string().optional(), + logprobs: z2.boolean().optional(), + topLogprobs: z2.number().int().min(0).max(8).optional(), + serviceTier: z2.enum(["default", "priority"]).optional(), +@@ -1165,6 +1165,14 @@ async function convertToXaiResponsesInput({ type: "input_file", file_url: block.data.toString() }); @@ -158,41 +168,41 @@ index a26af109585fc2bd3053b320142aa869c06d36f4..5faca56477b4e55a87f6f57850731c7d } else { throw new UnsupportedFunctionalityError3({ functionality: `file part media type ${block.mediaType} as inline data (xAI Responses requires a URL or a Files API reference for non-image files)` -@@ -1749,7 +1757,7 @@ var xaiLanguageModelResponsesOptions = z7.object({ - * tokens), `medium` and `high` (uses more reasoning tokens). Not all models - * support reasoning effort; see xAI's docs for the values each model accepts. +@@ -1849,7 +1857,7 @@ var xaiLanguageModelResponsesOptions = z8.object({ + * models support reasoning effort; see xAI's docs for the values each model + * accepts. `xhigh` is currently only supported by `grok-4.6`. */ -- reasoningEffort: z7.enum(["none", "low", "medium", "high"]).optional(), -+ reasoningEffort: z7.string().optional(), - logprobs: z7.boolean().optional(), - topLogprobs: z7.number().int().min(0).max(8).optional(), - /** -@@ -1763,6 +1771,10 @@ var xaiLanguageModelResponsesOptions = z7.object({ +- reasoningEffort: z8.enum(["none", "low", "medium", "high", "xhigh"]).optional(), ++ reasoningEffort: z8.string().optional(), + logprobs: z8.boolean().optional(), + topLogprobs: z8.number().int().min(0).max(8).optional(), + serviceTier: z8.enum(["default", "priority"]).optional(), +@@ -1864,6 +1872,10 @@ var xaiLanguageModelResponsesOptions = z8.object({ * The ID of the previous response from the model. */ - previousResponseId: z7.string().optional(), + previousResponseId: z8.string().optional(), + /** + * A stable key used by xAI to improve prompt cache routing. + */ -+ promptCacheKey: z7.string().optional(), ++ promptCacheKey: z8.string().optional(), /** * Specify additional output data to include in the model response. * Example values: 'file_search_call.results'. -@@ -2186,6 +2198,9 @@ var XaiResponsesLanguageModel = class { - }, +@@ -2309,6 +2321,9 @@ var XaiResponsesLanguageModel = class { ...options.previousResponseId != null && { previous_response_id: options.previousResponseId -+ }, + }, + ...options.promptCacheKey != null && { + prompt_cache_key: options.promptCacheKey ++ }, + ...options.serviceTier != null && { + service_tier: options.serviceTier } - }; - if (xaiTools2 && xaiTools2.length > 0) { diff --git a/src/responses/convert-to-xai-responses-input.ts b/src/responses/convert-to-xai-responses-input.ts -index 9a3712990cf646a6f7ba757d3f2893a3c37c6ef9..b018a9b95eb21e6c12794e6bddb3d953f40441e5 100644 +index 21af0cd..9b477ef 100644 --- a/src/responses/convert-to-xai-responses-input.ts +++ b/src/responses/convert-to-xai-responses-input.ts -@@ -64,6 +64,17 @@ export async function convertToXaiResponsesInput({ +@@ -78,6 +78,17 @@ export async function convertToXaiResponsesInput({ type: 'input_file', file_url: block.data.toString(), }); @@ -211,13 +221,13 @@ index 9a3712990cf646a6f7ba757d3f2893a3c37c6ef9..b018a9b95eb21e6c12794e6bddb3d953 throw new UnsupportedFunctionalityError({ functionality: `file part media type ${block.mediaType} as inline data (xAI Responses requires a URL or a Files API reference for non-image files)`, diff --git a/src/responses/xai-responses-api.ts b/src/responses/xai-responses-api.ts -index 70483ea4f7c340b84c23766ac1d1df21e052842b..4b4d052860caa019556151e9e46dbfda8dfb18a9 100644 +index 0bc0f91..aa3857a 100644 --- a/src/responses/xai-responses-api.ts +++ b/src/responses/xai-responses-api.ts -@@ -27,7 +27,13 @@ export type XaiResponsesSystemMessage = { - export type XaiResponsesUserMessageContentPart = - | { type: 'input_text'; text: string } - | { type: 'input_image'; image_url: string } +@@ -31,7 +31,13 @@ export type XaiResponsesUserMessageContentPart = + image_url: string; + detail?: 'low' | 'high' | 'auto'; + } - | { type: 'input_file'; file_url: string }; + | { + type: 'input_file'; @@ -230,33 +240,35 @@ index 70483ea4f7c340b84c23766ac1d1df21e052842b..4b4d052860caa019556151e9e46dbfda export type XaiResponsesUserMessage = { role: 'user'; diff --git a/src/responses/xai-responses-language-model.ts b/src/responses/xai-responses-language-model.ts -index f90df62eb9a30154388b1390e9f3acc3ccc022bf..00e61cba6cf048ae0045be692f33cb7e1e6a1188 100644 +index 06d6e5a..98a2d39 100644 --- a/src/responses/xai-responses-language-model.ts +++ b/src/responses/xai-responses-language-model.ts -@@ -182,6 +182,9 @@ export class XaiResponsesLanguageModel implements LanguageModelV3 { +@@ -201,6 +201,9 @@ export class XaiResponsesLanguageModel implements LanguageModelV3 { ...(options.previousResponseId != null && { previous_response_id: options.previousResponseId, }), + ...(options.promptCacheKey != null && { + prompt_cache_key: options.promptCacheKey, + }), - }; - - if (xaiTools && xaiTools.length > 0) { + ...(options.serviceTier != null && { + service_tier: options.serviceTier, + }), diff --git a/src/responses/xai-responses-options.ts b/src/responses/xai-responses-options.ts -index f8e96c061bf8793a402ababb8cad65bb2ad6aead..2a39a36221ab23ea0000bff1d7854c5bce3f9d74 100644 +index 6d52f46..9577200 100644 --- a/src/responses/xai-responses-options.ts +++ b/src/responses/xai-responses-options.ts -@@ -18,7 +18,7 @@ export const xaiLanguageModelResponsesOptions = z.object({ - * tokens), `medium` and `high` (uses more reasoning tokens). Not all models - * support reasoning effort; see xAI's docs for the values each model accepts. +@@ -22,9 +22,7 @@ export const xaiLanguageModelResponsesOptions = z.object({ + * models support reasoning effort; see xAI's docs for the values each model + * accepts. `xhigh` is currently only supported by `grok-4.6`. */ -- reasoningEffort: z.enum(['none', 'low', 'medium', 'high']).optional(), +- reasoningEffort: z +- .enum(['none', 'low', 'medium', 'high', 'xhigh']) +- .optional(), + reasoningEffort: z.string().optional(), logprobs: z.boolean().optional(), topLogprobs: z.number().int().min(0).max(8).optional(), - /** -@@ -32,6 +32,10 @@ export const xaiLanguageModelResponsesOptions = z.object({ + serviceTier: z.enum(['default', 'priority']).optional(), +@@ -39,6 +37,10 @@ export const xaiLanguageModelResponsesOptions = z.object({ * The ID of the previous response from the model. */ previousResponseId: z.string().optional(), @@ -268,14 +280,16 @@ index f8e96c061bf8793a402ababb8cad65bb2ad6aead..2a39a36221ab23ea0000bff1d7854c5b * Specify additional output data to include in the model response. * Example values: 'file_search_call.results'. diff --git a/src/xai-chat-options.ts b/src/xai-chat-options.ts -index d70a72a9fa01da2c711c291da5ce949efbde60b5..fd6b1ae025388b614f08b620244be553199479ca 100644 +index f3f1237..adb6ada 100644 --- a/src/xai-chat-options.ts +++ b/src/xai-chat-options.ts -@@ -51,7 +51,7 @@ const searchSourceSchema = z.discriminatedUnion('type', [ +@@ -54,9 +54,7 @@ const searchSourceSchema = z.discriminatedUnion('type', [ // xai-specific provider options export const xaiLanguageModelChatOptions = z.object({ -- reasoningEffort: z.enum(['none', 'low', 'medium', 'high']).optional(), +- reasoningEffort: z +- .enum(['none', 'low', 'medium', 'high', 'xhigh']) +- .optional(), + reasoningEffort: z.string().optional(), logprobs: z.boolean().optional(), topLogprobs: z.number().int().min(0).max(8).optional(),