refactor(registry): B2 migrate to LiteLLM-style schema — unified models[] with kind field

- 71 registry files: flat `media.*Config.models` → `models[]` with `kind` field
- `media` wrapper removed → serviceKinds, *Config fields promoted top-level
- `type` field renamed to `kind` (llm/image/tts/stt/embedding/embedding/video/music)
- providers/index.js: PROVIDER_MEDIA now built from flat top-level media fields
- shared/constants/providers.js: buildProviderEntry reads flat top-level media fields
- route /v1/models: modelKind() uses kind||type; removed subConfig merge block
- models/info route: removed sub-config fallback lookup (all models in PROVIDER_MODELS)
- ttsProviders/index.js: synthesizeViaConfig reads tts models from PROVIDER_MODELS
- test-models route, helpers.js, validate route: kind||type compat
- Baselines: PROVIDERS 62/62 ✅, Alias 90/90 ✅

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
decolua
2026-06-14 14:32:59 +07:00
parent e53ce79abb
commit dd1e0f9bcc
82 changed files with 2518 additions and 3651 deletions

View File

@@ -44,14 +44,14 @@ export async function POST(request, { params }) {
// Warm up with first model to trigger token refresh (if needed) before parallel calls.
// This prevents race condition where multiple requests concurrently refresh the same token.
const [first, ...rest] = models;
const firstKind = first.type || "llm";
const firstKind = first.kind || first.type || "llm";
const firstResult = await pingModelByKind(`${alias}/${first.id}`, firstKind, baseUrl);
const results = [{ modelId: first.id, name: first.name || first.id, ...firstResult }];
if (rest.length > 0) {
const restResults = await Promise.all(
rest.map(async (model) => {
const result = await pingModelByKind(`${alias}/${model.id}`, model.type || "llm", baseUrl);
const result = await pingModelByKind(`${alias}/${model.id}`, model.kind || model.type || "llm", baseUrl);
return { modelId: model.id, name: model.name || model.id, ...result };
})
);

View File

@@ -75,7 +75,7 @@ async function probeMediaProvider(provider, apiKey) {
const res = await fetch(cfg.baseUrl, {
method,
headers,
body: method === "GET" ? undefined : JSON.stringify({ input: "ping", text: "ping", prompt: "ping", model: cfg.models?.[0]?.id || "test" }),
body: method === "GET" ? undefined : JSON.stringify({ input: "ping", text: "ping", prompt: "ping", model: getDefaultModel(provider) || "test" }),
signal: AbortSignal.timeout(8000),
});
return res.status !== 401 && res.status !== 403;

View File

@@ -52,21 +52,10 @@ function lookup(fullId) {
const list = PROVIDER_MODELS[alias] || PROVIDER_MODELS[providerId] || [];
const m = list.find((x) => x.id === modelId);
if (m) {
const kind = m.type || "llm";
const kind = m.kind || m.type || "llm";
return buildInfo({ alias, providerId, model: m, kind, providerInfo });
}
// Sub-configs (TTS/STT/embedding only-in-config)
const subs = [
["tts", providerInfo?.ttsConfig],
["stt", providerInfo?.sttConfig],
["embedding", providerInfo?.embeddingConfig],
];
for (const [kind, cfg] of subs) {
const sm = cfg?.models?.find((x) => x.id === modelId);
if (sm) return buildInfo({ alias, providerId, model: sm, kind, providerInfo });
}
// Web search/fetch — virtual model id "search" / "fetch"
if (modelId === "search" && providerInfo?.searchConfig) {
return buildInfo({

View File

@@ -59,8 +59,9 @@ const MODEL_TYPE_TO_KIND = {
};
function modelKind(model) {
if (!model?.type) return LLM_KIND;
return MODEL_TYPE_TO_KIND[model.type] || LLM_KIND;
const k = model?.kind || model?.type;
if (!k) return LLM_KIND;
return MODEL_TYPE_TO_KIND[k] || LLM_KIND;
}
// For dynamic/unknown model IDs (compatible providers, alias map, custom models)
@@ -360,29 +361,8 @@ export async function buildModelsList(kindFilter) {
});
}
// Merge sub-config models (TTS / embedding) that live on AI_PROVIDERS, not PROVIDER_MODELS
const providerInfo = AI_PROVIDERS[providerId];
const subConfigModels = [];
if (kindFilter.includes("tts") && Array.isArray(providerInfo?.ttsConfig?.models)) {
for (const m of providerInfo.ttsConfig.models) {
if (m?.id) subConfigModels.push(m.id);
}
}
if (kindFilter.includes("embedding") && Array.isArray(providerInfo?.embeddingConfig?.models)) {
for (const m of providerInfo.embeddingConfig.models) {
if (m?.id) subConfigModels.push(m.id);
}
}
for (const subId of subConfigModels) {
if (isDisabled(outputAlias, subId) || isDisabled(staticAlias, subId)) continue;
models.push({
id: `${outputAlias}/${subId}`,
object: "model",
owned_by: outputAlias,
});
}
// Web search/fetch — provider IS the model, expose as {alias}/search and/or {alias}/fetch with explicit kind
const providerInfo = AI_PROVIDERS[providerId];
if (kindFilter.includes("webSearch") && providerInfo?.searchConfig) {
models.push({
id: `${outputAlias}/search`,