From 053b7b21038952b64c579e9f2a55e707635bc268 Mon Sep 17 00:00:00 2001
From: yokowu <18836617@qq.com>
Date: Wed, 23 Sep 2026 16:40:53 +0800
Subject: [PATCH 1/4] =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=9A=E4=BC=98?=
=?UTF-8?q?=E5=8C=96=E7=94=9F=E5=9B=BE=E8=83=BD=E5=8A=9B=E9=80=89=E6=8B=A9?=
=?UTF-8?q?=E4=BA=A4=E4=BA=92?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../components/image-capability-selector.tsx | 169 ++++++++++++++++++
monkeyai/admin/src/i18n/locales/ar.ts | 1 +
monkeyai/admin/src/i18n/locales/de-DE.ts | 1 +
monkeyai/admin/src/i18n/locales/en-US.ts | 1 +
monkeyai/admin/src/i18n/locales/es-419.ts | 1 +
monkeyai/admin/src/i18n/locales/fr-FR.ts | 1 +
monkeyai/admin/src/i18n/locales/ja-JP.ts | 1 +
monkeyai/admin/src/i18n/locales/ko-KR.ts | 1 +
monkeyai/admin/src/i18n/locales/ru-RU.ts | 1 +
monkeyai/admin/src/i18n/locales/zh-CN.ts | 1 +
monkeyai/admin/src/i18n/locales/zh-TW.ts | 1 +
monkeyai/admin/src/pages/models-page.tsx | 156 ++++++++--------
monkeyai/admin/test/models-api.test.mjs | 23 +++
13 files changed, 274 insertions(+), 84 deletions(-)
create mode 100644 monkeyai/admin/src/components/image-capability-selector.tsx
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..45938e3e0
--- /dev/null
+++ b/monkeyai/admin/src/components/image-capability-selector.tsx
@@ -0,0 +1,169 @@
+import { Tick02Icon } from "@hugeicons/core-free-icons"
+import { HugeiconsIcon } from "@hugeicons/react"
+
+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(36 / width, 24 / height)
+
+ return (
+
+
+
+ )
+}
+
+function SelectionHeader({
+ label,
+ selected,
+ total,
+ selectAllLabel,
+ onSelectAll,
+}: {
+ label: string
+ selected: number
+ total: number
+ selectAllLabel: string
+ onSelectAll: () => void
+}) {
+ return (
+
+
+ {label}
+
+ {selected}/{total}
+
+
+
+
+ )
+}
+
+function SelectedMark() {
+ return (
+
+
+
+ )
+}
+
+type ImageCapabilitySelectorProps = {
+ qualities: string[]
+ aspectRatios: string[]
+ selectedQualities: string[]
+ selectedAspectRatios: string[]
+ qualityMultipliers: Record
+ qualityLabel: string
+ aspectRatioLabel: string
+ selectAllLabel: string
+ onQualitiesChange: (values: string[]) => void
+ onAspectRatiosChange: (values: string[]) => void
+}
+
+export function ImageCapabilitySelector({
+ qualities,
+ aspectRatios,
+ selectedQualities,
+ selectedAspectRatios,
+ qualityMultipliers,
+ qualityLabel,
+ aspectRatioLabel,
+ selectAllLabel,
+ onQualitiesChange,
+ onAspectRatiosChange,
+}: ImageCapabilitySelectorProps) {
+ return (
+ <>
+
+ onQualitiesChange([...qualities])}
+ />
+
+ {qualities.map((value) => (
+
+
+ {value}
+
+
+ ×{qualityMultipliers[value] ?? "1"}
+
+
+
+ ))}
+
+
+
+
+ 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..4c8d19006 100644
--- a/monkeyai/admin/src/i18n/locales/ar.ts
+++ b/monkeyai/admin/src/i18n/locales/ar.ts
@@ -265,6 +265,7 @@ export const ar = {
imageProvider: "مزود الصور",
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..5a855e3db 100644
--- a/monkeyai/admin/src/i18n/locales/de-DE.ts
+++ b/monkeyai/admin/src/i18n/locales/de-DE.ts
@@ -274,6 +274,7 @@ export const deDE = {
imageProvider: "Bildanbieter",
imageCapabilitiesUnavailable:
"Unterstützte Modell-ID eingeben, um Bildoptionen zu laden.",
+ 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..b19de79fe 100644
--- a/monkeyai/admin/src/i18n/locales/en-US.ts
+++ b/monkeyai/admin/src/i18n/locales/en-US.ts
@@ -410,6 +410,7 @@ export const enUS = {
taskStatus: "Task status",
imageCapabilitiesUnavailable:
"Enter a supported upstream model ID to load available image options.",
+ 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..d3154345a 100644
--- a/monkeyai/admin/src/i18n/locales/es-419.ts
+++ b/monkeyai/admin/src/i18n/locales/es-419.ts
@@ -273,6 +273,7 @@ export const es419 = {
imageProvider: "Proveedor de imágenes",
imageCapabilitiesUnavailable:
"Ingresa un ID de modelo compatible para cargar las opciones.",
+ 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..32df4f76b 100644
--- a/monkeyai/admin/src/i18n/locales/fr-FR.ts
+++ b/monkeyai/admin/src/i18n/locales/fr-FR.ts
@@ -274,6 +274,7 @@ export const frFR = {
imageProvider: "Fournisseur d’images",
imageCapabilitiesUnavailable:
"Saisissez un ID de modèle pris en charge pour charger les options.",
+ 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..5d09caace 100644
--- a/monkeyai/admin/src/i18n/locales/ja-JP.ts
+++ b/monkeyai/admin/src/i18n/locales/ja-JP.ts
@@ -268,6 +268,7 @@ export const jaJP = {
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..f393293aa 100644
--- a/monkeyai/admin/src/i18n/locales/ko-KR.ts
+++ b/monkeyai/admin/src/i18n/locales/ko-KR.ts
@@ -265,6 +265,7 @@ export const koKR = {
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..3db87b11e 100644
--- a/monkeyai/admin/src/i18n/locales/ru-RU.ts
+++ b/monkeyai/admin/src/i18n/locales/ru-RU.ts
@@ -272,6 +272,7 @@ export const ruRU = {
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..db36c32f3 100644
--- a/monkeyai/admin/src/i18n/locales/zh-CN.ts
+++ b/monkeyai/admin/src/i18n/locales/zh-CN.ts
@@ -394,6 +394,7 @@ export const zhCN = {
taskStatus: "任务状态",
imageCapabilitiesUnavailable:
"请输入受支持的上游模型 ID,以加载画质和比例选项。",
+ 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..58cb0927c 100644
--- a/monkeyai/admin/src/i18n/locales/zh-TW.ts
+++ b/monkeyai/admin/src/i18n/locales/zh-TW.ts
@@ -257,6 +257,7 @@ export const zhTW = {
imageProvider: "生圖供應商",
imageCapabilitiesUnavailable:
"請輸入支援的上游模型 ID,以載入畫質與比例選項。",
+ selectAllSupported: "全選支援項目",
imageQuality: "畫質檔位",
aspectRatio: "比例",
defaultQuality: "預設畫質",
diff --git a/monkeyai/admin/src/pages/models-page.tsx b/monkeyai/admin/src/pages/models-page.tsx
index 1f7b67491..ec51ac3fd 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"
@@ -312,19 +313,46 @@ export function ModelsPage() {
useEffect(() => {
if (!dialogOpen || kind !== "image" || !imageModelId.trim()) return
+ const upstreamModel = imageModelId.trim()
let active = true
const timer = window.setTimeout(() => {
api(
- `/api/admin/v1/models/image-capabilities?provider=${encodeURIComponent(provider)}&model_id=${encodeURIComponent(imageModelId.trim())}`
+ `/api/admin/v1/models/image-capabilities?provider=${encodeURIComponent(provider)}&model_id=${encodeURIComponent(upstreamModel)}`
)
.then((capability) => {
if (!active) return
+
+ const preserveSavedSelection =
+ editingModel?.kind === "image" &&
+ editingModel.provider === provider &&
+ editingModel.modelId === upstreamModel
+ 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,7 +363,29 @@ export function ModelsPage() {
active = false
window.clearTimeout(timer)
}
- }, [dialogOpen, kind, provider, imageModelId])
+ }, [dialogOpen, editingModel, kind, provider, imageModelId])
+
+ 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")
@@ -853,84 +903,22 @@ export function ModelsPage() {
)}
{imageCapabilities && (
<>
-
-
- {t("pages.models.imageQuality")}
-
-
- {imageCapabilities.qualities.map((value) => (
-
- ))}
-
-
-
-
- {t("pages.models.aspectRatio")}
-
-
- {imageCapabilities.aspect_ratios.map(
- (value) => (
-
- )
- )}
-
-
+
diff --git a/monkeyai/admin/test/models-api.test.mjs b/monkeyai/admin/test/models-api.test.mjs
index 30d4fde51..c2c500c18 100644
--- a/monkeyai/admin/test/models-api.test.mjs
+++ b/monkeyai/admin/test/models-api.test.mjs
@@ -22,6 +22,7 @@ const modelKeys = [
"imageKind",
"imageProvider",
"imageCapabilitiesUnavailable",
+ "selectAllSupported",
"imageQuality",
"aspectRatio",
"defaultQuality",
@@ -136,6 +137,28 @@ test("model kinds use icon tabs and card badges", async () => {
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, /: \[\.\.\.capability\.qualities\]/)
+ assert.match(page, /: \[\.\.\.capability\.aspect_ratios\]/)
+ assert.match(page, /if \(next\.length === 0\) return/)
+ assert.match(selector, / {
const source = await readFile(
new URL("../src/components/image-generation-test.tsx", import.meta.url),
From 18ea06b7a523e951e33778a74f737a3ff52ec035 Mon Sep 17 00:00:00 2001
From: yokowu <18836617@qq.com>
Date: Wed, 23 Sep 2026 17:22:01 +0800
Subject: [PATCH 2/4] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E6=8C=89?=
=?UTF-8?q?=E4=BE=9B=E5=BA=94=E5=95=86=E5=B1=95=E7=A4=BA=E7=94=9F=E5=9B=BE?=
=?UTF-8?q?=E8=83=BD=E5=8A=9B=E5=B9=B6=E6=94=B6=E7=B4=A7=E5=B8=83=E5=B1=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../components/image-capability-selector.tsx | 34 +-
monkeyai/admin/src/pages/models-page.tsx | 324 ++++++++++--------
monkeyai/admin/test/models-api.test.mjs | 8 +-
monkeyai/backend/api/admin.yaml | 2 +-
monkeyai/backend/internal/app/app.go | 8 +-
.../internal/imagegen/openai/images/images.go | 14 +-
.../imagegen/openai/images/images_test.go | 3 +
.../imagegen/openai/responses/responses.go | 14 +-
.../openai/responses/responses_test.go | 3 +
monkeyai/backend/internal/imagegen/service.go | 25 +-
.../backend/internal/imagegen/service_test.go | 17 +
.../imagegen/volcengine/volcengine.go | 14 +-
.../imagegen/volcengine/volcengine_test.go | 3 +
monkeyai/backend/internal/imagegen/xai/xai.go | 8 +-
.../backend/internal/imagegen/xai/xai_test.go | 3 +
monkeyai/backend/internal/model/image_test.go | 13 +
monkeyai/backend/internal/model/service.go | 2 +-
17 files changed, 291 insertions(+), 204 deletions(-)
diff --git a/monkeyai/admin/src/components/image-capability-selector.tsx b/monkeyai/admin/src/components/image-capability-selector.tsx
index 45938e3e0..d8c89563e 100644
--- a/monkeyai/admin/src/components/image-capability-selector.tsx
+++ b/monkeyai/admin/src/components/image-capability-selector.tsx
@@ -1,6 +1,3 @@
-import { Tick02Icon } from "@hugeicons/core-free-icons"
-import { HugeiconsIcon } from "@hugeicons/react"
-
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Field, FieldLabel } from "@/components/ui/field"
@@ -10,18 +7,18 @@ 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(36 / width, 24 / height)
+ const scale = Math.min(28 / width, 16 / height)
return (
@@ -42,7 +39,7 @@ function SelectionHeader({
onSelectAll: () => void
}) {
return (
-
+
{label}
@@ -51,7 +48,7 @@ function SelectionHeader({