fix(models): 模型目录 - 识别公共视频能力并按声明发送分辨率

This commit is contained in:
Codex
2026-08-16 22:27:29 +08:00
parent 85aa4b3ee1
commit 6d6ad67dae
19 changed files with 792 additions and 51 deletions

View File

@@ -4,10 +4,9 @@ import { useState } from "react";
import { ChannelHeadersEditor, validateChannelHeaders } from "@/components/channel-headers-editor";
import { WorkspaceState } from "@/components/layout/workspace-state";
import { defaultModelCapabilityConfig } from "@/lib/model-capabilities";
import { modelProtocolCapability, protocolForModelCatalog } from "@/lib/model-protocols";
import { mergeFetchedChannelModelCosts } from "@/lib/channel-model-catalog";
import { desktopLocalChannelFormState, desktopLocalChannelPayloadValue, DESKTOP_LOCAL_CHANNEL_EXAMPLE_BASE_URL } from "@/lib/desktop-local-channel";
import { fetchChannelModels, type ChannelModelCatalogItem } from "@/services/api/image";
import { fetchChannelModels } from "@/services/api/image";
import {
createModelChannel,
defaultBaseUrlForApiFormat,
@@ -21,8 +20,6 @@ import { ChannelModelSettings } from "./channel-video-pricing";
import { useUserStore } from "@/stores/use-user-store";
type UserChannelConnection = "openai" | "gemini";
type ChannelModelCost = NonNullable<ModelChannel["modelCosts"]>[number];
export function ChannelSettingsPane({ onOpenModels }: { onOpenModels: () => void }) {
const { message } = App.useApp();
const config = useConfigStore((state) => state.config);
@@ -108,7 +105,7 @@ export function ChannelSettingsPane({ onOpenModels }: { onOpenModels: () => void
return;
}
updateChannels(
latestConfig.channels.map((item) => (item.id === channel.id ? { ...item, models: result.models, modelCosts: mergeFetchedModelCosts(item, result.catalog) } : item)),
latestConfig.channels.map((item) => (item.id === channel.id ? { ...item, models: result.models, modelCosts: mergeFetchedChannelModelCosts(item, result.catalog) } : item)),
latestConfig,
);
message.success(`${latestChannel.name || "当前渠道"}模型列表已更新`);
@@ -155,7 +152,7 @@ export function ChannelSettingsPane({ onOpenModels }: { onOpenModels: () => void
updateChannels(
latestConfig.channels.map((channel) => {
const fetched = resultMap.get(channel.id);
return fetched ? { ...channel, models: fetched.models, modelCosts: mergeFetchedModelCosts(channel, fetched.catalog) } : channel;
return fetched ? { ...channel, models: fetched.models, modelCosts: mergeFetchedChannelModelCosts(channel, fetched.catalog) } : channel;
}),
latestConfig,
);
@@ -310,25 +307,6 @@ function normalizeDefaultModel(value: string, options: string[]) {
return options.includes(value) ? value : options[0] || "";
}
function mergeFetchedModelCosts(channel: ModelChannel, catalog: ChannelModelCatalogItem[]): ChannelModelCost[] {
const existingByModel = new Map((channel.modelCosts || []).map((cost) => [cost.model, cost]));
const next: ChannelModelCost[] = [];
for (const item of catalog) {
const existing = existingByModel.get(item.id);
const inferredProtocol = protocolForModelCatalog(item.supportedEndpointTypes);
if (existing) {
const inferredCapability = modelProtocolCapability(inferredProtocol);
next.push(inferredProtocol && existing.protocol !== inferredProtocol ? { ...existing, protocol: inferredProtocol, capability: inferredCapability || existing.capability, capabilityConfig: inferredCapability === "image" || inferredCapability === "video" ? defaultModelCapabilityConfig(inferredProtocol, item.id) : undefined } : existing);
continue;
}
const catalogProtocol = inferredProtocol || channel.interfaceType;
const catalogCapability = catalogProtocol ? modelProtocolCapability(catalogProtocol) : undefined;
if (!catalogProtocol || !catalogCapability) continue;
next.push({ model: item.id, capability: catalogCapability, protocol: catalogProtocol, billingMode: "fixed_request", unitPriceMicrocredits: 0, capabilityConfig: catalogCapability === "image" || catalogCapability === "video" ? defaultModelCapabilityConfig(catalogProtocol, item.id) : undefined });
}
return next;
}
function uniqueModels(models: string[]) {
return Array.from(new Set(models.map((model) => model.trim()).filter(Boolean)));
}

View File

@@ -64,14 +64,15 @@ export function ChannelModelSettings({ channel, onChange }: { channel: ModelChan
const protocol = cost?.protocol || defaultProtocolForModel(channel, model);
const capability = cost?.capability || modelProtocolCapability(protocol) || "text";
const billingMode = cost?.billingMode || "fixed_request";
const displayName = cost?.displayName?.trim() || model;
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">
<span className="grid size-8 shrink-0 place-items-center rounded-md bg-foreground/[.045] text-foreground/65">
<Settings2 className="size-4" />
</span>
<div className="min-w-0 flex-1">
<div className="truncate text-xs font-medium" title={model}>
{model}
<div className="truncate text-xs font-medium" title={displayName === model ? model : `${displayName} (${model})`}>
{displayName}
</div>
<div className="mt-1 flex min-w-0 flex-wrap items-center gap-1.5">
<Tag className="mr-0 text-[var(--fs-tiny)]" bordered={false}>