Browse Source

优化模型分类映射

TEST-s
weige 2 months ago
parent
commit
ec549b242c
  1. 4
      src/app/api/__tests__/popiserverModels.test.ts
  2. 50
      src/app/api/_popiserverModels.ts
  3. 4
      src/app/api/models/route.ts
  4. 12
      src/components/FloatingActionBar.tsx
  5. 113
      src/components/modals/ModelSearchDialog.tsx
  6. 8
      src/i18n/index.tsx
  7. 3
      src/lib/providers/types.ts
  8. 30
      src/store/modelStore.ts
  9. 55
      src/utils/popiModelClassification.ts
  10. 15
      src/utils/popiModelFilters.ts

4
src/app/api/__tests__/popiserverModels.test.ts

@ -38,14 +38,14 @@ describe("popiserver model helpers", () => {
});
it("passes only the model subType filter when listing models", async () => {
const params = new URLSearchParams({ subType: "202,203,204" });
const params = new URLSearchParams({ subType: "202,203,204,205" });
await fetchPopiModels(makeRequest(), "login-token", undefined, params);
expect(global.fetch).toHaveBeenCalledTimes(1);
const url = String((global.fetch as unknown as ReturnType<typeof vi.fn>).mock.calls[0][0]);
const searchParams = new URL(url).searchParams;
expect(searchParams.get("subType")).toBe("202,203,204");
expect(searchParams.get("subType")).toBe("202,203,204,205");
expect(searchParams.get("origin")).toBe("canvas");
expect(searchParams.has("type")).toBe(false);
expect(searchParams.has("subtype")).toBe(false);

50
src/app/api/_popiserverModels.ts

@ -3,6 +3,11 @@ import { ApiError } from "./_errors";
import { logGatewayRequest, logGatewayResponse } from "./_gatewayLogging";
import type { ProviderType } from "@/types";
import type { ModelInput, ModelCapability, ModelParameter, ProviderModel } from "@/lib/providers/types";
import {
getPopiCapabilitiesForSubType,
getPopiSubTypeFilter,
type PopiModelMediaKind,
} from "@/utils/popiModelClassification";
const POPISERVER_MODEL_LIST_PATH = "/api_client/anime/ai/model/list";
const POPISERVER_MODEL_DETAIL_PATH = "/api_client/anime/ai/model/detail";
@ -17,13 +22,13 @@ type PopiCategory = {
taskSubType?: number | string;
};
export type PopiMediaKind = "image" | "video" | "audio" | "llm";
export type PopiMediaKind = PopiModelMediaKind;
export const POPI_MODEL_FILTERS: Record<PopiMediaKind, { subType: string }> = {
image: { subType: "102,103,106" },
video: { subType: "202,203,204" },
audio: { subType: "301" },
llm: { subType: "501" },
image: { subType: getPopiSubTypeFilter("image") },
video: { subType: getPopiSubTypeFilter("video") },
audio: { subType: getPopiSubTypeFilter("audio") },
llm: { subType: getPopiSubTypeFilter("llm") },
};
export type PopiAIModel = {
@ -88,25 +93,6 @@ export type SchemaErrorResponse = {
export type SchemaResponse = SchemaSuccessResponse | SchemaErrorResponse;
const SUBTYPE_TO_CAPABILITIES: Record<number, ModelCapability[]> = {
101: ["text-to-image", "image-to-image"],
102: ["text-to-image"],
103: ["image-to-image"],
104: ["image-to-image"],
105: ["image-to-image"],
106: ["image-to-image"],
107: ["image-to-image"],
108: ["image-to-image"],
109: ["image-to-image"],
201: ["text-to-video"],
202: ["image-to-video"],
203: ["image-to-video"],
204: ["image-to-video"],
205: ["image-to-video"],
301: ["text-to-audio"],
302: ["text-to-audio"],
};
export function getPopiserverBaseUrl(request: NextRequest): string {
const requestOrigin =
"nextUrl" in request && request.nextUrl
@ -178,23 +164,10 @@ function getTaskSubTypes(model: PopiAIModel): Set<number> {
export function inferPopiModelCapabilities(model: PopiAIModel): ModelCapability[] {
const capabilities = new Set<ModelCapability>();
for (const subType of getTaskSubTypes(model)) {
for (const capability of SUBTYPE_TO_CAPABILITIES[subType] || []) {
for (const capability of getPopiCapabilitiesForSubType(subType)) {
capabilities.add(capability);
}
}
if (capabilities.size === 0) {
if (model.isSupportVideos) {
capabilities.add("text-to-video");
if (model.isSupportImages) capabilities.add("image-to-video");
} else if (model.isSupportAudios) {
capabilities.add("text-to-audio");
} else {
capabilities.add("text-to-image");
if (model.isSupportImages) capabilities.add("image-to-image");
}
}
return [...capabilities];
}
@ -238,6 +211,7 @@ export function popiMediaKindForCapabilities(
capabilities: ModelCapability[] | null
): PopiMediaKind | null {
if (!capabilities || capabilities.length === 0) return null;
if (capabilities.some((capability) => capability === "text-to-text")) return "llm";
if (capabilities.some((capability) => capability.includes("audio"))) return "audio";
if (capabilities.some((capability) => capability.includes("video"))) return "video";
if (capabilities.some((capability) => capability.includes("image"))) return "image";

4
src/app/api/models/route.ts

@ -1137,7 +1137,9 @@ export async function GET(
}
const popiModels = await fetchPopiModels(request, popiLoginToken, searchQuery, popiserverParams);
const providerModels = popiModels.map(popiModelToProviderModel);
const providerModels = popiModels
.map(popiModelToProviderModel)
.filter((model) => model.capabilities.length > 0);
const filteredModels = filterProviderModelsByCapabilities(providerModels, capabilitiesFilter);
return NextResponse.json<ModelsSuccessResponse>({

12
src/components/FloatingActionBar.tsx

@ -17,7 +17,6 @@ const ALL_NODES_CATEGORIES: { labelKey: TranslationKey; nodes: { type: NodeType;
{ type: "imageInput", labelKey: "node.imageInput" },
{ type: "audioInput", labelKey: "node.audioInput" },
{ type: "videoInput", labelKey: "node.videoInput" },
{ type: "glbViewer", labelKey: "node.glbViewer" },
],
},
{
@ -33,7 +32,6 @@ const ALL_NODES_CATEGORIES: { labelKey: TranslationKey; nodes: { type: NodeType;
nodes: [
{ type: "nanoBanana", labelKey: "node.generateImage" },
{ type: "generateVideo", labelKey: "node.generateVideo" },
{ type: "generate3d", labelKey: "node.generate3d" },
{ type: "generateAudio", labelKey: "node.generateAudio" },
{ type: "llmGenerate", labelKey: "node.llmGenerate" },
],
@ -251,15 +249,17 @@ function GenerateComboButton() {
{t("toolbar.video")}
</button>
<button
onClick={() => handleAddNode("generate3d")}
onClick={() => handleAddNode("generateAudio")}
draggable
onDragStart={(e) => handleDragStart(e, "generate3d")}
onDragStart={(e) => handleDragStart(e, "generateAudio")}
className="w-full px-3 py-2 text-left text-[11px] font-medium text-neutral-300 hover:bg-neutral-700 hover:text-neutral-100 transition-colors flex items-center gap-2 cursor-grab active:cursor-grabbing"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="m21 7.5-9-5.25L3 7.5m18 0-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-9v9" />
<path strokeLinecap="round" strokeLinejoin="round" d="M9 18V5l12-2v13" />
<circle cx="6" cy="18" r="3" />
<circle cx="18" cy="16" r="3" />
</svg>
3D
{t("toolbar.audio")}
</button>
<button
onClick={() => handleAddNode("llmGenerate")}

113
src/components/modals/ModelSearchDialog.tsx

@ -14,6 +14,7 @@ import {
POPI_PROVIDER_ID,
} from "@/lib/providerMode";
import { useModelStore } from "@/store/modelStore";
import { getPopiCapabilitiesForKind, getPopiKindForSubType } from "@/utils/popiModelClassification";
// localStorage cache for models (persists across dev server restarts)
const MODELS_CACHE_KEY = "node-banana-models-cache-v2";
@ -117,9 +118,9 @@ function getPaneCenter() {
}
// Capability filter options
type CapabilityFilter = "all" | "image" | "video" | "3d" | "audio";
type CapabilityFilter = "all" | "image" | "video" | "3d" | "audio" | "llm";
const POPI_MEDIA_FILTERS: Exclude<CapabilityFilter, "all">[] = ["image", "video", "audio", "3d"];
const POPI_MEDIA_FILTERS: Exclude<CapabilityFilter, "all">[] = ["image", "video", "audio", "llm"];
function getEffectiveCapabilityFilter(
capabilityFilter: CapabilityFilter,
@ -130,15 +131,38 @@ function getEffectiveCapabilityFilter(
function modelMatchesCapability(model: ProviderModel, filter: CapabilityFilter): boolean {
if (filter === "all") return true;
if (filter === "image") return model.capabilities.some((capability) => capability === "text-to-image" || capability === "image-to-image");
if (filter === "video") return model.capabilities.some((capability) =>
capability === "text-to-video" ||
capability === "image-to-video" ||
capability === "video-to-video" ||
capability === "audio-to-video"
);
if (filter === "audio") return model.capabilities.some((capability) => capability === "text-to-audio");
return model.capabilities.some((capability) => capability === "text-to-3d" || capability === "image-to-3d");
if (filter === "3d") return model.capabilities.some((capability) => capability === "text-to-3d" || capability === "image-to-3d");
return modelHasPopiKind(model, filter);
}
function getModelSubTypes(model: ProviderModel): number[] {
const subTypes = model.metadata?.subTypes;
if (Array.isArray(subTypes)) {
return subTypes.map((subType) => Number(subType)).filter((subType) => Number.isFinite(subType));
}
const subType = Number(model.metadata?.subType);
return Number.isFinite(subType) ? [subType] : [];
}
function modelHasPopiKind(model: ProviderModel, filter: Exclude<CapabilityFilter, "all" | "3d">): boolean {
if (getModelSubTypes(model).some((subType) => getPopiKindForSubType(subType) === filter)) return true;
return model.capabilities.some((capability) => getPopiCapabilitiesForKind(filter).includes(capability));
}
function capabilitiesForFilter(filter: CapabilityFilter, fallback: ModelCapability[]): ModelCapability[] {
if (filter === "image" || filter === "video" || filter === "audio" || filter === "llm") {
return getPopiCapabilitiesForKind(filter);
}
if (filter === "3d") return ["text-to-3d", "image-to-3d"];
return fallback;
}
function capabilityFilterToNodeType(filter: CapabilityFilter): "nanoBanana" | "generateVideo" | "generate3d" | "generateAudio" | "llmGenerate" {
if (filter === "video") return "generateVideo";
if (filter === "3d") return "generate3d";
if (filter === "audio") return "generateAudio";
if (filter === "llm") return "llmGenerate";
return "nanoBanana";
}
function filterModelsBySearch(models: ProviderModel[], searchQuery: string): ProviderModel[] {
@ -288,15 +312,7 @@ export function ModelSearchDialog({
params.set("provider", activeProviderFilter);
}
if (effectiveCapabilityFilter !== "all") {
const capabilities =
effectiveCapabilityFilter === "image"
? "text-to-image,image-to-image"
: effectiveCapabilityFilter === "video"
? "text-to-video,image-to-video,audio-to-video"
: effectiveCapabilityFilter === "3d"
? "text-to-3d,image-to-3d"
: "text-to-audio";
params.set("capabilities", capabilities);
params.set("capabilities", capabilitiesForFilter(effectiveCapabilityFilter, []).join(","));
}
if (bypassCache) {
params.set("refresh", "true");
@ -428,7 +444,13 @@ export function ModelSearchDialog({
// If onModelSelected is provided, use it to update an existing node
if (onModelSelected) {
onModelSelected(model);
const effectiveCapabilityFilter = getEffectiveCapabilityFilter(capabilityFilter, popiProviderMode);
onModelSelected({
...model,
capabilities: popiProviderMode
? capabilitiesForFilter(effectiveCapabilityFilter, model.capabilities)
: model.capabilities,
});
onClose();
return;
}
@ -441,6 +463,7 @@ export function ModelSearchDialog({
});
// Determine node type based on model capabilities
const effectiveCapabilityFilter = getEffectiveCapabilityFilter(capabilityFilter, popiProviderMode);
const isVideoModel = model.capabilities.some(
(cap) => cap === "text-to-video" || cap === "image-to-video" || cap === "audio-to-video"
);
@ -450,15 +473,29 @@ export function ModelSearchDialog({
const isAudioModel = model.capabilities.some(
(cap) => cap === "text-to-audio"
);
const nodeType = isVideoModel ? "generateVideo" : is3DModel ? "generate3d" : isAudioModel ? "generateAudio" : "nanoBanana";
const isLlmModel = modelMatchesCapability(model, "llm");
const nodeType = popiProviderMode
? capabilityFilterToNodeType(effectiveCapabilityFilter)
: isLlmModel
? "llmGenerate"
: isVideoModel
? "generateVideo"
: is3DModel
? "generate3d"
: isAudioModel
? "generateAudio"
: "nanoBanana";
const selectedCapabilities = popiProviderMode
? capabilitiesForFilter(effectiveCapabilityFilter, model.capabilities)
: model.capabilities;
addNode(nodeType, position, {
selectedModel: {
provider: model.provider,
modelId: model.id,
displayName: model.name,
capabilities: model.capabilities,
capabilities: selectedCapabilities,
metadata: model.metadata,
pricing: model.pricing
? {
@ -472,7 +509,7 @@ export function ModelSearchDialog({
onClose();
},
[screenToFlowPosition, addNode, onClose, onModelSelected, trackModelUsage]
[screenToFlowPosition, addNode, onClose, onModelSelected, trackModelUsage, capabilityFilter, popiProviderMode]
);
// Handle escape key
@ -574,24 +611,7 @@ export function ModelSearchDialog({
if (effectiveCapabilityFilter === "all") return true;
if (!matchingModel) return true; // Show if we can't verify capabilities
const isImage = matchingModel.capabilities.some(
(cap) => cap === "text-to-image" || cap === "image-to-image"
);
const isVideo = matchingModel.capabilities.some(
(cap) => cap === "text-to-video" || cap === "image-to-video" || cap === "audio-to-video"
);
const is3D = matchingModel.capabilities.some(
(cap) => cap === "text-to-3d" || cap === "image-to-3d"
);
const isAudio = matchingModel.capabilities.some(
(cap) => cap === "text-to-audio"
);
if (effectiveCapabilityFilter === "image") return isImage;
if (effectiveCapabilityFilter === "video") return isVideo;
if (effectiveCapabilityFilter === "3d") return is3D;
if (effectiveCapabilityFilter === "audio") return isAudio;
return true;
return modelMatchesCapability(matchingModel, effectiveCapabilityFilter);
})
.slice(0, 4); // Show max 4
}, [recentModels, models, capabilityFilter, popiProviderMode]);
@ -667,6 +687,10 @@ export function ModelSearchDialog({
color = "bg-violet-500/20 text-violet-300";
label = "audio→vid";
break;
case "text-to-text":
color = "bg-sky-500/20 text-sky-300";
label = "txt→txt";
break;
}
if (label) {
@ -688,6 +712,7 @@ export function ModelSearchDialog({
if (filter === "image") return t("modelSearch.image");
if (filter === "video") return t("modelSearch.video");
if (filter === "audio") return t("modelSearch.audio");
if (filter === "llm") return t("modelSearch.llm");
if (filter === "3d") return "3D";
return t("modelSearch.allTypes");
};
@ -880,8 +905,8 @@ export function ModelSearchDialog({
<option value="all">{t("modelSearch.allTypes")}</option>
<option value="image">{t("modelSearch.image")}</option>
<option value="video">{t("modelSearch.video")}</option>
<option value="3d">3D</option>
<option value="audio">{t("modelSearch.audio")}</option>
<option value="llm">{t("modelSearch.llm")}</option>
</select>
)}

8
src/i18n/index.tsx

@ -220,6 +220,7 @@ const en = {
"toolbar.categoryOutput": "Output",
"toolbar.image": "Image",
"toolbar.video": "Video",
"toolbar.audio": "Audio",
"toolbar.prompt": "Prompt",
"toolbar.generate": "Generate",
"toolbar.output": "Output",
@ -699,6 +700,7 @@ const en = {
"modelSearch.image": "Image",
"modelSearch.video": "Video",
"modelSearch.audio": "Audio",
"modelSearch.llm": "LLM",
"modelSearch.refresh": "Refresh models & schemas",
"modelSearch.loading": "Loading models...",
"modelSearch.tryAgain": "Try Again",
@ -1036,6 +1038,7 @@ const zhCN: Dictionary = {
"toolbar.categoryOutput": "输出",
"toolbar.image": "图片",
"toolbar.video": "视频",
"toolbar.audio": "音频",
"toolbar.prompt": "提示词",
"toolbar.generate": "生成",
"toolbar.output": "输出",
@ -1515,6 +1518,7 @@ const zhCN: Dictionary = {
"modelSearch.image": "图片",
"modelSearch.video": "视频",
"modelSearch.audio": "音频",
"modelSearch.llm": "LLM",
"modelSearch.refresh": "刷新模型和参数",
"modelSearch.loading": "正在加载模型...",
"modelSearch.tryAgain": "重试",
@ -1727,6 +1731,7 @@ const zhTW: Dictionary = {
"toolbar.categoryOutput": "輸出",
"toolbar.image": "圖片",
"toolbar.video": "影片",
"toolbar.audio": "音訊",
"toolbar.prompt": "提示詞",
"toolbar.allNodes": "所有節點",
"toolbar.allModels": "所有模型",
@ -2081,6 +2086,7 @@ const zhTW: Dictionary = {
"modelSearch.image": "圖片",
"modelSearch.video": "影片",
"modelSearch.audio": "音訊",
"modelSearch.llm": "LLM",
"modelSearch.refresh": "重新整理模型和參數",
"modelSearch.loading": "正在載入模型...",
"modelSearch.tryAgain": "重試",
@ -2271,6 +2277,7 @@ const ja: Dictionary = {
"toolbar.categoryOutput": "出力",
"toolbar.image": "画像",
"toolbar.video": "動画",
"toolbar.audio": "音声",
"toolbar.prompt": "プロンプト",
"toolbar.generate": "生成",
"toolbar.output": "出力",
@ -2698,6 +2705,7 @@ const ja: Dictionary = {
"modelSearch.image": "画像",
"modelSearch.video": "動画",
"modelSearch.audio": "音声",
"modelSearch.llm": "LLM",
"modelSearch.refresh": "モデルとスキーマを更新",
"modelSearch.loading": "モデルを読み込み中...",
"modelSearch.tryAgain": "再試行",

3
src/lib/providers/types.ts

@ -20,7 +20,8 @@ export type ModelCapability =
| "text-to-3d"
| "image-to-3d"
| "text-to-audio"
| "audio-to-video";
| "audio-to-video"
| "text-to-text";
/**
* Model parameter schema for dynamic UI generation

30
src/store/modelStore.ts

@ -1,5 +1,10 @@
import { create } from "zustand";
import type { ProviderModel, ModelCapability } from "@/lib/providers/types";
import {
getPopiCapabilitiesForKind,
getPopiKindForSubType,
type PopiModelMediaKind,
} from "@/utils/popiModelClassification";
export type PopiModelKind = "image" | "video" | "audio" | "3d" | "llm";
@ -31,24 +36,27 @@ function hasCapability(model: ProviderModel, capabilities: ModelCapability[]): b
return model.capabilities.some((capability) => capabilities.includes(capability));
}
function hasMetadataSubType(model: ProviderModel, subType: number): boolean {
function getMetadataSubTypes(model: ProviderModel): number[] {
const subTypes = model.metadata?.subTypes;
if (Array.isArray(subTypes) && subTypes.some((item) => Number(item) === subType)) return true;
return Number(model.metadata?.subType) === subType;
if (Array.isArray(subTypes)) {
return subTypes.map((item) => Number(item)).filter((item) => Number.isFinite(item));
}
const subType = Number(model.metadata?.subType);
return Number.isFinite(subType) ? [subType] : [];
}
function isPopiLlmModel(model: ProviderModel): boolean {
return Number(model.metadata?.type) === 5 || hasMetadataSubType(model, 501);
function hasPopiKind(model: ProviderModel, kind: PopiModelMediaKind): boolean {
if (getMetadataSubTypes(model).some((subType) => getPopiKindForSubType(subType) === kind)) return true;
return hasCapability(model, getPopiCapabilitiesForKind(kind));
}
function groupPopiModels(models: ProviderModel[]): Record<PopiModelKind, ProviderModel[]> {
const nonLlmModels = models.filter((model) => !isPopiLlmModel(model));
return {
image: nonLlmModels.filter((model) => hasCapability(model, ["text-to-image", "image-to-image"])),
video: nonLlmModels.filter((model) => hasCapability(model, ["text-to-video", "image-to-video", "video-to-video", "audio-to-video"])),
audio: nonLlmModels.filter((model) => hasCapability(model, ["text-to-audio"])),
"3d": nonLlmModels.filter((model) => hasCapability(model, ["text-to-3d", "image-to-3d"])),
llm: models.filter(isPopiLlmModel),
image: models.filter((model) => hasPopiKind(model, "image")),
video: models.filter((model) => hasPopiKind(model, "video")),
audio: models.filter((model) => hasPopiKind(model, "audio")),
"3d": models.filter((model) => hasCapability(model, ["text-to-3d", "image-to-3d"])),
llm: models.filter((model) => hasPopiKind(model, "llm") || Number(model.metadata?.type) === 5),
};
}

55
src/utils/popiModelClassification.ts

@ -0,0 +1,55 @@
import type { ModelCapability } from "@/lib/providers/types";
export type PopiModelMediaKind = "image" | "video" | "audio" | "llm";
type PopiSubtypeConfig = {
kind: PopiModelMediaKind;
capabilities: ModelCapability[];
};
export const POPI_SUBTYPE_CLASSIFICATION: Record<number, PopiSubtypeConfig> = {
102: { kind: "image", capabilities: ["text-to-image"] },
103: { kind: "image", capabilities: ["image-to-image"] },
104: { kind: "image", capabilities: ["image-to-image"] },
105: { kind: "image", capabilities: ["image-to-image"] },
106: { kind: "image", capabilities: ["image-to-image"] },
107: { kind: "image", capabilities: ["image-to-image"] },
108: { kind: "image", capabilities: ["image-to-image"] },
109: { kind: "image", capabilities: ["image-to-image"] },
202: { kind: "video", capabilities: ["image-to-video"] },
203: { kind: "video", capabilities: ["image-to-video"] },
204: { kind: "video", capabilities: ["image-to-video"] },
205: { kind: "video", capabilities: ["image-to-video"] },
301: { kind: "audio", capabilities: ["text-to-audio"] },
501: { kind: "llm", capabilities: ["text-to-text"] },
};
export const POPI_SUBTYPES_BY_KIND: Record<PopiModelMediaKind, number[]> = {
image: [102, 103, 104, 105, 106, 107, 108, 109],
video: [202, 203, 204, 205],
audio: [301],
llm: [501],
};
export const POPI_CAPABILITIES_BY_KIND: Record<PopiModelMediaKind, ModelCapability[]> = {
image: ["text-to-image", "image-to-image"],
video: ["text-to-video", "image-to-video", "video-to-video", "audio-to-video"],
audio: ["text-to-audio"],
llm: ["text-to-text"],
};
export function getPopiSubTypeFilter(kind: PopiModelMediaKind): string {
return POPI_SUBTYPES_BY_KIND[kind].join(",");
}
export function getPopiCapabilitiesForSubType(subType: number): ModelCapability[] {
return POPI_SUBTYPE_CLASSIFICATION[subType]?.capabilities ?? [];
}
export function getPopiKindForSubType(subType: number): PopiModelMediaKind | null {
return POPI_SUBTYPE_CLASSIFICATION[subType]?.kind ?? null;
}
export function getPopiCapabilitiesForKind(kind: PopiModelMediaKind): ModelCapability[] {
return POPI_CAPABILITIES_BY_KIND[kind];
}

15
src/utils/popiModelFilters.ts

@ -1,16 +1,13 @@
export type PopiModelMediaFilter = "image" | "video" | "audio" | "llm";
import {
getPopiSubTypeFilter,
type PopiModelMediaKind,
} from "@/utils/popiModelClassification";
const POPI_MODEL_FILTERS: Record<PopiModelMediaFilter, { subType: string }> = {
image: { subType: "102,103,106" },
video: { subType: "202,203,204" },
audio: { subType: "301" },
llm: { subType: "501" },
};
export type PopiModelMediaFilter = PopiModelMediaKind;
export function appendPopiModelQueryParams(
params: URLSearchParams,
filter: PopiModelMediaFilter
): void {
const config = POPI_MODEL_FILTERS[filter];
params.set("subType", config.subType);
params.set("subType", getPopiSubTypeFilter(filter));
}

Loading…
Cancel
Save