From 72e794c281bef76162a1433a311059d381f5c31c Mon Sep 17 00:00:00 2001 From: gitlawr Date: Wed, 3 Jun 2026 10:56:27 +0800 Subject: [PATCH] fix: align playground default model id with org namespace Opening an org-scoped deployment in the Playground pre-selected the bare model name (e.g. `qwen3-0.6b`), which never matched the org-namespaced option the `/v1/models` dropdown actually lists (`org1/qwen3-0.6b`), so the selection showed an unmatched value. Resolve the owning org from the route row's `owner_principal_id` (falling back to the org the caller is currently acting under for the admin "All" view) and reconstruct the same `{org}/{name}` id the server reports. The platform org carries no prefix, matching the server's behaviour. - user.ts: add `getOrgById`/`getCurrentOrg` that scan both org caches (`organizationList` + admin-only `allOrganizations`) with string- normalised id comparison; `getOrgNameById` is now a thin wrapper. Helpers accept `undefined` so optional row fields type-check. - use-open-playground: build the prefix from the resolved org record, keying the skip-prefix decision off `is_platform`. - RouteItem: declare the `owner_principal_id` the list API returns. - RouteTargetFormItem: drop the duplicate `overridden_model_name` declaration that TS flagged as a duplicate identifier. --- src/atoms/user.ts | 57 ++++++++++++------- src/pages/model-routes/config/types.ts | 4 +- .../model-routes/hooks/use-open-playground.ts | 16 ++++-- 3 files changed, 51 insertions(+), 26 deletions(-) diff --git a/src/atoms/user.ts b/src/atoms/user.ts index f16bab40..73da1543 100644 --- a/src/atoms/user.ts +++ b/src/atoms/user.ts @@ -90,20 +90,33 @@ const getStoredCurrentOrgId = (): number | null => { // cluster-owner fallback. const ORG_CACHE_KEYS = ['organizationList', 'allOrganizations'] as const; -const lookupOrgNamespace = (id: number | null): string | null => { +export interface CachedOrg { + id: number; + name?: string; + // The platform Org (single global tenant). Its models are NOT + // namespaced in ``/v1/models`` — they appear under their bare name. + is_platform?: boolean; +} + +// Resolve the cached Org record for an owner/principal id by scanning both +// org caches. ``organizationList`` (the caller's member orgs) is checked +// alongside the admin-only ``allOrganizations`` so member sessions resolve +// too. Id types vary between localStorage payloads (some writers stringify, +// others persist as a JSON number), so compare as strings — strict equality +// would silently miss those cases. +export const getOrgById = ( + id: number | string | null | undefined +): CachedOrg | null => { if (id == null) return null; - // Normalise both sides to strings — the stored id type varies between - // localStorage payloads (some writers stringify, others persist as a - // JSON number); strict equality would silently miss those cases. const target = String(id); for (const key of ORG_CACHE_KEYS) { try { const raw = localStorage.getItem(key); if (!raw) continue; - const list = JSON.parse(raw) as Array<{ id: number; name?: string }>; + const list = JSON.parse(raw) as CachedOrg[]; if (!Array.isArray(list)) continue; const match = list.find((item) => String(item?.id) === target); - if (match?.name) return `gpustack-${match.name}`; + if (match) return match; } catch { // ignore malformed cache; continue checking other keys } @@ -111,17 +124,23 @@ const lookupOrgNamespace = (id: number | null): string | null => { return null; }; -export const getAllOrganizations = (): Array<{ id: number; name?: string }> => { - const allOrganizations = localStorage.getItem('allOrganizations'); - if (!allOrganizations) return []; - try { - const list = JSON.parse(allOrganizations) as Array<{ - id: number; - name?: string; - }>; - if (!Array.isArray(list)) return []; - return list; - } catch { - return []; - } +// Bare Org *name* (e.g. ``org1``) for an owner/principal id, or null. +export const getOrgNameById = ( + id: number | string | null | undefined +): string | null => { + return getOrgById(id)?.name ?? null; +}; + +const lookupOrgNamespace = (id: number | null): string | null => { + const name = getOrgNameById(id); + return name ? `gpustack-${name}` : null; +}; + +// The Org the caller is currently acting under, or null in the admin-"All" +// context. The org-switcher reloads the page on switch, so the list pages +// always show this org's resources — which is how ``/v1/models`` namespaces +// their model ids (``{org}/{name}``). Callers reconstructing that id use this +// as the fallback owner when a row carries no explicit ``owner_principal_id``. +export const getCurrentOrg = (): CachedOrg | null => { + return getOrgById(getStoredCurrentOrgId()); }; diff --git a/src/pages/model-routes/config/types.ts b/src/pages/model-routes/config/types.ts index da656340..c9dcda6f 100644 --- a/src/pages/model-routes/config/types.ts +++ b/src/pages/model-routes/config/types.ts @@ -4,7 +4,6 @@ export interface RouteTargetFormItem { weight?: number | null; model_id?: number; provider_id?: number; - overridden_model_name?: string; fallback_status_codes?: string[]; parentId?: string | number; } @@ -32,6 +31,9 @@ export interface RouteItem { targets: number; ready_targets: number; access_policy: string; + // Org principal that owns this route. Drives the ``{org}/{name}`` model-id + // prefix when opening the route in the Playground. + owner_principal_id?: number; } export interface RouteTarget extends RouteTargetFormItem { diff --git a/src/pages/model-routes/hooks/use-open-playground.ts b/src/pages/model-routes/hooks/use-open-playground.ts index 8c6cc283..a91a86c3 100644 --- a/src/pages/model-routes/hooks/use-open-playground.ts +++ b/src/pages/model-routes/hooks/use-open-playground.ts @@ -1,4 +1,4 @@ -import { getAllOrganizations } from '@/atoms/user'; +import { getCurrentOrg, getOrgById } from '@/atoms/user'; import { useNavigate } from '@umijs/max'; import { modelCategoriesMap } from '../../llmodels/config'; import { categoryToPathMap } from '../../llmodels/config/button-actions'; @@ -7,11 +7,15 @@ const useOpenPlayground = () => { const navigate = useNavigate(); const handleOpenPlayGround = (row: any) => { - const allOrganizations = getAllOrganizations(); - const orgName = allOrganizations.find( - (org) => org.id === row.owner_principal_id - )?.name; - const rawModel = orgName ? `${orgName}/${row.name}` : row.name; + // Match the id format the OpenAI ``/v1/models`` endpoint reports: an + // org's models are namespaced as ``{org}/{name}``, while the platform + // org's carry no prefix (the server strips it). Prefer the row's own + // ``owner_principal_id`` (an org principal id); fall back to the Org the + // caller is currently acting under for the admin "All" view, where the + // row's owner still resolves via the platform-wide org cache. + const org = getOrgById(row.owner_principal_id) ?? getCurrentOrg(); + const rawModel = + org?.name && !org.is_platform ? `${org.name}/${row.name}` : row.name; const modelName = encodeURIComponent(rawModel); for (const [category, path] of Object.entries(categoryToPathMap)) {