feat: support aggregated image model protocols
This commit is contained in:
@@ -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>
|
||||
);
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user