From 52d877836c8ba9e4dd2ae8655ad759879f62ee69 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 1 Aug 2026 23:38:31 +0800 Subject: [PATCH] feat: support aggregated image model protocols --- apps/web/app/globals.css | 6 + apps/web/components/settings-center.tsx | 101 +++++++++++- apps/web/types/settings.ts | 5 + docs/settings.md | 11 +- services/api/app/api/settings.py | 16 ++ .../api/app/integrations/openai_compatible.py | 88 +++++++++++ services/api/app/runtime_settings.py | 144 +++++++++++++++++- services/api/tests/test_image_gateway.py | 112 ++++++++++++++ 8 files changed, 474 insertions(+), 9 deletions(-) create mode 100644 services/api/tests/test_image_gateway.py diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index 655fdb8..55e4d01 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -1365,6 +1365,12 @@ textarea:focus-visible, font-weight: 650; } +.model-input > small { + color: var(--text-faint); + font-size: 8px; + line-height: 1.45; +} + .model-input input, .model-input select, .routing-row select { diff --git a/apps/web/components/settings-center.tsx b/apps/web/components/settings-center.tsx index a338958..c17490e 100644 --- a/apps/web/components/settings-center.tsx +++ b/apps/web/components/settings-center.tsx @@ -19,6 +19,7 @@ import { import { useEffect, useMemo, useState } from "react"; import type { + ImageParameterProfile, ModelCapability, ModelCategory, ModelPoolItem, @@ -52,6 +53,30 @@ const providerLabels: Record = { custom: "其他兼容接口", }; +const imagePresets: Record, Partial> = { + gpt_image_2: { + name: "GPT Image 2", model_id: "gpt-image-2", category: "multimodal", provider: "lingke", + base_url: "https://api.lk888.ai", capabilities: ["image_generation", "image_editing"], + models_path: "/v1/models", image_generation_path: "/v1/media/generate", + image_edit_path: "/v1/media/generate", image_status_path: "/v1/media/status", + image_protocol: "aigc_media", image_parameter_profile: "gpt_image_2", + }, + nano_banana_pro: { + name: "Nano Banana Pro", model_id: "gemini-3-pro-image-preview", category: "multimodal", provider: "lingke", + base_url: "https://api.lk888.ai", capabilities: ["image_generation", "image_editing"], + models_path: "/v1/models", image_generation_path: "/v1/media/generate", + image_edit_path: "/v1/media/generate", image_status_path: "/v1/media/status", + image_protocol: "aigc_media", image_parameter_profile: "nano_banana_pro", + }, + seedream_5_pro: { + name: "即梦 5.0 Pro", model_id: "doubao-seedream-5-0-pro-260628", category: "multimodal", provider: "lingke", + base_url: "https://api.lk888.ai", capabilities: ["image_generation", "image_editing"], + models_path: "/v1/models", image_generation_path: "/v1/media/generate", + image_edit_path: "/v1/media/generate", image_status_path: "/v1/media/status", + image_protocol: "aigc_media", image_parameter_profile: "seedream_5_pro", + }, +}; + type SettingsCenterProps = { open: boolean; onClose: () => void; @@ -211,6 +236,27 @@ export function SettingsCenter({ open, onClose, onReadinessChange }: SettingsCen } } + async function testModelGeneration(modelId: string) { + if (!window.confirm("这会真实生成一张低成本测试图,并消耗该模型的一次调用额度。是否继续?")) return; + const target = `generation:${modelId}`; + setBusy(target); + setNotice(null); + try { + await saveSettings(false); + setBusy(target); + const response = await fetch(`${API_BASE}/v1/settings/models/${encodeURIComponent(modelId)}/test-generation`, { + method: "POST", + }); + const result = await response.json() as TestResult; + setTestResults((current) => ({ ...current, [target]: result })); + setNotice({ tone: result.ok ? "success" : "error", text: result.message }); + } catch (error) { + setNotice({ tone: "error", text: getErrorMessage(error) }); + } finally { + setBusy(null); + } + } + async function generateFor(field: SettingField) { if (!field.generator) return; setBusy(field.key); @@ -327,6 +373,7 @@ export function SettingsCenter({ open, onClose, onReadinessChange }: SettingsCen busy={busy} onChange={updateField} onTest={(modelId) => void testModel(modelId)} + onTrial={(modelId) => void testModelGeneration(modelId)} /> ) : (
@@ -408,6 +455,7 @@ type ModelPoolPanelProps = { busy: string | null; onChange: (key: string, value: unknown) => void; onTest: (modelId: string) => void; + onTrial: (modelId: string) => void; }; function newModel(category: ModelCategory): ModelPoolItem { @@ -424,12 +472,15 @@ function newModel(category: ModelCategory): ModelPoolItem { chat_path: "/chat/completions", image_generation_path: "/images/generations", image_edit_path: "/images/edits", + image_status_path: "/v1/media/status", + image_protocol: "openai_images", + image_parameter_profile: "generic", enabled: true, api_key_configured: false, }; } -function ModelPoolPanel({ draft, tests, busy, onChange, onTest }: ModelPoolPanelProps) { +function ModelPoolPanel({ draft, tests, busy, onChange, onTest, onTrial }: ModelPoolPanelProps) { const pool = modelPoolFrom(draft); const [editor, setEditor] = useState(null); const [editorError, setEditorError] = useState(""); @@ -444,6 +495,16 @@ function ModelPoolPanel({ draft, tests, busy, onChange, onTest }: ModelPoolPanel onChange("model_pool", nextPool); } + function applyPreset(profile: ImageParameterProfile) { + if (!editor) return; + if (profile === "generic") { + setEditor({ ...editor, image_parameter_profile: "generic" }); + return; + } + setEditor({ ...editor, ...imagePresets[profile] }); + setShowAdvanced(false); + } + function saveEditor() { if (!editor) return; if (!editor.name.trim() || !editor.model_id.trim() || !editor.base_url.trim()) { @@ -515,6 +576,18 @@ function ModelPoolPanel({ draft, tests, busy, onChange, onTest }: ModelPoolPanel
+ {editor.category === "multimodal" ? ( + + ) : null} {([ ["models_path", "模型列表路径"], ["chat_path", "对话路径"], ["image_generation_path", "生图路径"], ["image_edit_path", "图片编辑路径"], + ["image_status_path", "任务查询路径"], ] as const).map(([key, label]) => (