From 6b5f176baac89c1316adf7e40e0419b6c5df61da Mon Sep 17 00:00:00 2001
From: ddCat <2695092045@qq.com>
Date: Fri, 7 Aug 2026 14:58:26 +0800
Subject: [PATCH] =?UTF-8?q?feat(*):=20=E5=9B=BE=E7=89=87=E6=A8=A1=E5=9E=8B?=
=?UTF-8?q?=E6=B8=A0=E9=81=93=20-=20=E6=96=B0=E5=A2=9E=20Grok=20JSON=20?=
=?UTF-8?q?=E7=94=9F=E5=9B=BE=E4=B8=8E=E5=8D=95=E5=9B=BE=E7=BC=96=E8=BE=91?=
=?UTF-8?q?=E5=8D=8F=E8=AE=AE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
backend/internal/handler/security.go | 4 +-
backend/internal/handler/security_test.go | 9 +++
backend/internal/model/models.go | 1 +
backend/internal/service/admin.go | 2 +-
backend/internal/service/channel_models.go | 2 +-
backend/internal/service/model_capability.go | 11 +++-
backend/internal/service/provider.go | 47 +++++++++++++-
.../service/provider_request_types.go | 12 ++++
backend/internal/service/provider_test.go | 63 +++++++++++++++++++
web/src/components/model-protocol-picker.tsx | 2 +-
web/src/lib/model-capabilities.ts | 13 +++-
web/src/lib/model-protocols.ts | 2 +
.../pages/settings/channel-video-pricing.tsx | 1 +
web/src/services/api/image.ts | 46 ++++++++++++++
web/src/stores/use-config-store.ts | 2 +-
15 files changed, 207 insertions(+), 10 deletions(-)
diff --git a/backend/internal/handler/security.go b/backend/internal/handler/security.go
index f86b805b9..b320bac05 100644
--- a/backend/internal/handler/security.go
+++ b/backend/internal/handler/security.go
@@ -82,7 +82,7 @@ func authorizeCustomRelay(method string, target *url.URL, apiFormat string, cont
}
if apiFormat == "openai" {
multipartAllowed := mediaType == "multipart/form-data" && (strings.HasSuffix(requestPath, "/images/edits") || strings.HasSuffix(requestPath, "/videos"))
- jsonAllowed := mediaType == "application/json" && (strings.HasSuffix(requestPath, "/responses") || strings.HasSuffix(requestPath, "/chat/completions") || strings.HasSuffix(requestPath, "/images/generations") || strings.HasSuffix(requestPath, "/audio/speech") || strings.HasSuffix(requestPath, "/video/generations") || strings.HasSuffix(requestPath, "/videos/generations") || strings.HasSuffix(requestPath, "/videos") || strings.HasSuffix(requestPath, "/contents/generations/tasks"))
+ jsonAllowed := mediaType == "application/json" && (strings.HasSuffix(requestPath, "/responses") || strings.HasSuffix(requestPath, "/chat/completions") || strings.HasSuffix(requestPath, "/images/generations") || strings.HasSuffix(requestPath, "/images/edits") || strings.HasSuffix(requestPath, "/audio/speech") || strings.HasSuffix(requestPath, "/video/generations") || strings.HasSuffix(requestPath, "/videos/generations") || strings.HasSuffix(requestPath, "/videos") || strings.HasSuffix(requestPath, "/contents/generations/tasks"))
if len(query) != 0 || (!multipartAllowed && !jsonAllowed) {
return errors.New("自定义渠道不允许访问该上游接口")
}
@@ -183,7 +183,7 @@ func interfaceAllowsProxyPath(interfaceType model.ChannelInterfaceType, requestP
return requestPath == "/chat/completions"
case model.ChannelInterfaceOpenAIResponse:
return requestPath == "/responses"
- case model.ChannelInterfaceOpenAIImage:
+ case model.ChannelInterfaceOpenAIImage, model.ChannelInterfaceGrokImage:
return requestPath == "/images/generations" || requestPath == "/images/edits"
case model.ChannelInterfaceVolcengineArkImage:
return requestPath == "/images/generations"
diff --git a/backend/internal/handler/security_test.go b/backend/internal/handler/security_test.go
index 89adb5cdc..db48d7462 100644
--- a/backend/internal/handler/security_test.go
+++ b/backend/internal/handler/security_test.go
@@ -18,6 +18,14 @@ func TestAuthorizeSystemProxyAllowsConfiguredGenerationModel(t *testing.T) {
}
}
+func TestAuthorizeSystemProxyAllowsGrokImageJSONEdits(t *testing.T) {
+ channel := &model.ModelChannel{APIFormat: "openai", ModelsJSON: `["grok-imagine-image"]`}
+ body := []byte(`{"model":"grok-imagine-image","prompt":"edit"}`)
+ if err := authorizeSystemProxy(channel, model.ChannelInterfaceGrokImage, http.MethodPost, "/images/edits", "application/json", body); err != nil {
+ t.Fatalf("authorizeSystemProxy() error = %v", err)
+ }
+}
+
func TestAuthorizeCustomRelayAllowsModelsAndAgentEndpoints(t *testing.T) {
tests := []struct {
method string
@@ -30,6 +38,7 @@ func TestAuthorizeCustomRelayAllowsModelsAndAgentEndpoints(t *testing.T) {
{method: http.MethodPost, target: "https://api.example.com/v1/chat/completions", apiFormat: "openai", contentType: "application/json; charset=utf-8"},
{method: http.MethodPost, target: "https://api.example.com/v1/audio/speech", apiFormat: "openai", contentType: "application/json"},
{method: http.MethodPost, target: "https://api.example.com/v1/images/edits", apiFormat: "openai", contentType: "multipart/form-data; boundary=test"},
+ {method: http.MethodPost, target: "https://api.example.com/v1/images/edits", apiFormat: "openai", contentType: "application/json"},
{method: http.MethodPost, target: "https://api.example.com/v1/videos", apiFormat: "openai", contentType: "application/json"},
{method: http.MethodPost, target: "https://api.example.com/v1/videos", apiFormat: "openai", contentType: "multipart/form-data; boundary=test"},
{method: http.MethodGet, target: "https://api.example.com/v1/videos/task-1/content", apiFormat: "openai"},
diff --git a/backend/internal/model/models.go b/backend/internal/model/models.go
index 3aade4cc1..126d67cfc 100644
--- a/backend/internal/model/models.go
+++ b/backend/internal/model/models.go
@@ -61,6 +61,7 @@ const (
ChannelInterfaceChatCompletion ChannelInterfaceType = "chat-completion"
ChannelInterfaceOpenAIResponse ChannelInterfaceType = "openai-response"
ChannelInterfaceOpenAIImage ChannelInterfaceType = "openai-image"
+ ChannelInterfaceGrokImage ChannelInterfaceType = "grok-image"
ChannelInterfaceVolcengineArkImage ChannelInterfaceType = "volcengine-ark-image"
ChannelInterfaceVolcengineJiMengImage ChannelInterfaceType = "volcengine-jimeng-image"
ChannelInterfaceOpenAIAudio ChannelInterfaceType = "openai-audio"
diff --git a/backend/internal/service/admin.go b/backend/internal/service/admin.go
index c34565834..7313f4a80 100644
--- a/backend/internal/service/admin.go
+++ b/backend/internal/service/admin.go
@@ -734,7 +734,7 @@ func mergeChannelRequest(req ChannelRequest, channel model.ModelChannel) Channel
func validChannelInterfaceType(value model.ChannelInterfaceType) bool {
switch value {
- case model.ChannelInterfaceChatCompletion, model.ChannelInterfaceOpenAIResponse, model.ChannelInterfaceOpenAIImage, model.ChannelInterfaceVolcengineArkImage, model.ChannelInterfaceVolcengineJiMengImage, model.ChannelInterfaceOpenAIAudio, model.ChannelInterfaceAsyncAudio, model.ChannelInterfaceNewAPIVideo, model.ChannelInterfaceNewAPIChannel1, model.ChannelInterfaceNewAPIChannel2, model.ChannelInterfaceXAIVideo, model.ChannelInterfaceVolcengineArkVideo, model.ChannelInterfaceVolcengineJiMengVideo, model.ChannelInterfaceGeminiVeo:
+ case model.ChannelInterfaceChatCompletion, model.ChannelInterfaceOpenAIResponse, model.ChannelInterfaceOpenAIImage, model.ChannelInterfaceGrokImage, model.ChannelInterfaceVolcengineArkImage, model.ChannelInterfaceVolcengineJiMengImage, model.ChannelInterfaceOpenAIAudio, model.ChannelInterfaceAsyncAudio, model.ChannelInterfaceNewAPIVideo, model.ChannelInterfaceNewAPIChannel1, model.ChannelInterfaceNewAPIChannel2, model.ChannelInterfaceXAIVideo, model.ChannelInterfaceVolcengineArkVideo, model.ChannelInterfaceVolcengineJiMengVideo, model.ChannelInterfaceGeminiVeo:
return true
default:
return false
diff --git a/backend/internal/service/channel_models.go b/backend/internal/service/channel_models.go
index fc8bc68c9..555108a6c 100644
--- a/backend/internal/service/channel_models.go
+++ b/backend/internal/service/channel_models.go
@@ -454,7 +454,7 @@ func (s *Service) syncChannelModelNames(channel *model.ModelChannel) error {
func capabilityForProtocol(protocol model.ChannelInterfaceType) string {
switch protocol {
- case model.ChannelInterfaceOpenAIImage, model.ChannelInterfaceVolcengineArkImage, model.ChannelInterfaceVolcengineJiMengImage:
+ case model.ChannelInterfaceOpenAIImage, model.ChannelInterfaceGrokImage, model.ChannelInterfaceVolcengineArkImage, model.ChannelInterfaceVolcengineJiMengImage:
return "image"
case model.ChannelInterfaceOpenAIAudio, model.ChannelInterfaceAsyncAudio:
return "audio"
diff --git a/backend/internal/service/model_capability.go b/backend/internal/service/model_capability.go
index ac85d57af..bc7ae8e20 100644
--- a/backend/internal/service/model_capability.go
+++ b/backend/internal/service/model_capability.go
@@ -106,6 +106,15 @@ func DefaultImageCapabilityConfig(protocol string, modelName string) *ImageCapab
MaxOutputs: 15,
}
switch model.ChannelInterfaceType(protocol) {
+ 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"}
+ image.TransparentBackground = VideoBooleanConfig{Supported: false, Default: false}
+ image.ResponseFormat = ParameterSupport{Supported: true}
+ image.OutputFormat = ParameterSupport{Supported: false}
+ image.MaxOutputs = 1
case model.ChannelInterfaceVolcengineArkImage:
image.References.MaskSupported = false
image.Quality.Supported = false
@@ -120,7 +129,7 @@ func DefaultImageCapabilityConfig(protocol string, modelName string) *ImageCapab
image.ResponseFormat.Supported = false
image.OutputFormat.Supported = false
}
- if strings.HasPrefix(strings.ToLower(strings.TrimSpace(modelName)), "grok-imagine-image") {
+ 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}
diff --git a/backend/internal/service/provider.go b/backend/internal/service/provider.go
index ad68418b6..0dd10840c 100644
--- a/backend/internal/service/provider.go
+++ b/backend/internal/service/provider.go
@@ -635,6 +635,9 @@ func systemChannelIDFromBaseURL(baseURL string) string {
}
func runImageTask(ctx context.Context, input canvasGenerationInput) (map[string]interface{}, error) {
+ if input.Config.InterfaceType == string(model.ChannelInterfaceGrokImage) {
+ return runGrokImageTask(ctx, input)
+ }
if input.Config.InterfaceType == string(model.ChannelInterfaceVolcengineJiMengImage) {
return runVolcengineJiMengImageTask(ctx, input)
}
@@ -720,6 +723,48 @@ func runImageTask(ctx context.Context, input canvasGenerationInput) (map[string]
return map[string]interface{}{"mode": "image", "images": images}, nil
}
+func runGrokImageTask(ctx context.Context, input canvasGenerationInput) (map[string]interface{}, error) {
+ body, path, err := grokImageRequestBody(input)
+ if err != nil {
+ return nil, err
+ }
+ var payload imageResponse
+ if err := postJSON(ctx, input.Config, path, body, &payload); err != nil {
+ return nil, err
+ }
+ images, err := imageDataURLs(payload)
+ if err != nil {
+ return nil, err
+ }
+ return map[string]interface{}{"mode": "image", "images": images}, nil
+}
+
+func grokImageRequestBody(input canvasGenerationInput) (grokImageRequest, string, error) {
+ 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"}
+ if len(input.ReferenceImages) == 0 {
+ return body, "/images/generations", nil
+ }
+ if len(input.ReferenceImages) != 1 {
+ return grokImageRequest{}, "", fmt.Errorf("Grok 图片编辑只支持 1 张参考图,当前连接了 %d 张", len(input.ReferenceImages))
+ }
+ imageURL, err := grokImageInputURL(input.ReferenceImages[0])
+ if err != nil {
+ return grokImageRequest{}, "", err
+ }
+ body.Image = &grokImageInput{URL: imageURL}
+ return body, "/images/edits", nil
+}
+
+func grokImageInputURL(media providerMedia) (string, error) {
+ if isPublicMediaURL(strings.TrimSpace(media.URL)) {
+ return strings.TrimSpace(media.URL), nil
+ }
+ return openAIImageInputURL(media)
+}
+
const volcengineArkImageMaxPixels = 4624220
func runVolcengineArkImageTask(ctx context.Context, input canvasGenerationInput) (map[string]interface{}, error) {
@@ -1909,7 +1954,7 @@ func validateGenerationInterface(mode string, interfaceType string) error {
}
allowed := map[string]map[string]bool{
"text": {"chat-completion": true, "openai-response": true},
- "image": {"openai-image": true, "volcengine-ark-image": true, "volcengine-jimeng-image": true},
+ "image": {"openai-image": true, "grok-image": true, "volcengine-ark-image": true, "volcengine-jimeng-image": true},
"video": {"newapi": true, "newapi-channel-1": true, "newapi-channel-2": true, "xai-video": true, "volcengine-ark-video": true, "volcengine-jimeng-video": true, "gemini-veo": true},
"audio": {"openai-audio": true, "async-audio": true},
}
diff --git a/backend/internal/service/provider_request_types.go b/backend/internal/service/provider_request_types.go
index 50a955a85..9956e95b9 100644
--- a/backend/internal/service/provider_request_types.go
+++ b/backend/internal/service/provider_request_types.go
@@ -52,6 +52,18 @@ type xaiVideoImage struct {
URL string `json:"url"`
}
+type grokImageRequest struct {
+ Model string `json:"model"`
+ Prompt string `json:"prompt"`
+ Image *grokImageInput `json:"image,omitempty"`
+ N int `json:"n"`
+ ResponseFormat string `json:"response_format"`
+}
+
+type grokImageInput struct {
+ URL string `json:"url"`
+}
+
type geminiVeoRequest struct {
Instances []geminiVeoInstance `json:"instances"`
Parameters geminiVeoParameters `json:"parameters"`
diff --git a/backend/internal/service/provider_test.go b/backend/internal/service/provider_test.go
index 4b71b9ed0..4687ffea8 100644
--- a/backend/internal/service/provider_test.go
+++ b/backend/internal/service/provider_test.go
@@ -104,6 +104,66 @@ func TestVolcengineArkImageRejectsMaskBeforeRequest(t *testing.T) {
}
}
+func TestRunGrokImageTaskUsesJSONEditContract(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path != "/v1/images/edits" {
+ t.Errorf("path = %q, want /v1/images/edits", r.URL.Path)
+ }
+ if contentType := r.Header.Get("Content-Type"); !strings.HasPrefix(contentType, "application/json") {
+ t.Errorf("Content-Type = %q, want application/json", contentType)
+ }
+ var body grokImageRequest
+ if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
+ t.Fatalf("decode request body: %v", err)
+ }
+ if body.Model != "grok-imagine-image-quality" || body.N != 1 || body.ResponseFormat != "url" {
+ t.Fatalf("request body = %#v", body)
+ }
+ if body.Image == nil || body.Image.URL != testReferenceImageDataURL {
+ t.Fatalf("image = %#v", body.Image)
+ }
+ w.Header().Set("Content-Type", "application/json")
+ _, _ = w.Write([]byte(`{"data":[{"url":"https://example.com/result.png"}]}`))
+ }))
+ defer server.Close()
+
+ result, err := runImageTask(context.Background(), canvasGenerationInput{
+ Mode: "image",
+ Prompt: "edit the reference",
+ Config: providerConfig{BaseURL: server.URL, APIKey: "key", Model: "grok-imagine-image-quality", InterfaceType: "grok-image"},
+ ReferenceImages: []providerMedia{{DataURL: testReferenceImageDataURL}},
+ })
+ if err != nil {
+ t.Fatalf("runImageTask() error = %v", err)
+ }
+ images, _ := result["images"].([]map[string]string)
+ if len(images) != 1 || images[0]["dataUrl"] != "https://example.com/result.png" {
+ t.Fatalf("images = %#v", result["images"])
+ }
+}
+
+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)
+ }
+ if _, _, err := grokImageRequestBody(canvasGenerationInput{Config: providerConfig{InterfaceType: "grok-image"}, ReferenceImages: []providerMedia{{DataURL: testReferenceImageDataURL}, {DataURL: testReferenceImageDataURL}}}); err == nil || !strings.Contains(err.Error(), "只支持 1 张") {
+ t.Fatalf("multiple reference error = %v", err)
+ }
+}
+
+func TestGrokImageRequestBodyPrefersPublicURL(t *testing.T) {
+ body, path, err := grokImageRequestBody(canvasGenerationInput{
+ Config: providerConfig{Model: "grok-imagine-image", InterfaceType: "grok-image"},
+ ReferenceImages: []providerMedia{{URL: "https://example.com/reference.png", DataURL: testReferenceImageDataURL}},
+ })
+ if err != nil {
+ t.Fatalf("grokImageRequestBody() error = %v", err)
+ }
+ if path != "/images/edits" || body.Image == nil || body.Image.URL != "https://example.com/reference.png" {
+ t.Fatalf("path = %q, image = %#v", path, body.Image)
+ }
+}
+
func TestNormalizePixelSizeConvertsCanvasAspectRatios(t *testing.T) {
tests := map[string]string{
"1:1": "1024x1024",
@@ -974,6 +1034,9 @@ func TestValidateGenerationInterfaceRejectsMismatchedType(t *testing.T) {
if err := validateGenerationInterface("video", "xai-video"); err != nil {
t.Fatalf("validateGenerationInterface() error = %v", err)
}
+ if err := validateGenerationInterface("image", "grok-image"); err != nil {
+ t.Fatalf("validateGenerationInterface() error = %v", err)
+ }
}
func TestProcessTaskValidatesInterfaceBeforeHydratingMedia(t *testing.T) {
diff --git a/web/src/components/model-protocol-picker.tsx b/web/src/components/model-protocol-picker.tsx
index 3a0da01ba..c6f9dea28 100644
--- a/web/src/components/model-protocol-picker.tsx
+++ b/web/src/components/model-protocol-picker.tsx
@@ -146,7 +146,7 @@ function ProtocolBrandMark({ protocol, compact = false }: { protocol: ModelProto
if (protocol.value.startsWith("volcengine-jimeng-")) return ;
if (protocol.value.startsWith("volcengine-")) return ;
if (protocol.value.startsWith("newapi-channel-")) return ;
- 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 ;
}
diff --git a/web/src/lib/model-capabilities.ts b/web/src/lib/model-capabilities.ts
index ed739cf0e..090bb41b2 100644
--- a/web/src/lib/model-capabilities.ts
+++ b/web/src/lib/model-capabilities.ts
@@ -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 };
diff --git a/web/src/lib/model-protocols.ts b/web/src/lib/model-protocols.ts
index e2ccdb403..2db4d5c19 100644
--- a/web/src/lib/model-protocols.ts
+++ b/web/src/lib/model-protocols.ts
@@ -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: "文本转语音" },
diff --git a/web/src/pages/settings/channel-video-pricing.tsx b/web/src/pages/settings/channel-video-pricing.tsx
index 3a5811629..1fad37972 100644
--- a/web/src/pages/settings/channel-video-pricing.tsx
+++ b/web/src/pages/settings/channel-video-pricing.tsx
@@ -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";
}
diff --git a/web/src/services/api/image.ts b/web/src/services/api/image.ts
index 5efa24927..fe08b5fbd 100644
--- a/web/src/services/api/image.ts
+++ b/web/src/services/api/image.ts
@@ -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(
+ 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(config: ReturnType(
+ 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;
diff --git a/web/src/stores/use-config-store.ts b/web/src/stores/use-config-store.ts
index 4e85da2c3..c2d166ec1 100644
--- a/web/src/stores/use-config-store.ts
+++ b/web/src/stores/use-config-store.ts
@@ -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;
}