From a3e9c18731abb765d489f0800a6b795f41c66f74 Mon Sep 17 00:00:00 2001 From: Afonso Dutra Nogueira Filho Date: Fri, 2 Oct 2026 19:33:55 -0300 Subject: [PATCH 01/10] feat(dashboard): expose full auto-routing catalog with Control Center and Test actions The /dashboard/combos auto-routing catalog rendered only the 17 static AUTO_COMBO_TEMPLATES, hiding runtime-supported combos such as auto/gemini and the other model families, variants, and category:tier entries. The catalog now lazy-loads GET /api/combos/auto so it lists every supported virtual combo with its kind and live candidate count, falling back to the static templates when the endpoint is unavailable. Each entry gains monitoring (Control Center) and play_arrow (Test) actions next to content_copy. Since virtual combos have no persisted UUID, a shared materializeAutoCombo helper backs a new GET /api/combos/auto?id= lookup for the Control Center (skipping the UUID-only combo-health call) and lets POST /api/combos/test probe the eight highest-weighted candidates instead of 404ing. --- config/quality/eslint-suppressions.json | 5 - .../dashboard/combos/AutoComboCatalog.tsx | 258 ++++++++++++++---- .../combos/ComboControlCenterClient.tsx | 21 +- src/app/(dashboard)/dashboard/combos/page.tsx | 31 ++- src/app/api/combos/auto/route.ts | 45 ++- src/app/api/combos/test/route.ts | 40 ++- src/i18n/messages/en.json | 9 + src/i18n/messages/pt-BR.json | 9 + src/lib/combos/autoVirtual.ts | 38 +++ tests/unit/AutoComboCatalog.test.tsx | 225 ++++++++++++++- tests/unit/combo-test-auto-route.test.ts | 195 +++++++++++++ 11 files changed, 799 insertions(+), 77 deletions(-) create mode 100644 src/lib/combos/autoVirtual.ts create mode 100644 tests/unit/combo-test-auto-route.test.ts diff --git a/config/quality/eslint-suppressions.json b/config/quality/eslint-suppressions.json index 8339e9dbc881..35b5d0f7dbe1 100644 --- a/config/quality/eslint-suppressions.json +++ b/config/quality/eslint-suppressions.json @@ -745,11 +745,6 @@ "count": 2 } }, - "src/app/(dashboard)/dashboard/combos/page.tsx": { - "@typescript-eslint/no-unused-vars": { - "count": 6 - } - }, "src/app/(dashboard)/dashboard/costs/CostOverviewTab.tsx": { "@typescript-eslint/no-unused-vars": { "count": 1 diff --git a/src/app/(dashboard)/dashboard/combos/AutoComboCatalog.tsx b/src/app/(dashboard)/dashboard/combos/AutoComboCatalog.tsx index c3d695514878..743bfc3c2658 100644 --- a/src/app/(dashboard)/dashboard/combos/AutoComboCatalog.tsx +++ b/src/app/(dashboard)/dashboard/combos/AutoComboCatalog.tsx @@ -1,6 +1,7 @@ "use client"; -import { useState, useCallback } from "react"; +import Link from "next/link"; +import { useCallback, useEffect, useRef, useState } from "react"; import { useTranslations } from "next-intl"; import { Card } from "@/shared/components"; import { AUTO_COMBO_TEMPLATES, type AutoComboTemplate } from "@/domain/assessment/types"; @@ -8,34 +9,127 @@ import { AUTO_COMBO_TEMPLATES, type AutoComboTemplate } from "@/domain/assessmen // Informational catalog of zero-config auto-routing combos. // Auto combos are resolved at request time by the chat handler based on the // currently connected providers / models — they have no row in the combos -// table, so they were previously invisible in the UI. This panel surfaces -// the static catalog (name, intent, categories, tiers, strategy) so users -// can discover the auto/ prefix without reading source. Duplicate icon lets -// you materialize a snapshot into an editable static combo you can customize. +// table, so they were previously invisible in the UI. This panel surfaces the +// LIVE catalog from /api/combos/auto (variants, curated templates, +// category:tier combos and model families like auto/gemini) — falling back to +// the static template list when the endpoint is unavailable. Duplicate icon +// lets you materialize a snapshot into an editable static combo; monitoring +// opens the combo control center for the virtual combo; play_arrow runs the +// same bounded health probe used for persisted combos. + +type AutoCatalogApiEntry = { + id?: string; + name?: string; + kind?: string; + candidateCount?: number; +}; + +type CatalogItem = { + id: string; + displayName: string; + kind: "variant" | "template" | "category" | "family"; + candidateCount: number | null; + template: AutoComboTemplate | null; +}; + +const KIND_LABEL_KEYS: Record = { + variant: "autoCatalogKindVariant", + template: "autoCatalogKindTemplate", + category: "autoCatalogKindCategory", + family: "autoCatalogKindFamily", +}; + +const VALID_KINDS = new Set(["variant", "template", "category", "family"]); + +function templateItem(template: AutoComboTemplate): CatalogItem { + return { + id: template.name, + displayName: template.displayName, + kind: "template", + candidateCount: null, + template, + }; +} + +const FALLBACK_ITEMS: CatalogItem[] = AUTO_COMBO_TEMPLATES.map(templateItem); + export default function AutoComboCatalog({ onComboCreated, + onTestCombo, + testingName, }: { onComboCreated?: (comboId: string) => void; + onTestCombo?: (combo: { name: string }) => void; + testingName?: string | null; }) { const t = useTranslations("combos"); const [open, setOpen] = useState(false); const [duplicatingName, setDuplicatingName] = useState(null); + const [liveItems, setLiveItems] = useState(null); + const catalogRequestedRef = useRef(false); - const handleDuplicateTemplate = useCallback( - async (template: AutoComboTemplate) => { + // Lazy-load the live catalog the first time the panel expands so the list + // reflects every built-in auto combo (incl. model families) with live + // candidate counts — not just the static AUTO_COMBO_TEMPLATES. + useEffect(() => { + if (!open || catalogRequestedRef.current) return; + catalogRequestedRef.current = true; + let cancelled = false; + (async () => { + try { + const res = await fetch("/api/combos/auto", { cache: "no-store" }); + if (!res.ok) return; + const data = (await res.json()) as { combos?: AutoCatalogApiEntry[] }; + if (!Array.isArray(data.combos) || data.combos.length === 0) return; + const items = data.combos + .filter((entry): entry is AutoCatalogApiEntry & { id: string } => { + return typeof entry?.id === "string" && entry.id.length > 0; + }) + .map((entry): CatalogItem => { + const kind = VALID_KINDS.has(entry.kind ?? "") + ? (entry.kind as CatalogItem["kind"]) + : "variant"; + return { + id: entry.id, + displayName: + typeof entry.name === "string" && entry.name.length > 0 ? entry.name : entry.id, + kind, + candidateCount: + typeof entry.candidateCount === "number" ? entry.candidateCount : null, + template: AUTO_COMBO_TEMPLATES.find((tpl) => tpl.name === entry.id) ?? null, + }; + }); + if (!cancelled) setLiveItems(items); + } catch { + // Endpoint unavailable — the static template fallback keeps rendering. + } + })(); + return () => { + cancelled = true; + }; + }, [open]); + + const handleDuplicateEntry = useCallback( + async (item: CatalogItem) => { if ( !confirm( - `${t("duplicateAutoComboConfirm", { name: template.name })}\n\n${t("duplicateAutoComboSnapshotMsg")}` + `${t("duplicateAutoComboConfirm", { name: item.id })}\n\n${t("duplicateAutoComboSnapshotMsg")}` ) ) return; - setDuplicatingName(template.name); + setDuplicatingName(item.id); try { const res = await fetch("/api/combos/duplicate", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ name: template.name, strategy: template.strategy }), + // Curated templates keep the strategy their author picked; every + // other auto combo snapshots as an "auto"-scored combo, which most + // closely preserves the virtual combo's routing semantics. + body: JSON.stringify({ + name: item.id, + strategy: item.template?.strategy ?? "auto", + }), }); if (!res.ok) { @@ -62,6 +156,8 @@ export default function AutoComboCatalog({ [t, onComboCreated] ); + const items = liveItems ?? FALLBACK_ITEMS; + return ( - -
- {tpl.name} - - {tpl.strategy} - -
-

{tpl.displayName}

-
- {tpl.categories.map((cat) => ( - - {cat} - - ))} - {tpl.tiers.map((tier) => ( - + - {tier} + monitoring + + {onTestCombo && ( + + )} + {item.id !== "auto" && ( + + )} +
+ +
+ {item.id} + + {item.template?.strategy ?? t(KIND_LABEL_KEYS[item.kind])} - ))} +
+

{item.displayName}

+
+ {item.template ? ( + <> + {item.template.categories.map((cat) => ( + + {cat} + + ))} + {item.template.tiers.map((tier) => ( + + {tier} + + ))} + + ) : ( + item.candidateCount !== null && ( + 0 + ? "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400" + : "bg-black/[0.04] text-text-muted dark:bg-white/[0.04]" + }`} + > + {item.candidateCount > 0 + ? t("autoCatalogCandidates", { count: item.candidateCount }) + : t("autoCatalogNoCandidates")} + + ) + )} +
+ {item.template?.systemMessage && ( +

+ {item.template.systemMessage} +

+ )} - {tpl.systemMessage && ( -

- {tpl.systemMessage} -

- )} - - ))} + ); + })} )}
diff --git a/src/app/(dashboard)/dashboard/combos/ComboControlCenterClient.tsx b/src/app/(dashboard)/dashboard/combos/ComboControlCenterClient.tsx index 9513e05ef372..85e7f60acc2d 100644 --- a/src/app/(dashboard)/dashboard/combos/ComboControlCenterClient.tsx +++ b/src/app/(dashboard)/dashboard/combos/ComboControlCenterClient.tsx @@ -253,14 +253,27 @@ export default function ComboControlCenterClient({ comboId }: { comboId: string const load = useCallback(async () => { setLoading(true); try { - const comboData = await fetchJson(`/api/combos/${comboId}`); + // Built-in auto combos (auto, auto/*) are virtual — no persisted row/UUID. + // They materialize through /api/combos/auto?id= which returns the same + // combo shape; the UUID-keyed combo-health endpoint can't serve them, so + // the health lookup is skipped (it would only 400 anyway). + const isVirtualAutoCombo = comboId === "auto" || comboId.startsWith("auto/"); + const comboData = await fetchJson( + isVirtualAutoCombo + ? `/api/combos/auto?id=${encodeURIComponent(comboId)}` + : `/api/combos/${encodeURIComponent(comboId)}` + ); const [metricsData, healthData, logsData] = await Promise.all([ fetchJson( `/api/combos/metrics?combo=${encodeURIComponent(comboData.name || "")}` ).catch(() => ({ metrics: null })), - fetchJson(`/api/usage/combo-health?range=${range}&comboId=${comboId}`) - .then((data) => data.combos?.[0] || null) - .catch(() => null), + isVirtualAutoCombo + ? Promise.resolve(null) + : fetchJson( + `/api/usage/combo-health?range=${range}&comboId=${comboId}` + ) + .then((data) => data.combos?.[0] || null) + .catch(() => null), fetchJson( `/api/usage/call-logs?combo=1&search=${encodeURIComponent(comboData.name || "")}&limit=8` ).catch(() => []), diff --git a/src/app/(dashboard)/dashboard/combos/page.tsx b/src/app/(dashboard)/dashboard/combos/page.tsx index a176ad09f77a..3785fd75f2e2 100644 --- a/src/app/(dashboard)/dashboard/combos/page.tsx +++ b/src/app/(dashboard)/dashboard/combos/page.tsx @@ -833,7 +833,6 @@ function formatComboEntryDisplay( function CombosPageContent() { const t = useTranslations("combos"); const tc = useTranslations("common"); - const emailsVisible = useEmailPrivacyStore((s) => s.emailsVisible); const router = useRouter(); const searchParams = useSearchParams(); const [combos, setCombos] = useState([]); @@ -973,7 +972,7 @@ function CombosPageContent() { const err = await res.json(); notify.error(err.error?.message || err.error || t("failedCreate")); } - } catch (error) { + } catch { notify.error(t("errorCreating")); } }; @@ -993,7 +992,7 @@ function CombosPageContent() { const err = await res.json(); notify.error(err.error?.message || err.error || t("failedUpdate")); } - } catch (error) { + } catch { notify.error(t("errorUpdating")); } }; @@ -1018,7 +1017,7 @@ function CombosPageContent() { const err = await res.json().catch(() => null); notify.error(err?.error?.message || err?.error || t("errorDeleting")); } - } catch (error) { + } catch { notify.error(t("errorDeleting")); } }; @@ -1066,7 +1065,7 @@ function CombosPageContent() { }); const data = await res.json(); setTestResults(data); - } catch (error) { + } catch { setTestResults({ error: t("testFailed") }); notify.error(t("testFailed")); } @@ -1092,7 +1091,7 @@ function CombosPageContent() { ); notify.error(resolveServerErrorMessage(errorBody, t("failedToggle"))); } - } catch (error) { + } catch { // Revert on network error setCombos((prev) => prev.map((c) => (c.id === combo.id ? { ...c, isActive: previousActive } : c)) @@ -1236,7 +1235,11 @@ function CombosPageContent() { - + s.emailsVisible); if (results.error) { @@ -2029,6 +2033,19 @@ function TestResultsView({ results }) { Targets are tested independently. This checks model health, not the combo’s routing strategy or fallback order.

+ {results.comboType === "auto" && + typeof results.totalCandidates === "number" && + Array.isArray(results.results) && + results.totalCandidates > results.results.length && ( +

+ {getI18nOrFallback( + t, + "autoComboTestTruncated", + `Tested ${results.results.length} of ${results.totalCandidates} live candidates (highest weights first).`, + { tested: results.results.length, total: results.totalCandidates } + )} +

+ )} {results.resolvedBy && (
diff --git a/src/app/api/combos/auto/route.ts b/src/app/api/combos/auto/route.ts index ee25f6f0e87f..c8f4dec20b30 100644 --- a/src/app/api/combos/auto/route.ts +++ b/src/app/api/combos/auto/route.ts @@ -1,5 +1,6 @@ import { NextResponse } from "next/server"; import { requireManagementAuth } from "@/lib/api/requireManagementAuth"; +import { isAutoComboId, materializeAutoCombo } from "@/lib/combos/autoVirtual"; import { VALID_VARIANTS, type AutoVariant, @@ -19,11 +20,18 @@ const ALL_VARIANTS: Array<{ variant: AutoVariant | undefined; name: string }> = })), ]; -// GET /api/combos/auto - List available auto combo variants with candidate info +// GET /api/combos/auto - List available auto combo variants with candidate info. +// GET /api/combos/auto?id= - Materialize ONE built-in auto combo as +// a control-center-shaped payload (models as combo steps). Virtual auto combos +// have no persisted row, so the UUID-keyed /api/combos/[id] route cannot serve +// them — this is the adapter the dashboard uses instead. export async function GET(request: Request) { const authError = await requireManagementAuth(request); if (authError) return authError; + const singleId = new URL(request.url).searchParams.get("id"); + if (singleId !== null) return getSingleAutoCombo(singleId); + try { const { prepareVirtualAutoComboInputs, createVirtualAutoComboFromPrepared } = await import("@omniroute/open-sse/services/autoCombo/virtualFactory"); @@ -45,6 +53,7 @@ export async function GET(request: Request) { name, variant: variant ?? null, type: "auto", + kind: "variant", isHidden: false, candidatePool: virtual.candidatePool ?? [], candidateCount: virtual.candidatePool?.length ?? 0, @@ -84,6 +93,7 @@ export async function GET(request: Request) { name: displayName, variant: null, type: "auto", + kind: "template", isHidden: false, candidatePool: virtual.candidatePool ?? [], candidateCount: virtual.candidatePool?.length ?? 0, @@ -131,6 +141,7 @@ export async function GET(request: Request) { name: `Auto ${displayName}`, variant: null, type: "auto", + kind: "category", isHidden: false, candidatePool: virtual.candidatePool ?? [], candidateCount: virtual.candidatePool?.length ?? 0, @@ -166,6 +177,7 @@ export async function GET(request: Request) { name: displayName, variant: null, type: "auto", + kind: "family", isHidden: false, candidatePool: virtual.candidatePool ?? [], candidateCount: virtual.candidatePool?.length ?? 0, @@ -189,3 +201,34 @@ export async function GET(request: Request) { return NextResponse.json({ combos: [] }); } } + +/** + * `?id=` handler — materialize a single built-in auto combo into the same + * combo-shaped payload the control center consumes: `models` are the virtual + * combo's live candidate steps ({kind:"model", providerId, model, weight,…}), + * `strategy` is always "auto". Unknown/unresolvable ids → 404. + */ +async function getSingleAutoCombo(rawId: string) { + const id = rawId.trim(); + if (!isAutoComboId(id)) { + return NextResponse.json({ error: `Not an auto combo id: "${id}"` }, { status: 400 }); + } + try { + const virtual = await materializeAutoCombo(id); + const models = Array.isArray(virtual.models) ? virtual.models : []; + return NextResponse.json({ + id, + name: id, + strategy: "auto", + models, + isActive: true, + type: "auto", + config: virtual.config ?? {}, + candidateCount: models.length, + context_length: virtual.advertisedContextLength || 128000, + max_output_tokens: virtual.advertisedMaxOutputTokens || 8192, + }); + } catch { + return NextResponse.json({ error: `Unknown auto combo: "${id}"` }, { status: 404 }); + } +} diff --git a/src/app/api/combos/test/route.ts b/src/app/api/combos/test/route.ts index c3dc7dc0a484..797acf501235 100644 --- a/src/app/api/combos/test/route.ts +++ b/src/app/api/combos/test/route.ts @@ -16,9 +16,17 @@ import { testComboSchema } from "@/shared/validation/schemas"; import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; import { requireManagementAuth } from "@/lib/api/requireManagementAuth"; import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error"; +import { isAutoComboId, materializeAutoCombo } from "@/lib/combos/autoVirtual"; export const COMBO_TEST_TIMEOUT_MS = 60_000; export const COMBO_TEST_TOTAL_TIMEOUT_MS = 180_000; +/** + * Virtual auto combos can hold every connected model in the pool — probing all + * of them would run far past the total budget. Cap the smoke test at the + * highest-weighted candidates; `totalCandidates` in the response reports the + * unbounded pool size so the UI can explain the truncation. + */ +export const AUTO_COMBO_TEST_MAX_PROBES = 8; async function getInternalApiKey(): Promise { // Combo health-check probes hit /v1/chat/completions, which enforces @@ -227,9 +235,34 @@ export async function POST(request) { } const { comboName } = validation.data; - const combo = await getComboByName(comboName); + let combo = await getComboByName(comboName); + let totalAutoCandidates: number | null = null; if (!combo) { - return NextResponse.json({ error: "Combo not found" }, { status: 404 }); + // Built-in auto/* combos have no persisted row — materialize the virtual + // combo the same way request-time routing does, then probe a bounded + // slice of its candidate pool instead of the (potentially huge) full set. + if (!isAutoComboId(comboName)) { + return NextResponse.json({ error: "Combo not found" }, { status: 404 }); + } + try { + combo = await materializeAutoCombo(comboName); + } catch { + return NextResponse.json({ error: "Combo not found" }, { status: 404 }); + } + const pool = Array.isArray(combo.models) ? combo.models : []; + totalAutoCandidates = pool.length; + if (pool.length === 0) { + return NextResponse.json( + { error: `Auto combo has no connected candidates matching "${comboName}"` }, + { status: 400 } + ); + } + combo = { + ...combo, + models: [...pool] + .sort((a, b) => (b?.weight ?? 0) - (a?.weight ?? 0)) + .slice(0, AUTO_COMBO_TEST_MAX_PROBES), + }; } const allCombos = await getCombos(); @@ -280,6 +313,9 @@ export async function POST(request) { label: resolvedResult.label, } : null, + ...(totalAutoCandidates !== null + ? { comboType: "auto", totalCandidates: totalAutoCandidates } + : {}), results, testedAt: new Date().toISOString(), }); diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 09c72e0c4715..dc4fe6456377 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "Built-in auto/* combos resolved dynamically from connected providers. Use any of these IDs as the model field — no setup needed.", "autoCatalogExpand": "Expand auto-routing catalog", "autoCatalogCollapse": "Collapse auto-routing catalog", + "autoCatalogComboCount": "{count} combos", + "autoCatalogCandidates": "{count} candidates", + "autoCatalogNoCandidates": "No matching providers", + "autoCatalogKindVariant": "variant", + "autoCatalogKindTemplate": "template", + "autoCatalogKindCategory": "category", + "autoCatalogKindFamily": "family", + "autoComboTestTruncated": "Tested {tested} of {total} live candidates (highest weights first).", + "controlCenter": "Control Center", "createCombo": "Create Combo", "editCombo": "Edit Combo", "deleteCombo": "Delete Combo", diff --git a/src/i18n/messages/pt-BR.json b/src/i18n/messages/pt-BR.json index 42f8e7a3e66e..ad3f9442bd9d 100644 --- a/src/i18n/messages/pt-BR.json +++ b/src/i18n/messages/pt-BR.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "Combos automáticos/* integrados resolvidos dinamicamente a partir de provedores conectados. Use qualquer um desses IDs como campo de modelo — nenhuma configuração é necessária.", "autoCatalogExpand": "Expanda o catálogo de roteamento automático", "autoCatalogCollapse": "Recolher catálogo de roteamento automático", + "autoCatalogComboCount": "{count} combos", + "autoCatalogCandidates": "{count} candidatos", + "autoCatalogNoCandidates": "Nenhum provedor correspondente", + "autoCatalogKindVariant": "variante", + "autoCatalogKindTemplate": "modelo", + "autoCatalogKindCategory": "categoria", + "autoCatalogKindFamily": "família", + "autoComboTestTruncated": "{tested} de {total} candidatos ativos testados (maiores pesos primeiro).", + "controlCenter": "Centro de Controle", "createCombo": "Criar Combo", "editCombo": "Editar Combo", "deleteCombo": "Excluir Combo", diff --git a/src/lib/combos/autoVirtual.ts b/src/lib/combos/autoVirtual.ts new file mode 100644 index 000000000000..f30eb4a80def --- /dev/null +++ b/src/lib/combos/autoVirtual.ts @@ -0,0 +1,38 @@ +/** + * @file autoVirtual.ts + * @description Shared helpers for materializing built-in `auto/*` virtual combos + * in dashboard/API read paths (auto catalog, control center, combo test probes). + * Virtual auto combos have no persisted row — they are resolved live from the + * currently connected providers/models, same as request-time routing. + */ + +/** + * True when `id` addresses a built-in virtual auto combo rather than a + * persisted combo row — either the bare default `auto` or an `auto/` id + * (variants, `auto/[:]`, `auto/`). + */ +export function isAutoComboId(id: string): boolean { + return id === "auto" || id.startsWith("auto/"); +} + +/** + * Resolve an `auto` / `auto/*` id into its live virtual combo using the same + * built-in catalog as request-time routing. Throws for ids the runtime would + * not resolve. + */ +export async function materializeAutoCombo(modelStr: string) { + if (!isAutoComboId(modelStr)) { + throw new Error(`Not an auto combo id: ${modelStr}`); + } + if (modelStr === "auto") { + const { createVirtualAutoCombo } = + await import("@omniroute/open-sse/services/autoCombo/virtualFactory"); + const combo = await createVirtualAutoCombo(undefined); + combo.name = modelStr; + combo.id = modelStr; + return combo; + } + const { createBuiltinAutoCombo } = + await import("@omniroute/open-sse/services/autoCombo/builtinCatalog"); + return createBuiltinAutoCombo(modelStr, modelStr.slice("auto/".length)); +} diff --git a/tests/unit/AutoComboCatalog.test.tsx b/tests/unit/AutoComboCatalog.test.tsx index d7ffd06f1abf..c62b1f494461 100644 --- a/tests/unit/AutoComboCatalog.test.tsx +++ b/tests/unit/AutoComboCatalog.test.tsx @@ -15,6 +15,13 @@ vi.mock("next-intl", () => ({ }, })); +// Minimal next/link stub — renders as a plain anchor element. +vi.mock("next/link", () => ({ + default: ({ children, ...props }: React.AnchorHTMLAttributes) => ( + {children} + ), +})); + const cleanupCallbacks: Array<() => void> = []; function makeContainer(): HTMLElement { @@ -34,9 +41,68 @@ function makeContainer(): HTMLElement { // that then failed every remaining test in the file in ~20ms ("You seem to have // overlapping act() calls"). Paying the import once here, on the hook's own // budget, keeps each test's timeout covering only render + assertions. -let AutoComboCatalog: React.ComponentType<{ onComboCreated?: (comboId: string) => void }>; +let AutoComboCatalog: React.ComponentType<{ + onComboCreated?: (comboId: string) => void; + onTestCombo?: (combo: { name: string }) => void; + testingName?: string | null; +}>; + +// Mirrors the real /api/combos/auto response: every curated template id plus +// the variants, category:tier combos and model families the static template +// list never showed. +const LIVE_CATALOG = { + combos: [ + { id: "auto", name: "Auto", kind: "variant", candidateCount: 12 }, + { id: "auto/coding", name: "Auto Coding", kind: "variant", candidateCount: 5 }, + { id: "auto/fast", name: "Auto Fast", kind: "variant", candidateCount: 4 }, + ...AUTO_COMBO_TEMPLATES.map((tpl) => ({ + id: tpl.name, + name: tpl.displayName, + kind: "template", + candidateCount: 4, + })), + { id: "auto/coding:fast", name: "Auto Coding Fast", kind: "category", candidateCount: 2 }, + { id: "auto/gemini", name: "Auto Gemini", kind: "family", candidateCount: 3 }, + { id: "auto/qwen", name: "Auto Qwen", kind: "family", candidateCount: 0 }, + // The real route dedupes by id (seenIds): auto/coding and auto/fast are + // emitted once as variants even though they are also template ids. + ].filter((entry, index, all) => all.findIndex((other) => other.id === entry.id) === index), +}; + +function stubAutoCatalogFetch(payload: unknown = LIVE_CATALOG, ok = true) { + globalThis.fetch = vi.fn(async (input) => { + const url = typeof input === "string" ? input : String(input); + if (url.includes("/api/combos/duplicate")) { + return new Response(JSON.stringify({ id: "static-gemini", name: "static-gemini" }), { + status: 201, + headers: { "content-type": "application/json" }, + }); + } + return new Response(JSON.stringify(payload), { + status: ok ? 200 : 500, + headers: { "content-type": "application/json" }, + }); + }) as unknown as typeof fetch; +} + +function findCard(container: HTMLElement, id: string): HTMLElement | null { + const code = [...container.querySelectorAll("code")].find((el) => el.textContent === id); + return (code?.closest(".rounded-lg") as HTMLElement | null) ?? null; +} + +async function expand(container: HTMLElement) { + await act(async () => { + (container.querySelector("button") as HTMLButtonElement | null)?.click(); + // Flush the lazy /api/combos/auto load — undici's Response.json() resolves + // on a macrotask, so a microtask-only flush can leave liveItems unset. + await Promise.resolve(); + await new Promise((resolve) => setTimeout(resolve, 0)); + }); +} describe("AutoComboCatalog", { timeout: 60_000 }, () => { + const realFetch = globalThis.fetch; + beforeAll(async () => { ({ default: AutoComboCatalog } = await import("@/app/(dashboard)/dashboard/combos/AutoComboCatalog")); @@ -46,6 +112,12 @@ describe("AutoComboCatalog", { timeout: 60_000 }, () => { ( globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean } ).IS_REACT_ACT_ENVIRONMENT = true; + stubAutoCatalogFetch(); + }); + + afterEach(() => { + globalThis.fetch = realFetch; + vi.unstubAllGlobals(); }); afterEach(() => { @@ -127,4 +199,155 @@ describe("AutoComboCatalog", { timeout: 60_000 }, () => { expect(container.textContent ?? "").toContain(s); } }); + + it("loads the live catalog on expand — families and category:tiers the static list lacks", async () => { + const container = makeContainer(); + const root = createRoot(container); + await act(async () => { + root.render(); + }); + await expand(container); + expect(globalThis.fetch).toHaveBeenCalledWith("/api/combos/auto", expect.anything()); + for (const id of ["auto", "auto/coding", "auto/coding:fast", "auto/gemini", "auto/qwen"]) { + expect(container.textContent ?? "").toContain(id); + } + expect(container.textContent ?? "").toContain( + `${LIVE_CATALOG.combos.length} autoCatalogComboCount` + ); + }); + + it("keeps the static template fallback when the live endpoint fails", async () => { + stubAutoCatalogFetch(LIVE_CATALOG, false); + const container = makeContainer(); + const root = createRoot(container); + await act(async () => { + root.render(); + }); + await expand(container); + for (const tpl of AUTO_COMBO_TEMPLATES) { + expect(container.textContent ?? "").toContain(tpl.name); + } + expect(container.textContent ?? "").not.toContain("auto/gemini"); + }); + + it("renders Control Center link, test and copy actions on every auto combo", async () => { + const container = makeContainer(); + const root = createRoot(container); + await act(async () => { + root.render(); + }); + await expand(container); + + const geminiCard = findCard(container, "auto/gemini"); + expect(geminiCard).toBeTruthy(); + const ccLink = geminiCard?.querySelector("a[href]"); + expect(ccLink?.getAttribute("href")).toBe( + `/dashboard/combos/${encodeURIComponent("auto/gemini")}` + ); + expect(geminiCard?.querySelector('button[title="testCombo"]')).toBeTruthy(); + expect(geminiCard?.querySelector('button[title="duplicateAutoComboTitle"]')).toBeTruthy(); + expect(geminiCard?.textContent).toContain("3 autoCatalogCandidates"); + + // Bare `auto` cannot be snapshotted by the duplicate endpoint — no copy button. + const autoCard = findCard(container, "auto"); + expect(autoCard).toBeTruthy(); + expect(autoCard?.querySelector('button[title="duplicateAutoComboTitle"]')).toBeNull(); + + // A family with no live candidates shows the empty-state chip. + const qwenCard = findCard(container, "auto/qwen"); + expect(qwenCard?.textContent).toContain("autoCatalogNoCandidates"); + }); + + it("calls onTestCombo with the auto combo id when the test action is clicked", async () => { + const onTestCombo = vi.fn(); + const container = makeContainer(); + const root = createRoot(container); + await act(async () => { + root.render(); + }); + await expand(container); + + const geminiCard = findCard(container, "auto/gemini"); + const testButton = geminiCard?.querySelector( + 'button[title="testCombo"]' + ) as HTMLButtonElement | null; + expect(testButton).toBeTruthy(); + await act(async () => { + testButton?.click(); + }); + expect(onTestCombo).toHaveBeenCalledWith({ name: "auto/gemini" }); + }); + + it("shows the running spinner on the entry being tested", async () => { + const container = makeContainer(); + const root = createRoot(container); + await act(async () => { + root.render( {}} testingName="auto/gemini" />); + }); + await expand(container); + const geminiCard = findCard(container, "auto/gemini"); + const testButton = geminiCard?.querySelector('button[title="testCombo"]'); + expect(testButton?.textContent).toContain("progress_activity"); + expect((testButton as HTMLButtonElement | null)?.disabled).toBe(true); + }); + + it("duplicates non-template auto combos with the auto strategy", async () => { + const onComboCreated = vi.fn(); + const confirmSpy = vi.fn(() => true); + vi.stubGlobal("confirm", confirmSpy); + vi.stubGlobal("alert", vi.fn()); + + const container = makeContainer(); + const root = createRoot(container); + await act(async () => { + root.render(); + }); + await expand(container); + + const geminiCard = findCard(container, "auto/gemini"); + const copyButton = geminiCard?.querySelector( + 'button[title="duplicateAutoComboTitle"]' + ) as HTMLButtonElement | null; + await act(async () => { + copyButton?.click(); + }); + + const duplicateCall = vi + .mocked(globalThis.fetch) + .mock.calls.find(([input]) => String(input).includes("/api/combos/duplicate")); + expect(duplicateCall).toBeTruthy(); + const body = JSON.parse(String((duplicateCall?.[1] as RequestInit | undefined)?.body)); + expect(body).toEqual({ name: "auto/gemini", strategy: "auto" }); + expect(onComboCreated).toHaveBeenCalledWith("static-gemini"); + }); + + it("duplicates curated templates with their authored strategy", async () => { + vi.stubGlobal( + "confirm", + vi.fn(() => true) + ); + vi.stubGlobal("alert", vi.fn()); + + const container = makeContainer(); + const root = createRoot(container); + await act(async () => { + root.render(); + }); + await expand(container); + + const tpl = AUTO_COMBO_TEMPLATES[0]; + const card = findCard(container, tpl.name); + const copyButton = card?.querySelector( + 'button[title="duplicateAutoComboTitle"]' + ) as HTMLButtonElement | null; + await act(async () => { + copyButton?.click(); + }); + + const duplicateCall = vi + .mocked(globalThis.fetch) + .mock.calls.find(([input]) => String(input).includes("/api/combos/duplicate")); + const body = JSON.parse(String((duplicateCall?.[1] as RequestInit | undefined)?.body)); + expect(body).toEqual({ name: tpl.name, strategy: tpl.strategy }); + }); }); diff --git a/tests/unit/combo-test-auto-route.test.ts b/tests/unit/combo-test-auto-route.test.ts new file mode 100644 index 000000000000..c95da96bf28a --- /dev/null +++ b/tests/unit/combo-test-auto-route.test.ts @@ -0,0 +1,195 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +type ComboTestResult = { + model?: string; + status?: string; + responseText?: string; + error?: string; +}; +type ComboTestBody = { + comboType?: string; + totalCandidates?: number; + strategy?: string; + resolvedBy?: string | null; + results: ComboTestResult[]; +}; + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-combo-test-auto-")); +process.env.DATA_DIR = TEST_DATA_DIR; +process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "combo-test-auto-secret"; + +const core = await import("../../src/lib/db/core.ts"); +const apiKeysDb = await import("../../src/lib/db/apiKeys.ts"); +const providersDb = await import("../../src/lib/db/providers.ts"); +const autoRoute = await import("../../src/app/api/combos/auto/route.ts"); +const testRoute = await import("../../src/app/api/combos/test/route.ts"); + +const originalFetch = globalThis.fetch; + +async function resetStorage() { + core.resetDbInstance(); + apiKeysDb.resetApiKeyState(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); + fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); +} + +async function seedGeminiConnection() { + // antigravity OAuth connections expose gemini-* registry models, which is + // what the auto/gemini family filter matches on. + await providersDb.createProviderConnection({ + provider: "antigravity", + authType: "oauth", + email: "antigravity-one@example.com", + accessToken: "fake-antigravity-access-token-one", + tokenExpiresAt: new Date(Date.now() + 60_000).toISOString(), + }); +} + +function postTest(comboName: string) { + return testRoute.POST( + new Request("http://localhost/api/combos/test", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ comboName }), + }) + ); +} + +function getAuto(query: string) { + return autoRoute.GET(new Request(`http://localhost/api/combos/auto?${query}`)); +} + +function stubHealthyProbes() { + const calls: Array<{ url: string; body: Record }> = []; + globalThis.fetch = (async (url: unknown, init: RequestInit = {}) => { + calls.push({ url: String(url), body: JSON.parse(String(init.body ?? "{}")) }); + return new Response( + JSON.stringify({ choices: [{ message: { role: "assistant", content: "OK" } }] }), + { status: 200, headers: { "content-type": "application/json" } } + ); + }) as typeof fetch; + return calls; +} + +test.beforeEach(async () => { + globalThis.fetch = originalFetch; + await resetStorage(); +}); + +test.afterEach(() => { + globalThis.fetch = originalFetch; +}); + +test.after(async () => { + globalThis.fetch = originalFetch; + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); +}); + +test("combo test route 404s unknown auto ids just like missing persisted combos", async () => { + const unknownAuto = await postTest("auto/zzz-not-a-thing"); + assert.equal(unknownAuto.status, 404); + assert.deepEqual(await unknownAuto.json(), { error: "Combo not found" }); + + const missingCombo = await postTest("missing-combo"); + assert.equal(missingCombo.status, 404); + assert.deepEqual(await missingCombo.json(), { error: "Combo not found" }); +}); + +test("combo test route reports an empty candidate pool for unmatched auto ids", async () => { + // No credentials + no no-auth provider exposes minimax models, so the + // materialized virtual combo has zero candidates. + const response = await postTest("auto/minimax"); + assert.equal(response.status, 400); + const body = (await response.json()) as { error?: string }; + assert.match(body.error ?? "", /no connected candidates/i); +}); + +test("combo test route probes a bounded slice of an auto combo's live pool", async () => { + await seedGeminiConnection(); + const calls = stubHealthyProbes(); + + const response = await postTest("auto/gemini"); + const body = (await response.json()) as ComboTestBody; + + assert.equal(response.status, 200); + assert.equal(body.comboType, "auto"); + assert.equal(body.strategy, "auto"); + assert.ok( + typeof body.totalCandidates === "number" && body.totalCandidates > 0, + "expected totalCandidates to report the unbounded pool size" + ); + assert.ok(body.results.length > 0, "expected at least one probe result"); + assert.ok( + body.results.length <= testRoute.AUTO_COMBO_TEST_MAX_PROBES, + "probe count must stay within the auto cap" + ); + assert.ok( + body.results.every((result) => /gemini/i.test(result.model ?? "")), + `auto/gemini probes must only hit gemini-family models, got: ${JSON.stringify(body.results.map((r) => r.model))}` + ); + assert.equal(calls.length, body.results.length); + assert.ok(calls.every(({ url }) => url.includes("/v1/chat/completions"))); + assert.equal(body.resolvedBy, body.results[0].model); + assert.equal(body.results[0].status, "ok"); +}); + +test("GET /api/combos/auto?id= materializes one virtual combo for the control center", async () => { + await seedGeminiConnection(); + + const response = await getAuto(`id=${encodeURIComponent("auto/gemini")}`); + assert.equal(response.status, 200); + const combo = (await response.json()) as { + id?: string; + name?: string; + strategy?: string; + type?: string; + models?: Array<{ kind?: string; providerId?: string; model?: string }>; + candidateCount?: number; + }; + assert.equal(combo.id, "auto/gemini"); + assert.equal(combo.name, "auto/gemini"); + assert.equal(combo.strategy, "auto"); + assert.equal(combo.type, "auto"); + assert.ok(Array.isArray(combo.models) && combo.models.length > 0); + assert.ok(combo.models.every((step) => step.kind === "model")); + assert.ok(combo.models.every((step) => /gemini/i.test(step.model ?? ""))); + assert.equal(combo.candidateCount, combo.models.length); +}); + +test("GET /api/combos/auto?id= handles the bare default and rejects non-auto ids", async () => { + await seedGeminiConnection(); + + const bare = await getAuto("id=auto"); + assert.equal(bare.status, 200); + const bareCombo = (await bare.json()) as { id?: string; models?: unknown[] }; + assert.equal(bareCombo.id, "auto"); + assert.ok(Array.isArray(bareCombo.models) && bareCombo.models.length > 0); + + const nonAuto = await getAuto("id=some-persisted-name"); + assert.equal(nonAuto.status, 400); + + const unknown = await getAuto(`id=${encodeURIComponent("auto/zzz-not-a-thing")}`); + assert.equal(unknown.status, 404); +}); + +test("GET /api/combos/auto list still emits every kind including families", async () => { + await seedGeminiConnection(); + const response = await getAuto(""); + assert.equal(response.status, 200); + const { combos } = (await response.json()) as { + combos: Array<{ id?: string; kind?: string; candidateCount?: number }>; + }; + const ids = new Set(combos.map((combo) => combo.id)); + assert.ok(ids.has("auto"), "bare auto variant missing"); + assert.ok(ids.has("auto/best-coding"), "curated template missing"); + assert.ok(ids.has("auto/coding:fast"), "category:tier combo missing"); + assert.ok(ids.has("auto/gemini"), "family combo missing"); + const gemini = combos.find((combo) => combo.id === "auto/gemini"); + assert.equal(gemini?.kind, "family"); + assert.ok((gemini?.candidateCount ?? 0) > 0); +}); From c09e6312d3fce59d6d23a868a198e886e41ba4be Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 22:44:59 +0000 Subject: [PATCH 02/10] fix(api): materialize auto combo into fresh object for ComboRecord VirtualAutoCombo is an interface without an implicit index signature, so it is not assignable to ComboRecord (Record). Spreading the materialized combo yields a fresh object type that satisfies the index signature, clearing the api-typecheck regression (TS2322) flagged by CI. Co-Authored-By: Afonso Dutra Nogueira Filho --- src/app/api/combos/test/route.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/api/combos/test/route.ts b/src/app/api/combos/test/route.ts index 797acf501235..686bb13d90d5 100644 --- a/src/app/api/combos/test/route.ts +++ b/src/app/api/combos/test/route.ts @@ -245,7 +245,10 @@ export async function POST(request) { return NextResponse.json({ error: "Combo not found" }, { status: 404 }); } try { - combo = await materializeAutoCombo(comboName); + // Spread into a fresh object so the result fits ComboRecord + // (Record) — interfaces like VirtualAutoCombo do not + // get an implicit index signature. + combo = { ...(await materializeAutoCombo(comboName)) }; } catch { return NextResponse.json({ error: "Combo not found" }, { status: 404 }); } From c0f62b4e42616b5eecc327ed092e700c999a94c3 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 3 Oct 2026 00:43:40 +0000 Subject: [PATCH 03/10] fix(i18n): backfill vi.json for auto-catalog keys (strict parity test) Co-Authored-By: Afonso Dutra Nogueira Filho --- src/i18n/messages/vi.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/i18n/messages/vi.json b/src/i18n/messages/vi.json index c98e2e6f9469..4e28bbdf0b43 100644 --- a/src/i18n/messages/vi.json +++ b/src/i18n/messages/vi.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "Các combo auto/* tích hợp sẵn được phân giải động từ các nhà cung cấp đã kết nối. Sử dụng bất kỳ ID nào trong số này làm trường mô hình — không cần thiết lập.", "autoCatalogExpand": "Mở rộng danh mục định tuyến tự động", "autoCatalogCollapse": "Thu gọn danh mục định tuyến tự động", + "autoCatalogComboCount": "{count} combo", + "autoCatalogCandidates": "{count} ứng viên", + "autoCatalogNoCandidates": "Không có nhà cung cấp phù hợp", + "autoCatalogKindVariant": "biến thể", + "autoCatalogKindTemplate": "mẫu", + "autoCatalogKindCategory": "danh mục", + "autoCatalogKindFamily": "họ", + "autoComboTestTruncated": "Đã kiểm tra {tested} trên {total} ứng viên trực tiếp (trọng số cao nhất trước).", + "controlCenter": "Trung tâm điều khiển", "createCombo": "Tạo Combo", "editCombo": "Chỉnh sửa combo", "deleteCombo": "Xóa combo", From 72e8a337d614ba8b0e7c9d47e02c28e3889ee1a3 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 3 Oct 2026 00:49:59 +0000 Subject: [PATCH 04/10] chore(skills): regenerate omni-providers endpoints reference Co-Authored-By: Afonso Dutra Nogueira Filho --- skills/omni-providers/references/endpoints.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/skills/omni-providers/references/endpoints.md b/skills/omni-providers/references/endpoints.md index 3a2b613c3ca3..9365c48f1ab8 100644 --- a/skills/omni-providers/references/endpoints.md +++ b/skills/omni-providers/references/endpoints.md @@ -172,6 +172,8 @@ curl -X POST https://localhost:20128/api/providers/test-batch \ Validate provider credentials +Spawns a fixed ACP summarizer binary to validate credentials — loopback-only (Hard Rules #15/#17, audit #15159 S-01). + ```bash curl -X POST https://localhost:20128/api/providers/validate \ -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ @@ -495,6 +497,8 @@ curl -X POST https://localhost:20128/api/providers/{id}/sync-models \ POST providers › bulk +Reaches the same spawn as /api/providers/validate (webProvidersB.ts) — loopback-only (Hard Rules #15/#17, audit #15159 S-01). + ```bash curl -X POST https://localhost:20128/api/providers/bulk \ -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ @@ -694,6 +698,8 @@ curl https://localhost:20128/api/providers/health-matrix \ POST providers › import +Reaches the same spawn as /api/providers/validate (webProvidersB.ts) — loopback-only (Hard Rules #15/#17, audit #15159 S-01). + ```bash curl -X POST https://localhost:20128/api/providers/import \ -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ From 66be061772101a3f07d0c46e0c0e77508cd55f37 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 3 Oct 2026 01:20:09 +0000 Subject: [PATCH 05/10] feat(combos): list only auto/* combos that have live candidates /auto/combos/auto and /v1/models no longer advertise auto/* ids whose materialized candidate pool is empty (no provider + model can ever dispatch). The dashboard catalog treats an empty live list as authoritative and shows an empty-state message instead of static fallback templates. Co-Authored-By: Afonso Dutra Nogueira Filho --- .../dashboard/combos/AutoComboCatalog.tsx | 10 +- src/app/api/combos/auto/route.ts | 5 + src/app/api/v1/models/catalog.ts | 2 + .../combos-auto-hide-empty-candidates.test.ts | 134 ++++++++++++++++++ .../models-catalog-auto-combos-4164.test.ts | 43 +++++- .../models-catalog-block-auto-5192.test.ts | 12 ++ 6 files changed, 199 insertions(+), 7 deletions(-) create mode 100644 tests/unit/combos-auto-hide-empty-candidates.test.ts diff --git a/src/app/(dashboard)/dashboard/combos/AutoComboCatalog.tsx b/src/app/(dashboard)/dashboard/combos/AutoComboCatalog.tsx index 743bfc3c2658..ba9594611dc2 100644 --- a/src/app/(dashboard)/dashboard/combos/AutoComboCatalog.tsx +++ b/src/app/(dashboard)/dashboard/combos/AutoComboCatalog.tsx @@ -80,7 +80,9 @@ export default function AutoComboCatalog({ const res = await fetch("/api/combos/auto", { cache: "no-store" }); if (!res.ok) return; const data = (await res.json()) as { combos?: AutoCatalogApiEntry[] }; - if (!Array.isArray(data.combos) || data.combos.length === 0) return; + // An empty live list is authoritative (no combo has candidates right + // now) — only a failed/malformed response falls back to the templates. + if (!Array.isArray(data.combos)) return; const items = data.combos .filter((entry): entry is AutoCatalogApiEntry & { id: string } => { return typeof entry?.id === "string" && entry.id.length > 0; @@ -184,7 +186,11 @@ export default function AutoComboCatalog({ - {open && ( + {open && items.length === 0 && ( +

{t("autoCatalogNoCandidates")}

+ )} + + {open && items.length > 0 && (
{items.map((item) => { const testing = testingName === item.id; diff --git a/src/app/api/combos/auto/route.ts b/src/app/api/combos/auto/route.ts index c8f4dec20b30..b12f7450c0b1 100644 --- a/src/app/api/combos/auto/route.ts +++ b/src/app/api/combos/auto/route.ts @@ -46,6 +46,8 @@ export async function GET(request: Request) { for (const { variant, name } of ALL_VARIANTS) { try { const virtual = await createVirtualAutoComboFromPrepared(prepared, variant); + // A combo with no live candidates can never dispatch — don't list it. + if (!virtual.candidatePool?.length) continue; const id = variant ? `auto/${variant}` : "auto"; seenIds.add(id); combos.push({ @@ -83,6 +85,7 @@ export async function GET(request: Request) { const variant = AUTO_TEMPLATE_VARIANTS[modelStr]; const spec = modelStr === "auto/best-free" ? { tier: "free" as const } : undefined; const virtual = await createVirtualAutoComboFromPrepared(prepared, variant, spec); + if (!virtual.candidatePool?.length) continue; const displayName = variant ? `Auto ${variant.charAt(0).toUpperCase() + variant.slice(1)}` @@ -126,6 +129,7 @@ export async function GET(request: Request) { category: parsed.category, tier: parsed.tier, }); + if (!virtual.candidatePool?.length) continue; // Build a human-readable name from the category and tier const catName = parsed.category @@ -169,6 +173,7 @@ export async function GET(request: Request) { const virtual = await createVirtualAutoComboFromPrepared(prepared, undefined, { family: suffix, }); + if (!virtual.candidatePool?.length) continue; const displayName = `Auto ${suffix.charAt(0).toUpperCase() + suffix.slice(1)}`; diff --git a/src/app/api/v1/models/catalog.ts b/src/app/api/v1/models/catalog.ts index 28b1c56d2bd9..57238c1ccf2a 100644 --- a/src/app/api/v1/models/catalog.ts +++ b/src/app/api/v1/models/catalog.ts @@ -897,6 +897,8 @@ async function buildUnifiedModelsResponseCore( await yieldCatalogBuildTurn(); } const virtualCombo = await createBuiltinAutoCombo(autoId, suffix, preparedAutoInputs); + // A combo with no live candidates can never dispatch — don't advertise it. + if (virtualCombo.models.length === 0) continue; const contextLength = virtualCombo.advertisedContextLength || 128000; const maxOutputTokens = virtualCombo.advertisedMaxOutputTokens || 8192; diff --git a/tests/unit/combos-auto-hide-empty-candidates.test.ts b/tests/unit/combos-auto-hide-empty-candidates.test.ts new file mode 100644 index 000000000000..b23f2307e96e --- /dev/null +++ b/tests/unit/combos-auto-hide-empty-candidates.test.ts @@ -0,0 +1,134 @@ +/** + * Auto combos with no live candidates must not be listed. + * + * `/api/combos/auto` (dashboard catalog source) and `/v1/models` (client-facing + * catalog) advertised every built-in `auto/*` id unconditionally, including + * families/categories with zero connected providers — cards showed "No matching + * providers" and clients got ids that can never dispatch. Both listings now + * skip combos whose materialized candidate pool is empty. + * + * Note: `opencode` is a no-auth provider, so even an "empty" DB has candidates + * for the unconstrained combos (auto, auto/coding, ...) and for families its + * oc/* models match (auto/mimo, auto/deepseek). Families with no matching + * connected model — auto/minimax, auto/zai — still come back with an empty + * pool and are the reliable negative cases. + */ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-hide-empty-auto-")); +process.env.DATA_DIR = TEST_DATA_DIR; +process.env.API_KEY_SECRET = process.env.API_KEY_SECRET ?? "hide-empty-auto-test-secret"; + +const core = await import("../../src/lib/db/core.ts"); +const settingsDb = await import("../../src/lib/db/settings.ts"); +const providersDb = await import("../../src/lib/db/providers.ts"); +const combosAutoRoute = await import("../../src/app/api/combos/auto/route.ts"); +const v1ModelsCatalog = await import("../../src/app/api/v1/models/catalog.ts"); + +// Families with no matching models in any seeded registry — the materialized +// pool is empty, so these ids must be filtered from every listing. +const EMPTY_FAMILY_IDS = ["auto/minimax", "auto/zai"]; + +const SEEDED_PROVIDERS = ["openai", "anthropic", "gemini", "groq", "deepseek", "mistral"]; + +type ComboEntry = { id: string; candidateCount: number }; + +async function listCombos(): Promise { + const res = await combosAutoRoute.GET(new Request("http://localhost/api/combos/auto")); + const body = (await res.json()) as { combos: ComboEntry[] }; + return body.combos; +} + +async function listModelIds(): Promise> { + const res = await v1ModelsCatalog.getUnifiedModelsResponse( + new Request("http://localhost/api/v1/models") + ); + const body = (await res.json()) as { data: Array<{ id: string }> }; + return new Set(body.data.map((m) => m.id)); +} + +async function resetStorage() { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); + fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); + await settingsDb.updateSettings({ requireLogin: false }); +} + +async function seedProviders(prefix: string) { + for (const provider of SEEDED_PROVIDERS) { + await providersDb.createProviderConnection({ + provider, + authType: "apikey", + apiKey: `sk-test-${prefix}-${provider}`, + name: `test-${prefix}-${provider}`, + isActive: true, + }); + } +} + +test.beforeEach(async () => { + await resetStorage(); +}); + +test.after(() => { + core.resetDbInstance(); + try { + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); + } catch { + // best-effort cleanup + } +}); + +test("/api/combos/auto lists only combos with candidates", async () => { + const combos = await listCombos(); + assert.ok(combos.length > 0, "the no-auth opencode pool should yield candidates"); + for (const combo of combos) { + assert.ok( + combo.candidateCount > 0, + `${combo.id} listed with ${combo.candidateCount} candidates — empty combos must be filtered` + ); + } + const ids = new Set(combos.map((c) => c.id)); + for (const emptyId of EMPTY_FAMILY_IDS) { + assert.ok(!ids.has(emptyId), `${emptyId} has no matching model — must not be listed`); + } +}); + +test("/v1/models does not advertise auto/* ids without candidates", async () => { + const ids = await listModelIds(); + for (const emptyId of EMPTY_FAMILY_IDS) { + assert.ok(!ids.has(emptyId), `${emptyId} has an empty pool — must not be advertised`); + } +}); + +test("seeded pool: every listed combo has candidates, empty families stay hidden", async () => { + await seedProviders("hide-empty"); + const combos = await listCombos(); + for (const combo of combos) { + assert.ok( + combo.candidateCount > 0, + `${combo.id} listed with ${combo.candidateCount} candidates — empty combos must be filtered` + ); + } + const ids = new Set(combos.map((c) => c.id)); + assert.ok(ids.has("auto/gemini"), "auto/gemini has a seeded provider and must stay listed"); + for (const emptyId of EMPTY_FAMILY_IDS) { + assert.ok(!ids.has(emptyId), `${emptyId} has no matching model — must not be listed`); + } +}); + +test("seeded pool: /v1/models advertises auto/* ids that have candidates only", async () => { + await seedProviders("hide-empty-v1"); + const ids = await listModelIds(); + assert.ok( + ids.has("auto/gemini"), + "auto/gemini has candidates (seeded gemini provider) and must be advertised" + ); + for (const emptyId of EMPTY_FAMILY_IDS) { + assert.ok(!ids.has(emptyId), `${emptyId} has an empty pool — must not be advertised`); + } +}); diff --git a/tests/unit/models-catalog-auto-combos-4164.test.ts b/tests/unit/models-catalog-auto-combos-4164.test.ts index 440878c3704c..f3394debfc12 100644 --- a/tests/unit/models-catalog-auto-combos-4164.test.ts +++ b/tests/unit/models-catalog-auto-combos-4164.test.ts @@ -20,6 +20,8 @@ process.env.DATA_DIR = TEST_DATA_DIR; process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "catalog-auto-test-secret"; const core = await import("../../src/lib/db/core.ts"); +const providersDb = await import("../../src/lib/db/providers.ts"); +const settingsDb = await import("../../src/lib/db/settings.ts"); const v1ModelsCatalog = await import("../../src/app/api/v1/models/catalog.ts"); const builtinCatalog = await import("../../open-sse/services/autoCombo/builtinCatalog.ts"); @@ -29,6 +31,21 @@ function resetStorage() { fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); } +// Auto combos with zero candidates are no longer advertised — these tests must +// seed providers so the materialized pools are non-empty. +async function seedProviders() { + await settingsDb.updateSettings({ requireLogin: false }); + for (const provider of ["openai", "anthropic", "gemini", "groq", "deepseek", "mistral"]) { + await providersDb.createProviderConnection({ + provider, + authType: "apikey", + apiKey: `sk-test-4164-${provider}`, + name: `test-4164-${provider}`, + isActive: true, + }); + } +} + test.beforeEach(() => { resetStorage(); }); @@ -38,7 +55,8 @@ test.after(() => { fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); }); -test("#4164 /v1/models advertises every built-in auto/* combo", async () => { +test("#4164 /v1/models advertises exactly the built-in auto/* combos with candidates", async () => { + await seedProviders(); const response = await v1ModelsCatalog.getUnifiedModelsResponse( new Request("http://localhost/api/v1/models") ); @@ -49,8 +67,18 @@ test("#4164 /v1/models advertises every built-in auto/* combo", async () => { const expected = Object.keys(builtinCatalog.AUTO_TEMPLATE_VARIANTS); assert.ok(expected.length > 0, "sanity: there are built-in auto/* variants"); + // New contract: an auto id is advertised iff its materialized pool is + // non-empty — ids with zero candidates must be absent. for (const autoId of expected) { - assert.ok(ids.has(autoId), `expected /v1/models to advertise ${autoId}`); + const virtual = await builtinCatalog.createBuiltinAutoCombo( + autoId, + autoId.slice("auto/".length) + ); + assert.equal( + ids.has(autoId), + virtual.models.length > 0, + `${autoId} must be advertised iff it has candidates (pool=${virtual.models.length})` + ); } // Spot-check a couple of well-known ones and their owner tag. @@ -60,14 +88,17 @@ test("#4164 /v1/models advertises every built-in auto/* combo", async () => { }); test("#4164 auto/* combos appear at the top of the list", async () => { + await seedProviders(); const response = await v1ModelsCatalog.getUnifiedModelsResponse( new Request("http://localhost/api/v1/models") ); const body = (await response.json()) as { data: Array<{ id: string }> }; - const expected = Object.keys(builtinCatalog.AUTO_TEMPLATE_VARIANTS); - // The first N entries (N = number of auto/* variants) should all be auto/*. - const head = body.data.slice(0, expected.length).map((m) => m.id); + // The advertised auto/* block stays at the top: the first N entries (N = + // number of emitted auto/* ids) should all be auto/*. + const autoCount = body.data.filter((m) => m.id.startsWith("auto/")).length; + assert.ok(autoCount > 0, "sanity: seeded pool should yield auto/* entries"); + const head = body.data.slice(0, autoCount).map((m) => m.id); for (const id of head) { assert.match(id, /^auto\//, `top-of-list entry ${id} should be an auto/* combo`); } @@ -76,6 +107,7 @@ test("#4164 auto/* combos appear at the top of the list", async () => { test("#4164 no duplicate auto/* ids even if a persisted combo shadows one", async () => { // Defensive: even if a DB combo were named like an auto/* id, the listing must // not emit the id twice. + await seedProviders(); const response = await v1ModelsCatalog.getUnifiedModelsResponse( new Request("http://localhost/api/v1/models") ); @@ -89,6 +121,7 @@ test("#4189 every auto/* entry exposes token limits + baseline capabilities", as // the combo's advertised context/output limits + baseline capabilities so // OpenAI-compatible clients that build their picker from /v1/models get a context // window before the first request. Without the fix these fields are absent. + await seedProviders(); const response = await v1ModelsCatalog.getUnifiedModelsResponse( new Request("http://localhost/api/v1/models") ); diff --git a/tests/unit/models-catalog-block-auto-5192.test.ts b/tests/unit/models-catalog-block-auto-5192.test.ts index 256e4930d9b4..e03ec6a9f81c 100644 --- a/tests/unit/models-catalog-block-auto-5192.test.ts +++ b/tests/unit/models-catalog-block-auto-5192.test.ts @@ -21,6 +21,7 @@ process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "block-auto-5192-test const core = await import("../../src/lib/db/core.ts"); const settingsDb = await import("../../src/lib/db/settings.ts"); const apiKeysDb = await import("../../src/lib/db/apiKeys.ts"); +const providersDb = await import("../../src/lib/db/providers.ts"); const v1ModelsCatalog = await import("../../src/app/api/v1/models/catalog.ts"); type ModelsResponseBody = { data: Array<{ id: string }> }; @@ -51,6 +52,17 @@ test.after(async () => { }); test("#5192 baseline: built-in auto/* combos are listed when Auto is not blocked", async () => { + // Combos with zero candidates are filtered from the catalog — seed providers + // so auto/best-coding + auto/coding materialize with a non-empty pool. + for (const provider of ["openai", "anthropic", "gemini", "groq", "deepseek", "mistral"]) { + await providersDb.createProviderConnection({ + provider, + authType: "apikey", + apiKey: `sk-test-5192-${provider}`, + name: `test-5192-${provider}`, + isActive: true, + }); + } const ids = await getIds(); assert.ok(ids.has("auto/best-coding"), "auto/best-coding should be listed by default"); assert.ok(ids.has("auto/coding"), "auto/coding should be listed by default"); From 0a07f2d2a1fab3bcf7150ad4157c6e2337bbc84b Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 3 Oct 2026 01:47:57 +0000 Subject: [PATCH 06/10] i18n: translate auto-combo catalog keys to all locales Co-Authored-By: Afonso Dutra Nogueira Filho --- src/i18n/messages/am.json | 9 +++++++++ src/i18n/messages/ar.json | 9 +++++++++ src/i18n/messages/az.json | 9 +++++++++ src/i18n/messages/bg.json | 9 +++++++++ src/i18n/messages/bn.json | 9 +++++++++ src/i18n/messages/bs.json | 9 +++++++++ src/i18n/messages/cs.json | 9 +++++++++ src/i18n/messages/da.json | 9 +++++++++ src/i18n/messages/de.json | 9 +++++++++ src/i18n/messages/el.json | 9 +++++++++ src/i18n/messages/es.json | 9 +++++++++ src/i18n/messages/et.json | 9 +++++++++ src/i18n/messages/fa.json | 9 +++++++++ src/i18n/messages/fi.json | 9 +++++++++ src/i18n/messages/fr.json | 9 +++++++++ src/i18n/messages/ga.json | 9 +++++++++ src/i18n/messages/gu.json | 9 +++++++++ src/i18n/messages/ha.json | 9 +++++++++ src/i18n/messages/he.json | 9 +++++++++ src/i18n/messages/hi.json | 9 +++++++++ src/i18n/messages/hr.json | 9 +++++++++ src/i18n/messages/hu.json | 9 +++++++++ src/i18n/messages/hy.json | 9 +++++++++ src/i18n/messages/id.json | 9 +++++++++ src/i18n/messages/ig.json | 9 +++++++++ src/i18n/messages/it.json | 9 +++++++++ src/i18n/messages/ja.json | 9 +++++++++ src/i18n/messages/ka.json | 9 +++++++++ src/i18n/messages/km.json | 9 +++++++++ src/i18n/messages/kn.json | 9 +++++++++ src/i18n/messages/ko.json | 9 +++++++++ src/i18n/messages/lt.json | 9 +++++++++ src/i18n/messages/lv.json | 9 +++++++++ src/i18n/messages/ml.json | 9 +++++++++ src/i18n/messages/mr.json | 9 +++++++++ src/i18n/messages/ms.json | 9 +++++++++ src/i18n/messages/mt.json | 9 +++++++++ src/i18n/messages/my.json | 9 +++++++++ src/i18n/messages/ne.json | 9 +++++++++ src/i18n/messages/nl.json | 9 +++++++++ src/i18n/messages/no.json | 9 +++++++++ src/i18n/messages/or.json | 9 +++++++++ src/i18n/messages/pa.json | 9 +++++++++ src/i18n/messages/phi.json | 9 +++++++++ src/i18n/messages/pl.json | 9 +++++++++ src/i18n/messages/pt.json | 9 +++++++++ src/i18n/messages/ro.json | 9 +++++++++ src/i18n/messages/ru.json | 9 +++++++++ src/i18n/messages/si.json | 9 +++++++++ src/i18n/messages/sk.json | 9 +++++++++ src/i18n/messages/sl.json | 9 +++++++++ src/i18n/messages/sr.json | 9 +++++++++ src/i18n/messages/sv.json | 9 +++++++++ src/i18n/messages/sw.json | 9 +++++++++ src/i18n/messages/ta.json | 9 +++++++++ src/i18n/messages/te.json | 9 +++++++++ src/i18n/messages/th.json | 9 +++++++++ src/i18n/messages/tr.json | 9 +++++++++ src/i18n/messages/uk-UA.json | 9 +++++++++ src/i18n/messages/ur.json | 9 +++++++++ src/i18n/messages/uz.json | 9 +++++++++ src/i18n/messages/yo.json | 9 +++++++++ src/i18n/messages/zh-CN.json | 9 +++++++++ src/i18n/messages/zh-TW.json | 9 +++++++++ 64 files changed, 576 insertions(+) diff --git a/src/i18n/messages/am.json b/src/i18n/messages/am.json index 95d503a47101..6904fccd3065 100644 --- a/src/i18n/messages/am.json +++ b/src/i18n/messages/am.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "አብረው የተካተቱ auto/* ጥምረቶች ከተገናኙ አቅራቢዎች በተለዋዋጭ ሁኔታ ይወሰናሉ። ከእነዚህ መታወቂያዎች ማንኛውንም እንደ ሞዴል መስክ ይጠቀሙ — ምንም ማዋቀር አያስፈልግም።", "autoCatalogExpand": "የራስ-ሰር ማስተላለፊያ ካታሎጉን ዘርጋ", "autoCatalogCollapse": "የራስ-ሰር ማስተላለፊያ ካታሎጉን ሰብስብ", + "autoCatalogComboCount": "{count} ኮምቦዎች", + "autoCatalogCandidates": "{count} ካንደይት", + "autoCatalogNoCandidates": "የሚስማማ አቅራቢዎች የለም", + "autoCatalogKindVariant": "ቫሪያንት", + "autoCatalogKindTemplate": "አቀራረብ", + "autoCatalogKindCategory": "ምድብ", + "autoCatalogKindFamily": "ቤተሰብ", + "autoComboTestTruncated": "{total} የተሞክሮ ካንደይት ውስጥ {tested} ተመን በተመለከተ ይታወቃል (ከፍተኛ ክብደቶች በመጀመሪያ).", + "controlCenter": "የመቆጣጠሪያ ማዕከል", "createCombo": "ጥምረት ፍጠር", "editCombo": "ጥምረት አርትዕ", "deleteCombo": "ጥምረት ሰርዝ", diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index 5eb2e3066d39..3e59b4e79693 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "تُحل مجموعات auto/* المضمّنة ديناميكيًا من المزوّدين المتصلين. استخدم أيًا من هذه المعرّفات في حقل النموذج، دون حاجة إلى إعداد.", "autoCatalogExpand": "توسيع كتالوج التوجيه التلقائي", "autoCatalogCollapse": "طي كتالوج التوجيه التلقائي", + "autoCatalogComboCount": "{count} مجموعات", + "autoCatalogCandidates": "{count} مرشحين", + "autoCatalogNoCandidates": "لا يوجد مزودون مطابقون", + "autoCatalogKindVariant": "متغير", + "autoCatalogKindTemplate": "قالب", + "autoCatalogKindCategory": "فئة", + "autoCatalogKindFamily": "عائلة", + "autoComboTestTruncated": "تم اختبار {tested} من {total} مرشحين نشطين (الأعلى وزناً أولاً).", + "controlCenter": "مركز التحكم", "createCombo": "إنشاء مجموعة", "editCombo": "تحرير المجموعة", "deleteCombo": "حذف المجموعة", diff --git a/src/i18n/messages/az.json b/src/i18n/messages/az.json index 06758ace65d4..a1c46db73131 100644 --- a/src/i18n/messages/az.json +++ b/src/i18n/messages/az.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "Daxili avtomatik/* kombinasiyaları əlaqəli provayderlərdən dinamik şəkildə həll edilir. Model sahəsi kimi bu identifikatorlardan hər hansı birini istifadə edin – quraşdırmaya ehtiyac yoxdur.", "autoCatalogExpand": "Avtomatik marşrutlaşdırma kataloqunu genişləndirin", "autoCatalogCollapse": "Avtomatik marşrutlaşdırma kataloqunu yığcamlaşdırın", + "autoCatalogComboCount": "{count} kombinasiyalar", + "autoCatalogCandidates": "{count} namizədlər", + "autoCatalogNoCandidates": "Uyğun təminatçılar yoxdur", + "autoCatalogKindVariant": "variant", + "autoCatalogKindTemplate": "şablon", + "autoCatalogKindCategory": "kateqoriya", + "autoCatalogKindFamily": "ailə", + "autoComboTestTruncated": "{total} canlı namizədin {tested} -ni test etdik (ən yüksək çəkilər əvvəl).", + "controlCenter": "İdarəetmə Mərkəzi", "createCombo": "Kombinasiya yaradın", "editCombo": "Kombinasiyanı redaktə edin", "deleteCombo": "Kombinasiyanı silin", diff --git a/src/i18n/messages/bg.json b/src/i18n/messages/bg.json index af6ff3bd81ac..99946d904770 100644 --- a/src/i18n/messages/bg.json +++ b/src/i18n/messages/bg.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "Вградени автоматични/* комбинации, разрешени динамично от свързани доставчици. Използвайте който и да е от тези идентификатори като поле за модел — не е необходима настройка.", "autoCatalogExpand": "Разширете каталога за автоматично маршрутизиране", "autoCatalogCollapse": "Свиване на каталога за автоматично маршрутизиране", + "autoCatalogComboCount": "{count} комбинации", + "autoCatalogCandidates": "{count} кандидати", + "autoCatalogNoCandidates": "Няма съвпадащи доставчици", + "autoCatalogKindVariant": "вариант", + "autoCatalogKindTemplate": "шаблон", + "autoCatalogKindCategory": "категория", + "autoCatalogKindFamily": "семейство", + "autoComboTestTruncated": "Тестирани {tested} от {total} активни кандидати (с най-високи тегла първо).", + "controlCenter": "Контролен център", "createCombo": "Създайте комбо", "editCombo": "Комбинация за редактиране", "deleteCombo": "Изтриване на комбо", diff --git a/src/i18n/messages/bn.json b/src/i18n/messages/bn.json index 6008143ca4f0..ee71d5a4320e 100644 --- a/src/i18n/messages/bn.json +++ b/src/i18n/messages/bn.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "অন্তর্নির্মিত স্বয়ংক্রিয়/* কম্বোগুলি সংযুক্ত প্রদানকারীদের থেকে গতিশীলভাবে সমাধান করা হয়েছে। মডেল ক্ষেত্র হিসাবে এই আইডিগুলির যে কোনও একটি ব্যবহার করুন — কোনও সেটআপের প্রয়োজন নেই৷", "autoCatalogExpand": "স্বয়ংক্রিয়-রাউটিং ক্যাটালগ প্রসারিত করুন", "autoCatalogCollapse": "স্বয়ংক্রিয়-রাউটিং ক্যাটালগ সঙ্কুচিত করুন", + "autoCatalogComboCount": "{count} কম্বো", + "autoCatalogCandidates": "{count} প্রার্থী", + "autoCatalogNoCandidates": "মিলছে না এমন প্রদানকারী", + "autoCatalogKindVariant": "ভ্যারিয়েন্ট", + "autoCatalogKindTemplate": "টেমপ্লেট", + "autoCatalogKindCategory": "শ্রেণী", + "autoCatalogKindFamily": "পরিবার", + "autoComboTestTruncated": "পরীক্ষিত {tested} এর {total} জীবন্ত প্রার্থী (সর্বোচ্চ ওজন প্রথম)।", + "controlCenter": "নিয়ন্ত্রণ কেন্দ্র", "createCombo": "কম্বো তৈরি করুন", "editCombo": "কম্বো সম্পাদনা করুন", "deleteCombo": "কম্বো মুছুন", diff --git a/src/i18n/messages/bs.json b/src/i18n/messages/bs.json index b03eaa35f35d..84e14a05dfba 100644 --- a/src/i18n/messages/bs.json +++ b/src/i18n/messages/bs.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "Ugrađene auto/* kombinacije se dinamički rješavaju iz povezanih provajdera. Koristite bilo koji od ovih ID-eva kao polje modela — nije potrebna konfiguracija.", "autoCatalogExpand": "Proširi katalog auto-rutiranja", "autoCatalogCollapse": "Skupljaj katalog auto-rutiranja", + "autoCatalogComboCount": "{count} kombinacija", + "autoCatalogCandidates": "{count} kandidata", + "autoCatalogNoCandidates": "Nema odgovarajućih provajdera", + "autoCatalogKindVariant": "varijanta", + "autoCatalogKindTemplate": "šablon", + "autoCatalogKindCategory": "kategorija", + "autoCatalogKindFamily": "porodica", + "autoComboTestTruncated": "Testirano {tested} od {total} aktivnih kandidata (najveće težine prvo).", + "controlCenter": "Kontrolni centar", "createCombo": "Kreiraj kombinaciju", "editCombo": "Uredi kombinaciju", "deleteCombo": "Obriši kombinaciju", diff --git a/src/i18n/messages/cs.json b/src/i18n/messages/cs.json index b42dfe40f850..de27fecc3a97 100644 --- a/src/i18n/messages/cs.json +++ b/src/i18n/messages/cs.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "Vestavěná automatická/* komba řešená dynamicky od připojených poskytovatelů. Použijte kterékoli z těchto ID jako pole modelu – není potřeba žádné nastavení.", "autoCatalogExpand": "Rozbalte katalog automatického směrování", "autoCatalogCollapse": "Sbalit katalog automatického směrování", + "autoCatalogComboCount": "{count} kombinací", + "autoCatalogCandidates": "{count} kandidátů", + "autoCatalogNoCandidates": "Žádní odpovídající poskytovatelé", + "autoCatalogKindVariant": "varianta", + "autoCatalogKindTemplate": "šablona", + "autoCatalogKindCategory": "kategorie", + "autoCatalogKindFamily": "rodina", + "autoComboTestTruncated": "Otestováno {tested} z {total} živých kandidátů (nejvyšší váhy první).", + "controlCenter": "Ovládací centrum", "createCombo": "Vytvořit kombinaci", "editCombo": "Upravit kombinaci", "deleteCombo": "Smazat kombinaci", diff --git a/src/i18n/messages/da.json b/src/i18n/messages/da.json index 964e6672f805..2d9719f8078c 100644 --- a/src/i18n/messages/da.json +++ b/src/i18n/messages/da.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "Indbyggede auto/*-kombinationer løst dynamisk fra tilsluttede udbydere. Brug et af disse id'er som modelfelt - ingen opsætning nødvendig.", "autoCatalogExpand": "Udvid auto-routing-kataloget", "autoCatalogCollapse": "Skjul kataloget til automatisk routing", + "autoCatalogComboCount": "{count} combos", + "autoCatalogCandidates": "{count} kandidater", + "autoCatalogNoCandidates": "Ingen matchende udbydere", + "autoCatalogKindVariant": "variant", + "autoCatalogKindTemplate": "skabelon", + "autoCatalogKindCategory": "kategori", + "autoCatalogKindFamily": "familie", + "autoComboTestTruncated": "Testet {tested} af {total} aktive kandidater (højeste vægte først).", + "controlCenter": "Kontrolcenter", "createCombo": "Opret Combo", "editCombo": "Rediger kombination", "deleteCombo": "Slet Combo", diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index 2410cdf3aebc..dbeb35fa750a 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "Integrierte Auto/*-Kombinationen werden dynamisch von verbundenen Anbietern aufgelöst. Verwenden Sie eine dieser IDs als Modellfeld – keine Einrichtung erforderlich.", "autoCatalogExpand": "Erweitern Sie den Katalog für automatisches Routing", "autoCatalogCollapse": "Auto-Routing-Katalog einklappen", + "autoCatalogComboCount": "{count} Kombinationen", + "autoCatalogCandidates": "{count} Kandidaten", + "autoCatalogNoCandidates": "Keine passenden Anbieter", + "autoCatalogKindVariant": "Variante", + "autoCatalogKindTemplate": "Vorlage", + "autoCatalogKindCategory": "Kategorie", + "autoCatalogKindFamily": "Familie", + "autoComboTestTruncated": "Getestet {tested} von {total} aktiven Kandidaten (höchste Gewichte zuerst).", + "controlCenter": "Steuerzentrale", "createCombo": "Combo erstellen", "editCombo": "Combo bearbeiten", "deleteCombo": "Combo löschen", diff --git a/src/i18n/messages/el.json b/src/i18n/messages/el.json index 31e4d1ac470c..031c3983fe35 100644 --- a/src/i18n/messages/el.json +++ b/src/i18n/messages/el.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "Ενσωματωμένοι συνδυασμοί auto/* που επιλύονται δυναμικά από συνδεδεμένους παρόχους. Χρησιμοποιήστε οποιοδήποτε από αυτά τα αναγνωριστικά ως τιμή του πεδίου μοντέλου — δεν απαιτείται ρύθμιση.", "autoCatalogExpand": "Ανάπτυξη καταλόγου αυτόματης δρομολόγησης", "autoCatalogCollapse": "Σύμπτυξη καταλόγου αυτόματης δρομολόγησης", + "autoCatalogComboCount": "{count} συνδυασμοί", + "autoCatalogCandidates": "{count} υποψήφιοι", + "autoCatalogNoCandidates": "Δεν υπάρχουν αντίστοιχοι πάροχοι", + "autoCatalogKindVariant": "παραλλαγή", + "autoCatalogKindTemplate": "πρότυπο", + "autoCatalogKindCategory": "κατηγορία", + "autoCatalogKindFamily": "οικογένεια", + "autoComboTestTruncated": "Δοκιμάστηκαν {tested} από {total} ζωντανούς υποψηφίους (με τις υψηλότερες βαρύτητες πρώτα).", + "controlCenter": "Κέντρο Ελέγχου", "createCombo": "Δημιουργία Συνδυασμού", "editCombo": "Επεξεργασία Συνδυασμού", "deleteCombo": "Διαγραφή Συνδυασμού", diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json index af222a2bdbd3..81b113acd4bf 100644 --- a/src/i18n/messages/es.json +++ b/src/i18n/messages/es.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "Las combinaciones automáticas/* integradas se resuelven dinámicamente desde proveedores conectados. Utilice cualquiera de estos ID como campo modelo; no es necesaria ninguna configuración.", "autoCatalogExpand": "Ampliar el catálogo de enrutamiento automático", "autoCatalogCollapse": "Contraer catálogo de enrutamiento automático", + "autoCatalogComboCount": "{count} combinaciones", + "autoCatalogCandidates": "{count} candidatos", + "autoCatalogNoCandidates": "No hay proveedores coincidentes", + "autoCatalogKindVariant": "variante", + "autoCatalogKindTemplate": "plantilla", + "autoCatalogKindCategory": "categoría", + "autoCatalogKindFamily": "familia", + "autoComboTestTruncated": "Se probaron {tested} de {total} candidatos en vivo (pesos más altos primero).", + "controlCenter": "Centro de Control", "createCombo": "Crear combinación", "editCombo": "Editar combinación", "deleteCombo": "Eliminar combinación", diff --git a/src/i18n/messages/et.json b/src/i18n/messages/et.json index a97c01eefcd0..21d205bfd2e1 100644 --- a/src/i18n/messages/et.json +++ b/src/i18n/messages/et.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "Sisseehitatud auto/* kombinatsioonid, mis määratakse dünaamiliselt ühendatud teenusepakkujate põhjal. Kasuta mudeli väljal ükskõik millist neist ID-dest — seadistamine pole vajalik.", "autoCatalogExpand": "Laienda automaatse marsruutimise kataloogi", "autoCatalogCollapse": "Ahenda automaatse marsruutimise kataloog", + "autoCatalogComboCount": "{count} kombinatsioonid", + "autoCatalogCandidates": "{count} kandidaate", + "autoCatalogNoCandidates": "Ei sobivaid teenusepakkujaid", + "autoCatalogKindVariant": "variant", + "autoCatalogKindTemplate": "mall", + "autoCatalogKindCategory": "kategooria", + "autoCatalogKindFamily": "perekond", + "autoComboTestTruncated": "Testitud {tested} {total} elavat kandidaati (kõrgeimad kaalud esimesena).", + "controlCenter": "Kontrollikeskus", "createCombo": "Loo kombinatsioon", "editCombo": "Muuda kombinatsiooni", "deleteCombo": "Kustuta kombinatsioon", diff --git a/src/i18n/messages/fa.json b/src/i18n/messages/fa.json index 8a03c7150f9e..9c2a99be193c 100644 --- a/src/i18n/messages/fa.json +++ b/src/i18n/messages/fa.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "ترکیب های خودکار/* داخلی به صورت پویا از ارائه دهندگان متصل حل می شوند. از هر یک از این شناسه ها به عنوان فیلد مدل استفاده کنید — نیازی به تنظیم نیست.", "autoCatalogExpand": "کاتالوگ مسیریابی خودکار را گسترش دهید", "autoCatalogCollapse": "جمع کردن کاتالوگ مسیریابی خودکار", + "autoCatalogComboCount": "{count} ترکیب ها", + "autoCatalogCandidates": "{count} نامزدها", + "autoCatalogNoCandidates": "هیچ ارائه دهنده ای مطابقت ندارد", + "autoCatalogKindVariant": "نوع", + "autoCatalogKindTemplate": "الگو", + "autoCatalogKindCategory": "دسته", + "autoCatalogKindFamily": "خانواده", + "autoComboTestTruncated": "{tested} از {total} نامزد زنده آزمایش شده (با بالاترین وزن ها اول).", + "controlCenter": "مرکز کنترل", "createCombo": "ایجاد ترکیب", "editCombo": "ویرایش ترکیب", "deleteCombo": "حذف ترکیب", diff --git a/src/i18n/messages/fi.json b/src/i18n/messages/fi.json index d808cde455ab..d541e37848e4 100644 --- a/src/i18n/messages/fi.json +++ b/src/i18n/messages/fi.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "Sisäänrakennetut auto/*-yhdistelmät, jotka on ratkaistu dynaamisesti yhdistettyjen palveluntarjoajien kautta. Käytä mitä tahansa näistä tunnuksista mallikenttänä – asetusta ei tarvita.", "autoCatalogExpand": "Laajenna automaattisen reitityksen luettelo", "autoCatalogCollapse": "Kutista automaattisen reitityksen luettelo", + "autoCatalogComboCount": "{count} yhdistelmää", + "autoCatalogCandidates": "{count} ehdokasta", + "autoCatalogNoCandidates": "Ei vastaavia palveluntarjoajia", + "autoCatalogKindVariant": "variantti", + "autoCatalogKindTemplate": "malli", + "autoCatalogKindCategory": "kategoria", + "autoCatalogKindFamily": "perhe", + "autoComboTestTruncated": "Testattu {tested} {total} elävää ehdokasta (korkeimmat painot ensin).", + "controlCenter": "Ohjauskeskus", "createCombo": "Luo yhdistelmä", "editCombo": "Muokkaa yhdistelmää", "deleteCombo": "Poista yhdistelmä", diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index 215e1bdd2c62..d73ceb7fc546 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "Combos auto/* intégrés résolus dynamiquement à partir des fournisseurs connectés. Utilisez l’un de ces identifiants comme champ de modèle – aucune configuration n’est nécessaire.", "autoCatalogExpand": "Développer le catalogue de routage automatique", "autoCatalogCollapse": "Réduire le catalogue de routage automatique", + "autoCatalogComboCount": "{count} combos", + "autoCatalogCandidates": "{count} candidats", + "autoCatalogNoCandidates": "Aucun fournisseur correspondant", + "autoCatalogKindVariant": "variante", + "autoCatalogKindTemplate": "modèle", + "autoCatalogKindCategory": "catégorie", + "autoCatalogKindFamily": "famille", + "autoComboTestTruncated": "Testé {tested} de {total} candidats en direct (poids les plus élevés en premier).", + "controlCenter": "Centre de contrôle", "createCombo": "Créer une combinaison", "editCombo": "Modifier la combinaison", "deleteCombo": "Supprimer la combinaison", diff --git a/src/i18n/messages/ga.json b/src/i18n/messages/ga.json index 5c647c4b6bcd..e91fc78c41c4 100644 --- a/src/i18n/messages/ga.json +++ b/src/i18n/messages/ga.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "Teaglamaí auto/* ionsuite a réitítear go dinimiciúil ó sholáthraithe ceangailte. Úsáid aon cheann de na IDs seo mar réimse model — níl aon chumrú de dhíth.", "autoCatalogExpand": "Leathnaigh an chatalóg ródaithe uathoibríoch", "autoCatalogCollapse": "Conlaigh an chatalóg ródaithe uathoibríoch", + "autoCatalogComboCount": "{count} comhoibrithe", + "autoCatalogCandidates": "{count} iarrthóirí", + "autoCatalogNoCandidates": "Níl soláthraithe comhoiriúnacha", + "autoCatalogKindVariant": "leagan", + "autoCatalogKindTemplate": "teimpléad", + "autoCatalogKindCategory": "catagóir", + "autoCatalogKindFamily": "teaghlach", + "autoComboTestTruncated": "Tástáilte {tested} de {total} iarrthóirí beo (na meáchain is airde ar dtús).", + "controlCenter": "Lárionad Rialaithe", "createCombo": "Cruthaigh Teaglama", "editCombo": "Cuir Teaglama in Eagar", "deleteCombo": "Scrios Teaglama", diff --git a/src/i18n/messages/gu.json b/src/i18n/messages/gu.json index bcb5e74ad306..7faeea029056 100644 --- a/src/i18n/messages/gu.json +++ b/src/i18n/messages/gu.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "બિલ્ટ-ઇન ઓટો/* કોમ્બોઝ કનેક્ટેડ પ્રદાતાઓ તરફથી ગતિશીલ રીતે ઉકેલાય છે. આમાંથી કોઈપણ ID નો મોડેલ ફીલ્ડ તરીકે ઉપયોગ કરો — કોઈ સેટઅપની જરૂર નથી.", "autoCatalogExpand": "સ્વતઃ-રાઉટિંગ કેટલોગ વિસ્તૃત કરો", "autoCatalogCollapse": "સ્વતઃ-રાઉટિંગ કેટલોગ સંકુચિત કરો", + "autoCatalogComboCount": "{count} કોમ્બોઝ", + "autoCatalogCandidates": "{count} ઉમેદવારો", + "autoCatalogNoCandidates": "મિલતા પ્રદાતાઓ નથી", + "autoCatalogKindVariant": "વેરિઅન્ટ", + "autoCatalogKindTemplate": "ટેમ્પલેટ", + "autoCatalogKindCategory": "શ્રેણી", + "autoCatalogKindFamily": "કુટુંબ", + "autoComboTestTruncated": "ટેસ્ટ કરેલા {tested} માંથી {total} જીવંત ઉમેદવારો (ઉચ્ચતમ વજન પહેલા).", + "controlCenter": "કંટ્રોલ સેન્ટર", "createCombo": "કોમ્બો બનાવો", "editCombo": "કોમ્બો સંપાદિત કરો", "deleteCombo": "કોમ્બો કાઢી નાખો", diff --git a/src/i18n/messages/ha.json b/src/i18n/messages/ha.json index 177ae7803a6c..29a7a6f7c2d4 100644 --- a/src/i18n/messages/ha.json +++ b/src/i18n/messages/ha.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "Haɗe-haɗen auto/* da aka tanada suna warwarewa kai tsaye bisa masu samarwa da aka haɗa. Yi amfani da kowane ɗayan waɗannan IDs a matsayin filin model — ba sai an saita komai ba.", "autoCatalogExpand": "Faɗaɗa kundin rarrabawa ta atomatik", "autoCatalogCollapse": "Naɗe kundin rarrabawa ta atomatik", + "autoCatalogComboCount": "{count} haɗin gwiwa", + "autoCatalogCandidates": "{count} masu yiwuwa", + "autoCatalogNoCandidates": "Babu masu bayarwa masu dacewa", + "autoCatalogKindVariant": "bambanci", + "autoCatalogKindTemplate": "samfuri", + "autoCatalogKindCategory": "rukunin", + "autoCatalogKindFamily": "iyali", + "autoComboTestTruncated": "An gwada {tested} daga {total} masu yiwuwa na rayuwa (matsakaicin nauyi na farko).", + "controlCenter": "Cibiyar Kulawa", "createCombo": "Ƙirƙiri Haɗi", "editCombo": "Gyara Haɗi", "deleteCombo": "Share Haɗi", diff --git a/src/i18n/messages/he.json b/src/i18n/messages/he.json index 2fdcbfa5e502..345560eec4d5 100644 --- a/src/i18n/messages/he.json +++ b/src/i18n/messages/he.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "שילובי אוטומטי/* מובנים שנפתרו באופן דינמי מספקים מחוברים. השתמש בכל אחד מהמזהים האלה בתור שדה הדגם - אין צורך בהגדרה.", "autoCatalogExpand": "הרחב את קטלוג הניתוב האוטומטי", "autoCatalogCollapse": "כווץ את קטלוג הניתוב האוטומטי", + "autoCatalogComboCount": "{count} קומבינציות", + "autoCatalogCandidates": "{count} מועמדים", + "autoCatalogNoCandidates": "אין ספקים תואמים", + "autoCatalogKindVariant": "וריאנט", + "autoCatalogKindTemplate": "תבנית", + "autoCatalogKindCategory": "קטגוריה", + "autoCatalogKindFamily": "משפחה", + "autoComboTestTruncated": "נבדקו {tested} מתוך {total} מועמדים חיים (משקלות הגבוהים קודם).", + "controlCenter": "מרכז בקרה", "createCombo": "צור קומבו", "editCombo": "ערוך משולבת", "deleteCombo": "מחק שילוב", diff --git a/src/i18n/messages/hi.json b/src/i18n/messages/hi.json index 80ee9e065554..18eccb1c9060 100644 --- a/src/i18n/messages/hi.json +++ b/src/i18n/messages/hi.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "अंतर्निहित ऑटो/* कॉम्बो को कनेक्टेड प्रदाताओं से गतिशील रूप से हल किया गया। इनमें से किसी भी आईडी को मॉडल फ़ील्ड के रूप में उपयोग करें - किसी सेटअप की आवश्यकता नहीं है।", "autoCatalogExpand": "ऑटो-रूटिंग कैटलॉग का विस्तार करें", "autoCatalogCollapse": "ऑटो-रूटिंग कैटलॉग को संक्षिप्त करें", + "autoCatalogComboCount": "{count} कॉम्बो", + "autoCatalogCandidates": "{count} उम्मीदवार", + "autoCatalogNoCandidates": "कोई मेल खाने वाले प्रदाता नहीं", + "autoCatalogKindVariant": "वेरिएंट", + "autoCatalogKindTemplate": "टेम्पलेट", + "autoCatalogKindCategory": "श्रेणी", + "autoCatalogKindFamily": "परिवार", + "autoComboTestTruncated": "{total} लाइव उम्मीदवारों में से {tested} का परीक्षण किया गया (उच्चतम वजन पहले)।", + "controlCenter": "नियंत्रण केंद्र", "createCombo": "कॉम्बो बनाएं", "editCombo": "कॉम्बो संपादित करें", "deleteCombo": "कॉम्बो हटाएँ", diff --git a/src/i18n/messages/hr.json b/src/i18n/messages/hr.json index 3bab8444c9a0..6c959191a8f2 100644 --- a/src/i18n/messages/hr.json +++ b/src/i18n/messages/hr.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "Ugrađene auto/* kombinacije dinamički se određuju na temelju povezanih pružatelja usluga. Upotrijebite bilo koji od ovih ID-ova kao vrijednost polja model — postavljanje nije potrebno.", "autoCatalogExpand": "Proširi katalog automatskog usmjeravanja", "autoCatalogCollapse": "Sažmi katalog automatskog usmjeravanja", + "autoCatalogComboCount": "{count} kombinacija", + "autoCatalogCandidates": "{count} kandidata", + "autoCatalogNoCandidates": "Nema odgovarajućih pružatelja", + "autoCatalogKindVariant": "varijanta", + "autoCatalogKindTemplate": "predložak", + "autoCatalogKindCategory": "kategorija", + "autoCatalogKindFamily": "obitelj", + "autoComboTestTruncated": "Testirano {tested} od {total} aktivnih kandidata (najveće težine prvo).", + "controlCenter": "Kontrolni centar", "createCombo": "Stvori kombinaciju", "editCombo": "Uredi kombinaciju", "deleteCombo": "Izbriši kombinaciju", diff --git a/src/i18n/messages/hu.json b/src/i18n/messages/hu.json index 90b061ff08fd..2230709b4dc5 100644 --- a/src/i18n/messages/hu.json +++ b/src/i18n/messages/hu.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "A csatlakoztatott szolgáltatóktól dinamikusan megoldott beépített auto/* kombók. Használja ezen azonosítók bármelyikét modellmezőként – nincs szükség beállításra.", "autoCatalogExpand": "Az automatikus útválasztási katalógus kibontása", "autoCatalogCollapse": "Az automatikus útválasztási katalógus összecsukása", + "autoCatalogComboCount": "{count} kombináció", + "autoCatalogCandidates": "{count} jelölt", + "autoCatalogNoCandidates": "Nincsenek megfelelő szolgáltatók", + "autoCatalogKindVariant": "változat", + "autoCatalogKindTemplate": "sablon", + "autoCatalogKindCategory": "kategória", + "autoCatalogKindFamily": "család", + "autoComboTestTruncated": "Tesztelt {tested} a {total} élő jelöltből (legmagasabb súlyok először).", + "controlCenter": "Irányítóközpont", "createCombo": "Combo létrehozása", "editCombo": "Combo szerkesztése", "deleteCombo": "Combo törlése", diff --git a/src/i18n/messages/hy.json b/src/i18n/messages/hy.json index 8ccfa89521a6..06e841657ac6 100644 --- a/src/i18n/messages/hy.json +++ b/src/i18n/messages/hy.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "Ներկառուցված auto/* կոմբինացիաները դինամիկորեն որոշվում են միացված պրովայդերների հիման վրա։ Այս նույնացուցիչներից ցանկացածն օգտագործեք որպես model դաշտի արժեք․ կարգավորում չի պահանջվում։", "autoCatalogExpand": "Ընդարձակել ավտոմատ երթուղավորման կատալոգը", "autoCatalogCollapse": "Ծալել ավտոմատ երթուղավորման կատալոգը", + "autoCatalogComboCount": "{count} կոմբոներ", + "autoCatalogCandidates": "{count} թեկնածուներ", + "autoCatalogNoCandidates": "Համապատասխան մատակարարներ չկան", + "autoCatalogKindVariant": "տարբերակ", + "autoCatalogKindTemplate": "տպագիր", + "autoCatalogKindCategory": "կատեգորիա", + "autoCatalogKindFamily": "ընտանիք", + "autoComboTestTruncated": "Թեստավորվել է {tested} {total} կենդանի թեկնածուներից (բարձրագույն քաշերով առաջին)։", + "controlCenter": "Կառավարման կենտրոն", "createCombo": "Ստեղծել կոմբինացիա", "editCombo": "Խմբագրել կոմբինացիան", "deleteCombo": "Ջնջել կոմբինացիան", diff --git a/src/i18n/messages/id.json b/src/i18n/messages/id.json index ccfed3aecabb..2663fc5e6a59 100644 --- a/src/i18n/messages/id.json +++ b/src/i18n/messages/id.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "Kombo otomatis/* bawaan diselesaikan secara dinamis dari penyedia yang terhubung. Gunakan salah satu ID ini sebagai bidang model — tidak perlu penyiapan.", "autoCatalogExpand": "Perluas katalog perutean otomatis", "autoCatalogCollapse": "Ciutkan katalog perutean otomatis", + "autoCatalogComboCount": "{count} kombinasi", + "autoCatalogCandidates": "{count} kandidat", + "autoCatalogNoCandidates": "Tidak ada penyedia yang cocok", + "autoCatalogKindVariant": "varian", + "autoCatalogKindTemplate": "template", + "autoCatalogKindCategory": "kategori", + "autoCatalogKindFamily": "keluarga", + "autoComboTestTruncated": "Diuji {tested} dari {total} kandidat langsung (berat tertinggi terlebih dahulu).", + "controlCenter": "Pusat Kontrol", "createCombo": "Buat Kombo", "editCombo": "Sunting Kombo", "deleteCombo": "Hapus Kombo", diff --git a/src/i18n/messages/ig.json b/src/i18n/messages/ig.json index 16f17a690cff..2539256397b8 100644 --- a/src/i18n/messages/ig.json +++ b/src/i18n/messages/ig.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "Ngwakọta auto/* arụnyere n'ime sistemụ, nke a na-ekpebi n'ụzọ na-agbanwe agbanwe site na provider ejikọrọ. Jiri ID ọ bụla n'ime ndị a dị ka mpaghara model — nhazi adịghị mkpa.", "autoCatalogExpand": "Gbasaa katalọgụ ntụgharị ụzọ akpaaka", "autoCatalogCollapse": "Kpokọta katalọgụ ntụgharị ụzọ akpaaka", + "autoCatalogComboCount": "{count} combos", + "autoCatalogCandidates": "{count} ndị na-eme nhọrọ", + "autoCatalogNoCandidates": "Enweghị ndị na-enye ọrụ kwekọrọ", + "autoCatalogKindVariant": "dị iche", + "autoCatalogKindTemplate": "usoro", + "autoCatalogKindCategory": "kategoria", + "autoCatalogKindFamily": "ezinụlọ", + "autoComboTestTruncated": "Nyochara {tested} nke {total} ndị na-eme nhọrọ ndụ (nke kachasị ibu mbụ).", + "controlCenter": "Ọrụ Nlekota", "createCombo": "Mepụta Ngwakọta", "editCombo": "Dezie Ngwakọta", "deleteCombo": "Hichapụ Ngwakọta", diff --git a/src/i18n/messages/it.json b/src/i18n/messages/it.json index 2fdfb078c00d..8c91c4b0c1a1 100644 --- a/src/i18n/messages/it.json +++ b/src/i18n/messages/it.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "Combo auto/* integrate risolte dinamicamente dai provider collegati. Utilizza uno qualsiasi di questi ID come campo del modello: non è necessaria alcuna configurazione.", "autoCatalogExpand": "Espandi il catalogo di routing automatico", "autoCatalogCollapse": "Comprimi il catalogo di routing automatico", + "autoCatalogComboCount": "{count} combinazioni", + "autoCatalogCandidates": "{count} candidati", + "autoCatalogNoCandidates": "Nessun fornitore corrispondente", + "autoCatalogKindVariant": "variante", + "autoCatalogKindTemplate": "modello", + "autoCatalogKindCategory": "categoria", + "autoCatalogKindFamily": "famiglia", + "autoComboTestTruncated": "Testati {tested} di {total} candidati attivi (pesi più alti per primi).", + "controlCenter": "Centro di Controllo", "createCombo": "Crea combinazione", "editCombo": "Modifica combinazione", "deleteCombo": "Elimina combinazione", diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json index bd0e0963b8d7..f42e7fe3fa2b 100644 --- a/src/i18n/messages/ja.json +++ b/src/i18n/messages/ja.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "接続されたプロバイダーから動的に解決される組み込みの auto/* コンボ。これらの ID のいずれかをモデル フィールドとして使用します。設定は必要ありません。", "autoCatalogExpand": "自動ルーティングカタログを展開する", "autoCatalogCollapse": "自動ルーティングカタログを折りたたむ", + "autoCatalogComboCount": "{count} コンボ", + "autoCatalogCandidates": "{count} 候補者", + "autoCatalogNoCandidates": "一致するプロバイダーはありません", + "autoCatalogKindVariant": "バリアント", + "autoCatalogKindTemplate": "テンプレート", + "autoCatalogKindCategory": "カテゴリ", + "autoCatalogKindFamily": "ファミリー", + "autoComboTestTruncated": "{total} のライブ候補者のうち {tested} をテストしました(最も重みのあるものから)。", + "controlCenter": "コントロールセンター", "createCombo": "コンボを作成する", "editCombo": "コンボを編集する", "deleteCombo": "コンボの削除", diff --git a/src/i18n/messages/ka.json b/src/i18n/messages/ka.json index 182123e5b532..d55660b381c1 100644 --- a/src/i18n/messages/ka.json +++ b/src/i18n/messages/ka.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "ჩაშენებული auto/* კომბინაციები დინამიკურად განისაზღვრება დაკავშირებული პროვაიდერების მიხედვით. გამოიყენეთ ამ ID-თაგან ნებისმიერი მოდელის ველში — კონფიგურაცია საჭირო არ არის.", "autoCatalogExpand": "ავტომარშრუტიზაციის კატალოგის გაშლა", "autoCatalogCollapse": "ავტომარშრუტიზაციის კატალოგის ჩაკეცვა", + "autoCatalogComboCount": "{count} კომბინაციები", + "autoCatalogCandidates": "{count} კანდიდატები", + "autoCatalogNoCandidates": "შესაბამისი პროვაიდერები არ არის", + "autoCatalogKindVariant": "ვარიანტი", + "autoCatalogKindTemplate": "შაბлонი", + "autoCatalogKindCategory": "კატეგორია", + "autoCatalogKindFamily": "ოჯახი", + "autoComboTestTruncated": "გამოცდილი {tested} {total} ცოცხალი კანდიდატიდან (მაღალი წონების მიხედვით).", + "controlCenter": "კონტროლის ცენტრი", "createCombo": "კომბოს შექმნა", "editCombo": "კომბოს რედაქტირება", "deleteCombo": "კომბოს წაშლა", diff --git a/src/i18n/messages/km.json b/src/i18n/messages/km.json index 9cdfbea3f861..3241b1b00e1c 100644 --- a/src/i18n/messages/km.json +++ b/src/i18n/messages/km.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "បន្សំ auto/* ដែលមានស្រាប់ ត្រូវបានកំណត់ជាថាមវន្តពីអ្នកផ្តល់សេវាដែលបានភ្ជាប់។ ប្រើ ID ណាមួយក្នុងចំណោមនេះជាវាលម៉ូដែល — មិនចាំបាច់កំណត់រចនាសម្ព័ន្ធទេ។", "autoCatalogExpand": "ពង្រីកកាតាឡុកបញ្ជូនដោយស្វ័យប្រវត្តិ", "autoCatalogCollapse": "បង្រួមកាតាឡុកបញ្ជូនដោយស្វ័យប្រវត្តិ", + "autoCatalogComboCount": "{count} កំបូប", + "autoCatalogCandidates": "{count} អ្នកដាក់ពាក្យ", + "autoCatalogNoCandidates": "គ្មានអ្នកផ្តល់សេវាដែលត្រូវគ្នា", + "autoCatalogKindVariant": "អ្វីដែលខុសគ្នា", + "autoCatalogKindTemplate": "គំរូ", + "autoCatalogKindCategory": "ប្រភេទ", + "autoCatalogKindFamily": "គ្រួសារ", + "autoComboTestTruncated": "បានសាកល្បង {tested} នៃ {total} អ្នកដាក់ពាក្យរស់ (ទំងន់ខ្ពស់បំផុតមុន)។", + "controlCenter": "មជ្ឈមណ្ឌលគ្រប់គ្រង", "createCombo": "បង្កើតបន្សំ", "editCombo": "កែសម្រួលបន្សំ", "deleteCombo": "លុបបន្សំ", diff --git a/src/i18n/messages/kn.json b/src/i18n/messages/kn.json index 28431bd423b1..4f1eed24ac11 100644 --- a/src/i18n/messages/kn.json +++ b/src/i18n/messages/kn.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "ಸಂಪರ್ಕಿತ ಪ್ರೊವೈಡರ್ಗಳಿಂದ ಡೈನಾಮಿಕ್ ಆಗಿ ನಿರ್ಧರಿಸಲಾಗುವ ಬಿಲ್ಟ್-ಇನ್ auto/* ಸಂಯೋಜನೆಗಳು. ಈ IDs ಗಳಲ್ಲಿ ಯಾವುದನ್ನಾದರೂ ಮಾದರಿ ಕ್ಷೇತ್ರವಾಗಿ ಬಳಸಿ — ಸೆಟಪ್ ಅಗತ್ಯವಿಲ್ಲ.", "autoCatalogExpand": "ಸ್ವಯಂಚಾಲಿತ ರೌಟಿಂಗ್ ಕ್ಯಾಟಲಾಗ್ ಅನ್ನು ವಿಸ್ತರಿಸಿ", "autoCatalogCollapse": "ಸ್ವಯಂಚಾಲಿತ ರೌಟಿಂಗ್ ಕ್ಯಾಟಲಾಗ್ ಅನ್ನು ಕುಗ್ಗಿಸಿ", + "autoCatalogComboCount": "{count} ಕಾಂಬೋಸ್", + "autoCatalogCandidates": "{count} ಅಭ್ಯರ್ಥಿಗಳು", + "autoCatalogNoCandidates": "ಹೋಲಿಸುವ ಒದಗಿಸುವವರು ಇಲ್ಲ", + "autoCatalogKindVariant": "ವ್ಯತ್ಯಾಸ", + "autoCatalogKindTemplate": "ಟೆಂಪ್ಲೇಟ್", + "autoCatalogKindCategory": "ವರ್ಗ", + "autoCatalogKindFamily": "ಕುಟುಂಬ", + "autoComboTestTruncated": "ಜೀವಂತ ಅಭ್ಯರ್ಥಿಗಳ {total}ರಲ್ಲಿ {tested} ಅನ್ನು ಪರೀಕ್ಷಿಸಲಾಗಿದೆ (ಅತ್ಯುತ್ತಮ ತೂಕಗಳು ಮೊದಲಿಗೆ).", + "controlCenter": "ನಿಯಂತ್ರಣ ಕೇಂದ್ರ", "createCombo": "ಸಂಯೋಜನೆಯನ್ನು ರಚಿಸಿ", "editCombo": "ಸಂಯೋಜನೆಯನ್ನು ಸಂಪಾದಿಸಿ", "deleteCombo": "ಸಂಯೋಜನೆಯನ್ನು ಅಳಿಸಿ", diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json index df20779c5e6c..c6fc54404b93 100644 --- a/src/i18n/messages/ko.json +++ b/src/i18n/messages/ko.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "내장된 auto/* 콤보는 연결된 공급자로부터 동적으로 확인됩니다. 이러한 ID 중 하나를 모델 필드로 사용하세요 — 설정이 필요하지 않습니다.", "autoCatalogExpand": "자동 라우팅 카탈로그 확장", "autoCatalogCollapse": "자동 라우팅 카탈로그 축소", + "autoCatalogComboCount": "{count} 콤보", + "autoCatalogCandidates": "{count} 후보", + "autoCatalogNoCandidates": "일치하는 공급자가 없습니다", + "autoCatalogKindVariant": "변형", + "autoCatalogKindTemplate": "템플릿", + "autoCatalogKindCategory": "카테고리", + "autoCatalogKindFamily": "가족", + "autoComboTestTruncated": "테스트된 {tested}개의 {total} 라이브 후보 (가장 높은 가중치 우선).", + "controlCenter": "제어 센터", "createCombo": "콤보 생성", "editCombo": "콤보 편집", "deleteCombo": "콤보 삭제", diff --git a/src/i18n/messages/lt.json b/src/i18n/messages/lt.json index dc0b18bd24b4..74b3d6302c7c 100644 --- a/src/i18n/messages/lt.json +++ b/src/i18n/messages/lt.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "Integruotos auto/* kombinacijos, dinamiškai parenkamos iš prijungtų teikėjų. Naudokite bet kurį iš šių ID kaip modelio lauko reikšmę — konfigūruoti nereikia.", "autoCatalogExpand": "Išskleisti automatinio maršrutizavimo katalogą", "autoCatalogCollapse": "Sutraukti automatinio maršrutizavimo katalogą", + "autoCatalogComboCount": "{count} kombinacijų", + "autoCatalogCandidates": "{count} kandidatų", + "autoCatalogNoCandidates": "Nėra atitinkančių tiekėjų", + "autoCatalogKindVariant": "variantas", + "autoCatalogKindTemplate": "šablonas", + "autoCatalogKindCategory": "kategorija", + "autoCatalogKindFamily": "šeima", + "autoComboTestTruncated": "Išbandyta {tested} iš {total} gyvų kandidatų (aukščiausi svoriai pirmiausia).", + "controlCenter": "Valdymo centras", "createCombo": "Sukurti kombinaciją", "editCombo": "Redaguoti kombinaciją", "deleteCombo": "Ištrinti kombinaciją", diff --git a/src/i18n/messages/lv.json b/src/i18n/messages/lv.json index 2958edc473e5..7753e3bd73da 100644 --- a/src/i18n/messages/lv.json +++ b/src/i18n/messages/lv.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "Iebūvētās auto/* kombinācijas tiek dinamiski noteiktas, pamatojoties uz pievienotajiem pakalpojumu sniedzējiem. Izmantojiet jebkuru no šiem ID kā modeļa lauka vērtību — iestatīšana nav nepieciešama.", "autoCatalogExpand": "Paplašināt automātiskās maršrutēšanas katalogu", "autoCatalogCollapse": "Sakļaut automātiskās maršrutēšanas katalogu", + "autoCatalogComboCount": "{count} kombinācijas", + "autoCatalogCandidates": "{count} kandidāti", + "autoCatalogNoCandidates": "Nav atbilstošu pakalpojumu sniedzēju", + "autoCatalogKindVariant": "varianta", + "autoCatalogKindTemplate": "veidne", + "autoCatalogKindCategory": "kategorija", + "autoCatalogKindFamily": "ģimene", + "autoComboTestTruncated": "Pārbaudīti {tested} no {total} dzīvajiem kandidātiem (visaugstākās svara pirmās).", + "controlCenter": "Kontroles centrs", "createCombo": "Izveidot kombināciju", "editCombo": "Rediģēt kombināciju", "deleteCombo": "Dzēst kombināciju", diff --git a/src/i18n/messages/ml.json b/src/i18n/messages/ml.json index cc20dc0cf258..568cd27a8a7e 100644 --- a/src/i18n/messages/ml.json +++ b/src/i18n/messages/ml.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "കണക്റ്റ് ചെയ്ത പ്രൊവൈഡർമാരിൽ നിന്ന് ഡൈനാമിക്കായി നിർണ്ണയിക്കപ്പെടുന്ന ബിൽറ്റ്-ഇൻ auto/* കോംബോകൾ. ഈ ID-കളിൽ ഏതും മോഡൽ ഫീൽഡായി ഉപയോഗിക്കാം — സജ്ജീകരണം ആവശ്യമില്ല.", "autoCatalogExpand": "ഓട്ടോ-റൂട്ടിംഗ് കാറ്റലോഗ് വിടർത്തുക", "autoCatalogCollapse": "ഓട്ടോ-റൂട്ടിംഗ് കാറ്റലോഗ് ചുരുക്കുക", + "autoCatalogComboCount": "{count} കോമ്പോസ്", + "autoCatalogCandidates": "{count} സ്ഥാനാർത്ഥികൾ", + "autoCatalogNoCandidates": "മത്സ്യബന്ധനങ്ങൾ ഇല്ല", + "autoCatalogKindVariant": "വ്യത്യാസം", + "autoCatalogKindTemplate": "ടെംപ്ലേറ്റ്", + "autoCatalogKindCategory": "വിഭാഗം", + "autoCatalogKindFamily": "കുടുംബം", + "autoComboTestTruncated": "പരിശോധിച്ച {tested} {total} ജീവൻ സ്ഥാനാർത്ഥികൾ (ഉയർന്ന ഭാരം ആദ്യം).", + "controlCenter": "കൺട്രോൾ സെന്റർ", "createCombo": "കോംബോ സൃഷ്ടിക്കുക", "editCombo": "കോംബോ എഡിറ്റ് ചെയ്യുക", "deleteCombo": "കോംബോ ഇല്ലാതാക്കുക", diff --git a/src/i18n/messages/mr.json b/src/i18n/messages/mr.json index f146ee10160a..ec30d93273f8 100644 --- a/src/i18n/messages/mr.json +++ b/src/i18n/messages/mr.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "बिल्ट-इन ऑटो/* कॉम्बो कनेक्ट केलेल्या प्रदात्यांकडून डायनॅमिकपणे निराकरण केले. यापैकी कोणतेही आयडी मॉडेल फील्ड म्हणून वापरा — कोणत्याही सेटअपची आवश्यकता नाही.", "autoCatalogExpand": "स्वयं-राउटिंग कॅटलॉग विस्तृत करा", "autoCatalogCollapse": "ऑटो-राउटिंग कॅटलॉग संकुचित करा", + "autoCatalogComboCount": "{count} संयोजन", + "autoCatalogCandidates": "{count} उमेदवार", + "autoCatalogNoCandidates": "कोणतेही जुळणारे प्रदाते नाहीत", + "autoCatalogKindVariant": "पर्यायी", + "autoCatalogKindTemplate": "टेम्पलेट", + "autoCatalogKindCategory": "श्रेणी", + "autoCatalogKindFamily": "कुटुंब", + "autoComboTestTruncated": "{total} जिवंत उमेदवारांपैकी {tested} चाचणी घेतली (उच्चतम वजन प्रथम).", + "controlCenter": "नियंत्रण केंद्र", "createCombo": "कॉम्बो तयार करा", "editCombo": "कॉम्बो संपादित करा", "deleteCombo": "कॉम्बो हटवा", diff --git a/src/i18n/messages/ms.json b/src/i18n/messages/ms.json index d319746558ac..ead7c30d2c3c 100644 --- a/src/i18n/messages/ms.json +++ b/src/i18n/messages/ms.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "Gabungan auto/* terbina dalam diselesaikan secara dinamik daripada pembekal yang disambungkan. Gunakan mana-mana ID ini sebagai medan model — tiada persediaan diperlukan.", "autoCatalogExpand": "Kembangkan katalog penghalaan automatik", "autoCatalogCollapse": "Runtuhkan katalog penghalaan automatik", + "autoCatalogComboCount": "{count} gabungan", + "autoCatalogCandidates": "{count} calon", + "autoCatalogNoCandidates": "Tiada penyedia yang sepadan", + "autoCatalogKindVariant": "varian", + "autoCatalogKindTemplate": "templat", + "autoCatalogKindCategory": "kategori", + "autoCatalogKindFamily": "keluarga", + "autoComboTestTruncated": "Uji {tested} daripada {total} calon langsung (berat tertinggi dahulu).", + "controlCenter": "Pusat Kawalan", "createCombo": "Buat Combo", "editCombo": "Edit Kombo", "deleteCombo": "Padamkan Kombo", diff --git a/src/i18n/messages/mt.json b/src/i18n/messages/mt.json index 1531ce4bd24e..eaa8b1f11b55 100644 --- a/src/i18n/messages/mt.json +++ b/src/i18n/messages/mt.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "Kombinazzjonijiet auto/* integrati li jiġu riżolti b’mod dinamiku mill-fornituri konnessi. Uża kwalunkwe wieħed minn dawn l-IDs bħala l-qasam tal-mudell — ebda konfigurazzjoni meħtieġa.", "autoCatalogExpand": "Espandi l-katalgu tal-instradar awtomatiku", "autoCatalogCollapse": "Agħlaq il-katalgu tal-instradar awtomatiku", + "autoCatalogComboCount": "{count} kombinazzjonijiet", + "autoCatalogCandidates": "{count} kandidati", + "autoCatalogNoCandidates": "Ebda fornituri li jaqblu", + "autoCatalogKindVariant": "varjant", + "autoCatalogKindTemplate": "template", + "autoCatalogKindCategory": "kategorija", + "autoCatalogKindFamily": "familja", + "autoComboTestTruncated": "Testati {tested} minn {total} kandidati live (l-ogħla piżijiet l-ewwel).", + "controlCenter": "Ċentru ta' Kontroll", "createCombo": "Oħloq Kombinazzjoni", "editCombo": "Editja l-Kombinazzjoni", "deleteCombo": "Ħassar il-Kombinazzjoni", diff --git a/src/i18n/messages/my.json b/src/i18n/messages/my.json index 2c3bf29989a3..848d3fb5e862 100644 --- a/src/i18n/messages/my.json +++ b/src/i18n/messages/my.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "ချိတ်ဆက်ထားသော ဝန်ဆောင်မှုပေးသူများမှ ပြောင်းလဲမှုနှင့်အညီ သတ်မှတ်ပေးသည့် အသင့်ပါ auto/* ပေါင်းစပ်မှုများ။ ဤ ID များထဲမှ မည်သည့်တစ်ခုကိုမဆို model အကွက်အဖြစ် အသုံးပြုနိုင်ပြီး စနစ်ထည့်သွင်းရန် မလိုပါ။", "autoCatalogExpand": "အလိုအလျောက် လမ်းကြောင်းခွဲခြင်း ကတ်တလောက်ကို ဖြန့်ကြည့်ရန်", "autoCatalogCollapse": "အလိုအလျောက် လမ်းကြောင်းခွဲခြင်း ကတ်တလောက်ကို ခေါက်သိမ်းရန်", + "autoCatalogComboCount": "{count} ကွန်ဘို", + "autoCatalogCandidates": "{count} လျှောက်လွှာများ", + "autoCatalogNoCandidates": "မကိုက်ညီသော ပေးသွင်းသူများ မရှိပါ", + "autoCatalogKindVariant": "မျိုးစုံ", + "autoCatalogKindTemplate": "နမူနာ", + "autoCatalogKindCategory": "အမျိုးအစား", + "autoCatalogKindFamily": "မိသားစု", + "autoComboTestTruncated": "အသက်ရှင်သော လျှောက်လွှာ {total} ခုထဲမှ {tested} ခုကို စမ်းသပ်ပြီး (အမြင့်ဆုံး အလေးချိန်များ ပထမ)။", + "controlCenter": "ထိန်းချုပ်မှု စင်တာ", "createCombo": "ပေါင်းစပ်မော်ဒယ် ဖန်တီးရန်", "editCombo": "ပေါင်းစပ်မော်ဒယ်ကို တည်းဖြတ်ရန်", "deleteCombo": "ပေါင်းစပ်မော်ဒယ်ကို ဖျက်ရန်", diff --git a/src/i18n/messages/ne.json b/src/i18n/messages/ne.json index a77f71181c77..30501098c197 100644 --- a/src/i18n/messages/ne.json +++ b/src/i18n/messages/ne.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "जडान गरिएका प्रदायकहरूबाट गतिशील रूपमा समाधान गरिएका बिल्ट-इन auto/* कम्बोहरू। यीमध्ये कुनै पनि ID लाई मोडेल फिल्डको रूपमा प्रयोग गर्नुहोस् — कुनै सेटअप आवश्यक छैन।", "autoCatalogExpand": "स्वतः-राउटिङ क्याटलग विस्तार गर्नुहोस्", "autoCatalogCollapse": "स्वतः-राउटिङ क्याटलग खुम्च्याउनुहोस्", + "autoCatalogComboCount": "{count} संयोजन", + "autoCatalogCandidates": "{count} उम्मेदवार", + "autoCatalogNoCandidates": "कोई मेल खाने प्रदायक नहीं", + "autoCatalogKindVariant": "भिन्नता", + "autoCatalogKindTemplate": "टेम्पलेट", + "autoCatalogKindCategory": "श्रेणी", + "autoCatalogKindFamily": "परिवार", + "autoComboTestTruncated": "{total} जीवित उम्मेदवारों में से {tested} का परीक्षण किया गया (उच्चतम वजन पहले)।", + "controlCenter": "नियंत्रण केन्द्र", "createCombo": "कम्बो सिर्जना गर्नुहोस्", "editCombo": "कम्बो सम्पादन गर्नुहोस्", "deleteCombo": "कम्बो मेटाउनुहोस्", diff --git a/src/i18n/messages/nl.json b/src/i18n/messages/nl.json index 4532778a63e7..a57ee9866a94 100644 --- a/src/i18n/messages/nl.json +++ b/src/i18n/messages/nl.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "Ingebouwde auto/*-combinaties die dynamisch worden opgelost door aangesloten providers. Gebruik een van deze ID's als modelveld; er is geen configuratie nodig.", "autoCatalogExpand": "Vouw de autoroutingcatalogus uit", "autoCatalogCollapse": "Catalogus voor automatische routering samenvouwen", + "autoCatalogComboCount": "{count} combinaties", + "autoCatalogCandidates": "{count} kandidaten", + "autoCatalogNoCandidates": "Geen overeenkomende aanbieders", + "autoCatalogKindVariant": "variant", + "autoCatalogKindTemplate": "sjabloon", + "autoCatalogKindCategory": "categorie", + "autoCatalogKindFamily": "familie", + "autoComboTestTruncated": "Getest {tested} van {total} live kandidaten (hoogste gewichten eerst).", + "controlCenter": "Besturingscentrum", "createCombo": "Combinatie maken", "editCombo": "Combinatie bewerken", "deleteCombo": "Combinatie verwijderen", diff --git a/src/i18n/messages/no.json b/src/i18n/messages/no.json index 5a67f9beffc7..7a0dbe00d314 100644 --- a/src/i18n/messages/no.json +++ b/src/i18n/messages/no.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "Innebygde auto/*-kombinasjoner løst dynamisk fra tilkoblede leverandører. Bruk en av disse ID-ene som modellfelt – ingen oppsett er nødvendig.", "autoCatalogExpand": "Utvid katalogen for automatisk ruting", "autoCatalogCollapse": "Skjul katalogen for automatisk ruting", + "autoCatalogComboCount": "{count} kombinasjoner", + "autoCatalogCandidates": "{count} kandidater", + "autoCatalogNoCandidates": "Ingen matchende leverandører", + "autoCatalogKindVariant": "variant", + "autoCatalogKindTemplate": "mal", + "autoCatalogKindCategory": "kategori", + "autoCatalogKindFamily": "familie", + "autoComboTestTruncated": "Testet {tested} av {total} aktive kandidater (høyeste vekter først).", + "controlCenter": "Kontrollsenter", "createCombo": "Opprett kombinasjon", "editCombo": "Rediger kombinasjon", "deleteCombo": "Slett kombinasjon", diff --git a/src/i18n/messages/or.json b/src/i18n/messages/or.json index ba4be2901fff..e299d7bca509 100644 --- a/src/i18n/messages/or.json +++ b/src/i18n/messages/or.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "ସଂଯୁକ୍ତ ପ୍ରଦାନକାରୀଙ୍କଠାରୁ ଗତିଶୀଳ ଭାବରେ ନିର୍ଦ୍ଧାରିତ ହେଉଥିବା ବିଲ୍ଟ-ଇନ୍ auto/* କମ୍ବୋ। ଏହି IDଗୁଡ଼ିକ ମଧ୍ୟରୁ ଯେକୌଣସିଟିକୁ ମଡେଲ୍ ଫିଲ୍ଡ ଭାବେ ବ୍ୟବହାର କରନ୍ତୁ — କୌଣସି ସେଟଅପ୍ ଆବଶ୍ୟକ ନାହିଁ।", "autoCatalogExpand": "ସ୍ୱୟଂଚାଳିତ ରାଉଟିଂ କ୍ୟାଟାଲଗ୍ ବିସ୍ତାର କରନ୍ତୁ", "autoCatalogCollapse": "ସ୍ୱୟଂଚାଳିତ ରାଉଟିଂ କ୍ୟାଟାଲଗ୍ ସଂକୁଚିତ କରନ୍ତୁ", + "autoCatalogComboCount": "{count} କମ୍ବୋସ", + "autoCatalogCandidates": "{count} ପ୍ରାର୍ଥୀ", + "autoCatalogNoCandidates": "ମେଳ ଖୋଜିବାକୁ କୌଣସି ପ୍ରଦାନକାରୀ ନାହିଁ", + "autoCatalogKindVariant": "ବିକଳ୍ପ", + "autoCatalogKindTemplate": "ଟେମ୍ପଲେଟ୍", + "autoCatalogKindCategory": "ଶ୍ରେଣୀ", + "autoCatalogKindFamily": "ପରିବାର", + "autoComboTestTruncated": "ସର୍ବୋଚ୍ଚ ଓଜନ ପ୍ରଥମେ {total} ଜୀବନ୍ତ ପ୍ରାର୍ଥୀଙ୍କର {tested} ପରୀକ୍ଷା କରାଯାଇଛି।", + "controlCenter": "ନିୟନ୍ତ୍ରଣ କେନ୍ଦ୍ର", "createCombo": "କମ୍ବୋ ତିଆରି କରନ୍ତୁ", "editCombo": "କମ୍ବୋ ସମ୍ପାଦନା କରନ୍ତୁ", "deleteCombo": "କମ୍ବୋ ବିଲୋପ କରନ୍ତୁ", diff --git a/src/i18n/messages/pa.json b/src/i18n/messages/pa.json index 81b5b77ba65a..1c349b310613 100644 --- a/src/i18n/messages/pa.json +++ b/src/i18n/messages/pa.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "ਬਿਲਟ-ਇਨ auto/* ਕੌਂਬੋ, ਜੋ ਕਨੈਕਟ ਕੀਤੇ ਪ੍ਰਦਾਤਾਵਾਂ ਤੋਂ ਗਤੀਸ਼ੀਲ ਢੰਗ ਨਾਲ ਹੱਲ ਕੀਤੇ ਜਾਂਦੇ ਹਨ। ਇਨ੍ਹਾਂ ਵਿੱਚੋਂ ਕਿਸੇ ਵੀ ID ਨੂੰ ਮਾਡਲ ਫੀਲਡ ਵਜੋਂ ਵਰਤੋ — ਕਿਸੇ ਸੈੱਟਅੱਪ ਦੀ ਲੋੜ ਨਹੀਂ।", "autoCatalogExpand": "ਆਟੋ-ਰਾਊਟਿੰਗ ਕੈਟਾਲਾਗ ਫੈਲਾਓ", "autoCatalogCollapse": "ਆਟੋ-ਰਾਊਟਿੰਗ ਕੈਟਾਲਾਗ ਸਮੇਟੋ", + "autoCatalogComboCount": "{count} ਕੰਬੋ", + "autoCatalogCandidates": "{count} ਉਮੀਦਵਾਰ", + "autoCatalogNoCandidates": "ਕੋਈ ਮੇਲ ਖਾਣ ਵਾਲੇ ਪ੍ਰਦਾਤਾ ਨਹੀਂ", + "autoCatalogKindVariant": "ਵੈਰੀਐਂਟ", + "autoCatalogKindTemplate": "ਟੈਮਪਲੇਟ", + "autoCatalogKindCategory": "ਸ਼੍ਰੇਣੀ", + "autoCatalogKindFamily": "ਪਰਿਵਾਰ", + "autoComboTestTruncated": "ਜੀਵੰਤ ਉਮੀਦਵਾਰਾਂ ਵਿੱਚੋਂ {tested} ਦਾ {total} ਦੀ ਜਾਂਚ ਕੀਤੀ (ਉੱਚੇ ਭਾਰ ਪਹਿਲਾਂ)।", + "controlCenter": "ਕੰਟਰੋਲ ਸੈਂਟਰ", "createCombo": "ਕੌਂਬੋ ਬਣਾਓ", "editCombo": "ਕੌਂਬੋ ਸੋਧੋ", "deleteCombo": "ਕੌਂਬੋ ਮਿਟਾਓ", diff --git a/src/i18n/messages/phi.json b/src/i18n/messages/phi.json index d1a827a30814..afc66f750736 100644 --- a/src/i18n/messages/phi.json +++ b/src/i18n/messages/phi.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "Ang mga built-in na auto/* combo ay dynamic na naresolba mula sa mga konektadong provider. Gamitin ang alinman sa mga ID na ito bilang field ng modelo — hindi kailangan ng setup.", "autoCatalogExpand": "Palawakin ang auto-routing catalog", "autoCatalogCollapse": "I-collapse ang auto-routing catalog", + "autoCatalogComboCount": "{count} combos", + "autoCatalogCandidates": "{count} kandidato", + "autoCatalogNoCandidates": "Walang tumutugmang mga provider", + "autoCatalogKindVariant": "variant", + "autoCatalogKindTemplate": "template", + "autoCatalogKindCategory": "kategorya", + "autoCatalogKindFamily": "pamilya", + "autoComboTestTruncated": "Sinubukan ang {tested} sa {total} mga live na kandidato (pinakamataas na timbang muna).", + "controlCenter": "Control Center", "createCombo": "Lumikha ng Combo", "editCombo": "I-edit ang Combo", "deleteCombo": "Tanggalin ang Combo", diff --git a/src/i18n/messages/pl.json b/src/i18n/messages/pl.json index 3bad4555ba73..ff345844f49d 100644 --- a/src/i18n/messages/pl.json +++ b/src/i18n/messages/pl.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "Wbudowane combos auto/* rozwiązywane dynamicznie z połączonych providers. Użyj dowolnego z tych IDs jako pola model — konfiguracja nie jest wymagana.", "autoCatalogExpand": "Rozwiń katalog auto-routingu", "autoCatalogCollapse": "Zwiń katalog auto-routingu", + "autoCatalogComboCount": "{count} kombinacji", + "autoCatalogCandidates": "{count} kandydatów", + "autoCatalogNoCandidates": "Brak pasujących dostawców", + "autoCatalogKindVariant": "wariant", + "autoCatalogKindTemplate": "szablon", + "autoCatalogKindCategory": "kategoria", + "autoCatalogKindFamily": "rodzina", + "autoComboTestTruncated": "Przetestowano {tested} z {total} aktywnych kandydatów (najwyższe wagi najpierw).", + "controlCenter": "Centrum sterowania", "createCombo": "Utwórz Combo", "editCombo": "Edytuj Combo", "deleteCombo": "Usuń Combo", diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index b9cddd55498d..68cd6b129dec 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "Combos automáticos/* integrados resolvidos dinamicamente a partir de provedores conectados. Use qualquer um desses IDs como campo de modelo — nenhuma configuração é necessária.", "autoCatalogExpand": "Expanda o catálogo de roteamento automático", "autoCatalogCollapse": "Recolher catálogo de roteamento automático", + "autoCatalogComboCount": "{count} combinações", + "autoCatalogCandidates": "{count} candidatos", + "autoCatalogNoCandidates": "Nenhum provedor correspondente", + "autoCatalogKindVariant": "variante", + "autoCatalogKindTemplate": "modelo", + "autoCatalogKindCategory": "categoria", + "autoCatalogKindFamily": "família", + "autoComboTestTruncated": "Testados {tested} de {total} candidatos ativos (maiores pesos primeiro).", + "controlCenter": "Central de Controle", "createCombo": "Criar combinação", "editCombo": "Editar combinação", "deleteCombo": "Excluir combinação", diff --git a/src/i18n/messages/ro.json b/src/i18n/messages/ro.json index 0752a8908637..c3140a06c74b 100644 --- a/src/i18n/messages/ro.json +++ b/src/i18n/messages/ro.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "Combo auto/* încorporate rezolvate dinamic de la furnizorii conectați. Utilizați oricare dintre aceste ID-uri ca câmp de model - nu este necesară configurarea.", "autoCatalogExpand": "Extindeți catalogul de rutare automată", "autoCatalogCollapse": "Restrânge catalogul de rutare automată", + "autoCatalogComboCount": "{count} combinații", + "autoCatalogCandidates": "{count} candidați", + "autoCatalogNoCandidates": "Niciun furnizor corespunzător", + "autoCatalogKindVariant": "variantă", + "autoCatalogKindTemplate": "șablon", + "autoCatalogKindCategory": "categorie", + "autoCatalogKindFamily": "familie", + "autoComboTestTruncated": "Testate {tested} din {total} candidați activi (cele mai mari greutăți mai întâi).", + "controlCenter": "Centrul de control", "createCombo": "Creați Combo", "editCombo": "Editează Combo", "deleteCombo": "Ștergeți Combo", diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json index 4aca925be340..c3c980a0897a 100644 --- a/src/i18n/messages/ru.json +++ b/src/i18n/messages/ru.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "Встроенные комбинации auto/* разрешаются динамически от подключенных провайдеров. Используйте любой из этих идентификаторов в качестве поля модели — настройка не требуется.", "autoCatalogExpand": "Развернуть каталог автотрассировки", "autoCatalogCollapse": "Свернуть каталог автотрассировки", + "autoCatalogComboCount": "{count} комбинаций", + "autoCatalogCandidates": "{count} кандидатов", + "autoCatalogNoCandidates": "Нет подходящих провайдеров", + "autoCatalogKindVariant": "вариант", + "autoCatalogKindTemplate": "шаблон", + "autoCatalogKindCategory": "категория", + "autoCatalogKindFamily": "семья", + "autoComboTestTruncated": "Проверено {tested} из {total} живых кандидатов (сначала с наибольшими весами).", + "controlCenter": "Центр управления", "createCombo": "Создать комбо", "editCombo": "Редактировать комбо", "deleteCombo": "Удалить комбо", diff --git a/src/i18n/messages/si.json b/src/i18n/messages/si.json index beb37ebe01b3..c82fe4fc9a2a 100644 --- a/src/i18n/messages/si.json +++ b/src/i18n/messages/si.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "සම්බන්ධිත සැපයුම්කරුවන් මත පදනම්ව ගතිකව විසඳනු ලබන, පද්ධතිය තුළම අන්තර්ගත auto/* සංයෝජන. මෙම ඕනෑම ID එකක් model ක්ෂේත්රය ලෙස භාවිත කරන්න — සැකසීමක් අවශ්ය නොවේ.", "autoCatalogExpand": "ස්වයංක්රීය මාර්ගගත කිරීමේ නාමාවලිය දිගහරින්න", "autoCatalogCollapse": "ස්වයංක්රීය මාර්ගගත කිරීමේ නාමාවලිය හකුළන්න", + "autoCatalogComboCount": "{count} කොම්බෝ", + "autoCatalogCandidates": "{count} අපේක්ෂක", + "autoCatalogNoCandidates": "ගැලපෙන සැපයුම්කරුවන් නැත", + "autoCatalogKindVariant": "වර්ගය", + "autoCatalogKindTemplate": "ආකෘතිය", + "autoCatalogKindCategory": "ප්‍රවර්ගය", + "autoCatalogKindFamily": "කොටස", + "autoComboTestTruncated": "සජීවී අපේක්ෂක {total} හි {tested} පරීක්ෂා කර ඇත (ඉහළ බර පළමුව).", + "controlCenter": "පාලන මධ්‍යස්ථානය", "createCombo": "සංයෝජනයක් සාදන්න", "editCombo": "සංයෝජනය සංස්කරණය කරන්න", "deleteCombo": "සංයෝජනය මකන්න", diff --git a/src/i18n/messages/sk.json b/src/i18n/messages/sk.json index 666a5d4f7dee..12037f475752 100644 --- a/src/i18n/messages/sk.json +++ b/src/i18n/messages/sk.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "Vstavané automatické/* kombá rozlíšené dynamicky od pripojených poskytovateľov. Ako pole modelu použite ktorýkoľvek z týchto identifikátorov – nie je potrebné žiadne nastavenie.", "autoCatalogExpand": "Rozbaľte katalóg automatického smerovania", "autoCatalogCollapse": "Zbaliť katalóg automatického smerovania", + "autoCatalogComboCount": "{count} kombinácií", + "autoCatalogCandidates": "{count} kandidátov", + "autoCatalogNoCandidates": "Žiadni zhodujúci sa poskytovatelia", + "autoCatalogKindVariant": "variant", + "autoCatalogKindTemplate": "šablóna", + "autoCatalogKindCategory": "kategória", + "autoCatalogKindFamily": "rodina", + "autoComboTestTruncated": "Testovaných {tested} z {total} živých kandidátov (najvyššie váhy najprv).", + "controlCenter": "Ovládacie centrum", "createCombo": "Vytvorte kombináciu", "editCombo": "Upraviť Combo", "deleteCombo": "Odstrániť Combo", diff --git a/src/i18n/messages/sl.json b/src/i18n/messages/sl.json index eb6c6260250e..6dbb481743dc 100644 --- a/src/i18n/messages/sl.json +++ b/src/i18n/messages/sl.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "Vgrajene kombinacije auto/*, ki se dinamično razrešijo na podlagi povezanih ponudnikov. Katerega koli od teh ID-jev lahko uporabite kot polje modela — nastavitev ni potrebna.", "autoCatalogExpand": "Razširi katalog samodejnega usmerjanja", "autoCatalogCollapse": "Strni katalog samodejnega usmerjanja", + "autoCatalogComboCount": "{count} kombinacij", + "autoCatalogCandidates": "{count} kandidatov", + "autoCatalogNoCandidates": "Brez ujemajočih se ponudnikov", + "autoCatalogKindVariant": "različica", + "autoCatalogKindTemplate": "predloga", + "autoCatalogKindCategory": "kategorija", + "autoCatalogKindFamily": "družina", + "autoComboTestTruncated": "Preizkušeno {tested} od {total} aktivnih kandidatov (najvišje teže najprej).", + "controlCenter": "Nadzorni center", "createCombo": "Ustvari kombinacijo", "editCombo": "Uredi kombinacijo", "deleteCombo": "Izbriši kombinacijo", diff --git a/src/i18n/messages/sr.json b/src/i18n/messages/sr.json index ed9c7a0409de..64568dfcba24 100644 --- a/src/i18n/messages/sr.json +++ b/src/i18n/messages/sr.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "Уграђене комбинације auto/* динамички се одређују на основу повезаних провајдера. Користите било који од ових ID-ова као поље модела — подешавање није потребно.", "autoCatalogExpand": "Прошири каталог аутоматског рутирања", "autoCatalogCollapse": "Сажми каталог аутоматског рутирања", + "autoCatalogComboCount": "{count} комбинација", + "autoCatalogCandidates": "{count} кандидата", + "autoCatalogNoCandidates": "Нема одговарајућих провајдера", + "autoCatalogKindVariant": "варијанта", + "autoCatalogKindTemplate": "шаблон", + "autoCatalogKindCategory": "категорија", + "autoCatalogKindFamily": "породица", + "autoComboTestTruncated": "Тестирано {tested} од {total} активних кандидата (највише тежине прво).", + "controlCenter": "Контролни центар", "createCombo": "Креирај комбинацију", "editCombo": "Уреди комбинацију", "deleteCombo": "Обриши комбинацију", diff --git a/src/i18n/messages/sv.json b/src/i18n/messages/sv.json index e8d9dc3b927a..b2bf99c09b9c 100644 --- a/src/i18n/messages/sv.json +++ b/src/i18n/messages/sv.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "Inbyggda auto/*-kombinationer lösts dynamiskt från anslutna leverantörer. Använd något av dessa ID:n som modellfält – ingen installation behövs.", "autoCatalogExpand": "Utöka katalogen för automatisk routing", "autoCatalogCollapse": "Komprimera katalogen för automatisk routing", + "autoCatalogComboCount": "{count} kombinationer", + "autoCatalogCandidates": "{count} kandidater", + "autoCatalogNoCandidates": "Inga matchande leverantörer", + "autoCatalogKindVariant": "variant", + "autoCatalogKindTemplate": "mall", + "autoCatalogKindCategory": "kategori", + "autoCatalogKindFamily": "familj", + "autoComboTestTruncated": "Testade {tested} av {total} aktiva kandidater (högsta vikter först).", + "controlCenter": "Kontrollcenter", "createCombo": "Skapa Combo", "editCombo": "Redigera kombination", "deleteCombo": "Ta bort Combo", diff --git a/src/i18n/messages/sw.json b/src/i18n/messages/sw.json index 87c5d29e918e..9fa9451463ed 100644 --- a/src/i18n/messages/sw.json +++ b/src/i18n/messages/sw.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "Michanganyiko ya kiotomatiki/* iliyojengewa ndani imetatuliwa kutoka kwa watoa huduma waliounganishwa. Tumia kitambulisho chochote kati ya hivi kama sehemu ya kielelezo - hakuna usanidi unaohitajika.", "autoCatalogExpand": "Panua katalogi ya uelekezaji kiotomatiki", "autoCatalogCollapse": "Kunja katalogi ya uelekezaji kiotomatiki", + "autoCatalogComboCount": "{count} mchanganyiko", + "autoCatalogCandidates": "{count} wagombea", + "autoCatalogNoCandidates": "Hakuna watoa huduma wanaofanana", + "autoCatalogKindVariant": "toleo", + "autoCatalogKindTemplate": "kigezo", + "autoCatalogKindCategory": "jamii", + "autoCatalogKindFamily": "familia", + "autoComboTestTruncated": "Imepimwa {tested} kati ya {total} wagombea hai (uzito mkubwa kwanza).", + "controlCenter": "Kituo cha Udhibiti", "createCombo": "Unda Mkusanyiko", "editCombo": "Hariri Mkusanyiko", "deleteCombo": "Futa Mkusanyiko", diff --git a/src/i18n/messages/ta.json b/src/i18n/messages/ta.json index edeaeb3ffd59..520f9c1a09de 100644 --- a/src/i18n/messages/ta.json +++ b/src/i18n/messages/ta.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "இணைக்கப்பட்ட வழங்குநர்களிடமிருந்து உள்ளமைந்த தானியங்கு/* சேர்க்கைகள் மாறும் வகையில் தீர்க்கப்படுகின்றன. இந்த ஐடிகளில் ஏதேனும் ஒன்றை மாதிரி புலமாகப் பயன்படுத்தவும் - அமைப்பு தேவையில்லை.", "autoCatalogExpand": "தானாக ரூட்டிங் பட்டியலை விரிவாக்குங்கள்", "autoCatalogCollapse": "தானாக ரூட்டிங் பட்டியலைச் சுருக்கவும்", + "autoCatalogComboCount": "{count} கம்போஸ்", + "autoCatalogCandidates": "{count} வேட்பாளர்கள்", + "autoCatalogNoCandidates": "ஒத்திசைவு வழங்குநர்கள் இல்லை", + "autoCatalogKindVariant": "மாறுபாடு", + "autoCatalogKindTemplate": "மாதிரி", + "autoCatalogKindCategory": "வகை", + "autoCatalogKindFamily": "குடும்பம்", + "autoComboTestTruncated": "உயிருள்ள {total} வேட்பாளர்களில் {tested} ஐ சோதித்தோம் (அதிகபட்ச எடைகள் முதலில்).", + "controlCenter": "கட்டுப்பாட்டு மையம்", "createCombo": "காம்போவை உருவாக்கு", "editCombo": "காம்போவைத் திருத்து", "deleteCombo": "காம்போவை நீக்கு", diff --git a/src/i18n/messages/te.json b/src/i18n/messages/te.json index fcccd32a573b..fa42a227a930 100644 --- a/src/i18n/messages/te.json +++ b/src/i18n/messages/te.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "కనెక్ట్ చేయబడిన ప్రొవైడర్ల నుండి అంతర్నిర్మిత ఆటో/* కాంబోలు డైనమిక్‌గా పరిష్కరించబడతాయి. ఈ IDలలో దేనినైనా మోడల్ ఫీల్డ్‌గా ఉపయోగించండి — సెటప్ అవసరం లేదు.", "autoCatalogExpand": "ఆటో-రూటింగ్ కేటలాగ్‌ని విస్తరించండి", "autoCatalogCollapse": "ఆటో-రూటింగ్ కేటలాగ్‌ను కుదించు", + "autoCatalogComboCount": "{count} కాంబోలు", + "autoCatalogCandidates": "{count} అభ్యర్థులు", + "autoCatalogNoCandidates": "సమానమైన ప్రొవైడర్లు లేవు", + "autoCatalogKindVariant": "వేరియంట్", + "autoCatalogKindTemplate": "టెంప్లేట్", + "autoCatalogKindCategory": "వర్గం", + "autoCatalogKindFamily": "కుటుంబం", + "autoComboTestTruncated": "ఉన్న {tested} లో {total} ప్రత్యక్ష అభ్యర్థులను పరీక్షించారు (అత్యధిక బరువులు మొదట).", + "controlCenter": "నియంత్రణ కేంద్రం", "createCombo": "కాంబోను సృష్టించండి", "editCombo": "కాంబోను సవరించండి", "deleteCombo": "కాంబోను తొలగించండి", diff --git a/src/i18n/messages/th.json b/src/i18n/messages/th.json index 154c7ab9e86b..fd6d3bad83a3 100644 --- a/src/i18n/messages/th.json +++ b/src/i18n/messages/th.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "คอมโบอัตโนมัติ/* ในตัวได้รับการแก้ไขแบบไดนามิกจากผู้ให้บริการที่เชื่อมต่อ ใช้รหัสเหล่านี้เป็นช่องโมเดล โดยไม่จำเป็นต้องตั้งค่าใดๆ", "autoCatalogExpand": "ขยายแค็ตตาล็อกการกำหนดเส้นทางอัตโนมัติ", "autoCatalogCollapse": "ยุบแค็ตตาล็อกการกำหนดเส้นทางอัตโนมัติ", + "autoCatalogComboCount": "{count} คอมโบ", + "autoCatalogCandidates": "{count} ผู้สมัคร", + "autoCatalogNoCandidates": "ไม่มีผู้ให้บริการที่ตรงกัน", + "autoCatalogKindVariant": "ตัวแปร", + "autoCatalogKindTemplate": "แม่แบบ", + "autoCatalogKindCategory": "หมวดหมู่", + "autoCatalogKindFamily": "ครอบครัว", + "autoComboTestTruncated": "ทดสอบ {tested} จาก {total} ผู้สมัครที่ใช้งานอยู่ (น้ำหนักสูงสุดก่อน)", + "controlCenter": "ศูนย์ควบคุม", "createCombo": "สร้างคอมโบ", "editCombo": "แก้ไขคอมโบ", "deleteCombo": "ลบคอมโบ", diff --git a/src/i18n/messages/tr.json b/src/i18n/messages/tr.json index aedcc059e9ad..b58f896f620a 100644 --- a/src/i18n/messages/tr.json +++ b/src/i18n/messages/tr.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "Yerleşik otomatik/* kombinasyonları, bağlı sağlayıcılardan dinamik olarak çözümlenir. Bu kimliklerden herhangi birini model alanı olarak kullanın; kurulum gerekmez.", "autoCatalogExpand": "Otomatik yönlendirme kataloğunu genişletin", "autoCatalogCollapse": "Otomatik yönlendirme kataloğunu daralt", + "autoCatalogComboCount": "{count} kombinasyon", + "autoCatalogCandidates": "{count} aday", + "autoCatalogNoCandidates": "Eşleşen sağlayıcı yok", + "autoCatalogKindVariant": "varyant", + "autoCatalogKindTemplate": "şablon", + "autoCatalogKindCategory": "kategori", + "autoCatalogKindFamily": "aile", + "autoComboTestTruncated": "En yüksek ağırlıklar önce olmak üzere {total} canlı adaydan {tested} test edildi.", + "controlCenter": "Kontrol Merkezi", "createCombo": "Kombo Oluştur", "editCombo": "Komboyu Düzenle", "deleteCombo": "Komboyu Sil", diff --git a/src/i18n/messages/uk-UA.json b/src/i18n/messages/uk-UA.json index 554fde54c362..ee83e745b7d3 100644 --- a/src/i18n/messages/uk-UA.json +++ b/src/i18n/messages/uk-UA.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "Вбудовані комбінації auto/*, які динамічно вирішуються від підключених постачальників. Використовуйте будь-який із цих ідентифікаторів як поле моделі — налаштування не потрібно.", "autoCatalogExpand": "Розгорнути каталог автомаршрутизації", "autoCatalogCollapse": "Згорнути каталог автоматичного маршрутизації", + "autoCatalogComboCount": "{count} комбінацій", + "autoCatalogCandidates": "{count} кандидатів", + "autoCatalogNoCandidates": "Немає відповідних постачальників", + "autoCatalogKindVariant": "варіант", + "autoCatalogKindTemplate": "шаблон", + "autoCatalogKindCategory": "категорія", + "autoCatalogKindFamily": "сім'я", + "autoComboTestTruncated": "Перевірено {tested} з {total} живих кандидатів (найвищі ваги спочатку).", + "controlCenter": "Контрольний центр", "createCombo": "Створити комбо", "editCombo": "Редагувати комбінацію", "deleteCombo": "Видалити комбо", diff --git a/src/i18n/messages/ur.json b/src/i18n/messages/ur.json index 547c7a25f3c9..3e02f6c2bfc6 100644 --- a/src/i18n/messages/ur.json +++ b/src/i18n/messages/ur.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "بلٹ ان آٹو/* کومبوز مربوط فراہم کنندگان سے متحرک طور پر حل ہوئے۔ ان میں سے کسی بھی ID کو بطور ماڈل فیلڈ استعمال کریں — کسی سیٹ اپ کی ضرورت نہیں۔", "autoCatalogExpand": "آٹو روٹنگ کیٹلاگ کو پھیلائیں۔", "autoCatalogCollapse": "آٹو روٹنگ کیٹلاگ کو سکیڑیں۔", + "autoCatalogComboCount": "{count} کمبینیشن", + "autoCatalogCandidates": "{count} امیدوار", + "autoCatalogNoCandidates": "کوئی میچنگ فراہم کنندہ نہیں", + "autoCatalogKindVariant": "متغیر", + "autoCatalogKindTemplate": "سانچہ", + "autoCatalogKindCategory": "زمرہ", + "autoCatalogKindFamily": "خاندان", + "autoComboTestTruncated": "{total} زندہ امیدواروں میں سے {tested} کی جانچ کی گئی (سب سے زیادہ وزن پہلے)۔", + "controlCenter": "کنٹرول سینٹر", "createCombo": "کمبو بنائیں", "editCombo": "کمبو میں ترمیم کریں", "deleteCombo": "کمبو حذف کریں", diff --git a/src/i18n/messages/uz.json b/src/i18n/messages/uz.json index 97b87a20f1e0..7e3d65e1e763 100644 --- a/src/i18n/messages/uz.json +++ b/src/i18n/messages/uz.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "Ulangan provayderlar asosida dinamik ravishda aniqlanadigan ichki auto/* kombinatsiyalari. Ushbu IDlardan istalganini model maydoni sifatida ishlating — sozlash talab qilinmaydi.", "autoCatalogExpand": "Avtomatik yoʻnaltirish katalogini kengaytirish", "autoCatalogCollapse": "Avtomatik yoʻnaltirish katalogini yigʻish", + "autoCatalogComboCount": "{count} kombinatsiyalar", + "autoCatalogCandidates": "{count} nomzodlar", + "autoCatalogNoCandidates": "Mos keluvchi provayderlar yo'q", + "autoCatalogKindVariant": "variant", + "autoCatalogKindTemplate": "shablon", + "autoCatalogKindCategory": "kategoriya", + "autoCatalogKindFamily": "oilasi", + "autoComboTestTruncated": "{total} jonli nomzodlardan {tested} tasdiqlangan (eng yuqori vaznlar birinchi).", + "controlCenter": "Boshqaruv markazi", "createCombo": "Kombinatsiya yaratish", "editCombo": "Kombinatsiyani tahrirlash", "deleteCombo": "Kombinatsiyani oʻchirish", diff --git a/src/i18n/messages/yo.json b/src/i18n/messages/yo.json index ed0a4b80b2da..8bcbf03d5416 100644 --- a/src/i18n/messages/yo.json +++ b/src/i18n/messages/yo.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "Àwọn àkójọpọ̀ auto/* tí a fi sínú rẹ̀ ni a ń yanjú ní ìmúdàgba láti ọ̀dọ̀ àwọn olùpèsè tí a ti sopọ̀. Lo èyíkéyìí nínú àwọn ID wọ̀nyí gẹ́gẹ́ bí ààyè awoṣe — kò nílò ìṣètò.", "autoCatalogExpand": "Fẹ̀ àkójọ ìtọ́sọ́nà aládàáṣe sí i", "autoCatalogCollapse": "Kó àkójọ ìtọ́sọ́nà aládàáṣe jọ", + "autoCatalogComboCount": "{count} awọn akojọpọ", + "autoCatalogCandidates": "{count} awọn oludije", + "autoCatalogNoCandidates": "Ko si awọn olupese ti o baamu", + "autoCatalogKindVariant": "iyato", + "autoCatalogKindTemplate": "àtẹle", + "autoCatalogKindCategory": "ẹka", + "autoCatalogKindFamily": "idile", + "autoComboTestTruncated": "Ti danwo {tested} ti {total} awọn oludije laaye (wọn ti o ni iwuwo giga julọ akọkọ).", + "controlCenter": "Ibi iṣakoso", "createCombo": "Ṣẹ̀dá Àkójọpọ̀", "editCombo": "Ṣàtúnṣe Àkójọpọ̀", "deleteCombo": "Pa Àkójọpọ̀ Náà Rẹ́", diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index 37b00bcf2d6d..164f1546ed55 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "内置 auto/* 组合,从已连接的提供者动态解析。直接将这些 ID 作为 model 字段使用——无需设置。", "autoCatalogExpand": "展开自动路由目录", "autoCatalogCollapse": "收起自动路由目录", + "autoCatalogComboCount": "{count} 组合", + "autoCatalogCandidates": "{count} 候选人", + "autoCatalogNoCandidates": "没有匹配的提供者", + "autoCatalogKindVariant": "变体", + "autoCatalogKindTemplate": "模板", + "autoCatalogKindCategory": "类别", + "autoCatalogKindFamily": "家族", + "autoComboTestTruncated": "测试了 {tested} 个中的 {total} 个实时候选人(按最高权重优先)。", + "controlCenter": "控制中心", "createCombo": "创建组合", "editCombo": "编辑组合", "deleteCombo": "删除组合", diff --git a/src/i18n/messages/zh-TW.json b/src/i18n/messages/zh-TW.json index 2f9d317d8960..c0688a2705f3 100644 --- a/src/i18n/messages/zh-TW.json +++ b/src/i18n/messages/zh-TW.json @@ -3225,6 +3225,15 @@ "autoCatalogDescription": "內建 auto/* 組合,從已連線的提供者動態解析。直接將這些 ID 作為 model 欄位使用——無需設定。", "autoCatalogExpand": "展開自動路由目錄", "autoCatalogCollapse": "收起自動路由目錄", + "autoCatalogComboCount": "{count} 組合", + "autoCatalogCandidates": "{count} 候選人", + "autoCatalogNoCandidates": "沒有匹配的提供者", + "autoCatalogKindVariant": "變體", + "autoCatalogKindTemplate": "模板", + "autoCatalogKindCategory": "類別", + "autoCatalogKindFamily": "家族", + "autoComboTestTruncated": "測試了 {tested} 個中的 {total} 個現場候選人(按最高權重排序)。", + "controlCenter": "控制中心", "createCombo": "建立組合", "editCombo": "編輯組合", "deleteCombo": "刪除組合", From 7b383931d6e6a594398af176144681394e0e43bf Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 3 Oct 2026 03:23:59 +0000 Subject: [PATCH 07/10] test(4235): advertise auto/: combos only when they have candidates The zero-candidate filter hides auto ids whose materialized pool is empty. Update the suffix-variants baseline to the new contract: seed providers and assert each AUTO_SUFFIX_VARIANTS id is present iff its pool is non-empty. Co-Authored-By: Afonso Dutra Nogueira Filho --- tests/unit/auto-combos-suffixes-4235.test.ts | 32 ++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/tests/unit/auto-combos-suffixes-4235.test.ts b/tests/unit/auto-combos-suffixes-4235.test.ts index c67ebf383fba..9c77e144473b 100644 --- a/tests/unit/auto-combos-suffixes-4235.test.ts +++ b/tests/unit/auto-combos-suffixes-4235.test.ts @@ -15,6 +15,8 @@ process.env.DATA_DIR = TEST_DATA_DIR; process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "auto-4235b-test-secret"; const core = await import("../../src/lib/db/core.ts"); +const providersDb = await import("../../src/lib/db/providers.ts"); +const settingsDb = await import("../../src/lib/db/settings.ts"); const suffix = await import("../../open-sse/services/autoCombo/suffixComposition.ts"); const modePacks = await import("../../open-sse/services/autoCombo/modePacks.ts"); const builtinCatalog = await import("../../open-sse/services/autoCombo/builtinCatalog.ts"); @@ -26,6 +28,21 @@ function resetStorage() { fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); } +// Auto combos with zero candidates are no longer advertised — seed providers +// so the materialized pools are non-empty where a matching model exists. +async function seedProviders() { + await settingsDb.updateSettings({ requireLogin: false }); + for (const provider of ["openai", "anthropic", "gemini", "groq", "deepseek", "mistral"]) { + await providersDb.createProviderConnection({ + provider, + authType: "apikey", + apiKey: `sk-test-4235-${provider}`, + name: `test-4235-${provider}`, + isActive: true, + }); + } +} + test.beforeEach(() => resetStorage()); test.after(() => { core.resetDbInstance(); @@ -114,7 +131,8 @@ test("#4235 createBuiltinAutoCombo composes reliability weights for auto/coding: assert.deepEqual(combo.weights, modePacks.MODE_PACKS["reliability-first"]); }); -test("#4235 /v1/models advertises the curated auto/: combos", async () => { +test("#4235 /v1/models advertises the curated auto/: combos with candidates", async () => { + await seedProviders(); const response = await v1ModelsCatalog.getUnifiedModelsResponse( new Request("http://localhost/api/v1/models") ); @@ -122,8 +140,18 @@ test("#4235 /v1/models advertises the curated auto/: combos", as const body = (await response.json()) as { data: Array<{ id: string; owned_by?: string }> }; const ids = new Set(body.data.map((m) => m.id)); + // An auto id is advertised iff its materialized pool is non-empty — ids + // with zero candidates must be absent. for (const autoId of builtinCatalog.AUTO_SUFFIX_VARIANTS) { - assert.ok(ids.has(autoId), `expected /v1/models to advertise ${autoId}`); + const virtual = await builtinCatalog.createBuiltinAutoCombo( + autoId, + autoId.slice("auto/".length) + ); + assert.equal( + ids.has(autoId), + virtual.models.length > 0, + `${autoId} must be advertised iff it has candidates (pool=${virtual.models.length})` + ); } const codingFast = body.data.find((m) => m.id === "auto/coding:fast"); assert.equal(codingFast?.owned_by, "combo"); From f14f33040c0e7b8335275095d8210cf98092d7be Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 3 Oct 2026 04:18:38 +0000 Subject: [PATCH 08/10] refactor: satisfy quality gates for auto catalog (extract helpers, split POST/card) Co-Authored-By: Afonso Dutra Nogueira Filho --- .../dashboard/combos/AutoComboCatalog.tsx | 279 +++++++++++------- .../dashboard/combos/comboPageHelpers.tsx | 29 ++ src/app/(dashboard)/dashboard/combos/page.tsx | 23 +- src/app/api/combos/test/route.ts | 188 +++++++----- src/app/api/v1/models/catalog.ts | 4 +- 5 files changed, 311 insertions(+), 212 deletions(-) create mode 100644 src/app/(dashboard)/dashboard/combos/comboPageHelpers.tsx diff --git a/src/app/(dashboard)/dashboard/combos/AutoComboCatalog.tsx b/src/app/(dashboard)/dashboard/combos/AutoComboCatalog.tsx index ba9594611dc2..e6d5f9b4a1a6 100644 --- a/src/app/(dashboard)/dashboard/combos/AutoComboCatalog.tsx +++ b/src/app/(dashboard)/dashboard/combos/AutoComboCatalog.tsx @@ -53,6 +53,159 @@ function templateItem(template: AutoComboTemplate): CatalogItem { const FALLBACK_ITEMS: CatalogItem[] = AUTO_COMBO_TEMPLATES.map(templateItem); +function CatalogItemActions({ + item, + testing, + duplicatingName, + onTestCombo, + onDuplicate, + t, +}: { + item: CatalogItem; + testing: boolean; + duplicatingName: string | null; + onTestCombo?: (combo: { name: string }) => void; + onDuplicate: (item: CatalogItem) => void; + t: ReturnType; +}) { + return ( +
+ + monitoring + + {onTestCombo && ( + + )} + {item.id !== "auto" && ( + + )} +
+ ); +} + +function CatalogItemCard({ + item, + testing, + duplicatingName, + onTestCombo, + onDuplicate, + t, +}: { + item: CatalogItem; + testing: boolean; + duplicatingName: string | null; + onTestCombo?: (combo: { name: string }) => void; + onDuplicate: (item: CatalogItem) => void; + t: ReturnType; +}) { + return ( +
+ +
+ {item.id} + + {item.template?.strategy ?? t(KIND_LABEL_KEYS[item.kind])} + +
+

{item.displayName}

+
+ {item.template ? ( + <> + {item.template.categories.map((cat) => ( + + {cat} + + ))} + {item.template.tiers.map((tier) => ( + + {tier} + + ))} + + ) : ( + item.candidateCount !== null && ( + 0 + ? "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400" + : "bg-black/[0.04] text-text-muted dark:bg-white/[0.04]" + }`} + > + {item.candidateCount > 0 + ? t("autoCatalogCandidates", { count: item.candidateCount }) + : t("autoCatalogNoCandidates")} + + ) + )} +
+ {item.template?.systemMessage && ( +

+ {item.template.systemMessage} +

+ )} +
+ ); +} + +function toCatalogItems(combos: AutoCatalogApiEntry[]): CatalogItem[] { + return combos + .filter((entry): entry is AutoCatalogApiEntry & { id: string } => { + return typeof entry?.id === "string" && entry.id.length > 0; + }) + .map((entry): CatalogItem => { + const kind = VALID_KINDS.has(entry.kind ?? "") + ? (entry.kind as CatalogItem["kind"]) + : "variant"; + return { + id: entry.id, + displayName: + typeof entry.name === "string" && entry.name.length > 0 ? entry.name : entry.id, + kind, + candidateCount: typeof entry.candidateCount === "number" ? entry.candidateCount : null, + template: AUTO_COMBO_TEMPLATES.find((tpl) => tpl.name === entry.id) ?? null, + }; + }); +} + export default function AutoComboCatalog({ onComboCreated, onTestCombo, @@ -83,24 +236,7 @@ export default function AutoComboCatalog({ // An empty live list is authoritative (no combo has candidates right // now) — only a failed/malformed response falls back to the templates. if (!Array.isArray(data.combos)) return; - const items = data.combos - .filter((entry): entry is AutoCatalogApiEntry & { id: string } => { - return typeof entry?.id === "string" && entry.id.length > 0; - }) - .map((entry): CatalogItem => { - const kind = VALID_KINDS.has(entry.kind ?? "") - ? (entry.kind as CatalogItem["kind"]) - : "variant"; - return { - id: entry.id, - displayName: - typeof entry.name === "string" && entry.name.length > 0 ? entry.name : entry.id, - kind, - candidateCount: - typeof entry.candidateCount === "number" ? entry.candidateCount : null, - template: AUTO_COMBO_TEMPLATES.find((tpl) => tpl.name === entry.id) ?? null, - }; - }); + const items = toCatalogItems(data.combos); if (!cancelled) setLiveItems(items); } catch { // Endpoint unavailable — the static template fallback keeps rendering. @@ -192,102 +328,17 @@ export default function AutoComboCatalog({ {open && items.length > 0 && (
- {items.map((item) => { - const testing = testingName === item.id; - return ( -
-
- - monitoring - - {onTestCombo && ( - - )} - {item.id !== "auto" && ( - - )} -
- -
- {item.id} - - {item.template?.strategy ?? t(KIND_LABEL_KEYS[item.kind])} - -
-

{item.displayName}

-
- {item.template ? ( - <> - {item.template.categories.map((cat) => ( - - {cat} - - ))} - {item.template.tiers.map((tier) => ( - - {tier} - - ))} - - ) : ( - item.candidateCount !== null && ( - 0 - ? "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400" - : "bg-black/[0.04] text-text-muted dark:bg-white/[0.04]" - }`} - > - {item.candidateCount > 0 - ? t("autoCatalogCandidates", { count: item.candidateCount }) - : t("autoCatalogNoCandidates")} - - ) - )} -
- {item.template?.systemMessage && ( -

- {item.template.systemMessage} -

- )} -
- ); - })} + {items.map((item) => ( + + ))}
)} diff --git a/src/app/(dashboard)/dashboard/combos/comboPageHelpers.tsx b/src/app/(dashboard)/dashboard/combos/comboPageHelpers.tsx new file mode 100644 index 000000000000..4abe974dbbd4 --- /dev/null +++ b/src/app/(dashboard)/dashboard/combos/comboPageHelpers.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useTranslations } from "next-intl"; + +export function getI18nOrFallback(t, key, fallback, values = undefined) { + try { + if (typeof t.has === "function" && t.has(key)) return t(key, values); + } catch {} + return fallback; +} + +export function AutoComboTruncatedNote({ results }) { + const t = useTranslations("combos"); + const tested = Array.isArray(results.results) ? results.results.length : 0; + const total = results.totalCandidates; + if (results.comboType !== "auto" || typeof total !== "number" || total <= tested) { + return null; + } + return ( +

+ {getI18nOrFallback( + t, + "autoComboTestTruncated", + `Tested ${tested} of ${total} live candidates (highest weights first).`, + { tested, total } + )} +

+ ); +} diff --git a/src/app/(dashboard)/dashboard/combos/page.tsx b/src/app/(dashboard)/dashboard/combos/page.tsx index 3785fd75f2e2..30a00b4d9b36 100644 --- a/src/app/(dashboard)/dashboard/combos/page.tsx +++ b/src/app/(dashboard)/dashboard/combos/page.tsx @@ -63,6 +63,7 @@ import { } from "@/lib/combos/builderDraft"; import { normalizeComboConfigMode } from "@/shared/constants/comboConfigMode"; import AutoComboCatalog from "./AutoComboCatalog"; +import { AutoComboTruncatedNote, getI18nOrFallback } from "./comboPageHelpers"; import KimiComboPresetCard from "./KimiComboPresetCard"; import { KIMI_CODING_PRESET, hasKimiCodingPreset } from "./kimiComboPreset"; import BuilderIntelligentStep from "./BuilderIntelligentStep"; @@ -596,13 +597,6 @@ function getStrategyBadgeClass(strategy) { return "bg-blue-500/15 text-blue-600 dark:text-blue-400"; } -function getI18nOrFallback(t, key, fallback, values = undefined) { - try { - if (typeof t.has === "function" && t.has(key)) return t(key, values); - } catch {} - return fallback; -} - function moveArrayItem(items, fromIndex, toIndex) { const nextItems = [...items]; const [movedItem] = nextItems.splice(fromIndex, 1); @@ -2015,7 +2009,6 @@ function ComboCardInner({ const ComboCard = memo(ComboCardInner); function TestResultsView({ results }) { - const t = useTranslations("combos"); const emailsVisible = useEmailPrivacyStore((s) => s.emailsVisible); if (results.error) { @@ -2033,19 +2026,7 @@ function TestResultsView({ results }) { Targets are tested independently. This checks model health, not the combo’s routing strategy or fallback order.

- {results.comboType === "auto" && - typeof results.totalCandidates === "number" && - Array.isArray(results.results) && - results.totalCandidates > results.results.length && ( -

- {getI18nOrFallback( - t, - "autoComboTestTruncated", - `Tested ${results.results.length} of ${results.totalCandidates} live candidates (highest weights first).`, - { tested: results.results.length, total: results.totalCandidates } - )} -

- )} + {results.resolvedBy && (
diff --git a/src/app/api/combos/test/route.ts b/src/app/api/combos/test/route.ts index 686bb13d90d5..82a3740d563b 100644 --- a/src/app/api/combos/test/route.ts +++ b/src/app/api/combos/test/route.ts @@ -11,12 +11,13 @@ import { import { getRuntimePorts } from "@/lib/runtime/ports"; import { requiresWebSessionCredential } from "@/shared/providers/webSessionCredentials"; import { resolveNestedComboTargets } from "@omniroute/open-sse/services/combo.ts"; -import type { ResolvedComboTarget } from "@omniroute/open-sse/services/combo/types.ts"; +import type { ComboLike, ResolvedComboTarget } from "@omniroute/open-sse/services/combo/types.ts"; import { testComboSchema } from "@/shared/validation/schemas"; import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; import { requireManagementAuth } from "@/lib/api/requireManagementAuth"; import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error"; import { isAutoComboId, materializeAutoCombo } from "@/lib/combos/autoVirtual"; +import type { ComboRecord } from "@/domain/persistence/comboRepositories"; export const COMBO_TEST_TIMEOUT_MS = 60_000; export const COMBO_TEST_TOTAL_TIMEOUT_MS = 180_000; @@ -28,6 +29,44 @@ export const COMBO_TEST_TOTAL_TIMEOUT_MS = 180_000; */ export const AUTO_COMBO_TEST_MAX_PROBES = 8; +type AutoTestComboResolution = { combo: ComboRecord; totalAutoCandidates: number }; + +// Built-in auto/* combos have no persisted row — materialize the virtual +// combo the same way request-time routing does, then probe a bounded slice +// of its candidate pool instead of the (potentially huge) full set. +async function resolveAutoTestCombo( + comboName: string +): Promise { + if (!isAutoComboId(comboName)) { + return NextResponse.json({ error: "Combo not found" }, { status: 404 }); + } + let virtual: Record; + try { + // Spread into a fresh object so the result fits ComboRecord + // (Record) — interfaces like VirtualAutoCombo do not + // get an implicit index signature. + virtual = { ...(await materializeAutoCombo(comboName)) }; + } catch { + return NextResponse.json({ error: "Combo not found" }, { status: 404 }); + } + const pool = Array.isArray(virtual.models) ? virtual.models : []; + if (pool.length === 0) { + return NextResponse.json( + { error: `Auto combo has no connected candidates matching "${comboName}"` }, + { status: 400 } + ); + } + return { + combo: { + ...virtual, + models: [...pool] + .sort((a, b) => (b?.weight ?? 0) - (a?.weight ?? 0)) + .slice(0, AUTO_COMBO_TEST_MAX_PROBES), + }, + totalAutoCandidates: pool.length, + }; +} + async function getInternalApiKey(): Promise { // Combo health-check probes hit /v1/chat/completions, which enforces // per-key model allowlists (see shared/utils/apiKeyPolicy.ts). Picking @@ -238,38 +277,14 @@ export async function POST(request) { let combo = await getComboByName(comboName); let totalAutoCandidates: number | null = null; if (!combo) { - // Built-in auto/* combos have no persisted row — materialize the virtual - // combo the same way request-time routing does, then probe a bounded - // slice of its candidate pool instead of the (potentially huge) full set. - if (!isAutoComboId(comboName)) { - return NextResponse.json({ error: "Combo not found" }, { status: 404 }); - } - try { - // Spread into a fresh object so the result fits ComboRecord - // (Record) — interfaces like VirtualAutoCombo do not - // get an implicit index signature. - combo = { ...(await materializeAutoCombo(comboName)) }; - } catch { - return NextResponse.json({ error: "Combo not found" }, { status: 404 }); - } - const pool = Array.isArray(combo.models) ? combo.models : []; - totalAutoCandidates = pool.length; - if (pool.length === 0) { - return NextResponse.json( - { error: `Auto combo has no connected candidates matching "${comboName}"` }, - { status: 400 } - ); - } - combo = { - ...combo, - models: [...pool] - .sort((a, b) => (b?.weight ?? 0) - (a?.weight ?? 0)) - .slice(0, AUTO_COMBO_TEST_MAX_PROBES), - }; + const resolution = await resolveAutoTestCombo(comboName); + if (resolution instanceof NextResponse) return resolution; + combo = resolution.combo; + totalAutoCandidates = resolution.totalAutoCandidates; } const allCombos = await getCombos(); - const targets = resolveNestedComboTargets(combo, allCombos); + const targets = resolveNestedComboTargets(combo as unknown as ComboLike, allCombos); if (targets.length === 0) { return NextResponse.json({ error: "Combo has no models" }, { status: 400 }); @@ -277,57 +292,82 @@ export async function POST(request) { const baseInternalUrl = getInternalBaseUrl(); const internalApiKey = await getInternalApiKey(); - const results: ComboTestResult[] = []; - const loopStarted = Date.now(); - for (const target of targets) { - // Client disconnects surface through request.signal (passed as - // parentSignal at the call site below). Stop instead of starting another doomed probe. - if (request.signal?.aborted) { - break; - } - if (Date.now() - loopStarted >= COMBO_TEST_TOTAL_TIMEOUT_MS) { - results.push( - buildComboTestResult(target, { - status: "error", - error: `Timeout (${COMBO_TEST_TOTAL_TIMEOUT_MS / 1000}s total)`, - latencyMs: 0, - }) - ); - continue; - } - results.push(await testComboTarget(target, baseInternalUrl, internalApiKey, request.signal)); - } - const resolvedResult = results.find((result) => result.status === "ok") || null; - const resolvedBy = resolvedResult?.model || null; + const results = await probeComboTargets( + targets, + baseInternalUrl, + internalApiKey, + request.signal ?? null + ); - return NextResponse.json({ - comboName, - strategy: combo.strategy || "priority", - testMode: "target-health-check", - resolvedBy, - resolvedByExecutionKey: resolvedResult?.executionKey || null, - resolvedByTarget: resolvedResult - ? { - model: resolvedResult.model, - provider: resolvedResult.provider, - stepId: resolvedResult.stepId, - executionKey: resolvedResult.executionKey, - connectionId: resolvedResult.connectionId, - label: resolvedResult.label, - } - : null, - ...(totalAutoCandidates !== null - ? { comboType: "auto", totalCandidates: totalAutoCandidates } - : {}), - results, - testedAt: new Date().toISOString(), - }); + return NextResponse.json( + buildComboTestResponse(comboName, combo, results, totalAutoCandidates) + ); } catch (error) { console.log("Error testing combo:", error); return NextResponse.json({ error: "Failed to test combo" }, { status: 500 }); } } +async function probeComboTargets( + targets: ResolvedComboTarget[], + baseInternalUrl: string, + internalApiKey: string | null, + signal: AbortSignal | null +): Promise { + const results: ComboTestResult[] = []; + const loopStarted = Date.now(); + for (const target of targets) { + // Client disconnects surface through signal (from request.signal at the + // call site). Stop instead of starting another doomed probe. + if (signal?.aborted) { + break; + } + if (Date.now() - loopStarted >= COMBO_TEST_TOTAL_TIMEOUT_MS) { + results.push( + buildComboTestResult(target, { + status: "error", + error: `Timeout (${COMBO_TEST_TOTAL_TIMEOUT_MS / 1000}s total)`, + latencyMs: 0, + }) + ); + continue; + } + results.push(await testComboTarget(target, baseInternalUrl, internalApiKey, signal)); + } + return results; +} + +function buildComboTestResponse( + comboName: string, + combo: ComboRecord, + results: ComboTestResult[], + totalAutoCandidates: number | null +) { + const resolvedResult = results.find((result) => result.status === "ok") || null; + return { + comboName, + strategy: combo.strategy || "priority", + testMode: "target-health-check", + resolvedBy: resolvedResult?.model || null, + resolvedByExecutionKey: resolvedResult?.executionKey || null, + resolvedByTarget: resolvedResult + ? { + model: resolvedResult.model, + provider: resolvedResult.provider, + stepId: resolvedResult.stepId, + executionKey: resolvedResult.executionKey, + connectionId: resolvedResult.connectionId, + label: resolvedResult.label, + } + : null, + ...(totalAutoCandidates !== null + ? { comboType: "auto", totalCandidates: totalAutoCandidates } + : {}), + results, + testedAt: new Date().toISOString(), + }; +} + function getInternalBaseUrl(): string { const { apiPort } = getRuntimePorts(); return `http://127.0.0.1:${apiPort}`; diff --git a/src/app/api/v1/models/catalog.ts b/src/app/api/v1/models/catalog.ts index 57238c1ccf2a..56fdeb7d9598 100644 --- a/src/app/api/v1/models/catalog.ts +++ b/src/app/api/v1/models/catalog.ts @@ -897,11 +897,9 @@ async function buildUnifiedModelsResponseCore( await yieldCatalogBuildTurn(); } const virtualCombo = await createBuiltinAutoCombo(autoId, suffix, preparedAutoInputs); - // A combo with no live candidates can never dispatch — don't advertise it. - if (virtualCombo.models.length === 0) continue; + if (virtualCombo.models.length === 0) continue; // zero live candidates — can't dispatch const contextLength = virtualCombo.advertisedContextLength || 128000; const maxOutputTokens = virtualCombo.advertisedMaxOutputTokens || 8192; - // #11947: derive modalities and vision from the effective target pool so // OpenAI-compatible clients can detect vision support for auto/* combos. const autoTargets: ComboCatalogTarget[] = virtualCombo.models.map((m) => ({ From f1f0fcecdfe9dc4c50c96962bbc01ff3ad8a323b Mon Sep 17 00:00:00 2001 From: Afonso Dutra Nogueira Filho Date: Tue, 6 Oct 2026 16:51:15 -0300 Subject: [PATCH 09/10] fix(autoCombo): resolve auto/claude-* as model families, not weight packs (#15675) auto/claude-opus|sonnet|haiku lived in AUTO_TEMPLATE_VARIANTS mapped to the smart/coding/fast weight packs, so the whole connected provider pool was scored and auto/claude-opus could route to an unrelated model (observed: cheaperinference/aion-3.0). The ids now live on the auto/ axis: claude-opus/claude-sonnet/claude-haiku are registered ModelFamily values detected via word-bounded tier tokens (\bopus/\bsonnet/\bhaiku), so the candidate pool is restricted to matching Claude tier models before scoring. --- open-sse/services/autoCombo/builtinCatalog.ts | 7 +- open-sse/services/autoCombo/modelFamily.ts | 14 ++- .../autoCombo/provider-family-combos.test.ts | 106 ++++++++++++++---- 3 files changed, 101 insertions(+), 26 deletions(-) diff --git a/open-sse/services/autoCombo/builtinCatalog.ts b/open-sse/services/autoCombo/builtinCatalog.ts index 16f08108673b..5759fb47abc7 100644 --- a/open-sse/services/autoCombo/builtinCatalog.ts +++ b/open-sse/services/autoCombo/builtinCatalog.ts @@ -41,9 +41,10 @@ export const AUTO_TEMPLATE_VARIANTS: Record = { "auto/cheap": "cheap", "auto/offline": "offline", "auto/smart": "smart", - "auto/claude-opus": "smart", - "auto/claude-sonnet": "coding", - "auto/claude-haiku": "fast", + // #15675: `auto/claude-*` are NOT flat variants — they live on the + // `auto/` axis (modelFamily.ts) so the candidate pool is restricted to + // matching Claude tier models (`*opus*`/`*sonnet*`/`*haiku*`) instead of + // scoring the whole connected pool and routing to an unrelated model. "auto/best-free": "cheap", // Subscription-first routing (see `subscriptionLadder.ts`). `auto/subscription` // maps to no weight variant on purpose: its pool is already restricted to diff --git a/open-sse/services/autoCombo/modelFamily.ts b/open-sse/services/autoCombo/modelFamily.ts index aa333eb1ca3b..fbe7ed7dc07f 100644 --- a/open-sse/services/autoCombo/modelFamily.ts +++ b/open-sse/services/autoCombo/modelFamily.ts @@ -24,7 +24,10 @@ export type ModelFamily = | "kimi" | "qwen" | "deepseek" - | "gpt"; + | "gpt" + | "claude-opus" + | "claude-sonnet" + | "claude-haiku"; export const MODEL_FAMILIES: readonly ModelFamily[] = [ "glm", @@ -38,6 +41,9 @@ export const MODEL_FAMILIES: readonly ModelFamily[] = [ "qwen", "deepseek", "gpt", + "claude-opus", + "claude-sonnet", + "claude-haiku", ]; const MODEL_FAMILY_SET: ReadonlySet = new Set(MODEL_FAMILIES); @@ -55,6 +61,12 @@ const FAMILY_ID_PATTERNS: ReadonlyArray<{ family: ModelFamily; pattern: RegExp } { family: "qwen", pattern: /^qwen/i }, { family: "deepseek", pattern: /^deepseek-/i }, { family: "gpt", pattern: /^gpt-/i }, + // #15675: Claude tier names are substrings, not prefixes — ids come in shapes + // like `claude-opus-4.8`, `anthropic.claude-3-opus`, `claude-3-5-sonnet-*`. + // The \b word boundary keeps `octopus`-style ids from matching `opus`. + { family: "claude-opus", pattern: /\bopus/i }, + { family: "claude-sonnet", pattern: /\bsonnet/i }, + { family: "claude-haiku", pattern: /\bhaiku/i }, ]; /** diff --git a/tests/unit/autoCombo/provider-family-combos.test.ts b/tests/unit/autoCombo/provider-family-combos.test.ts index 0930e2f3ca93..a689b3eb8965 100644 --- a/tests/unit/autoCombo/provider-family-combos.test.ts +++ b/tests/unit/autoCombo/provider-family-combos.test.ts @@ -19,6 +19,7 @@ import { buildFamilyCandidateFilter, isValidModelFamily, AUTO_FAMILY_IDS, + MODEL_FAMILIES, } from "../../../open-sse/services/autoCombo/modelFamily"; // First-touch DB migrations run once per worker and can exceed vitest's 5s @@ -79,7 +80,7 @@ describe("detectModelFamily (pure)", () => { assert.equal(detectModelFamily("zai-glm-5.2"), null); }); - it("isValidModelFamily accepts exactly the 11 advertised families", () => { + it("isValidModelFamily accepts exactly the advertised families", () => { for (const family of [ "glm", "minimax", @@ -92,6 +93,9 @@ describe("detectModelFamily (pure)", () => { "qwen", "deepseek", "gpt", + "claude-opus", + "claude-sonnet", + "claude-haiku", ]) { assert.equal(isValidModelFamily(family), true); } @@ -101,6 +105,9 @@ describe("detectModelFamily (pure)", () => { it("advertises exactly one auto/ catalog id per family", () => { assert.deepEqual([...AUTO_FAMILY_IDS].sort(), [ + "auto/claude-haiku", + "auto/claude-opus", + "auto/claude-sonnet", "auto/deepseek", "auto/gemini", "auto/gemma", @@ -247,19 +254,7 @@ describe("auto/ materialization (#6453)", () => { }); it("isRecognizedBuiltinAuto recognizes every auto/ id", () => { - for (const family of [ - "glm", - "minimax", - "mimo", - "zai", - "gemma", - "llama", - "gemini", - "kimi", - "qwen", - "deepseek", - "gpt", - ]) { + for (const family of MODEL_FAMILIES) { assert.equal(builtinCatalog.isRecognizedBuiltinAuto(`auto/${family}`, family), true); } assert.equal(builtinCatalog.isRecognizedBuiltinAuto("auto/unknownfam", "unknownfam"), false); @@ -357,17 +352,84 @@ describe("additional auto-routing families (#13214)", () => { assert.equal(buildFamilyCandidateFilter("zai")({ provider: "glm", model: "glm-5.2" }), false); }); - it("maps Haiku to fast while preserving Opus and Sonnet variants", () => { - for (const [suffix, variant] of [ - ["claude-haiku", "fast"], - ["claude-opus", "smart"], - ["claude-sonnet", "coding"], - ]) { - assert.equal(builtinCatalog.AUTO_TEMPLATE_VARIANTS[`auto/${suffix}`], variant); + it("resolves auto/claude-* as model families, not flat variants (#15675)", () => { + // Regression: auto/claude-opus|sonnet|haiku used to live in + // AUTO_TEMPLATE_VARIANTS mapped to the smart/coding/fast weight packs, so the + // whole connected pool was scored and auto/claude-opus could route to an + // unrelated model (e.g. cheaperinference/aion-3.0). They are `auto/` + // combos now: the candidate pool is restricted to matching Claude tier models. + for (const suffix of ["claude-opus", "claude-sonnet", "claude-haiku"]) { + assert.equal( + Object.prototype.hasOwnProperty.call( + builtinCatalog.AUTO_TEMPLATE_VARIANTS, + `auto/${suffix}` + ), + false, + `auto/${suffix} must not be a flat template variant` + ); + assert.equal(isValidModelFamily(suffix), true, suffix); assert.equal(builtinCatalog.isRecognizedBuiltinAuto(`auto/${suffix}`, suffix), true); assert.deepEqual(builtinCatalog.resolveBuiltinAutoSpec(`auto/${suffix}`, suffix), { - variant, + variant: undefined, }); } }); + + it("detects Claude tier names across real-world model id shapes", () => { + for (const [model, family] of [ + ["claude-opus-4.8", "claude-opus"], + ["claude-opus-5", "claude-opus"], + ["anthropic/claude-3-opus-20240229", "claude-opus"], + ["us.anthropic.claude-opus-4-1", "claude-opus"], + ["claude-sonnet-4-5", "claude-sonnet"], + ["anthropic/claude-3-5-sonnet-20241022", "claude-sonnet"], + ["claude-haiku-4-5-20251001", "claude-haiku"], + ["anthropic.claude-haiku-4-5", "claude-haiku"], + ]) { + assert.equal(detectModelFamily(model), family, model); + } + // Non-tier Claude ids and unrelated models must not match any Claude family. + assert.equal(detectModelFamily("claude-fable-5"), null); + assert.equal(detectModelFamily("aion-3.0"), null); + // The \b boundary keeps substring lookalikes out. + assert.equal(detectModelFamily("octopus-v2"), null); + }); + + it("auto/claude-opus pool contains only *opus* models (#15675 repro)", async () => { + // Reproduces the issue: a connected cheaperinference serving aion-3.0 must + // NOT be routable through auto/claude-opus, while its real claude-opus-* + // catalog models legitimately are. + await providersDb.createProviderConnection({ + provider: "cheaperinference", + authType: "apikey", + name: "CheaperInference", + apiKey: "sk-test-cinf", + defaultModel: "aion-3.0", + }); + await providersDb.createProviderConnection({ + provider: "claude", + authType: "apikey", + name: "Claude", + apiKey: "sk-test-claude", + defaultModel: "claude-opus-5", + }); + + const combo = await builtinCatalog.createBuiltinAutoCombo("auto/claude-opus", "claude-opus"); + + assert.equal(combo.id, "auto/claude-opus"); + assert.equal(combo.strategy, "auto"); + assert.ok(combo.models.length > 0, "auto/claude-opus must materialize candidates"); + assert.ok( + combo.models.every((m) => detectModelFamily(m.model) === "claude-opus"), + "every auto/claude-opus candidate must be an *opus* model" + ); + assert.ok( + combo.models.every((m) => !m.model.toLowerCase().includes("aion")), + "the issue's aion-3.0 target must never enter the opus pool" + ); + assert.ok( + combo.models.some((m) => m.providerId === "claude"), + "the connected Claude provider must contribute opus candidates" + ); + }); }); From 7a01ed88e1c8f9e85ae5d8b6a4fb8180f4405bfe Mon Sep 17 00:00:00 2001 From: Afonso Dutra Nogueira Filho Date: Thu, 8 Oct 2026 07:10:59 -0300 Subject: [PATCH 10/10] docs(changelog): add fragment for the auto-routing catalog actions Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --- changelog.d/features/15397-auto-catalog-actions.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/features/15397-auto-catalog-actions.md diff --git a/changelog.d/features/15397-auto-catalog-actions.md b/changelog.d/features/15397-auto-catalog-actions.md new file mode 100644 index 000000000000..94da8152c314 --- /dev/null +++ b/changelog.d/features/15397-auto-catalog-actions.md @@ -0,0 +1 @@ +- **feat(combos):** the `/dashboard/combos` auto-routing catalog now loads the live built-in `auto/*` combos from `GET /api/combos/auto` (kind badge and candidate count per card) with Control Center, Test and Duplicate actions; `GET /api/combos/auto?id=` materializes one auto combo and `POST /api/combos/test` can test an `auto/*` combo (up to 8 highest-weight candidates). `auto/*` combos with no live candidates are no longer listed in `/api/combos/auto` or advertised in `/v1/models` ([#15397](https://github.com/diegosouzapw/OmniRoute/pull/15397) — thanks @afonsoft).