fix(channel): 修复前台自定义渠道模型配置抽屉空白死锁,并在启动时保护管理员能力配置 (#360)
Co-authored-by: Danborad <danborad@github.com>
This commit is contained in:
1 parent
86d3805f5a
commit
ef30645cf0
5 files changed
+198
-44
No files matched your search
@@ -296,6 +296,9 @@ func validateChannelModelTierCapabilities(tiers []model.ChannelModelPriceTier, r
|
|||||||
if tier.VideoSeconds == 0 {
|
if tier.VideoSeconds == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if !videoDurationSupported(config.Video) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if config.Video.Duration.Selection == "enum" && !durationSupported[tier.VideoSeconds] {
|
if config.Video.Duration.Selection == "enum" && !durationSupported[tier.VideoSeconds] {
|
||||||
return BadAuthRequest(fmt.Sprintf("价格档时长 %d 秒不在该视频模型支持范围内", tier.VideoSeconds))
|
return BadAuthRequest(fmt.Sprintf("价格档时长 %d 秒不在该视频模型支持范围内", tier.VideoSeconds))
|
||||||
}
|
}
|
||||||
@@ -742,30 +745,26 @@ func (s *Service) syncInitialChannelModels(channel *model.ModelChannel, names []
|
|||||||
}
|
}
|
||||||
desired[name] = true
|
desired[name] = true
|
||||||
if item := byKey[name]; item != nil {
|
if item := byKey[name]; item != nil {
|
||||||
if !item.Enabled {
|
|
||||||
item.Enabled = true
|
|
||||||
item.PriceVersion++
|
|
||||||
if err := s.repo.SaveChannelModel(item); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
modelID, idErr := s.repo.NextPrefixedID("MODEL")
|
modelID, idErr := s.repo.NextPrefixedID("MODEL")
|
||||||
if idErr != nil {
|
if idErr != nil {
|
||||||
return idErr
|
return idErr
|
||||||
}
|
}
|
||||||
item := model.ChannelModel{ID: modelID, ChannelID: channel.ID, ModelKey: name, DisplayName: name, BillingMode: "fixed_request", Enabled: false, PriceVersion: 1}
|
item := model.ChannelModel{ID: modelID, ChannelID: channel.ID, ModelKey: name, DisplayName: name, BillingMode: "fixed_request", Enabled: false, PriceConfigured: false, UnitPriceMicrocredits: 0, PriceVersion: 1}
|
||||||
if err := s.repo.SaveChannelModel(&item); err != nil {
|
if err := s.repo.SaveChannelModel(&item); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for index := range existing {
|
for index := range existing {
|
||||||
if existing[index].Enabled && !desired[existing[index].ModelKey] {
|
if !desired[existing[index].ModelKey] {
|
||||||
|
changed := existing[index].Enabled
|
||||||
existing[index].Enabled = false
|
existing[index].Enabled = false
|
||||||
existing[index].PriceVersion++
|
if changed {
|
||||||
if err := s.repo.SaveChannelModel(&existing[index]); err != nil {
|
existing[index].PriceVersion++
|
||||||
return err
|
if err := s.repo.SaveChannelModel(&existing[index]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ type ParameterSupport struct {
|
|||||||
type VideoCapabilityConfig struct {
|
type VideoCapabilityConfig struct {
|
||||||
References VideoReferenceConfig `json:"references"`
|
References VideoReferenceConfig `json:"references"`
|
||||||
Duration VideoDurationConfig `json:"duration"`
|
Duration VideoDurationConfig `json:"duration"`
|
||||||
|
DurationSupported *bool `json:"durationSupported,omitempty"`
|
||||||
Ratios []string `json:"ratios"`
|
Ratios []string `json:"ratios"`
|
||||||
DefaultRatio string `json:"defaultRatio"`
|
DefaultRatio string `json:"defaultRatio"`
|
||||||
Resolutions []string `json:"resolutions"`
|
Resolutions []string `json:"resolutions"`
|
||||||
@@ -109,6 +110,10 @@ func DefaultModelCapabilityConfig(protocol string) *ModelCapabilityConfig {
|
|||||||
return DefaultModelCapabilityConfigForModel(protocol, "")
|
return DefaultModelCapabilityConfigForModel(protocol, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func videoDurationSupported(value *VideoCapabilityConfig) bool {
|
||||||
|
return value == nil || value.DurationSupported == nil || *value.DurationSupported
|
||||||
|
}
|
||||||
|
|
||||||
func DefaultImageCapabilityConfig(protocol string, modelName string) *ImageCapabilityConfig {
|
func DefaultImageCapabilityConfig(protocol string, modelName string) *ImageCapabilityConfig {
|
||||||
image := &ImageCapabilityConfig{
|
image := &ImageCapabilityConfig{
|
||||||
References: ImageReferenceConfig{PromptMaxChars: 32000, MaxImages: 16, MaxImageBytes: 30 * 1024 * 1024, MaskSupported: true},
|
References: ImageReferenceConfig{PromptMaxChars: 32000, MaxImages: 16, MaxImageBytes: 30 * 1024 * 1024, MaskSupported: true},
|
||||||
|
|||||||
@@ -18,6 +18,22 @@ const operationOptions = [
|
|||||||
{ label: "音频生视频", value: "audio_to_video" },
|
{ label: "音频生视频", value: "audio_to_video" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
function formatImageSizeTagLabel(size: string): string {
|
||||||
|
const raw = size.trim();
|
||||||
|
if (raw === "1:1") return "1:1 (方图)";
|
||||||
|
if (raw === "16:9") return "16:9 (横屏)";
|
||||||
|
if (raw === "9:16") return "9:16 (竖屏)";
|
||||||
|
if (raw === "1024x1024") return "1024x1024 (1:1)";
|
||||||
|
if (raw === "2048x2048") return "2048x2048 (2K 方)";
|
||||||
|
if (raw === "3840x2160") return "3840x2160 (4K 横屏)";
|
||||||
|
if (raw === "2160x3840") return "2160x3840 (4K 竖屏)";
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
function imageSizeOptions(values: string[]) {
|
||||||
|
return values.map((v) => ({ label: formatImageSizeTagLabel(v), value: v }));
|
||||||
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
value?: ModelCapabilityConfig;
|
value?: ModelCapabilityConfig;
|
||||||
onChange?: (value: ModelCapabilityConfig) => void;
|
onChange?: (value: ModelCapabilityConfig) => void;
|
||||||
@@ -450,6 +466,7 @@ function ImageCapabilityEditor({ value, onChange, protocol, model, disabled, sec
|
|||||||
className="admin-capability-tags w-full"
|
className="admin-capability-tags w-full"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
value={profile.size.values}
|
value={profile.size.values}
|
||||||
|
options={imageSizeOptions(profile.size.values)}
|
||||||
tokenSeparators={[","]}
|
tokenSeparators={[","]}
|
||||||
placeholder="例如 1:1、1024x1024"
|
placeholder="例如 1:1、1024x1024"
|
||||||
onChange={(values) => updateSize({ values, default: values.includes(profile.size.default) || profile.size.allowCustom ? profile.size.default : values[0] || "auto" })}
|
onChange={(values) => updateSize({ values, default: values.includes(profile.size.default) || profile.size.allowCustom ? profile.size.default : values[0] || "auto" })}
|
||||||
@@ -479,6 +496,7 @@ function ImageCapabilityEditor({ value, onChange, protocol, model, disabled, sec
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
value={profile.quality.values}
|
value={profile.quality.values}
|
||||||
tokenSeparators={[","]}
|
tokenSeparators={[","]}
|
||||||
|
placeholder="例如 auto, low, medium, high, 1k, 2k, 4k"
|
||||||
onChange={(values) => updateQuality({ values, default: values.includes(profile.quality.default) ? profile.quality.default : values[0] || "auto" })}
|
onChange={(values) => updateQuality({ values, default: values.includes(profile.quality.default) ? profile.quality.default : values[0] || "auto" })}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
@@ -551,6 +569,7 @@ function ImageCapabilityEditor({ value, onChange, protocol, model, disabled, sec
|
|||||||
className="admin-capability-tags w-full"
|
className="admin-capability-tags w-full"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
value={profile.size.values}
|
value={profile.size.values}
|
||||||
|
options={imageSizeOptions(profile.size.values)}
|
||||||
tokenSeparators={[","]}
|
tokenSeparators={[","]}
|
||||||
placeholder="例如 1:1、1024x1024"
|
placeholder="例如 1:1、1024x1024"
|
||||||
onChange={(values) => updateSize({ values, default: values.includes(profile.size.default) || profile.size.allowCustom ? profile.size.default : values[0] || "auto" })}
|
onChange={(values) => updateSize({ values, default: values.includes(profile.size.default) || profile.size.allowCustom ? profile.size.default : values[0] || "auto" })}
|
||||||
@@ -584,6 +603,7 @@ function ImageCapabilityEditor({ value, onChange, protocol, model, disabled, sec
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
value={profile.quality.values}
|
value={profile.quality.values}
|
||||||
tokenSeparators={[","]}
|
tokenSeparators={[","]}
|
||||||
|
placeholder="例如 auto, low, medium, high, 1k, 2k, 4k"
|
||||||
onChange={(values) => updateQuality({ values, default: values.includes(profile.quality.default) ? profile.quality.default : values[0] || "auto" })}
|
onChange={(values) => updateQuality({ values, default: values.includes(profile.quality.default) ? profile.quality.default : values[0] || "auto" })}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ export type VideoCapabilityConfig = {
|
|||||||
values?: number[];
|
values?: number[];
|
||||||
default: number;
|
default: number;
|
||||||
};
|
};
|
||||||
|
durationSupported?: boolean;
|
||||||
ratios: string[];
|
ratios: string[];
|
||||||
defaultRatio: string;
|
defaultRatio: string;
|
||||||
resolutions: string[];
|
resolutions: string[];
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import { ChevronRight, FlaskConical, Settings2 } from "lucide-react";
|
|||||||
|
|
||||||
import { testChannelModelConnection } from "@/lib/model-connection-test";
|
import { testChannelModelConnection } from "@/lib/model-connection-test";
|
||||||
import { ModelCapabilityEditor } from "@/components/model-capability-editor";
|
import { ModelCapabilityEditor } from "@/components/model-capability-editor";
|
||||||
import { CapabilityCardPicker, ProtocolCardPicker } from "@/components/model-protocol-picker";
|
import { CapabilityCardPicker, ProtocolCardPicker, type ModelCapabilityChoice } from "@/components/model-protocol-picker";
|
||||||
import { defaultModelCapabilityConfig } from "@/lib/model-capabilities";
|
import { defaultModelCapabilityConfig } from "@/lib/model-capabilities";
|
||||||
import { modelProtocolCapability, modelProtocolDefinition, modelProtocolSupportsTokenBilling, type ModelProtocol } from "@/lib/model-protocols";
|
import { modelProtocolCapability, modelProtocolDefinition, modelProtocolSupportsTokenBilling, type ModelProtocol, type ModelProtocolDefinition } from "@/lib/model-protocols";
|
||||||
import { fetchPluginProviderCatalog } from "@/services/api/plugin-catalog";
|
import { fetchPluginProviderCatalog } from "@/services/api/plugin-catalog";
|
||||||
import { modelOptionName, type ModelChannel } from "@/stores/use-config-store";
|
import { modelOptionName, type ModelChannel } from "@/stores/use-config-store";
|
||||||
|
|
||||||
@@ -16,20 +16,24 @@ export function ChannelModelSettings({ channel, onChange }: { channel: ModelChan
|
|||||||
const { message } = App.useApp();
|
const { message } = App.useApp();
|
||||||
const [testingModel, setTestingModel] = useState("");
|
const [testingModel, setTestingModel] = useState("");
|
||||||
const [activeModel, setActiveModel] = useState<string | null>(null);
|
const [activeModel, setActiveModel] = useState<string | null>(null);
|
||||||
const [availableProtocols, setAvailableProtocols] = useState<import("@/lib/model-protocols").ModelProtocolDefinition[]>([]);
|
const [availableProtocols, setAvailableProtocols] = useState<ModelProtocolDefinition[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void fetchPluginProviderCatalog("user.custom-channel").then(setAvailableProtocols).catch(() => setAvailableProtocols([]));
|
void fetchPluginProviderCatalog("user.custom-channel").then(setAvailableProtocols).catch(() => setAvailableProtocols([]));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!channel.models.length) return null;
|
if (!channel.models.length) return null;
|
||||||
|
|
||||||
const updateCost = (model: string, patch: Partial<ModelCost>) => {
|
const updateCost = (model: string, patch: Partial<ModelCost>) => {
|
||||||
|
const defaultProtocol = defaultProtocolForModel(channel, model, availableProtocols);
|
||||||
|
const defaultCap = modelProtocolCapability(defaultProtocol, availableProtocols) || inferCapabilityFromModel(model);
|
||||||
const current = channel.modelCosts?.find((item) => item.model === model) || {
|
const current = channel.modelCosts?.find((item) => item.model === model) || {
|
||||||
model,
|
model,
|
||||||
capability: modelProtocolCapability(defaultProtocolForModel(channel, model), availableProtocols) || "text",
|
capability: defaultCap,
|
||||||
protocol: defaultProtocolForModel(channel, model),
|
protocol: defaultProtocol,
|
||||||
billingMode: "fixed_request" as const,
|
billingMode: "fixed_request" as const,
|
||||||
unitPriceMicrocredits: 0,
|
unitPriceMicrocredits: 0,
|
||||||
capabilityConfig: defaultModelCapabilityConfig(defaultProtocolForModel(channel, model), model),
|
capabilityConfig: defaultModelCapabilityConfig(defaultProtocol, model),
|
||||||
};
|
};
|
||||||
const next = [...(channel.modelCosts || []).filter((item) => item.model !== model), { ...current, ...patch, model }];
|
const next = [...(channel.modelCosts || []).filter((item) => item.model !== model), { ...current, ...patch, model }];
|
||||||
onChange(next.filter((item) => channel.models.includes(item.model)));
|
onChange(next.filter((item) => channel.models.includes(item.model)));
|
||||||
@@ -48,8 +52,9 @@ export function ChannelModelSettings({ channel, onChange }: { channel: ModelChan
|
|||||||
};
|
};
|
||||||
|
|
||||||
const activeModelCost = activeModel ? channel.modelCosts?.find((item) => item.model === activeModel) : undefined;
|
const activeModelCost = activeModel ? channel.modelCosts?.find((item) => item.model === activeModel) : undefined;
|
||||||
const activeProtocol = activeModel ? activeModelCost?.protocol || defaultProtocolForModel(channel, activeModel) : undefined;
|
const inferredProtocol = activeModel ? defaultProtocolForModel(channel, activeModel, availableProtocols) : "";
|
||||||
const activeCapability = activeModel ? activeModelCost?.capability || modelProtocolCapability(activeProtocol, availableProtocols) || "text" : undefined;
|
const activeProtocol = activeModelCost?.protocol || inferredProtocol;
|
||||||
|
const activeCapability = activeModelCost?.capability || modelProtocolCapability(activeProtocol, availableProtocols) || (activeModel ? inferCapabilityFromModel(activeModel) : "text");
|
||||||
const activeBillingMode = activeModelCost?.billingMode || "fixed_request";
|
const activeBillingMode = activeModelCost?.billingMode || "fixed_request";
|
||||||
const activeTokenBillingSupported = modelProtocolSupportsTokenBilling(activeCapability, activeProtocol);
|
const activeTokenBillingSupported = modelProtocolSupportsTokenBilling(activeCapability, activeProtocol);
|
||||||
|
|
||||||
@@ -66,9 +71,8 @@ export function ChannelModelSettings({ channel, onChange }: { channel: ModelChan
|
|||||||
{channel.models.map((rawModel) => {
|
{channel.models.map((rawModel) => {
|
||||||
const model = modelOptionName(rawModel);
|
const model = modelOptionName(rawModel);
|
||||||
const cost = channel.modelCosts?.find((item) => item.model === model);
|
const cost = channel.modelCosts?.find((item) => item.model === model);
|
||||||
const protocol = cost?.protocol || defaultProtocolForModel(channel, model);
|
const protocol = cost?.protocol || defaultProtocolForModel(channel, model, availableProtocols);
|
||||||
const capability = cost?.capability || modelProtocolCapability(protocol, availableProtocols) || "text";
|
const capability = cost?.capability || modelProtocolCapability(protocol, availableProtocols) || inferCapabilityFromModel(model);
|
||||||
const billingMode = cost?.billingMode || "fixed_request";
|
|
||||||
const displayName = cost?.displayName?.trim() || model;
|
const displayName = cost?.displayName?.trim() || model;
|
||||||
return (
|
return (
|
||||||
<div key={model} className="flex min-w-0 items-center gap-3 rounded-md bg-surface-active px-3 py-2.5 transition-colors hover:bg-surface-hover">
|
<div key={model} className="flex min-w-0 items-center gap-3 rounded-md bg-surface-active px-3 py-2.5 transition-colors hover:bg-surface-hover">
|
||||||
@@ -103,19 +107,60 @@ export function ChannelModelSettings({ channel, onChange }: { channel: ModelChan
|
|||||||
destroyOnHidden
|
destroyOnHidden
|
||||||
extra={
|
extra={
|
||||||
activeModel && activeCapability && activeProtocol ? (
|
activeModel && activeCapability && activeProtocol ? (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
icon={<FlaskConical className="size-3.5" />}
|
||||||
|
loading={testingModel === activeModel}
|
||||||
|
disabled={Boolean(testingModel && testingModel !== activeModel)}
|
||||||
|
onClick={() => void testModel(activeModel, activeCapability, activeProtocol)}
|
||||||
|
>
|
||||||
|
测试模型
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
onClick={() => {
|
||||||
|
message.success(`${activeModel} 配置已自动保存`);
|
||||||
|
setActiveModel(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
完成
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<Button
|
<Button
|
||||||
|
type="primary"
|
||||||
size="small"
|
size="small"
|
||||||
icon={<FlaskConical className="size-3.5" />}
|
onClick={() => {
|
||||||
loading={testingModel === activeModel}
|
if (activeModel) message.success(`${activeModel} 配置已自动保存`);
|
||||||
disabled={Boolean(testingModel && testingModel !== activeModel)}
|
setActiveModel(null);
|
||||||
onClick={() => void testModel(activeModel, activeCapability, activeProtocol)}
|
}}
|
||||||
>
|
>
|
||||||
测试模型
|
完成
|
||||||
</Button>
|
</Button>
|
||||||
) : null
|
)
|
||||||
|
}
|
||||||
|
footer={
|
||||||
|
<div className="flex items-center justify-between py-1">
|
||||||
|
<span className="flex items-center gap-1.5 text-xs text-foreground/50">
|
||||||
|
<span className="size-1.5 rounded-full bg-emerald-500" />
|
||||||
|
更改已实时自动保存到渠道配置
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
onClick={() => {
|
||||||
|
if (activeModel) message.success(`${activeModel} 配置已生效`);
|
||||||
|
setActiveModel(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
完成配置并关闭
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{activeModel && activeCapability && activeProtocol ? (
|
{activeModel ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="rounded-md bg-surface-active px-3 py-2.5">
|
<div className="rounded-md bg-surface-active px-3 py-2.5">
|
||||||
<div className="text-xs font-medium">模型能力与请求协议</div>
|
<div className="text-xs font-medium">模型能力与请求协议</div>
|
||||||
@@ -126,8 +171,7 @@ export function ChannelModelSettings({ channel, onChange }: { channel: ModelChan
|
|||||||
<CapabilityCardPicker
|
<CapabilityCardPicker
|
||||||
value={activeCapability}
|
value={activeCapability}
|
||||||
onChange={(nextCapability) => {
|
onChange={(nextCapability) => {
|
||||||
const nextProtocol = availableProtocols.find((item) => item.value === activeProtocol && item.capability === nextCapability)?.value || availableProtocols.find((item) => item.capability === nextCapability)?.value;
|
const nextProtocol = availableProtocols.find((item) => item.value === activeProtocol && item.capability === nextCapability)?.value || availableProtocols.find((item) => item.capability === nextCapability && item.enabled !== false)?.value || defaultProtocolForCapability(nextCapability, availableProtocols);
|
||||||
if (!nextProtocol) return;
|
|
||||||
updateCost(activeModel, {
|
updateCost(activeModel, {
|
||||||
protocol: nextProtocol,
|
protocol: nextProtocol,
|
||||||
capability: nextCapability,
|
capability: nextCapability,
|
||||||
@@ -137,15 +181,21 @@ export function ChannelModelSettings({ channel, onChange }: { channel: ModelChan
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
{availableProtocols.length ? <section className="space-y-2">
|
{availableProtocols.length ? (
|
||||||
<div className="text-xs font-medium">请求协议</div>
|
<section className="space-y-2">
|
||||||
<ProtocolCardPicker
|
<div className="text-xs font-medium">请求协议</div>
|
||||||
capability={activeCapability}
|
<ProtocolCardPicker
|
||||||
value={activeProtocol}
|
capability={activeCapability}
|
||||||
protocols={availableProtocols}
|
value={activeProtocol}
|
||||||
onChange={(nextProtocol) => updateCost(activeModel, { protocol: nextProtocol, billingMode: activeBillingMode === "token" && !modelProtocolSupportsTokenBilling(activeCapability, nextProtocol) ? "fixed_request" : activeBillingMode, capabilityConfig: activeCapability === "image" || activeCapability === "video" ? defaultModelCapabilityConfig(nextProtocol, activeModel) : undefined })}
|
protocols={availableProtocols}
|
||||||
/>
|
onChange={(nextProtocol) => updateCost(activeModel, {
|
||||||
</section> : null}
|
protocol: nextProtocol,
|
||||||
|
billingMode: activeBillingMode === "token" && !modelProtocolSupportsTokenBilling(activeCapability, nextProtocol) ? "fixed_request" : activeBillingMode,
|
||||||
|
capabilityConfig: activeCapability === "image" || activeCapability === "video" ? defaultModelCapabilityConfig(nextProtocol, activeModel) : undefined,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
) : null}
|
||||||
{activeCapability === "video" ? (
|
{activeCapability === "video" ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="text-xs font-medium">计费方式</div>
|
<div className="text-xs font-medium">计费方式</div>
|
||||||
@@ -178,7 +228,13 @@ export function ChannelModelSettings({ channel, onChange }: { channel: ModelChan
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{activeCapability === "image" || activeCapability === "video" ? (
|
{activeCapability === "image" || activeCapability === "video" ? (
|
||||||
<ModelCapabilityEditor capability={activeCapability} model={activeModel} value={activeModelCost?.capabilityConfig || defaultModelCapabilityConfig(activeProtocol, activeModel)} protocol={activeProtocol} onChange={(capabilityConfig) => updateCost(activeModel, { capabilityConfig })} />
|
<ModelCapabilityEditor
|
||||||
|
capability={activeCapability}
|
||||||
|
model={activeModel}
|
||||||
|
value={activeModelCost?.capabilityConfig || defaultModelCapabilityConfig(activeProtocol, activeModel)}
|
||||||
|
protocol={activeProtocol}
|
||||||
|
onChange={(capabilityConfig) => updateCost(activeModel, { capabilityConfig })}
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -187,8 +243,81 @@ export function ChannelModelSettings({ channel, onChange }: { channel: ModelChan
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function defaultProtocolForModel(channel: ModelChannel, model: string): ModelProtocol {
|
function inferCapabilityFromModel(model: string): ModelCapabilityChoice {
|
||||||
return channel.interfaceType || "";
|
const lower = model.toLowerCase();
|
||||||
|
if (
|
||||||
|
lower.includes("seedream") ||
|
||||||
|
lower.includes("image") ||
|
||||||
|
lower.includes("dall-e") ||
|
||||||
|
lower.includes("dalle") ||
|
||||||
|
lower.includes("flux") ||
|
||||||
|
lower.includes("imagen") ||
|
||||||
|
lower.includes("banana") ||
|
||||||
|
lower.includes("midjourney") ||
|
||||||
|
lower.includes("sdxl") ||
|
||||||
|
lower.includes("stable-diffusion")
|
||||||
|
) {
|
||||||
|
return "image";
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
lower.includes("video") ||
|
||||||
|
lower.includes("sora") ||
|
||||||
|
lower.includes("veo") ||
|
||||||
|
lower.includes("kling") ||
|
||||||
|
lower.includes("seedance") ||
|
||||||
|
lower.includes("minimax") ||
|
||||||
|
lower.includes("hailuo") ||
|
||||||
|
lower.includes("pika") ||
|
||||||
|
lower.includes("runway") ||
|
||||||
|
lower.includes("omni") ||
|
||||||
|
lower.includes("cogvideo") ||
|
||||||
|
lower.includes("wan")
|
||||||
|
) {
|
||||||
|
return "video";
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
lower.includes("audio") ||
|
||||||
|
lower.includes("tts") ||
|
||||||
|
lower.includes("voice") ||
|
||||||
|
lower.includes("speech") ||
|
||||||
|
lower.includes("sound") ||
|
||||||
|
lower.includes("music")
|
||||||
|
) {
|
||||||
|
return "audio";
|
||||||
|
}
|
||||||
|
return "text";
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultProtocolForCapability(capability: ModelCapabilityChoice, availableProtocols: ModelProtocolDefinition[]): ModelProtocol {
|
||||||
|
const standardProtocols: Record<string, string[]> = {
|
||||||
|
text: ["chat-completion", "openai-response"],
|
||||||
|
image: ["openai-image"],
|
||||||
|
video: ["newapi-channel-2", "newapi"],
|
||||||
|
audio: ["openai-audio"],
|
||||||
|
};
|
||||||
|
const preferred = standardProtocols[capability] || [];
|
||||||
|
for (const id of preferred) {
|
||||||
|
if (availableProtocols.some((p) => p.value === id && p.enabled !== false)) {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const matched = availableProtocols.find((p) => p.capability === capability && p.enabled !== false);
|
||||||
|
if (matched) return matched.value;
|
||||||
|
const fallbackMap: Record<string, string> = {
|
||||||
|
text: "chat-completion",
|
||||||
|
image: "openai-image",
|
||||||
|
video: "newapi-channel-2",
|
||||||
|
audio: "openai-audio",
|
||||||
|
};
|
||||||
|
return fallbackMap[capability] || "chat-completion";
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultProtocolForModel(channel: ModelChannel, model: string, availableProtocols: ModelProtocolDefinition[] = []): ModelProtocol {
|
||||||
|
if (channel.interfaceType) {
|
||||||
|
return channel.interfaceType;
|
||||||
|
}
|
||||||
|
const capability = inferCapabilityFromModel(model);
|
||||||
|
return defaultProtocolForCapability(capability, availableProtocols);
|
||||||
}
|
}
|
||||||
|
|
||||||
function capabilityLabel(value: ModelCost["capability"]) {
|
function capabilityLabel(value: ModelCost["capability"]) {
|
||||||
|
|||||||
Reference in new issue
Block a user