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.
This commit is contained in:
+38
-19
@@ -90,20 +90,33 @@ const getStoredCurrentOrgId = (): number | null => {
|
|||||||
// cluster-owner fallback.
|
// cluster-owner fallback.
|
||||||
const ORG_CACHE_KEYS = ['organizationList', 'allOrganizations'] as const;
|
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;
|
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);
|
const target = String(id);
|
||||||
for (const key of ORG_CACHE_KEYS) {
|
for (const key of ORG_CACHE_KEYS) {
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(key);
|
const raw = localStorage.getItem(key);
|
||||||
if (!raw) continue;
|
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;
|
if (!Array.isArray(list)) continue;
|
||||||
const match = list.find((item) => String(item?.id) === target);
|
const match = list.find((item) => String(item?.id) === target);
|
||||||
if (match?.name) return `gpustack-${match.name}`;
|
if (match) return match;
|
||||||
} catch {
|
} catch {
|
||||||
// ignore malformed cache; continue checking other keys
|
// ignore malformed cache; continue checking other keys
|
||||||
}
|
}
|
||||||
@@ -111,17 +124,23 @@ const lookupOrgNamespace = (id: number | null): string | null => {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAllOrganizations = (): Array<{ id: number; name?: string }> => {
|
// Bare Org *name* (e.g. ``org1``) for an owner/principal id, or null.
|
||||||
const allOrganizations = localStorage.getItem('allOrganizations');
|
export const getOrgNameById = (
|
||||||
if (!allOrganizations) return [];
|
id: number | string | null | undefined
|
||||||
try {
|
): string | null => {
|
||||||
const list = JSON.parse(allOrganizations) as Array<{
|
return getOrgById(id)?.name ?? null;
|
||||||
id: number;
|
};
|
||||||
name?: string;
|
|
||||||
}>;
|
const lookupOrgNamespace = (id: number | null): string | null => {
|
||||||
if (!Array.isArray(list)) return [];
|
const name = getOrgNameById(id);
|
||||||
return list;
|
return name ? `gpustack-${name}` : null;
|
||||||
} catch {
|
};
|
||||||
return [];
|
|
||||||
}
|
// 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());
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ export interface RouteTargetFormItem {
|
|||||||
weight?: number | null;
|
weight?: number | null;
|
||||||
model_id?: number;
|
model_id?: number;
|
||||||
provider_id?: number;
|
provider_id?: number;
|
||||||
overridden_model_name?: string;
|
|
||||||
fallback_status_codes?: string[];
|
fallback_status_codes?: string[];
|
||||||
parentId?: string | number;
|
parentId?: string | number;
|
||||||
}
|
}
|
||||||
@@ -32,6 +31,9 @@ export interface RouteItem {
|
|||||||
targets: number;
|
targets: number;
|
||||||
ready_targets: number;
|
ready_targets: number;
|
||||||
access_policy: string;
|
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 {
|
export interface RouteTarget extends RouteTargetFormItem {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getAllOrganizations } from '@/atoms/user';
|
import { getCurrentOrg, getOrgById } from '@/atoms/user';
|
||||||
import { useNavigate } from '@umijs/max';
|
import { useNavigate } from '@umijs/max';
|
||||||
import { modelCategoriesMap } from '../../llmodels/config';
|
import { modelCategoriesMap } from '../../llmodels/config';
|
||||||
import { categoryToPathMap } from '../../llmodels/config/button-actions';
|
import { categoryToPathMap } from '../../llmodels/config/button-actions';
|
||||||
@@ -7,11 +7,15 @@ const useOpenPlayground = () => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const handleOpenPlayGround = (row: any) => {
|
const handleOpenPlayGround = (row: any) => {
|
||||||
const allOrganizations = getAllOrganizations();
|
// Match the id format the OpenAI ``/v1/models`` endpoint reports: an
|
||||||
const orgName = allOrganizations.find(
|
// org's models are namespaced as ``{org}/{name}``, while the platform
|
||||||
(org) => org.id === row.owner_principal_id
|
// org's carry no prefix (the server strips it). Prefer the row's own
|
||||||
)?.name;
|
// ``owner_principal_id`` (an org principal id); fall back to the Org the
|
||||||
const rawModel = orgName ? `${orgName}/${row.name}` : row.name;
|
// 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);
|
const modelName = encodeURIComponent(rawModel);
|
||||||
|
|
||||||
for (const [category, path] of Object.entries(categoryToPathMap)) {
|
for (const [category, path] of Object.entries(categoryToPathMap)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user