fix(dashboard): disambiguate colliding passthrough model aliases (port from 9router#1850) (#6431)

disambiguate colliding passthrough model aliases (port #1850) (net +1/-0, test OK). Integrated into release/v3.8.46.
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-06 19:24:59 -03:00
committed by GitHub
parent 2ecaae7c40
commit b67f2c58da
4 changed files with 103 additions and 10 deletions

View File

@@ -14,6 +14,7 @@
*/
import React, { useState, useMemo } from "react";
import { Button } from "@/shared/components";
import { generateUniqueModelAlias } from "./passthroughAlias.ts";
import {
matchesModelCatalogQuery,
normalizeModelCatalogSource,
@@ -323,22 +324,18 @@ export default function PassthroughModelsSection({
: filteredModels;
const activeCount = allModels.filter((model) => !model.isHidden).length;
// Generate default alias from modelId (last part after /)
const generateDefaultAlias = (modelId: string) => {
const parts = modelId.split("/");
return parts[parts.length - 1];
};
const handleAdd = async () => {
if (!newModel.trim() || adding) return;
const modelId = newModel.trim();
const defaultAlias = generateDefaultAlias(modelId);
// Check if alias already exists
if (modelAliases[defaultAlias]) {
alert(t("aliasExistsAlert", { alias: defaultAlias }));
// #1850: block re-adding the SAME model, but disambiguate DISTINCT models
// that would otherwise collapse to the same last-segment alias (e.g.
// enx/gpt-5.5 vs enx/codebuddy/gpt-5.5 → both "gpt-5.5").
if (Object.values(modelAliases).includes(modelId)) {
alert(t("aliasExistsAlert", { alias: modelId }));
return;
}
const defaultAlias = generateUniqueModelAlias(modelId, modelAliases);
setAdding(true);
try {

View File

@@ -0,0 +1,47 @@
/**
* Generate a unique default alias for a passthrough model id (9router#1850).
*
* The naive "last path segment" alias collapses distinct namespaced ids to the
* same alias — e.g. `enx/codebuddy/gpt-5.5` and `enx/gpt-5.5` both become
* `gpt-5.5` — so the second model could never be added (the UI only alerted
* "alias already exists"). This disambiguates deterministically:
* 1. the bare last segment, if free;
* 2. progressively more-qualified names joined with "-" (parent segments
* prepended), if the shorter form is taken;
* 3. a numeric suffix on the last segment as a final fallback.
*
* Pure — no React/DOM deps — so it is unit-testable.
*/
export function generateUniqueModelAlias(
modelId: string,
existingAliases: Record<string, unknown> = {}
): string {
const parts = String(modelId ?? "")
.split("/")
.filter(Boolean);
if (parts.length === 0) {
// No usable segments (e.g. "" or "///") — fall back to the raw id + numeric.
const base = String(modelId ?? "").trim() || "model";
return isTaken(base, existingAliases) ? nextNumeric(base, existingAliases) : base;
}
// 1 + 2: try last segment, then last-2 joined, … up to the full path.
for (let take = 1; take <= parts.length; take++) {
const candidate = parts.slice(parts.length - take).join("-");
if (!isTaken(candidate, existingAliases)) return candidate;
}
// 3: every qualified form is taken → numeric suffix on the last segment.
return nextNumeric(parts[parts.length - 1], existingAliases);
}
function isTaken(alias: string, existingAliases: Record<string, unknown>): boolean {
return Object.prototype.hasOwnProperty.call(existingAliases, alias);
}
function nextNumeric(base: string, existingAliases: Record<string, unknown>): string {
let i = 2;
while (isTaken(`${base}-${i}`, existingAliases)) i++;
return `${base}-${i}`;
}