feat: support aggregated image model protocols

This commit is contained in:
Codex
2026-08-01 23:38:31 +08:00
parent 4430149ee4
commit 52d877836c
8 changed files with 474 additions and 9 deletions
+97 -4
View File
@@ -19,6 +19,7 @@ import {
import { useEffect, useMemo, useState } from "react";
import type {
ImageParameterProfile,
ModelCapability,
ModelCategory,
ModelPoolItem,
@@ -52,6 +53,30 @@ const providerLabels: Record<string, string> = {
custom: "其他兼容接口",
};
const imagePresets: Record<Exclude<ImageParameterProfile, "generic">, Partial<ModelPoolItem>> = {
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)}
/>
) : (
<div className="settings-fields">
@@ -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<ModelPoolItem | null>(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
</div>
<div className="model-editor-grid">
{editor.category === "multimodal" ? (
<label className="model-input model-input-wide">
<span></span>
<select value={editor.image_parameter_profile} onChange={(event) => applyPreset(event.target.value as ImageParameterProfile)}>
<option value="generic"> / </option>
<option value="gpt_image_2">GPT Image 2 · AIGC</option>
<option value="nano_banana_pro">Nano Banana Pro · AIGC</option>
<option value="seedream_5_pro"> 5.0 Pro · AIGC</option>
</select>
<small> ID</small>
</label>
) : null}
<label className="model-input">
<span></span>
<select
@@ -590,11 +663,19 @@ function ModelPoolPanel({ draft, tests, busy, onChange, onTest }: ModelPoolPanel
</button>
{showAdvanced ? (
<div className="model-editor-grid model-path-grid">
<label className="model-input model-input-wide">
<span></span>
<select value={editor.image_protocol} onChange={(event) => setEditor({ ...editor, image_protocol: event.target.value as ModelPoolItem["image_protocol"] })}>
<option value="openai_images">OpenAI Images </option>
<option value="aigc_media">AIGC </option>
</select>
</label>
{([
["models_path", "模型列表路径"],
["chat_path", "对话路径"],
["image_generation_path", "生图路径"],
["image_edit_path", "图片编辑路径"],
["image_status_path", "任务查询路径"],
] as const).map(([key, label]) => (
<label className="model-input" key={key}>
<span>{label}</span>
@@ -623,6 +704,7 @@ function ModelPoolPanel({ draft, tests, busy, onChange, onTest }: ModelPoolPanel
onEdit={(model) => { setEditor({ ...model, api_key: "" }); setShowAdvanced(false); }}
onRemove={removeModel}
onTest={onTest}
onTrial={onTrial}
/>
<ModelGroup
title="多模态模型"
@@ -635,6 +717,7 @@ function ModelPoolPanel({ draft, tests, busy, onChange, onTest }: ModelPoolPanel
onEdit={(model) => { setEditor({ ...model, api_key: "" }); setShowAdvanced(false); }}
onRemove={removeModel}
onTest={onTest}
onTrial={onTrial}
/>
<section className="model-routing">
@@ -690,9 +773,10 @@ type ModelGroupProps = {
onEdit: (model: ModelPoolItem) => void;
onRemove: (model: ModelPoolItem) => void;
onTest: (modelId: string) => void;
onTrial: (modelId: string) => void;
};
function ModelGroup({ title, description, category, models, tests, busy, onAdd, onEdit, onRemove, onTest }: ModelGroupProps) {
function ModelGroup({ title, description, category, models, tests, busy, onAdd, onEdit, onRemove, onTest, onTrial }: ModelGroupProps) {
return (
<section className="model-group">
<div className="model-group-heading">
@@ -713,6 +797,8 @@ function ModelGroup({ title, description, category, models, tests, busy, onAdd,
{models.map((model) => {
const target = `model:${model.id}`;
const result = tests[target];
const generationTarget = `generation:${model.id}`;
const generationResult = tests[generationTarget];
return (
<article className={`model-row ${!model.enabled ? "disabled" : ""}`} key={model.id}>
<div className={`model-state ${result?.ok ? "ok" : result ? "failed" : "untested"}`}>
@@ -729,8 +815,14 @@ function ModelGroup({ title, description, category, models, tests, busy, onAdd,
<div className="model-row-actions">
<button className="button button-secondary compact" type="button" onClick={() => onTest(model.id)} disabled={Boolean(busy) || !model.enabled}>
{busy === target ? <ArrowClockwiseIcon className="spin" size={14} /> : <PlugIcon size={14} />}
</button>
{model.capabilities.includes("image_generation") ? (
<button className="button button-secondary compact" type="button" onClick={() => onTrial(model.id)} disabled={Boolean(busy) || !model.enabled} title="会消耗一次模型调用额度">
{busy === generationTarget ? <ArrowClockwiseIcon className="spin" size={14} /> : <ImageIcon size={14} />}
</button>
) : null}
<button className="icon-button small" type="button" onClick={() => onEdit(model)} disabled={Boolean(busy)} aria-label={`编辑${model.name}`}>
<PencilSimpleIcon size={15} />
</button>
@@ -738,7 +830,8 @@ function ModelGroup({ title, description, category, models, tests, busy, onAdd,
<TrashIcon size={15} />
</button>
</div>
{result ? <p className={`model-test-message ${result.ok ? "ok" : "failed"}`}>{result.message}</p> : <p className="model-test-message"></p>}
{result ? <p className={`model-test-message ${result.ok ? "ok" : "failed"}`}>{result.message}</p> : <p className="model-test-message"> ID</p>}
{generationResult ? <p className={`model-test-message ${generationResult.ok ? "ok" : "failed"}`}>{generationResult.message}</p> : null}
</article>
);
})}