fix(models): normalize media endpoint metadata (#11397)

Validado em lote combinado (batch-0824g) contra o tip de release/v3.8.50: typecheck:core limpo, gates estáticos OK, 62/62 testes focados passando (endpoint/parser/schema/static-model + catálogo).

Canonicaliza metadados de endpoint legados (video/audio) para IDs específicos por operação, mantendo compatibilidade retroativa via `normalizeModelSupportedEndpoints` (valores antigos `audio`/`video` continuam válidos como entrada e são normalizados na escrita). Obrigado pela contribuição, primeira PR bem-vinda!
This commit is contained in:
Marcelo Karval
2026-08-24 17:23:48 -03:00
committed by GitHub
parent f93fecd86b
commit 11cbd7d4e0
11 changed files with 233 additions and 55 deletions

View File

@@ -69,6 +69,7 @@ import {
import { createModelCapabilityResolutionSnapshot } from "@/lib/modelCapabilityResolutionSnapshot";
import { getModelsDevPricing, getSyncedCapability } from "@/lib/modelsDevSync";
import { getModelSpec } from "@/shared/constants/modelSpecs";
import { classifyModelSupportedEndpoints } from "@/shared/constants/modelSupportedEndpoints";
import { getModelsCatalogPrefixMode } from "@/shared/utils/featureFlags";
import { buildReservedPrefixes, selectCompatibleNodeForPrefix } from "@/lib/providerNodePrefixes";
import { applyCatalogPostFilters, finalizeCatalogResponse } from "./catalogResponse";
@@ -1152,18 +1153,15 @@ async function buildUnifiedModelsResponseCore(
const aliasId = `${alias}/${displayModelId}`;
const endpoints = Array.isArray(sm.supportedEndpoints) ? sm.supportedEndpoints : ["chat"];
const apiFormat = typeof sm.apiFormat === "string" ? sm.apiFormat : "chat-completions";
let modelType: string | undefined;
if (endpoints.includes("embeddings")) modelType = "embedding";
else if (endpoints.includes("rerank")) modelType = "rerank";
else if (endpoints.includes("images")) modelType = "image";
else if (endpoints.includes("audio")) modelType = "audio";
const classification = classifyModelSupportedEndpoints(endpoints);
const modelType = classification.type;
// Same owned_by the alias/canonical entries below will carry — computed once
// so the effort_tiers exclusion (codex/glm/kimi) and the entries agree.
const syncedOwnedBy = resolvePublicOwnerId(providerId, canonicalProviderId);
const syncedFields = {
...(modelType ? { type: modelType } : {}),
...(apiFormat !== "chat-completions" ? { api_format: apiFormat } : {}),
...(modelType === "audio" ? { subtype: "transcription" } : {}),
...(classification.subtype ? { subtype: classification.subtype } : {}),
...(sm.inputTokenLimit ? { context_length: sm.inputTokenLimit } : {}),
...(typeof sm.outputTokenLimit === "number"
? { max_output_tokens: sm.outputTokenLimit }
@@ -1604,11 +1602,8 @@ async function buildUnifiedModelsResponseCore(
: ["chat"];
const apiFormat =
typeof model.apiFormat === "string" ? model.apiFormat : "chat-completions";
let modelType: string | undefined;
if (endpoints.includes("embeddings")) modelType = "embedding";
else if (endpoints.includes("rerank")) modelType = "rerank";
else if (endpoints.includes("images")) modelType = "image";
else if (endpoints.includes("audio")) modelType = "audio";
const classification = classifyModelSupportedEndpoints(endpoints);
const modelType = classification.type;
if (
modelType &&
hasEquivalentSpecialtyModel(canonicalProviderId, modelId, modelType, aliasId)
@@ -1631,6 +1626,7 @@ async function buildUnifiedModelsResponseCore(
parent: null,
custom: true,
...(modelType ? { type: modelType } : {}),
...(classification.subtype ? { subtype: classification.subtype } : {}),
...(apiFormat !== "chat-completions" ? { api_format: apiFormat } : {}),
...(endpoints.length > 1 || !endpoints.includes("chat")
? { supported_endpoints: endpoints }