feat(*): 图片模型渠道 - 新增 Grok JSON 生图与单图编辑协议

This commit is contained in:
ddCat
2026-08-07 14:58:26 +08:00
parent 84f13fffa8
commit abec135d26
15 changed files with 207 additions and 10 deletions

View File

@@ -146,7 +146,7 @@ function ProtocolBrandMark({ protocol, compact = false }: { protocol: ModelProto
if (protocol.value.startsWith("volcengine-jimeng-")) return <span className={cn("grid shrink-0 place-items-center rounded-md bg-muted text-foreground/65", iconSize)}><Sparkles className={compact ? "size-3" : "size-4"} /></span>;
if (protocol.value.startsWith("volcengine-")) return <span className={cn("grid shrink-0 place-items-center rounded-md bg-muted text-foreground/65", iconSize)}><Flame className={compact ? "size-3" : "size-4"} /></span>;
if (protocol.value.startsWith("newapi-channel-")) return <span className={cn("grid shrink-0 place-items-center rounded-md bg-muted text-foreground/65", iconSize)}><Network className={compact ? "size-3" : "size-4"} /></span>;
const brand = protocol.value === "gemini-veo" ? "gemini" : protocol.value === "xai-video" ? "grok" : "openai";
const brand = protocol.value === "gemini-veo" ? "gemini" : protocol.value === "grok-image" || protocol.value === "xai-video" ? "grok" : "openai";
return <BrandIconRow models={[brand]} compact={compact} />;
}

View File

@@ -74,7 +74,16 @@ export function defaultImageCapabilityConfig(protocol?: ModelProtocol, model = "
outputFormat: { supported: true },
maxOutputs: 15,
};
if (protocol === "volcengine-ark-image") {
if (protocol === "grok-image") {
image.references.maxImages = 1;
image.references.maskSupported = false;
image.size = { parameter: "none", values: [], default: "auto", allowCustom: false };
image.quality = { supported: false, values: [], default: "auto" };
image.transparentBackground = { supported: false, default: false };
image.responseFormat = { supported: true };
image.outputFormat = { supported: false };
image.maxOutputs = 1;
} else if (protocol === "volcengine-ark-image") {
image.references.maskSupported = false;
image.quality.supported = false;
image.transparentBackground.supported = false;
@@ -89,7 +98,7 @@ export function defaultImageCapabilityConfig(protocol?: ModelProtocol, model = "
image.responseFormat.supported = false;
image.outputFormat.supported = false;
}
if (model.trim().toLowerCase().startsWith("grok-imagine-image")) {
if (protocol !== "grok-image" && model.trim().toLowerCase().startsWith("grok-imagine-image")) {
image.references.maxImages = 0;
image.references.maskSupported = false;
image.size = { parameter: "none", values: [], default: "auto", allowCustom: false };

View File

@@ -2,6 +2,7 @@ export type ModelProtocol =
| "chat-completion"
| "openai-response"
| "openai-image"
| "grok-image"
| "volcengine-ark-image"
| "volcengine-jimeng-image"
| "openai-audio"
@@ -30,6 +31,7 @@ export const MODEL_PROTOCOLS: ModelProtocolDefinition[] = [
{ value: "chat-completion", label: "OpenAI Chat Completions", capability: "text", create: "POST /v1/chat/completions", contentType: "application/json", media: "文本与多模态消息" },
{ value: "openai-response", label: "OpenAI Responses", capability: "text", create: "POST /v1/responses", contentType: "application/json", media: "文本与多模态输入" },
{ value: "openai-image", label: "OpenAI Images", capability: "image", create: "POST /v1/images/generations", contentType: "application/json / multipart", media: "生成、编辑与参考图" },
{ value: "grok-image", label: "Grok Images", capability: "image", create: "POST /v1/images/generations / edits", contentType: "application/json", media: "文生图与单张 URL 参考图,不支持蒙版" },
{ value: "volcengine-ark-image", label: "火山方舟图片", capability: "image", create: "POST /api/v3/images/generations", contentType: "application/json", media: "文生图与 image 参考图,不支持蒙版" },
{ value: "volcengine-jimeng-image", label: "即梦官方图片", capability: "image", create: "POST CVSync2AsyncSubmitTask", poll: "POST CVSync2AsyncGetResult", contentType: "application/json + AK/SK 签名", media: "0-14 张参考图,模型标识填写 req_key" },
{ value: "openai-audio", label: "OpenAI Audio", capability: "audio", create: "POST /v1/audio/speech", contentType: "application/json", media: "文本转语音" },

View File

@@ -181,6 +181,7 @@ function defaultProtocolForModel(channel: ModelChannel, model: string): ModelPro
if (channel.interfaceType) return channel.interfaceType;
if (channel.apiFormat === "gemini" && modelMatchesCapability(model, "video")) return "gemini-veo";
if (modelMatchesCapability(model, "video")) return "newapi";
if (modelOptionName(model).trim().toLowerCase().startsWith("grok-imagine-image")) return "grok-image";
if (modelMatchesCapability(model, "image")) return "openai-image";
return "chat-completion";
}

View File

@@ -812,6 +812,24 @@ export async function requestGeneration(config: AiConfig, prompt: string, option
throw new Error(readAxiosError(error, "请求失败"));
}
}
if (requestConfig.interfaceType === "grok-image") {
try {
const responseData = await postChannelJSON<ImageApiResponse>(
requestConfig,
aiApiUrl(requestConfig, "/images/generations"),
{
model: requestConfig.model,
prompt: withSystemPrompt(requestConfig, prompt),
n,
response_format: "url",
},
options,
);
return parseImagePayload(responseData);
} catch (error) {
throw new Error(readAxiosError(error, "Grok 图片生成失败"));
}
}
const quality = imageProfile.quality.supported && normalizedImage.quality !== "auto" ? normalizeQuality(normalizedImage.quality) || normalizedImage.quality : undefined;
const requestSize = resolveImageRequestSize(imageProfile, quality, normalizedImage.size);
const isVolcengineArk = requestConfig.interfaceType === "volcengine-ark-image";
@@ -853,6 +871,12 @@ async function postChannelJSON<T>(config: ReturnType<typeof resolveModelRequestC
).data;
}
async function grokImageInputURL(image: ReferenceImage) {
const candidate = image.url?.trim() || "";
if (/^https?:\/\//i.test(candidate)) return candidate;
return imageToDataUrl(image);
}
export async function requestEdit(config: AiConfig, prompt: string, references: ReferenceImage[], mask?: ReferenceImage, options?: RequestOptions) {
const selectedModel = config.model || config.imageModel;
const requestConfig = resolveModelRequestConfig(config, selectedModel);
@@ -869,6 +893,28 @@ export async function requestEdit(config: AiConfig, prompt: string, references:
throw new Error(readAxiosError(error, "请求失败"));
}
}
if (requestConfig.interfaceType === "grok-image") {
if (mask) throw new Error("Grok 图片协议不支持蒙版编辑,请移除蒙版后重试");
if (references.length !== 1) throw new Error("Grok 图片编辑必须提供且仅支持 1 张参考图");
try {
const imageUrl = await grokImageInputURL(references[0]);
const response = await postChannelJSON<ImageApiResponse>(
requestConfig,
aiApiUrl(requestConfig, "/images/edits"),
{
model: requestConfig.model,
prompt: withSystemPrompt(requestConfig, requestPrompt),
image: { url: imageUrl },
n,
response_format: "url",
},
options,
);
return parseImagePayload(response);
} catch (error) {
throw new Error(readAxiosError(error, "Grok 图片编辑失败"));
}
}
if (requestConfig.interfaceType === "volcengine-ark-image") {
if (mask) throw new Error("火山方舟图片协议不支持蒙版编辑,请移除蒙版后重试");
const quality = imageProfile.quality.supported && normalizedImage.quality !== "auto" ? normalizeQuality(normalizedImage.quality) || normalizedImage.quality : undefined;

View File

@@ -491,7 +491,7 @@ export function defaultBaseUrlForChannelInterface(interfaceType?: ChannelInterfa
if (interfaceType === "gemini-veo") return GEMINI_BASE_URL;
if (interfaceType === "volcengine-ark-image" || interfaceType === "volcengine-ark-video") return "https://ark.cn-beijing.volces.com/api/v3";
if (interfaceType === "volcengine-jimeng-image" || interfaceType === "volcengine-jimeng-video") return "https://visual.volcengineapi.com";
if (interfaceType === "newapi" || interfaceType === "newapi-channel-1" || interfaceType === "newapi-channel-2" || interfaceType === "xai-video") return "";
if (interfaceType === "grok-image" || interfaceType === "newapi" || interfaceType === "newapi-channel-1" || interfaceType === "newapi-channel-2" || interfaceType === "xai-video") return "";
return OPENAI_BASE_URL;
}