diff --git a/monkeyai/admin/src/components/image-capability-selector.tsx b/monkeyai/admin/src/components/image-capability-selector.tsx
new file mode 100644
index 000000000..d8c89563e
--- /dev/null
+++ b/monkeyai/admin/src/components/image-capability-selector.tsx
@@ -0,0 +1,151 @@
+import { Badge } from "@/components/ui/badge"
+import { Button } from "@/components/ui/button"
+import { Field, FieldLabel } from "@/components/ui/field"
+import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
+
+function AspectRatioGlyph({ value }: { value: string }) {
+ const [rawWidth, rawHeight] = value.split(":").map(Number)
+ const width = Number.isFinite(rawWidth) && rawWidth > 0 ? rawWidth : 1
+ const height = Number.isFinite(rawHeight) && rawHeight > 0 ? rawHeight : 1
+ const scale = Math.min(28 / width, 16 / height)
+
+ return (
+
+
+
+ )
+}
+
+function SelectionHeader({
+ label,
+ selected,
+ total,
+ selectAllLabel,
+ onSelectAll,
+}: {
+ label: string
+ selected: number
+ total: number
+ selectAllLabel: string
+ onSelectAll: () => void
+}) {
+ return (
+
+
+ {label}
+
+ {selected}/{total}
+
+
+
+
+ )
+}
+
+type ImageCapabilitySelectorProps = {
+ qualities: string[]
+ aspectRatios: string[]
+ selectedQualities: string[]
+ selectedAspectRatios: string[]
+ qualityLabel: string
+ aspectRatioLabel: string
+ selectAllLabel: string
+ onQualitiesChange: (values: string[]) => void
+ onAspectRatiosChange: (values: string[]) => void
+}
+
+export function ImageCapabilitySelector({
+ qualities,
+ aspectRatios,
+ selectedQualities,
+ selectedAspectRatios,
+ qualityLabel,
+ aspectRatioLabel,
+ selectAllLabel,
+ onQualitiesChange,
+ onAspectRatiosChange,
+}: ImageCapabilitySelectorProps) {
+ return (
+ <>
+
+ onQualitiesChange([...qualities])}
+ />
+
+ {qualities.map((value) => (
+
+
+ {value}
+
+
+ ))}
+
+
+
+
+ onAspectRatiosChange([...aspectRatios])}
+ />
+
+ {aspectRatios.map((value) => (
+
+
+
+ {value}
+
+
+ ))}
+
+
+ >
+ )
+}
diff --git a/monkeyai/admin/src/i18n/locales/ar.ts b/monkeyai/admin/src/i18n/locales/ar.ts
index 0e294fc71..e31dda282 100644
--- a/monkeyai/admin/src/i18n/locales/ar.ts
+++ b/monkeyai/admin/src/i18n/locales/ar.ts
@@ -263,8 +263,8 @@ export const ar = {
textKind: "نموذج لغوي كبير",
imageKind: "توليد الصور",
imageProvider: "مزود الصور",
- imageCapabilitiesUnavailable:
- "أدخل معرّف نموذج مدعوم لتحميل خيارات الصور.",
+ imageCapabilitiesUnavailable: "تعذر تحميل خيارات الصور لهذا المزوّد.",
+ selectAllSupported: "تحديد كل الخيارات المدعومة",
imageQuality: "دقة الصورة",
aspectRatio: "نسبة الأبعاد",
defaultQuality: "الدقة الافتراضية",
diff --git a/monkeyai/admin/src/i18n/locales/de-DE.ts b/monkeyai/admin/src/i18n/locales/de-DE.ts
index a58cb5979..b71838a93 100644
--- a/monkeyai/admin/src/i18n/locales/de-DE.ts
+++ b/monkeyai/admin/src/i18n/locales/de-DE.ts
@@ -273,7 +273,8 @@ export const deDE = {
imageKind: "Bildgenerierung",
imageProvider: "Bildanbieter",
imageCapabilitiesUnavailable:
- "Unterstützte Modell-ID eingeben, um Bildoptionen zu laden.",
+ "Bildoptionen für diesen Anbieter konnten nicht geladen werden.",
+ selectAllSupported: "Alle unterstützten auswählen",
imageQuality: "Auflösungsstufe",
aspectRatio: "Seitenverhältnis",
defaultQuality: "Standardstufe",
diff --git a/monkeyai/admin/src/i18n/locales/en-US.ts b/monkeyai/admin/src/i18n/locales/en-US.ts
index e5814fe99..7f76199ae 100644
--- a/monkeyai/admin/src/i18n/locales/en-US.ts
+++ b/monkeyai/admin/src/i18n/locales/en-US.ts
@@ -409,7 +409,8 @@ export const enUS = {
mask: "Mask",
taskStatus: "Task status",
imageCapabilitiesUnavailable:
- "Enter a supported upstream model ID to load available image options.",
+ "Unable to load image options for this provider.",
+ selectAllSupported: "Select all supported",
imageQuality: "Resolution tier",
aspectRatio: "Aspect ratio",
defaultQuality: "Default tier",
diff --git a/monkeyai/admin/src/i18n/locales/es-419.ts b/monkeyai/admin/src/i18n/locales/es-419.ts
index 73a6e6a62..3d82a1a86 100644
--- a/monkeyai/admin/src/i18n/locales/es-419.ts
+++ b/monkeyai/admin/src/i18n/locales/es-419.ts
@@ -272,7 +272,8 @@ export const es419 = {
imageKind: "Generación de imágenes",
imageProvider: "Proveedor de imágenes",
imageCapabilitiesUnavailable:
- "Ingresa un ID de modelo compatible para cargar las opciones.",
+ "No se pudieron cargar las opciones de este proveedor.",
+ selectAllSupported: "Seleccionar compatibles",
imageQuality: "Nivel de resolución",
aspectRatio: "Relación de aspecto",
defaultQuality: "Nivel predeterminado",
diff --git a/monkeyai/admin/src/i18n/locales/fr-FR.ts b/monkeyai/admin/src/i18n/locales/fr-FR.ts
index b41105500..a843a9084 100644
--- a/monkeyai/admin/src/i18n/locales/fr-FR.ts
+++ b/monkeyai/admin/src/i18n/locales/fr-FR.ts
@@ -273,7 +273,8 @@ export const frFR = {
imageKind: "Génération d’images",
imageProvider: "Fournisseur d’images",
imageCapabilitiesUnavailable:
- "Saisissez un ID de modèle pris en charge pour charger les options.",
+ "Impossible de charger les options de ce fournisseur.",
+ selectAllSupported: "Tout sélectionner",
imageQuality: "Niveau de résolution",
aspectRatio: "Format d’image",
defaultQuality: "Niveau par défaut",
diff --git a/monkeyai/admin/src/i18n/locales/ja-JP.ts b/monkeyai/admin/src/i18n/locales/ja-JP.ts
index 785d3e66c..59db75b93 100644
--- a/monkeyai/admin/src/i18n/locales/ja-JP.ts
+++ b/monkeyai/admin/src/i18n/locales/ja-JP.ts
@@ -267,7 +267,8 @@ export const jaJP = {
imageKind: "画像生成",
imageProvider: "画像プロバイダー",
imageCapabilitiesUnavailable:
- "対応するモデル ID を入力すると画像設定を読み込みます。",
+ "このプロバイダーの画像設定を読み込めませんでした。",
+ selectAllSupported: "対応項目をすべて選択",
imageQuality: "解像度ランク",
aspectRatio: "アスペクト比",
defaultQuality: "既定のランク",
diff --git a/monkeyai/admin/src/i18n/locales/ko-KR.ts b/monkeyai/admin/src/i18n/locales/ko-KR.ts
index 6614a2190..a9234653e 100644
--- a/monkeyai/admin/src/i18n/locales/ko-KR.ts
+++ b/monkeyai/admin/src/i18n/locales/ko-KR.ts
@@ -264,7 +264,8 @@ export const koKR = {
imageKind: "이미지 생성",
imageProvider: "이미지 공급자",
imageCapabilitiesUnavailable:
- "지원되는 모델 ID를 입력하면 이미지 옵션이 표시됩니다.",
+ "이 공급자의 이미지 옵션을 불러올 수 없습니다.",
+ selectAllSupported: "지원 항목 모두 선택",
imageQuality: "해상도 등급",
aspectRatio: "화면 비율",
defaultQuality: "기본 등급",
diff --git a/monkeyai/admin/src/i18n/locales/ru-RU.ts b/monkeyai/admin/src/i18n/locales/ru-RU.ts
index ce5cceea2..ad7f3f2b7 100644
--- a/monkeyai/admin/src/i18n/locales/ru-RU.ts
+++ b/monkeyai/admin/src/i18n/locales/ru-RU.ts
@@ -271,7 +271,8 @@ export const ruRU = {
imageKind: "Генерация изображений",
imageProvider: "Поставщик изображений",
imageCapabilitiesUnavailable:
- "Введите поддерживаемый ID модели, чтобы загрузить параметры.",
+ "Не удалось загрузить параметры этого поставщика.",
+ selectAllSupported: "Выбрать все поддерживаемые",
imageQuality: "Уровень разрешения",
aspectRatio: "Соотношение сторон",
defaultQuality: "Уровень по умолчанию",
diff --git a/monkeyai/admin/src/i18n/locales/zh-CN.ts b/monkeyai/admin/src/i18n/locales/zh-CN.ts
index 6afcae10a..89360e63e 100644
--- a/monkeyai/admin/src/i18n/locales/zh-CN.ts
+++ b/monkeyai/admin/src/i18n/locales/zh-CN.ts
@@ -392,8 +392,8 @@ export const zhCN = {
referenceImages: "参考图/原图",
mask: "蒙版",
taskStatus: "任务状态",
- imageCapabilitiesUnavailable:
- "请输入受支持的上游模型 ID,以加载画质和比例选项。",
+ imageCapabilitiesUnavailable: "无法加载该供应商的画质和比例选项。",
+ selectAllSupported: "全选支持项",
imageQuality: "画质档位",
aspectRatio: "比例",
defaultQuality: "默认画质",
diff --git a/monkeyai/admin/src/i18n/locales/zh-TW.ts b/monkeyai/admin/src/i18n/locales/zh-TW.ts
index cbdb22f21..86cff9307 100644
--- a/monkeyai/admin/src/i18n/locales/zh-TW.ts
+++ b/monkeyai/admin/src/i18n/locales/zh-TW.ts
@@ -255,8 +255,8 @@ export const zhTW = {
textKind: "大型語言模型",
imageKind: "生圖模型",
imageProvider: "生圖供應商",
- imageCapabilitiesUnavailable:
- "請輸入支援的上游模型 ID,以載入畫質與比例選項。",
+ imageCapabilitiesUnavailable: "無法載入此供應商的畫質與比例選項。",
+ selectAllSupported: "全選支援項目",
imageQuality: "畫質檔位",
aspectRatio: "比例",
defaultQuality: "預設畫質",
diff --git a/monkeyai/admin/src/pages/models-page.tsx b/monkeyai/admin/src/pages/models-page.tsx
index 1f7b67491..34a99343d 100644
--- a/monkeyai/admin/src/pages/models-page.tsx
+++ b/monkeyai/admin/src/pages/models-page.tsx
@@ -14,6 +14,7 @@ import { useTranslation } from "react-i18next"
import { useAppToast } from "@/components/animated-toast-provider"
import { AuthorizationSelect } from "@/components/authorization-select"
+import { ImageCapabilitySelector } from "@/components/image-capability-selector"
import { ImageGenerationTest } from "@/components/image-generation-test"
import { SkillTagSelect } from "@/components/skill-tag-select"
import { ResourceTagSummary } from "@/components/resource-tag-summary"
@@ -65,6 +66,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
+import { Separator } from "@/components/ui/separator"
import { Switch } from "@/components/ui/switch"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Iconfont } from "@/components/iconfont"
@@ -251,7 +253,6 @@ export function ModelsPage() {
)
const [kind, setKind] = useState("text")
const [provider, setProvider] = useState("openai_images")
- const [imageModelId, setImageModelId] = useState("")
const [imageCapabilities, setImageCapabilities] =
useState(null)
const [qualities, setQualities] = useState([])
@@ -311,20 +312,43 @@ export function ModelsPage() {
}, [loadRevision, showToast, t])
useEffect(() => {
- if (!dialogOpen || kind !== "image" || !imageModelId.trim()) return
+ if (!dialogOpen || kind !== "image") return
let active = true
const timer = window.setTimeout(() => {
- api(
- `/api/admin/v1/models/image-capabilities?provider=${encodeURIComponent(provider)}&model_id=${encodeURIComponent(imageModelId.trim())}`
- )
+ const providerPath = `/api/admin/v1/models/image-capabilities?provider=${encodeURIComponent(provider)}`
+ api(providerPath)
.then((capability) => {
if (!active) return
+
+ const preserveSavedSelection =
+ editingModel?.kind === "image" && editingModel.provider === provider
+ const savedConfig = preserveSavedSelection
+ ? editingModel.imageConfig
+ : undefined
+ const nextQualities = savedConfig
+ ? capability.qualities.filter((value) =>
+ savedConfig.qualities.includes(value)
+ )
+ : [...capability.qualities]
+ const nextAspectRatios = savedConfig
+ ? capability.aspect_ratios.filter((value) =>
+ savedConfig.aspect_ratios.includes(value)
+ )
+ : [...capability.aspect_ratios]
+
setImageCapabilities(capability)
- setQualities((current) =>
- current.filter((value) => capability.qualities.includes(value))
+ setQualities(nextQualities)
+ setAspectRatios(nextAspectRatios)
+ setDefaultQuality(
+ savedConfig && nextQualities.includes(savedConfig.default_quality)
+ ? savedConfig.default_quality
+ : (nextQualities[0] ?? "")
)
- setAspectRatios((current) =>
- current.filter((value) => capability.aspect_ratios.includes(value))
+ setDefaultAspectRatio(
+ savedConfig &&
+ nextAspectRatios.includes(savedConfig.default_aspect_ratio)
+ ? savedConfig.default_aspect_ratio
+ : (nextAspectRatios[0] ?? "")
)
})
.catch(() => {
@@ -335,13 +359,34 @@ export function ModelsPage() {
active = false
window.clearTimeout(timer)
}
- }, [dialogOpen, kind, provider, imageModelId])
+ }, [dialogOpen, editingModel, kind, provider])
+
+ const handleQualitySelectionChange = (values: string[]) => {
+ if (!imageCapabilities) return
+ const next = imageCapabilities.qualities.filter((value) =>
+ values.includes(value)
+ )
+ if (next.length === 0) return
+ setQualities(next)
+ setDefaultQuality((current) => (next.includes(current) ? current : next[0]))
+ }
+
+ const handleAspectRatioSelectionChange = (values: string[]) => {
+ if (!imageCapabilities) return
+ const next = imageCapabilities.aspect_ratios.filter((value) =>
+ values.includes(value)
+ )
+ if (next.length === 0) return
+ setAspectRatios(next)
+ setDefaultAspectRatio((current) =>
+ next.includes(current) ? current : next[0]
+ )
+ }
const resetModelOptions = () => {
setProtocol("openai_chat_completions")
setKind("text")
setProvider("openai_images")
- setImageModelId("")
setImageCapabilities(null)
setQualities([])
setAspectRatios([])
@@ -375,7 +420,6 @@ export function ModelsPage() {
setProvider(
model.provider === "passthrough" ? "openai_images" : model.provider
)
- setImageModelId(model.modelId)
setQualities(model.imageConfig?.qualities ?? [])
setAspectRatios(model.imageConfig?.aspect_ratios ?? [])
setDefaultQuality(model.imageConfig?.default_quality ?? "")
@@ -656,7 +700,7 @@ export function ModelsPage() {
-
+
@@ -666,10 +710,6 @@ export function ModelsPage() {
id="model-id"
name="modelId"
defaultValue={editingModel?.modelId}
- onChange={(event) => {
- setImageModelId(event.target.value)
- setImageCapabilities(null)
- }}
placeholder={t("pages.models.modelIdPlaceholder")}
required
/>
@@ -845,223 +885,171 @@ export function ModelsPage() {
)}
{kind === "image" && (
-
- {!imageCapabilities && (
-
- {t("pages.models.imageCapabilitiesUnavailable")}
-
- )}
- {imageCapabilities && (
- <>
-
-
- {t("pages.models.imageQuality")}
-
-
- {imageCapabilities.qualities.map((value) => (
-
- ))}
-
-
-
-
- {t("pages.models.aspectRatio")}
-
-
- {imageCapabilities.aspect_ratios.map(
- (value) => (
-
- )
- )}
-
-
-
-
-
- {t("pages.models.defaultQuality")}
-
-
-
-
-
- {t("pages.models.defaultAspectRatio")}
-
-
-
-
-
-
- {t("pages.models.baseCreditsPerImage")}
-
-
- setBaseCredits(event.target.value)
+ <>
+
+
+ {!imageCapabilities && (
+
+ {t("pages.models.imageCapabilitiesUnavailable")}
+
+ )}
+ {imageCapabilities && (
+ <>
+
-
-
- {qualities.map((value) => (
-
-
+
+
+ {t("pages.models.defaultQuality")}
+
+
+
+
+
+ {t("pages.models.defaultAspectRatio")}
+
+
+
+
+
+
+
+
+ {t("pages.models.baseCreditsPerImage")}
- setQualityMultipliers((current) => ({
- ...current,
- [value]: event.target.value,
- }))
+ setBaseCredits(event.target.value)
}
inputMode="decimal"
+ required
/>
- ))}
-
- >
- )}
-
+
+ {qualities.map((value) => (
+
+
+ {value}{" "}
+ {t("pages.models.qualityMultiplier")}
+
+
+ setQualityMultipliers((current) => ({
+ ...current,
+ [value]: event.target.value,
+ }))
+ }
+ inputMode="decimal"
+ />
+
+ ))}
+
+
+ >
+ )}
+
+ >
)}
-
-
- {t("pages.skills.tags")}
-
-
-
+
+
+
+
+ {t("pages.skills.tags")}
+
+
+
-
-
- {t("pages.models.authorizedScope")}
-
-
-
+
+
+ {t("pages.models.authorizedScope")}
+
+
+
+
{
assert.equal(zhCN.pages.models.textKind, "大语言模型")
})
+test("image capabilities use card multi-selects and supported defaults", async () => {
+ const page = await readFile(
+ new URL("../src/pages/models-page.tsx", import.meta.url),
+ "utf8"
+ )
+ const selector = await readFile(
+ new URL("../src/components/image-capability-selector.tsx", import.meta.url),
+ "utf8"
+ )
+
+ assert.match(page, /preserveSavedSelection/)
+ assert.match(page, /api\(providerPath\)/)
+ assert.doesNotMatch(page, /imageModelId|modelSpecific/)
+ assert.match(page, /: \[\.\.\.capability\.qualities\]/)
+ assert.match(page, /: \[\.\.\.capability\.aspect_ratios\]/)
+ assert.match(page, /if \(next\.length === 0\) return/)
+ assert.match(page, / {
const source = await readFile(
new URL("../src/components/image-generation-test.tsx", import.meta.url),
diff --git a/monkeyai/backend/api/admin.yaml b/monkeyai/backend/api/admin.yaml
index dbd51b9f8..4cb8533ed 100644
--- a/monkeyai/backend/api/admin.yaml
+++ b/monkeyai/backend/api/admin.yaml
@@ -1915,7 +1915,6 @@ paths:
summary: 查询生图模型支持的画质、比例和操作
parameters:
- {name: provider, in: query, required: true, schema: {$ref: '#/components/schemas/ModelProvider'}}
- - {name: model_id, in: query, required: true, schema: {type: string}}
responses:
'200':
description: 供应商代码声明的原始能力,供管理员选择开放画质与比例。
diff --git a/monkeyai/backend/internal/app/app.go b/monkeyai/backend/internal/app/app.go
index 36d14e5f3..97740e5b8 100644
--- a/monkeyai/backend/internal/app/app.go
+++ b/monkeyai/backend/internal/app/app.go
@@ -136,10 +136,10 @@ func newApplicationHandler(ctx context.Context, logger *slog.Logger, pool *pgxpo
gptResponses := openairesponses.New(upstreamClient)
seedream := volcengine.New(upstreamClient)
grok := xai.New(upstreamClient)
- imageService.WithAdapter(model.ProviderOpenAIImages, imagegen.Adapter{Capabilities: gptImages.Capabilities, Generator: gptImages, Editor: gptImages})
- imageService.WithAdapter(model.ProviderOpenAIResponses, imagegen.Adapter{Capabilities: gptResponses.Capabilities, Generator: gptResponses, Editor: gptResponses})
- imageService.WithAdapter(model.ProviderVolcengine, imagegen.Adapter{Capabilities: seedream.Capabilities, Generator: seedream, Editor: seedream})
- imageService.WithAdapter(model.ProviderXAI, imagegen.Adapter{Capabilities: grok.Capabilities, Generator: grok})
+ imageService.WithAdapter(model.ProviderOpenAIImages, imagegen.Adapter{Capabilities: gptImages.Capabilities, DefaultCapabilities: gptImages.DefaultCapabilities, Generator: gptImages, Editor: gptImages})
+ imageService.WithAdapter(model.ProviderOpenAIResponses, imagegen.Adapter{Capabilities: gptResponses.Capabilities, DefaultCapabilities: gptResponses.DefaultCapabilities, Generator: gptResponses, Editor: gptResponses})
+ imageService.WithAdapter(model.ProviderVolcengine, imagegen.Adapter{Capabilities: seedream.Capabilities, DefaultCapabilities: seedream.DefaultCapabilities, Generator: seedream, Editor: seedream})
+ imageService.WithAdapter(model.ProviderXAI, imagegen.Adapter{Capabilities: grok.Capabilities, DefaultCapabilities: grok.DefaultCapabilities, Generator: grok, Editor: grok})
models.WithImageCapabilities(imageService.Capabilities)
store := resource.NewStore(pool)
rules := rule.NewService(store)
diff --git a/monkeyai/backend/internal/imagegen/openai/images/images.go b/monkeyai/backend/internal/imagegen/openai/images/images.go
index 86facd43a..9f28634c8 100644
--- a/monkeyai/backend/internal/imagegen/openai/images/images.go
+++ b/monkeyai/backend/internal/imagegen/openai/images/images.go
@@ -42,45 +42,39 @@ func (p *Provider) Capabilities(model string) (imagegen.Capabilities, error) {
return capabilities(model)
}
-func capabilities(model string) (imagegen.Capabilities, error) {
- cap := imagegen.Capabilities{Operations: []string{"generate", "edit"},
- MaxCount: 4, MaxReferences: 4, SupportsMask: true}
- switch model {
- case "gpt-image-2.5-sunburst", "gpt-image-2.5-flare":
- cap.Qualities = []string{"1K", "2K", "4K"}
- cap.AspectRatios = ratios
- case "gpt-image-1":
- cap.Qualities = []string{"1K"}
- cap.AspectRatios = []string{"1:1", "3:2", "2:3"}
- default:
- return imagegen.Capabilities{}, errors.New("不支持此 GPT Image 模型版本")
- }
- return cap, nil
+func (p *Provider) DefaultCapabilities() imagegen.Capabilities {
+ return defaultCapabilities()
+}
+
+func defaultCapabilities() imagegen.Capabilities {
+ return imagegen.Capabilities{
+ Operations: []string{"generate", "edit"}, Qualities: []string{"1K", "2K", "4K"},
+ AspectRatios: ratios, MaxCount: 4, MaxReferences: 4, SupportsMask: true,
+ }
+}
+
+func capabilities(string) (imagegen.Capabilities, error) {
+ return defaultCapabilities(), nil
}
func Size(model, quality, aspect string) (string, error) {
- cap, err := capabilities(model)
- if err != nil {
- return "", err
- }
- for _, q := range cap.Qualities {
- if q == quality {
- for _, a := range cap.AspectRatios {
- if a == aspect {
- if model == "gpt-image-1" {
- switch aspect {
- case "1:1":
- return "1024x1024", nil
- case "3:2":
- return "1536x1024", nil
- case "2:3":
- return "1024x1536", nil
- }
- }
- return dimensions[quality][aspect], nil
- }
+ if model == "gpt-image-1" {
+ if quality == "1K" {
+ switch aspect {
+ case "1:1":
+ return "1024x1024", nil
+ case "3:2":
+ return "1536x1024", nil
+ case "2:3":
+ return "1024x1536", nil
}
}
+ return "", errors.New("不支持此 GPT Image 画质比例组合")
+ }
+ if sizes, ok := dimensions[quality]; ok {
+ if size, ok := sizes[aspect]; ok {
+ return size, nil
+ }
}
return "", errors.New("不支持此 GPT Image 画质比例组合")
}
diff --git a/monkeyai/backend/internal/imagegen/openai/images/images_test.go b/monkeyai/backend/internal/imagegen/openai/images/images_test.go
index d7602e0f7..203c3887a 100644
--- a/monkeyai/backend/internal/imagegen/openai/images/images_test.go
+++ b/monkeyai/backend/internal/imagegen/openai/images/images_test.go
@@ -51,6 +51,9 @@ func TestGenerateMapsTierAndAspect(t *testing.T) {
if err != nil || cap.MaxCount != 4 || len(cap.Qualities) != 3 || cap.Qualities[2] != "4K" || len(cap.AspectRatios) != 8 {
t.Fatalf("GPT Image 能力错误: %+v, %v", cap, err)
}
+ if defaults := p.DefaultCapabilities(); len(defaults.Qualities) != 3 || len(defaults.AspectRatios) != 8 {
+ t.Fatalf("GPT Image 默认能力错误: %+v", defaults)
+ }
result, err := p.Generate(context.Background(), proxy.Target{BaseURL: server.URL + "/v1", APIKey: "upstream-key", UpstreamModel: "gpt-image-2.5-flare"},
imagegen.ProviderRequest{Prompt: "猫", Quality: "4K", AspectRatio: "9:16", Count: 2})
if err != nil || result.Status != "succeeded" || len(result.Images) != 1 || !bytes.Equal(result.Images[0].Data, content) {
@@ -82,10 +85,16 @@ func TestEditUsesMultipart(t *testing.T) {
}
}
-func TestUnsupportedModelAndRejectedResponse(t *testing.T) {
+func TestCustomModelAndRejectedResponse(t *testing.T) {
p := New(&http.Client{})
- if _, err := p.Capabilities("gpt-text"); err == nil {
- t.Fatal("不能猜测未知模型的生图能力")
+ if cap, err := p.Capabilities("custom-image-alias"); err != nil || len(cap.Qualities) != 3 || len(cap.AspectRatios) != 8 {
+ t.Fatalf("自定义模型应使用供应商默认能力: %+v, %v", cap, err)
+ }
+ if cap, err := p.Capabilities("gpt-image-1"); err != nil || len(cap.Qualities) != 3 || len(cap.AspectRatios) != 8 {
+ t.Fatalf("模型 ID 不应收窄管理能力: %+v, %v", cap, err)
+ }
+ if _, err := Size("gpt-image-1", "2K", "1:1"); err == nil {
+ t.Fatal("调用上游时应保留 GPT Image 1 尺寸限制")
}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusBadRequest) }))
defer server.Close()
diff --git a/monkeyai/backend/internal/imagegen/openai/responses/responses.go b/monkeyai/backend/internal/imagegen/openai/responses/responses.go
index bddab70bf..f38a5e2b0 100644
--- a/monkeyai/backend/internal/imagegen/openai/responses/responses.go
+++ b/monkeyai/backend/internal/imagegen/openai/responses/responses.go
@@ -16,19 +16,18 @@ type Provider struct{ client *http.Client }
func New(client *http.Client) *Provider { return &Provider{client: client} }
-func (p *Provider) Capabilities(model string) (imagegen.Capabilities, error) {
- switch model {
- case "gpt-5", "gpt-5.1", "gpt-5.2", "gpt-6-astra":
- return imagegen.Capabilities{
- Operations: []string{"generate", "edit"}, Qualities: []string{"1K", "2K", "4K"},
- AspectRatios: []string{"1:1", "4:3", "3:4", "16:9", "9:16", "3:2", "2:3", "21:9"},
- MaxCount: 1, MaxReferences: 4, SupportsReference: true,
- }, nil
- default:
- return imagegen.Capabilities{}, errors.New("不支持此 Responses 生图模型版本")
+func (p *Provider) DefaultCapabilities() imagegen.Capabilities {
+ return imagegen.Capabilities{
+ Operations: []string{"generate", "edit"}, Qualities: []string{"1K", "2K", "4K"},
+ AspectRatios: []string{"1:1", "4:3", "3:4", "16:9", "9:16", "3:2", "2:3", "21:9"},
+ MaxCount: 1, MaxReferences: 4, SupportsReference: true,
}
}
+func (p *Provider) Capabilities(string) (imagegen.Capabilities, error) {
+ return p.DefaultCapabilities(), nil
+}
+
func (p *Provider) Generate(ctx context.Context, target proxy.Target, req imagegen.ProviderRequest) (imagegen.ProviderResult, error) {
return p.call(ctx, target, req, "generate")
}
diff --git a/monkeyai/backend/internal/imagegen/openai/responses/responses_test.go b/monkeyai/backend/internal/imagegen/openai/responses/responses_test.go
index eb5b59b65..774036d4b 100644
--- a/monkeyai/backend/internal/imagegen/openai/responses/responses_test.go
+++ b/monkeyai/backend/internal/imagegen/openai/responses/responses_test.go
@@ -48,6 +48,12 @@ func TestResponsesImageTool(t *testing.T) {
if err != nil || len(cap.Qualities) != 3 || cap.Qualities[2] != "4K" {
t.Fatalf("Responses 生图画质档位错误: %+v, %v", cap, err)
}
+ if defaults := p.DefaultCapabilities(); len(defaults.Qualities) != 3 || len(defaults.AspectRatios) != 8 {
+ t.Fatalf("Responses 默认能力错误: %+v", defaults)
+ }
+ if custom, err := p.Capabilities("gpt-5.6-sol"); err != nil || len(custom.Qualities) != 3 {
+ t.Fatalf("自定义模型应使用 Responses 默认能力: %+v, %v", custom, err)
+ }
result, err := p.Generate(context.Background(), proxy.Target{BaseURL: server.URL + "/v1", APIKey: "upstream-key", UpstreamModel: "gpt-5"},
imagegen.ProviderRequest{Prompt: "猫", Quality: "4K", AspectRatio: "16:9", Count: 1})
if err != nil || result.Status != "succeeded" || result.RequestID != "response-1" || len(result.Images) != 1 {
diff --git a/monkeyai/backend/internal/imagegen/service.go b/monkeyai/backend/internal/imagegen/service.go
index 0c21d69cc..168d1f0b1 100644
--- a/monkeyai/backend/internal/imagegen/service.go
+++ b/monkeyai/backend/internal/imagegen/service.go
@@ -7,6 +7,7 @@ import (
"encoding/json"
"log/slog"
"slices"
+ "strings"
"sync"
"time"
@@ -58,10 +59,11 @@ type TaskQuerier interface {
}
type Adapter struct {
- Capabilities func(string) (Capabilities, error)
- Generator Generator
- Editor Editor
- TaskQuerier TaskQuerier
+ Capabilities func(string) (Capabilities, error)
+ DefaultCapabilities func() Capabilities
+ Generator Generator
+ Editor Editor
+ TaskQuerier TaskQuerier
}
type ModelReader interface {
@@ -124,9 +126,18 @@ func (s *Service) Capabilities(provider model.Provider, modelID string) (model.I
if !ok || adapter.Capabilities == nil {
return model.ImageCapabilities{}, resource.Invalid("生图供应商未配置")
}
- cap, err := adapter.Capabilities(modelID)
- if err != nil {
- return model.ImageCapabilities{}, err
+ var cap Capabilities
+ if strings.TrimSpace(modelID) == "" {
+ if adapter.DefaultCapabilities == nil {
+ return model.ImageCapabilities{}, resource.Invalid("生图供应商默认能力未配置")
+ }
+ cap = adapter.DefaultCapabilities()
+ } else {
+ var err error
+ cap, err = adapter.Capabilities(strings.TrimSpace(modelID))
+ if err != nil {
+ return model.ImageCapabilities{}, err
+ }
}
operations := make([]string, 0, len(cap.Operations))
for _, operation := range cap.Operations {
diff --git a/monkeyai/backend/internal/imagegen/service_test.go b/monkeyai/backend/internal/imagegen/service_test.go
index 8aec9c09c..f1e212399 100644
--- a/monkeyai/backend/internal/imagegen/service_test.go
+++ b/monkeyai/backend/internal/imagegen/service_test.go
@@ -157,6 +157,23 @@ func testImageService(call generatorFunc) (*Service, *testJobs, *testBilling) {
return svc, jobs, bills
}
+func TestCapabilitiesUsesProviderDefaultWithoutModel(t *testing.T) {
+ modelSpecificCalled := false
+ svc := NewService(nil, nil, nil, nil, nil).WithAdapter(model.ProviderXAI, Adapter{
+ Capabilities: func(string) (Capabilities, error) {
+ modelSpecificCalled = true
+ return Capabilities{}, nil
+ },
+ DefaultCapabilities: func() Capabilities {
+ return Capabilities{Qualities: []string{"1K", "2K", "4K"}, AspectRatios: []string{"1:1", "16:9"}}
+ },
+ })
+ cap, err := svc.Capabilities(model.ProviderXAI, " ")
+ if err != nil || modelSpecificCalled || len(cap.Qualities) != 3 || len(cap.AspectRatios) != 2 {
+ t.Fatalf("供应商默认能力错误: %+v, %v", cap, err)
+ }
+}
+
func TestGenerateReservesAndSettlesActualImages(t *testing.T) {
var imageBytes bytes.Buffer
if err := png.Encode(&imageBytes, image.NewRGBA(image.Rect(0, 0, 2, 2))); err != nil {
diff --git a/monkeyai/backend/internal/imagegen/volcengine/volcengine.go b/monkeyai/backend/internal/imagegen/volcengine/volcengine.go
index 361d0e7c8..5adf9aa2e 100644
--- a/monkeyai/backend/internal/imagegen/volcengine/volcengine.go
+++ b/monkeyai/backend/internal/imagegen/volcengine/volcengine.go
@@ -21,15 +21,16 @@ var sizes = map[string]map[string]string{
"4K": {"1:1": "4096x4096", "4:3": "4608x3456", "3:4": "3456x4608", "16:9": "5120x2880", "9:16": "2880x5120", "3:2": "4992x3328", "2:3": "3328x4992", "21:9": "5376x2304"},
}
-func (p *Provider) Capabilities(model string) (imagegen.Capabilities, error) {
- if model != "doubao-seedream-5-0-pro-260628" {
- return imagegen.Capabilities{}, errors.New("不支持此 Seedream 生图模型版本")
- }
+func (p *Provider) DefaultCapabilities() imagegen.Capabilities {
return imagegen.Capabilities{
Operations: []string{"generate", "edit"}, Qualities: []string{"1K", "2K", "4K"},
AspectRatios: []string{"1:1", "4:3", "3:4", "16:9", "9:16", "3:2", "2:3", "21:9"},
MaxCount: 1, MaxReferences: 4, SupportsReference: true,
- }, nil
+ }
+}
+
+func (p *Provider) Capabilities(string) (imagegen.Capabilities, error) {
+ return p.DefaultCapabilities(), nil
}
func (p *Provider) Generate(ctx context.Context, target proxy.Target, req imagegen.ProviderRequest) (imagegen.ProviderResult, error) {
diff --git a/monkeyai/backend/internal/imagegen/volcengine/volcengine_test.go b/monkeyai/backend/internal/imagegen/volcengine/volcengine_test.go
index 6f1a91b07..de89d4361 100644
--- a/monkeyai/backend/internal/imagegen/volcengine/volcengine_test.go
+++ b/monkeyai/backend/internal/imagegen/volcengine/volcengine_test.go
@@ -46,13 +46,16 @@ func TestSeedreamReferenceEdit(t *testing.T) {
if err != nil || cap.MaxCount != 1 || len(cap.Qualities) != 3 || cap.Qualities[2] != "4K" || !cap.SupportsReference {
t.Fatalf("Seedream 5.0 pro 能力错误: %+v, %v", cap, err)
}
+ if defaults := p.DefaultCapabilities(); len(defaults.Qualities) != 3 || len(defaults.AspectRatios) != 8 {
+ t.Fatalf("Seedream 默认能力错误: %+v", defaults)
+ }
result, err := p.Edit(context.Background(), proxy.Target{BaseURL: server.URL + "/api/v3", UpstreamModel: "doubao-seedream-5-0-pro-260628", APIKey: "upstream-key"},
imagegen.ProviderRequest{Prompt: "改成日落", Quality: "4K", AspectRatio: "9:16", Count: 1,
Images: []imagegen.Input{{Data: imageBytes.Bytes(), MIMEType: "image/png"}}})
if err != nil || result.Status != "succeeded" || len(result.Images) != 1 {
t.Fatalf("Seedream 结果解析失败: %+v, %v", result, err)
}
- if _, err := p.Capabilities("doubao-seedream-unknown"); err == nil {
- t.Fatal("未知模型不应被当成 Seedream 5.0 pro")
+ if custom, err := p.Capabilities("custom-seedream-alias"); err != nil || len(custom.Qualities) != 3 {
+ t.Fatalf("自定义模型应使用 Seedream 默认能力: %+v, %v", custom, err)
}
}
diff --git a/monkeyai/backend/internal/imagegen/xai/xai.go b/monkeyai/backend/internal/imagegen/xai/xai.go
index ae67da738..fd93e1632 100644
--- a/monkeyai/backend/internal/imagegen/xai/xai.go
+++ b/monkeyai/backend/internal/imagegen/xai/xai.go
@@ -2,6 +2,7 @@ package xai
import (
"context"
+ "encoding/base64"
"encoding/json"
"errors"
"net/http"
@@ -18,12 +19,13 @@ func New(client *http.Client) *Provider { return &Provider{client: client} }
var ratios = []string{"1:1", "4:3", "3:4", "16:9", "9:16", "3:2", "2:3", "21:9"}
-func (p *Provider) Capabilities(model string) (imagegen.Capabilities, error) {
- if model != "grok-imagine-image-2.0" {
- return imagegen.Capabilities{}, errors.New("不支持此 Grok 生图模型版本")
- }
- return imagegen.Capabilities{Operations: []string{"generate"}, Qualities: []string{"1K", "2K", "4K"},
- AspectRatios: ratios, MaxCount: 4}, nil
+func (p *Provider) DefaultCapabilities() imagegen.Capabilities {
+ return imagegen.Capabilities{Operations: []string{"generate", "edit"}, Qualities: []string{"1K", "2K", "4K"},
+ AspectRatios: ratios, MaxCount: 4, MaxReferences: 5, SupportsReference: true}
+}
+
+func (p *Provider) Capabilities(string) (imagegen.Capabilities, error) {
+ return p.DefaultCapabilities(), nil
}
func (p *Provider) Generate(ctx context.Context, target proxy.Target, req imagegen.ProviderRequest) (imagegen.ProviderResult, error) {
@@ -31,11 +33,44 @@ func (p *Provider) Generate(ctx context.Context, target proxy.Target, req imageg
if err != nil || !slices.Contains(cap.Qualities, req.Quality) || !slices.Contains(cap.AspectRatios, req.AspectRatio) || len(req.Images) != 0 {
return imagegen.ProviderResult{Status: "failed", ErrorCode: "unsupported_image_parameter"}, nil
}
- content, status, err := imagegen.Call(ctx, p.client, target, "images/generations", map[string]any{
+ return p.call(ctx, target, "images/generations", map[string]any{
"model": target.UpstreamModel, "prompt": req.Prompt, "n": req.Count,
"aspect_ratio": req.AspectRatio, "resolution": strings.ToLower(req.Quality),
"quality": "medium", "response_format": "b64_json",
})
+}
+
+func (p *Provider) Edit(ctx context.Context, target proxy.Target, req imagegen.ProviderRequest) (imagegen.ProviderResult, error) {
+ cap, err := p.Capabilities(target.UpstreamModel)
+ if err != nil || !slices.Contains(cap.Qualities, req.Quality) || !slices.Contains(cap.AspectRatios, req.AspectRatio) ||
+ len(req.Images) == 0 || len(req.Images) > cap.MaxReferences || req.Mask != nil {
+ return imagegen.ProviderResult{Status: "failed", ErrorCode: "invalid_reference_image"}, nil
+ }
+ references := make([]map[string]string, 0, len(req.Images))
+ for _, input := range req.Images {
+ if len(input.Data) == 0 || !strings.HasPrefix(input.MIMEType, "image/") {
+ return imagegen.ProviderResult{Status: "failed", ErrorCode: "invalid_reference_image"}, nil
+ }
+ references = append(references, map[string]string{
+ "type": "image_url",
+ "url": "data:" + input.MIMEType + ";base64," + base64.StdEncoding.EncodeToString(input.Data),
+ })
+ }
+ body := map[string]any{
+ "model": target.UpstreamModel, "prompt": req.Prompt, "n": req.Count,
+ "aspect_ratio": req.AspectRatio, "resolution": strings.ToLower(req.Quality),
+ "quality": "medium", "response_format": "b64_json",
+ }
+ if len(references) == 1 {
+ body["image"] = references[0]
+ } else {
+ body["images"] = references
+ }
+ return p.call(ctx, target, "images/edits", body)
+}
+
+func (p *Provider) call(ctx context.Context, target proxy.Target, endpoint string, body map[string]any) (imagegen.ProviderResult, error) {
+ content, status, err := imagegen.Call(ctx, p.client, target, endpoint, body)
if err != nil {
return imagegen.ProviderResult{}, err
}
diff --git a/monkeyai/backend/internal/imagegen/xai/xai_test.go b/monkeyai/backend/internal/imagegen/xai/xai_test.go
index 02039c454..45ac2ab51 100644
--- a/monkeyai/backend/internal/imagegen/xai/xai_test.go
+++ b/monkeyai/backend/internal/imagegen/xai/xai_test.go
@@ -6,6 +6,7 @@ import (
"encoding/json"
"net/http"
"net/http/httptest"
+ "strings"
"testing"
"github.com/chaitin/MonkeyCode/monkeyai/backend/internal/imagegen"
@@ -34,15 +35,81 @@ func TestGrokImageGeneration(t *testing.T) {
defer server.Close()
p := New(server.Client())
cap, err := p.Capabilities("grok-imagine-image-2.0")
- if err != nil || len(cap.Operations) != 1 || len(cap.Qualities) != 3 || cap.Qualities[2] != "4K" || cap.MaxCount != 4 {
+ if err != nil || len(cap.Operations) != 2 || len(cap.Qualities) != 3 || cap.Qualities[2] != "4K" || cap.MaxCount != 4 || cap.MaxReferences != 5 || !cap.SupportsReference {
t.Fatalf("Grok 能力错误: %+v, %v", cap, err)
}
+ if defaults := p.DefaultCapabilities(); len(defaults.Qualities) != 3 || len(defaults.AspectRatios) != 8 {
+ t.Fatalf("Grok 默认能力错误: %+v", defaults)
+ }
result, err := p.Generate(context.Background(), proxy.Target{BaseURL: server.URL + "/v1", APIKey: "upstream-key", UpstreamModel: "grok-imagine-image-2.0"},
imagegen.ProviderRequest{Prompt: "test", Quality: "4K", AspectRatio: "9:16", Count: 2})
if err != nil || result.Status != "succeeded" || len(result.Images) != 1 {
t.Fatalf("Grok 解析错误: %+v, %v", result, err)
}
- if _, err := p.Capabilities("grok-unknown"); err == nil {
- t.Fatal("未知 Grok 模型不应被误判支持")
+ if custom, err := p.Capabilities("custom-grok-alias"); err != nil || len(custom.Qualities) != 3 {
+ t.Fatalf("自定义模型应使用 Grok 默认能力: %+v, %v", custom, err)
+ }
+}
+
+func TestGrokImageEditing(t *testing.T) {
+ type reference struct {
+ Type string `json:"type"`
+ URL string `json:"url"`
+ }
+ type request struct {
+ Prompt string `json:"prompt"`
+ Image *reference `json:"image"`
+ Images []reference `json:"images"`
+ Resolution string `json:"resolution"`
+ AspectRatio string `json:"aspect_ratio"`
+ }
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path != "/v1/images/edits" || r.Header.Get("Content-Type") != "application/json" {
+ t.Errorf("Grok 编辑路由或内容类型错误: %s %s", r.URL.Path, r.Header.Get("Content-Type"))
+ }
+ var body request
+ if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
+ t.Error(err)
+ }
+ if body.Resolution != "2k" || body.AspectRatio != "16:9" {
+ t.Errorf("Grok 编辑参数未映射: %+v", body)
+ }
+ switch body.Prompt {
+ case "single":
+ if body.Image == nil || len(body.Images) != 0 || body.Image.Type != "image_url" || !strings.HasPrefix(body.Image.URL, "data:image/png;base64,") {
+ t.Errorf("Grok 单图编辑参数错误: %+v", body)
+ }
+ case "multi":
+ if body.Image != nil || len(body.Images) != 2 || !strings.HasPrefix(body.Images[0].URL, "data:image/png;base64,") || !strings.HasPrefix(body.Images[1].URL, "data:image/jpeg;base64,") {
+ t.Errorf("Grok 多图编辑参数错误: %+v", body)
+ }
+ }
+ json.NewEncoder(w).Encode(map[string]any{"data": []map[string]string{{"b64_json": base64.StdEncoding.EncodeToString([]byte("edited-image"))}}})
+ }))
+ defer server.Close()
+
+ p := New(server.Client())
+ target := proxy.Target{BaseURL: server.URL + "/v1", APIKey: "upstream-key", UpstreamModel: "grok-imagine-image-2.0"}
+ for _, test := range []struct {
+ prompt string
+ images []imagegen.Input
+ }{
+ {prompt: "single", images: []imagegen.Input{{Data: []byte("one"), MIMEType: "image/png"}}},
+ {prompt: "multi", images: []imagegen.Input{{Data: []byte("one"), MIMEType: "image/png"}, {Data: []byte("two"), MIMEType: "image/jpeg"}}},
+ } {
+ result, err := p.Edit(context.Background(), target, imagegen.ProviderRequest{
+ Prompt: test.prompt, Quality: "2K", AspectRatio: "16:9", Count: 1, Images: test.images,
+ })
+ if err != nil || result.Status != "succeeded" || len(result.Images) != 1 {
+ t.Fatalf("Grok 编辑失败: %+v, %v", result, err)
+ }
+ }
+
+ tooMany := make([]imagegen.Input, 6)
+ result, err := p.Edit(context.Background(), target, imagegen.ProviderRequest{
+ Quality: "2K", AspectRatio: "16:9", Count: 1, Images: tooMany,
+ })
+ if err != nil || result.Status != "failed" || result.ErrorCode != "invalid_reference_image" {
+ t.Fatalf("Grok 应拒绝超过五张参考图: %+v, %v", result, err)
}
}
diff --git a/monkeyai/backend/internal/model/admin.go b/monkeyai/backend/internal/model/admin.go
index d18c68d6f..49ba0daf5 100644
--- a/monkeyai/backend/internal/model/admin.go
+++ b/monkeyai/backend/internal/model/admin.go
@@ -31,7 +31,7 @@ func (s *Service) RegisterAdmin(router chi.Router) {
modelJSON(w, http.StatusOK, subjects)
})
router.Get("/models/image-capabilities", func(w http.ResponseWriter, r *http.Request) {
- cap, err := s.DescribeImage(Provider(r.URL.Query().Get("provider")), r.URL.Query().Get("model_id"))
+ cap, err := s.DescribeImage(Provider(r.URL.Query().Get("provider")), "")
if err != nil {
modelError(w, http.StatusBadRequest, err.Error())
return
diff --git a/monkeyai/backend/internal/model/image_test.go b/monkeyai/backend/internal/model/image_test.go
index 5b2e5dfec..1cbc5cf7a 100644
--- a/monkeyai/backend/internal/model/image_test.go
+++ b/monkeyai/backend/internal/model/image_test.go
@@ -77,6 +77,19 @@ func TestImageCapabilitiesRestrictAdminConfigAndCatalog(t *testing.T) {
}
}
+func TestDescribeImageUsesProviderDefaultsWithoutModelID(t *testing.T) {
+ service := NewService(&repositoryStub{}).WithImageCapabilities(func(provider Provider, upstream string) (ImageCapabilities, error) {
+ if provider != ProviderXAI || upstream != "" {
+ t.Fatalf("供应商默认能力查询参数错误: %s %q", provider, upstream)
+ }
+ return ImageCapabilities{Qualities: []string{"1K", "2K", "4K"}, AspectRatios: []string{"1:1", "16:9"}}, nil
+ })
+ cap, err := service.DescribeImage(ProviderXAI, " ")
+ if err != nil || len(cap.Qualities) != 3 || len(cap.AspectRatios) != 2 {
+ t.Fatalf("供应商默认能力错误: %+v, %v", cap, err)
+ }
+}
+
func TestRejectInvalidImageModel(t *testing.T) {
cases := []struct {
name string
diff --git a/monkeyai/backend/internal/model/service.go b/monkeyai/backend/internal/model/service.go
index eeddbbc47..2512f1e9b 100644
--- a/monkeyai/backend/internal/model/service.go
+++ b/monkeyai/backend/internal/model/service.go
@@ -60,7 +60,7 @@ func (s *Service) WithImageCapabilities(describe func(Provider, string) (ImageCa
}
func (s *Service) DescribeImage(provider Provider, modelID string) (ImageCapabilities, error) {
- if s.imageCapabilities == nil || strings.TrimSpace(modelID) == "" {
+ if s.imageCapabilities == nil {
return ImageCapabilities{}, errors.New("生图模型能力不可用")
}
return s.imageCapabilities(provider, strings.TrimSpace(modelID))