diff --git a/backend/internal/service/model_capability.go b/backend/internal/service/model_capability.go
index bc7ae8e20..a67c6c6d9 100644
--- a/backend/internal/service/model_capability.go
+++ b/backend/internal/service/model_capability.go
@@ -109,8 +109,9 @@ func DefaultImageCapabilityConfig(protocol string, modelName string) *ImageCapab
case model.ChannelInterfaceGrokImage:
image.References.MaxImages = 1
image.References.MaskSupported = false
- image.Size = ImageSizeConfig{Parameter: "none", Values: []string{}, Default: "auto", AllowCustom: false}
- image.Quality = ImageQualityConfig{Supported: false, Values: []string{}, Default: "auto"}
+ // grok2api / xAI Imagine:size→aspect_ratio,quality→resolution(1k/2k)。
+ image.Size = ImageSizeConfig{Parameter: "aspect_ratio", Values: []string{"1:1", "3:4", "4:3", "9:16", "16:9", "2:3", "3:2"}, Default: "1:1", AllowCustom: false}
+ image.Quality = ImageQualityConfig{Supported: true, Values: []string{"1k", "2k"}, Default: "2k"}
image.TransparentBackground = VideoBooleanConfig{Supported: false, Default: false}
image.ResponseFormat = ParameterSupport{Supported: true}
image.OutputFormat = ParameterSupport{Supported: false}
@@ -132,8 +133,8 @@ func DefaultImageCapabilityConfig(protocol string, modelName string) *ImageCapab
if model.ChannelInterfaceType(protocol) != model.ChannelInterfaceGrokImage && strings.HasPrefix(strings.ToLower(strings.TrimSpace(modelName)), "grok-imagine-image") {
image.References.MaxImages = 0
image.References.MaskSupported = false
- image.Size = ImageSizeConfig{Parameter: "none", Values: []string{}, Default: "auto", AllowCustom: false}
- image.Quality = ImageQualityConfig{Supported: false, Values: []string{}, Default: "auto"}
+ image.Size = ImageSizeConfig{Parameter: "aspect_ratio", Values: []string{"1:1", "3:4", "4:3", "9:16", "16:9", "2:3", "3:2"}, Default: "1:1", AllowCustom: false}
+ image.Quality = ImageQualityConfig{Supported: true, Values: []string{"1k", "2k"}, Default: "2k"}
image.TransparentBackground = VideoBooleanConfig{Supported: false, Default: false}
image.ResponseFormat = ParameterSupport{Supported: true}
image.OutputFormat = ParameterSupport{Supported: false}
diff --git a/backend/internal/service/provider.go b/backend/internal/service/provider.go
index a2e54c031..19e5dd6fb 100644
--- a/backend/internal/service/provider.go
+++ b/backend/internal/service/provider.go
@@ -746,7 +746,15 @@ func grokImageRequestBody(input canvasGenerationInput) (grokImageRequest, string
if input.Mask != nil {
return grokImageRequest{}, "", errors.New("Grok 图片协议不支持蒙版编辑,请移除蒙版后重试")
}
- body := grokImageRequest{Model: input.Config.Model, Prompt: withSystemPrompt(input.Config, input.Prompt), N: 1, ResponseFormat: "url"}
+ body := grokImageRequest{
+ Model: input.Config.Model,
+ Prompt: withSystemPrompt(input.Config, input.Prompt),
+ N: 1,
+ ResponseFormat: "url",
+ Size: strings.TrimSpace(input.Config.Size),
+ AspectRatio: normalizeGrokImageAspectRatio(input.Config.Size),
+ Resolution: normalizeGrokImageResolution(input.Config.Quality),
+ }
if len(input.ReferenceImages) == 0 {
return body, "/images/generations", nil
}
@@ -761,6 +769,63 @@ func grokImageRequestBody(input canvasGenerationInput) (grokImageRequest, string
return body, "/images/edits", nil
}
+// normalizeGrokImageResolution 把画布 quality(1k/2k/high…)映射为 grok2api / xAI 的 resolution。
+func normalizeGrokImageResolution(quality string) string {
+ raw := strings.ToLower(strings.TrimSpace(quality))
+ switch raw {
+ case "", "auto":
+ return ""
+ case "1k", "low", "standard":
+ return "1k"
+ case "2k", "medium", "hd", "high", "4k":
+ // xAI Imagine 图片通常最高 2k;超出则夹到 2k,避免上游拒参。
+ return "2k"
+ default:
+ return ""
+ }
+}
+
+// normalizeGrokImageAspectRatio 把画布 size(如 1280x720 / 9:16)转成 grok2api / xAI 接受的 aspect_ratio。
+func normalizeGrokImageAspectRatio(size string) string {
+ raw := strings.ToLower(strings.TrimSpace(strings.ReplaceAll(size, "×", "x")))
+ if raw == "" || raw == "auto" {
+ return ""
+ }
+ if strings.Contains(raw, ":") {
+ switch raw {
+ case "1:1", "3:4", "4:3", "9:16", "16:9", "2:3", "3:2", "9:19.5", "19.5:9", "1:2", "2:1":
+ return raw
+ }
+ }
+ parts := strings.Split(raw, "x")
+ if len(parts) != 2 {
+ return ""
+ }
+ w, wErr := strconv.Atoi(parts[0])
+ h, hErr := strconv.Atoi(parts[1])
+ if wErr != nil || hErr != nil || w <= 0 || h <= 0 {
+ return ""
+ }
+ if w == h {
+ return "1:1"
+ }
+ ratio := float64(w) / float64(h)
+ switch {
+ case w*9 == h*16 || (ratio >= 1.7 && ratio <= 1.8):
+ return "16:9"
+ case h*9 == w*16 || (ratio > 0 && ratio <= 1.0/1.7 && ratio >= 1.0/1.8):
+ return "9:16"
+ case w*3 == h*4 || (ratio > 1.2 && ratio < 1.4):
+ return "4:3"
+ case h*3 == w*4 || (ratio > 0.7 && ratio < 0.85):
+ return "3:4"
+ case w > h:
+ return "16:9"
+ default:
+ return "9:16"
+ }
+}
+
func grokImageInputURL(media providerMedia) (string, error) {
if isPublicMediaURL(strings.TrimSpace(media.URL)) {
return strings.TrimSpace(media.URL), nil
diff --git a/backend/internal/service/provider_request_types.go b/backend/internal/service/provider_request_types.go
index 9956e95b9..c6be13f0c 100644
--- a/backend/internal/service/provider_request_types.go
+++ b/backend/internal/service/provider_request_types.go
@@ -58,6 +58,10 @@ type grokImageRequest struct {
Image *grokImageInput `json:"image,omitempty"`
N int `json:"n"`
ResponseFormat string `json:"response_format"`
+ Size string `json:"size,omitempty"`
+ AspectRatio string `json:"aspect_ratio,omitempty"`
+ // Resolution 对应 xAI / grok2api 的 resolution(常见 1k / 2k)。
+ Resolution string `json:"resolution,omitempty"`
}
type grokImageInput struct {
diff --git a/backend/internal/service/provider_test.go b/backend/internal/service/provider_test.go
index 90bf341ce..321f4a5fd 100644
--- a/backend/internal/service/provider_test.go
+++ b/backend/internal/service/provider_test.go
@@ -196,6 +196,40 @@ func TestRunGrokImageTaskUsesJSONEditContract(t *testing.T) {
}
}
+func TestGrokImageRequestBodyMapsAspectRatio(t *testing.T) {
+ body, path, err := grokImageRequestBody(canvasGenerationInput{
+ Prompt: "a cat",
+ Config: providerConfig{Model: "grok-imagine-image", InterfaceType: "grok-image", Size: "9:16", Quality: "2k"},
+ })
+ if err != nil {
+ t.Fatalf("grokImageRequestBody() error = %v", err)
+ }
+ if path != "/images/generations" {
+ t.Fatalf("path = %q", path)
+ }
+ if body.AspectRatio != "9:16" || body.Size != "9:16" || body.Resolution != "2k" {
+ t.Fatalf("body = %#v", body)
+ }
+ if got := normalizeGrokImageAspectRatio("1280x720"); got != "16:9" {
+ t.Fatalf("normalize 1280x720 = %q", got)
+ }
+ if got := normalizeGrokImageAspectRatio("720x1280"); got != "9:16" {
+ t.Fatalf("normalize 720x1280 = %q", got)
+ }
+}
+
+func TestNormalizeGrokImageResolution(t *testing.T) {
+ if got := normalizeGrokImageResolution("1k"); got != "1k" {
+ t.Fatalf("1k = %q", got)
+ }
+ if got := normalizeGrokImageResolution("high"); got != "2k" {
+ t.Fatalf("high = %q", got)
+ }
+ if got := normalizeGrokImageResolution("auto"); got != "" {
+ t.Fatalf("auto = %q", got)
+ }
+}
+
func TestGrokImageRequestBodyRejectsMaskAndMultipleReferences(t *testing.T) {
if _, _, err := grokImageRequestBody(canvasGenerationInput{Config: providerConfig{InterfaceType: "grok-image"}, Mask: &providerMedia{DataURL: testReferenceImageDataURL}}); err == nil || !strings.Contains(err.Error(), "不支持蒙版") {
t.Fatalf("mask error = %v", err)
diff --git a/web/src/components/image-settings-panel.tsx b/web/src/components/image-settings-panel.tsx
index 032cdb697..0fb151a37 100644
--- a/web/src/components/image-settings-panel.tsx
+++ b/web/src/components/image-settings-panel.tsx
@@ -10,6 +10,8 @@ const qualityOptions = [
{ value: "high", label: "高" },
{ value: "medium", label: "中" },
{ value: "low", label: "低" },
+ { value: "1k", label: "1K" },
+ { value: "2k", label: "2K" },
];
const DIMENSION_STEP = 16;
@@ -78,8 +80,8 @@ export function ImageSettingsPanel({ config, onConfigChange, theme, showTitle =
>
{showTitle ?
图像设置
: null}
{profile.quality.supported ?
-
质量
-
+
{isGrokResolutionQuality(profile) ? "分辨率" : "质量"}
+
{activeQualityOptions.map((item) => (
onConfigChange("quality", item.value)}>
{item.label}
@@ -181,7 +183,12 @@ export function ImageSettingsTheme({ theme, children }: { theme: CanvasTheme; ch
}
export function imageQualityLabel(value: string) {
- return ({ auto: "自动", high: "高", medium: "中", low: "低" } as Record)[value] || "默认";
+ return ({ auto: "自动", high: "高", medium: "中", low: "低", "1k": "1K", "2k": "2K" } as Record)[value] || value || "默认";
+}
+
+function isGrokResolutionQuality(profile: ImageCapabilityConfig) {
+ const values = profile.quality.values.map((item) => item.toLowerCase());
+ return values.includes("1k") || values.includes("2k");
}
export function imageSizeLabel(size: string) {
diff --git a/web/src/lib/model-capabilities.ts b/web/src/lib/model-capabilities.ts
index 090bb41b2..5f847d18a 100644
--- a/web/src/lib/model-capabilities.ts
+++ b/web/src/lib/model-capabilities.ts
@@ -77,8 +77,14 @@ export function defaultImageCapabilityConfig(protocol?: ModelProtocol, model = "
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" };
+ // grok2api / xAI Imagine:size→aspect_ratio,quality→resolution(1k/2k)。
+ image.size = {
+ parameter: "aspect_ratio",
+ values: ["1:1", "3:4", "4:3", "9:16", "16:9", "2:3", "3:2"],
+ default: "1:1",
+ allowCustom: false,
+ };
+ image.quality = { supported: true, values: ["1k", "2k"], default: "2k" };
image.transparentBackground = { supported: false, default: false };
image.responseFormat = { supported: true };
image.outputFormat = { supported: false };
@@ -101,8 +107,13 @@ export function defaultImageCapabilityConfig(protocol?: ModelProtocol, model = "
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 };
- image.quality = { supported: false, values: [], default: "auto" };
+ image.size = {
+ parameter: "aspect_ratio",
+ values: ["1:1", "3:4", "4:3", "9:16", "16:9", "2:3", "3:2"],
+ default: "1:1",
+ allowCustom: false,
+ };
+ image.quality = { supported: true, values: ["1k", "2k"], default: "2k" };
image.transparentBackground = { supported: false, default: false };
image.responseFormat = { supported: true };
image.outputFormat = { supported: false };
@@ -166,7 +177,9 @@ export function modelCapabilityConfigFor(config: { channels: Array<{ id: string;
export function normalizeImageValue(profile: ImageCapabilityConfig, value: { size?: string; quality?: string; count?: string; transparentBackground?: string }) {
const size = normalizeImageSizeSetting(profile, value.size);
- const quality = profile.quality.supported && profile.quality.values.includes(value.quality || "") ? value.quality! : profile.quality.default || "auto";
+ const quality = profile.quality.supported
+ ? (value.quality && profile.quality.values.includes(value.quality) ? value.quality : profile.quality.default || "auto")
+ : profile.quality.default || "auto";
const count = String(Math.max(1, Math.min(profile.maxOutputs, Math.floor(Math.abs(Number(value.count)) || 1))));
const transparentBackground = profile.transparentBackground.supported && value.transparentBackground === "true" ? "true" : "false";
return { size, quality, count, transparentBackground };
diff --git a/web/src/pages/create/index.tsx b/web/src/pages/create/index.tsx
index a0ae970b6..93cb2ac26 100644
--- a/web/src/pages/create/index.tsx
+++ b/web/src/pages/create/index.tsx
@@ -61,6 +61,9 @@ const qualityOptions = [
{ value: "low", label: "低", description: "更快生成" },
{ value: "medium", label: "中", description: "均衡模式" },
{ value: "high", label: "高", description: "优先细节" },
+ // grok2api / xAI Imagine:quality 映射 resolution
+ { value: "1k", label: "1K", description: "标准清晰度" },
+ { value: "2k", label: "2K", description: "更高清晰度" },
];
const resolutionOptions = VIDEO_RESOLUTION_OPTIONS.map((value) => ({ value: String(value), label: videoResolutionLabel(value) }));
const countOptions = ["1", "2", "3", "4"];
@@ -777,7 +780,8 @@ function ModePicker({ mode, onModeChange }: { mode: CreationMode; onModeChange:
function GenerationSettingsMenu(props: ComposerProps) {
const [open, setOpen] = useState(false);
const [customRatioOpen, setCustomRatioOpen] = useState(!ratioOptions.some((option) => option.value === props.ratio));
- const qualityLabel = qualityOptions.find((item) => item.value === props.quality)?.label || "自动";
+ const activeQualityOptions = props.imageProfile.quality.values.map((value) => qualityOptions.find((item) => item.value === value) || { value, label: value.toUpperCase(), description: "模型支持的质量/分辨率" });
+ const qualityLabel = activeQualityOptions.find((item) => item.value === props.quality)?.label || qualityOptions.find((item) => item.value === props.quality)?.label || props.quality || "自动";
const ratios = props.mode === "video" ? props.videoProfile.ratios : props.imageProfile.size.values.length ? props.imageProfile.size.values : ratioOptions.map((item) => item.value);
const resolutions = props.mode === "video" ? props.videoProfile.resolutions.map((value) => ({ value: value.replace(/p$/i, ""), label: videoResolutionLabel(value) })) : resolutionOptions;
const imageSummary = [
@@ -789,7 +793,7 @@ function GenerationSettingsMenu(props: ComposerProps) {
const panel =
{props.mode === "video" || props.imageProfile.size.parameter !== "none" ?
{ratios.map((value) => )}
{props.mode !== "video" && props.imageProfile.size.allowCustom && (customRatioOpen ?
:
)}
: null}
{props.mode === "video" ?
{resolutions.map((option) => )}
: <>
- {props.imageProfile.quality.supported ?
{qualityOptions.filter((option) => props.imageProfile.quality.values.includes(option.value)).map((option) => )}
: null}
+ {props.imageProfile.quality.supported ?
item.value === "1k" || item.value === "2k") ? "分辨率" : "图片质量"} value={qualityLabel}>{activeQualityOptions.map((option) => )}
: null}
{props.imageProfile.maxOutputs > 1 ?
: null}
>}
;
diff --git a/web/src/services/api/image.ts b/web/src/services/api/image.ts
index fe08b5fbd..a857c7cc9 100644
--- a/web/src/services/api/image.ts
+++ b/web/src/services/api/image.ts
@@ -124,6 +124,15 @@ function normalizeQuality(quality: string) {
return QUALITY_BASE[normalized] ? normalized : undefined;
}
+/** grok2api / xAI Imagine:画布 quality 映射为 resolution(1k/2k)。 */
+function normalizeGrokImageResolution(quality: string | undefined) {
+ const value = (quality || "").trim().toLowerCase();
+ if (!value || value === "auto") return undefined;
+ if (value === "1k" || value === "low" || value === "standard") return "1k";
+ if (value === "2k" || value === "medium" || value === "hd" || value === "high" || value === "4k") return "2k";
+ return undefined;
+}
+
/** Map "quality + ratio" to an explicit pixel dimension like "3840x2160". */
function resolveSize(quality: string | undefined, ratio: string): string {
const parsedRatio = parseImageRatio(ratio);
@@ -814,6 +823,9 @@ export async function requestGeneration(config: AiConfig, prompt: string, option
}
if (requestConfig.interfaceType === "grok-image") {
try {
+ const size = normalizedImage.size && normalizedImage.size !== "auto" ? normalizedImage.size : undefined;
+ const aspectRatio = size?.includes(":") ? size : undefined;
+ const resolution = normalizeGrokImageResolution(normalizedImage.quality);
const responseData = await postChannelJSON(
requestConfig,
aiApiUrl(requestConfig, "/images/generations"),
@@ -822,6 +834,9 @@ export async function requestGeneration(config: AiConfig, prompt: string, option
prompt: withSystemPrompt(requestConfig, prompt),
n,
response_format: "url",
+ ...(size ? { size } : {}),
+ ...(aspectRatio ? { aspect_ratio: aspectRatio } : {}),
+ ...(resolution ? { resolution } : {}),
},
options,
);
@@ -898,6 +913,9 @@ export async function requestEdit(config: AiConfig, prompt: string, references:
if (references.length !== 1) throw new Error("Grok 图片编辑必须提供且仅支持 1 张参考图");
try {
const imageUrl = await grokImageInputURL(references[0]);
+ const size = normalizedImage.size && normalizedImage.size !== "auto" ? normalizedImage.size : undefined;
+ const aspectRatio = size?.includes(":") ? size : undefined;
+ const resolution = normalizeGrokImageResolution(normalizedImage.quality);
const response = await postChannelJSON(
requestConfig,
aiApiUrl(requestConfig, "/images/edits"),
@@ -907,6 +925,9 @@ export async function requestEdit(config: AiConfig, prompt: string, references:
image: { url: imageUrl },
n,
response_format: "url",
+ ...(size ? { size } : {}),
+ ...(aspectRatio ? { aspect_ratio: aspectRatio } : {}),
+ ...(resolution ? { resolution } : {}),
},
options,
);