Cherry Studio v2 绘画图像生成链路剖析:AI SDK 补丁如何让 generateImage 兼容 HTTP URL 与 gpt-image 系列模型
2026/9/19 1:49:59 网站建设 项目流程

Cherry Studio v2 绘画图像生成链路剖析:AI SDK 补丁如何让 generateImage 兼容 HTTP URL 与 gpt-image 系列模型

【免费下载链接】cherry-studio🍒 Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端项目地址: https://gitcode.com/CherryHQ/cherry-studio

导读

Cherry Studio 的绘画(paintings)功能需要让不同提供商的图像生成网关(OpenAI 兼容网关、Google Gemini/Imagen 网关)统一收敛到 AI SDK 的generateImage调用上。但上游 AI SDK 及其 Provider 包对图像响应形态(b64_jsonurl)、对response_format参数的支持并不一致。@cherrystudio/ai-core@cherrystudio/ai-sdk-provider通过一组针对性补丁(patch)补齐了这条链路:ai@6.0.185补丁为generateImage增加experimental_download选项,让 SDK 能识别并下载 HTTP(S) 图像输出;@ai-sdk/openai-compatible@2.0.72补丁扩展了对url字段图像响应和gpt-image-*模型的处理;@ai-sdk/google@3.0.113补丁修正了 Gemini/Imagen 模型的路径前缀解析。读完本文,你将理解这组补丁逐行做了什么、它们如何在 Cherry Studio 的绘画主流程中生效,以及如何用仓库内测试验证补丁行为。

背景:绘画功能的调用链与补丁的作用位置

从用户指令到图像落盘的主链路

Cherry Studio 的绘画功能并不直接调用 AI SDK,而是经过一条完整的调用链:

  1. 渲染进程发出ai.image.generateIPC 请求(携带经 catalogimageParamsSchema校验的paramValues);
  2. 主进程AiService.generateImage(AiService.ts)负责 provider/model 解析、vendor 参数映射(WireProfile 引擎)、同步与异步任务两种传输方式,以及 FileEntry 持久化;
  3. 对于支持异步提交/轮询的自定义 provider(ppio / dashscope / modelscope / dmxapi-bespoke),会走generateImageViaJob(AiService.ts)进入任务系统,由 imageGenerationJobHandler.ts 完成 submit → poll → download → persist;
  4. 对于走 AI SDK 直接调用的路径,最终落到aiCoreGenerateImage(runtime/index.ts),它创建 RuntimeExecutor 并转发给 AI SDK 的generateImage

generate_image内置工具与 Claude Code 进程内 MCP 桥都是generateImageFromPrompt(painting.ts)的薄封装,绘画模型由feature.paintings.default_model_id偏好解析。

为什么需要补丁:上游 SDK 的三个缺口

上游 AI SDK 的generateImage默认只接收模型直接返回的 base64 数据,而 Cherry Studio 的绘画提供商会返回三种形态的输出:

  • b64_json字段(OpenAI 风格);
  • url字段(如 DMXAPI 的flux-1会显式请求"response_format": "url",见 dmxapi.boundary.test.ts.snap 中的请求快照);
  • Gemini/Imagen 网关的models/{modelId}:predict端点与models/{modelId}前缀的模型路径。

补丁的作用就是把这三类差异收敛在 SDK 层,保证上层绘画代码无需感知。

补丁一:ai@6.0.185 的 experimental_download 与图像分类下载

类型层:generateImage 新增 experimental_download 选项

补丁在dist/index.d.tsdist/index.d.mtsgenerateImage声明中新增了可选参数(ai@6.0.185.patch):

declare function generateImage({ model, prompt, n, maxImagesPerCall, size, aspectRatio, seed, providerOptions, maxRetries, abortSignal, headers, experimental_download: download, }: { // ... /** * Custom download function to use for URLs. * * By default, files are downloaded if the model returns URLs instead of binary data. */ experimental_download?: DownloadFunction | undefined; }): Promise<GenerateImageResult>;

experimental_download接收一个下载函数:入参是[{ url, isUrlSupportedByModel }]列表,返回{ data, mediaType }列表。这与packages/aiCore中 runtime/types.ts 的generateImageParams类型定义保持一致——aiCore 已将experimental_download?: Experimental_DownloadFunction纳入自己的参数类型。

实现层:图像分类、下载与容错

补丁在generate-image.ts的运行时实现中加入了一个关键工具函数:

function toDownloadableImageUrl(value) { try { const url = new URL(value); return url.protocol === "http:" || url.protocol === "https:" ? url : void 0; } catch (invalidUrl) { return void 0; } }

随后对每个图像结果依次处理:

  1. data:URL 原样解析:若结果以data:开头,直接splitDataUrl拆出 mediaType 与 base64 内容,构造DefaultGeneratedFile,不做任何下载;
  2. HTTP(S) URL 交由下载函数toDownloadableImageUrl只接受http:/https:协议(大小写不敏感),下载成功则包装成DefaultGeneratedFile(mediaType 缺失时兜底为image/png);
  3. 失败即丢弃而非落库:下载抛错或返回空时返回null,最终通过filter剔除。若整批有图像被丢弃,会向warnings追加"N of M generated images could not be downloaded and were dropped"警告;
  4. 坏数据容错:既不是可下载 URL 也不是可解码 base64 的结果会被丢弃,而不是让单个畸形条目使整批生成失败(patch 注释明确写道"dropping it beats persisting the url string as if it were the base64 bytes")。

最后如果所有图像都下载失败,generateImage会抛出NoImageGeneratedError(与 aiCore 的 generateImage.test.ts 中对该错误的处理一致)。

专项测试:generateImageDownloadPatch.test.ts

仓库为这半个补丁准备了专门的守卫测试 generateImageDownloadPatch.test.ts,覆盖五种典型场景:

测试场景断言要点
下载失败只丢对应图images长度 1,且warnings包含1 of 2 ... dropped
下载函数抛错只影响该图抛错的 URL 被丢弃,其余图像保留
大写 scheme 可下载、不可解析 URL 被丢弃HTTPS://img/upper.png可下载,https://exa mple.com/x.png被丢弃
全部下载失败抛出NoImageGeneratedError
b64_json结果不做下载下载函数不被调用,base64原样透传

在 AiService 中的实际接线

AiService.generateImageexperimental_download提供了真实实现(AiService.ts):

experimental_download: async (downloads) => { return Promise.all( downloads.map(async ({ url }) => { if (signal?.aborted) return null const downloaded = await downloadImageAsBase64(url.toString()) if (signal?.aborted) return null if (!downloaded) return null return { data: Buffer.from(downloaded.data, 'base64'), mediaType: downloaded.media_type } }) ) }

注意两点工程细节:下载期间再次检查signal.aborted,确保取消优先;数据以二进制 Buffer 返回,交由 SDK 侧统一构造文件对象。补丁丢弃失败下载后,AiService 侧(AiService.ts)还会过滤掉没有base64的结果并记录Filtered invalid generated images警告,形成双重保险。

补丁二:@ai-sdk/openai-compatible 的 url 字段与 gpt-image-* 兼容

响应解析:从只认 b64_json 到 b64_json / url 双通道

上游openaiCompatibleImageResponseSchema只允许data[]中的b64_json字符串字段,补丁将其扩展为二选一可空字段:

var openaiCompatibleImageResponseSchema = z8.object({ data: z8.array(z8.object({ b64_json: z8.string().nullish(), url: z8.string().nullish() })) });

解析逻辑也相应改为flatMap双通道输出(openai-compatible patch):

images: response.data.flatMap((item) => { if (typeof item.b64_json === 'string') return [item.b64_json]; if (typeof item.url === 'string') return [item.url]; return []; })

response_format 的按模型条件发送与 400/422 重试

上游实现无条件发送response_format: "b64_json"。补丁引入defaultResponseFormatPrefixes列表:

var defaultResponseFormatPrefixes = [ "chatgpt-image-", "gpt-image-1-mini", "gpt-image-1.5", "gpt-image-1", "gpt-image-2" ]; function hasDefaultResponseFormat(modelId) { return defaultResponseFormatPrefixes.some((prefix) => modelId.startsWith(prefix)); }

modelId命中这些前缀(或调用方已显式传入response_format)时,不再强制附加response_format;否则默认带"b64_json"。同时,对不支持response_format的模型(拒绝时返回 400 或 422),补丁实现了单次无参重试:第一次带response_format被拒后,去掉该参数重发一次;若重试仍失败,则抛出原始拒绝错误,避免掩盖真正原因。

const defaultResponseFormat = hasDefaultResponseFormat(this.modelId) || args.response_format !== void 0 ? null : "b64_json"; let posted; try { posted = await postImageRequest(defaultResponseFormat); } catch (error) { const isRejectedRequest = !!error && (error.statusCode === 400 || error.statusCode === 422); if (defaultResponseFormat == null || !isRejectedRequest) throw error; try { posted = await postImageRequest(null); } catch (retryError) { throw error; // 保留原始拒绝 } }

为什么绘画需要它:真实网关的响应形态

DMXAPI 边界测试快照显示,flux-1的请求体为{ "model": "flux-1", "n": 2, "prompt": "a fox", "response_format": "url", "size": "1328x1328" }(dmxapi.boundary.test.ts.snap),即该网关以 URL 而非 base64 返回图像。没有 url 通道,这类输出会在 schema 校验阶段直接被丢弃。此外该补丁还顺带修复了 embeddingusageprompt_tokens缺失时回退total_tokens的问题,以及思维链reasoning_content在工具调用轮次被丢弃导致 DeepSeek/GLM/Kimi/MiniMax 等方言拒绝请求的问题——这些都属于同一包的非图像兼容性加固。

补丁三:@ai-sdk/google 的模型路径与 isGeminiModel 前缀处理

getModelPath 的路径拼接修正

上游getModelPath的逻辑是「modelId 包含/则原样使用,否则加models/前缀」:

// 上游 return modelId.includes("/") ? modelId : `models/${modelId}`; // 补丁后 return modelId.includes("models/") ? modelId : `models/${modelId}`;

修正点在于:当 modelId 本身已包含models/前缀(例如models/gemini-2.0-flash)时,上游会因为其中包含/而原样返回——但若调用方传的是models/gemini-2.0-flash这种已带前缀的 id,旧逻辑会直接透传,导致后续:predict端点拼出重复前缀。补丁改为检查models/是否存在,避免models/models/...双重拼接。该函数同时作用于图像生成的:predict端点:

url: `${this.config.baseURL}/${getModelPath(this.modelId)}:predict`,

isGeminiModel 的前缀剥离

上游判断 Gemini 模型只用modelId.startsWith("gemini-"),对google/gemini-...models/gemini-...这类带前缀的 id 会误判为「非 Gemini」。补丁先剥离google/models/前缀再判断:

function isGeminiModel(modelId) { return modelId.replace(/^(google\/|models\/)/i, "").startsWith("gemini-"); }

这让 Imagen/Gemini 图像模型在带 provider 前缀的 id 下也能正确走到图像响应 schema(googleImageResponseSchema)与对应参数分支。注意dist/internal/index.jsdist/internal/index.mjs也同步应用了 getModelPath 的修正,保证内部入口与公开入口行为一致。

补丁的版本定位与变更管理

  • 变更记录(changeset)paintings-image-gen-patches.md 标注了三个包:@cherrystudio/ai-core@cherrystudio/ai-sdk-provider均为patch级别变更,属于向后兼容的缺陷修复。
  • changeset 撰写时ai补丁目标版本为6.0.143,当前仓库已随依赖升级到ai@6.0.185(见 package.json),补丁文件 ai@6.0.185.patch 在同一基础上持续应用;@ai-sdk/openai-compatible@2.0.72@ai-sdk/google@3.0.113与 pnpm-workspace.yaml 中的patchedDependencies映射一一对应。
  • 仓库通过 pnpm 的patchedDependencies机制管理全部补丁(pnpm-workspace.yaml),每个 patch 均锁定精确依赖版本,避免依赖漂移导致补丁失配。

兼容性边界与设计取舍

  • 非图像调用不受影响:changeset 明确指出这组补丁是针对 OpenAI 兼容 / Google 图像网关的定向 shim("targeted shims"),非图像的 OpenAI 兼容调用不受影响——openai-compatible 补丁中的images字段扩展只作用于 chat 响应里的image_url内容块与delta.images流式块。
  • 失败语义清晰:补丁在「宁可丢弃也不要存坏数据」与「不能静默吞掉付费生成」之间取了平衡——单图失败丢弃并告警,整批失败抛NoImageGeneratedError;异步任务路径的 imageGenerationJobHandler.ts 同样在「成功但零 URL」时抛错,避免把已计费的生成报告为静默成功。
  • 提供商侧自有实现并行存在:除 AI SDK 补丁外,仓库还维护了各自的图像模型实现,如 SiliconFlow 的 SiliconImageModel.ts(同样支持url/b64_json双通道 flatMap 解析,见其doGenerate返回值)。补丁与自研模型各司其职:补丁解决 SDK 默认路径,自研模型解决需要定制 body 形状的提供商。

总结

这组补丁以极小的 diff 面积解决了绘画功能落地的三个真实痛点:AI SDK 无法消费 URL 形态的图像输出(experimental_download)、OpenAI 兼容网关对gpt-image-*等模型拒绝response_format参数且返回url字段(双通道解析 + 条件发送 + 400/422 重试)、Google 网关模型路径前缀与 Gemini 判定错误(getModelPath/isGeminiModel)。它们与AiService.generateImage、任务系统、自研图像模型共同构成 Cherry Studio v2 绘画功能的完整底座,并且每一处行为都有仓库内测试与边界快照守护,是理解该项目图像生成链路的最佳切入点。

【免费下载链接】cherry-studio🍒 Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端项目地址: https://gitcode.com/CherryHQ/cherry-studio

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询