diff --git a/litellm/proxy/management_endpoints/model_management_endpoints.py b/litellm/proxy/management_endpoints/model_management_endpoints.py index af84bc123ff..f04b3044968 100644 --- a/litellm/proxy/management_endpoints/model_management_endpoints.py +++ b/litellm/proxy/management_endpoints/model_management_endpoints.py @@ -446,9 +446,28 @@ def _get_public_model_name( patch_data: updateDeployment, db_model: Deployment, ) -> str: - """Determine the public model name from patch or existing model.""" - if patch_data.model_name: - return patch_data.model_name + """Determine the public model name from patch or existing model. + + Defends against a stale client round-tripping the internal mangled + `model_name` (the routing key `model_name_{team_id}_{uuid}`) instead + of the actual public name. A no-op (incoming equals current DB column) + or an internal-shape match is treated as "no rename intent" and falls + through to the existing public name. + """ + incoming = patch_data.model_name + if incoming: + team_id = ( + patch_data.model_info.team_id if patch_data.model_info else None + ) or ( + db_model.model_info.team_id if db_model.model_info else None + ) + is_internal_shape = ( + team_id is not None + and incoming.startswith(f"model_name_{team_id}_") + ) + is_no_op = incoming == db_model.model_name + if not (is_internal_shape or is_no_op): + return incoming if db_model.model_info and db_model.model_info.team_public_model_name: return db_model.model_info.team_public_model_name diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 5a379183e33..a3467a1cd40 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -11081,6 +11081,9 @@ async def model_info_v2( # Update total count to include agents search_total_count = len(all_models) + # Translate `model_name` to the public name for team-scoped rows. + all_models = [_translate_model_name_for_response(m) for m in all_models] + return _paginate_models_response( all_models=all_models, page=page, @@ -11515,6 +11518,28 @@ async def model_metrics_exceptions( return {"data": response, "exception_types": list(exception_types)} +def _translate_model_name_for_response(model: dict) -> dict: + """For team-scoped DB rows, replace `model_name` with the public name + in `model_info.team_public_model_name` before returning. The DB column + and the in-memory router index keep the internal mangled name + (`model_name_{team_id}_{uuid}`) as the routing key — this swap is a + presentation-layer concern. Returns a shallow copy; never mutates. + """ + if not isinstance(model, dict): + return model + model_info = model.get("model_info") or {} + if not isinstance(model_info, dict): + return model + team_public = model_info.get("team_public_model_name") + team_id = model_info.get("team_id") + if not team_public or not team_id: + return model + current = model.get("model_name") or "" + if not current.startswith(f"model_name_{team_id}_"): + return model + return {**model, "model_name": team_public} + + def _get_proxy_model_info(model: dict) -> dict: # provided model_info in config.yaml model_info = model.get("model_info", {}) @@ -11555,7 +11580,7 @@ def _get_proxy_model_info(model: dict) -> dict: deployment_dict=model, excluded_keys={"litellm_credential_name"} ) - return model + return _translate_model_name_for_response(model) @router.get( diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/AgentBuilderView.tsx b/ui/litellm-dashboard/src/components/playground/chat_ui/AgentBuilderView.tsx index c47c201d074..feef6175f10 100644 --- a/ui/litellm-dashboard/src/components/playground/chat_ui/AgentBuilderView.tsx +++ b/ui/litellm-dashboard/src/components/playground/chat_ui/AgentBuilderView.tsx @@ -191,22 +191,31 @@ export default function AgentBuilderView({ const [deleting, setDeleting] = useState(false); const effectiveApiKey = apiKey || accessToken || ""; - const selectedAgent = selectedId === NEW_AGENT_ID ? null : agentModels.find((a) => a.model_name === selectedId) ?? null; + const selectedAgent = + selectedId === NEW_AGENT_ID + ? null + : agentModels.find((a) => getAgentModelId(a) === selectedId) ?? null; const isNewAgent = selectedId === NEW_AGENT_ID; const selectedAgentModelId = selectedAgent ? getAgentModelId(selectedAgent) : null; - const loadAgents = useCallback(async () => { - if (!accessToken || !userID || !userRole) return; + const loadAgents = useCallback(async (): Promise => { + if (!accessToken || !userID || !userRole) return []; setLoadingAgents(true); try { const list = await fetchAvailableAgentModels(accessToken, userID, userRole); setAgentModels(list); - if (!selectedId || (selectedId !== NEW_AGENT_ID && !list.some((a) => a.model_name === selectedId))) { - setSelectedId(list.length > 0 ? list[0].model_name : null); + if ( + !selectedId || + (selectedId !== NEW_AGENT_ID && + !list.some((a) => getAgentModelId(a) === selectedId)) + ) { + setSelectedId(list.length > 0 ? getAgentModelId(list[0]) : null); } + return list; } catch (e) { console.error(e); NotificationsManager.fromBackend("Failed to load agents"); + return []; } finally { setLoadingAgents(false); } @@ -309,8 +318,9 @@ export default function AgentBuilderView({ model_info: {}, }); const newName = draftName.trim(); - await loadAgents(); - setSelectedId(newName); + const list = await loadAgents(); + const created = list.find((a) => a.model_name === newName); + setSelectedId(getAgentModelId(created ?? list[0]) ?? null); setActiveTab("chat"); } catch (e) { NotificationsManager.fromBackend("Failed to save agent"); @@ -342,8 +352,11 @@ export default function AgentBuilderView({ selectedAgentModelId, ); NotificationsManager.success("Agent updated successfully"); - await loadAgents(); - setSelectedId(draftName.trim()); + const list = await loadAgents(); + const stillSelected = list.find( + (a) => getAgentModelId(a) === selectedAgentModelId, + ); + setSelectedId(getAgentModelId(stillSelected ?? list[0]) ?? null); } catch (e) { NotificationsManager.fromBackend("Failed to update agent"); } finally { @@ -387,9 +400,13 @@ export default function AgentBuilderView({ try { await modelDeleteCall(accessToken, selectedAgentModelId); NotificationsManager.success("Agent deleted"); - await loadAgents(); - const remaining = agentModels.filter((a) => a.model_name !== selectedAgent.model_name); - setSelectedId(remaining.length > 0 ? remaining[0].model_name : null); + const list = await loadAgents(); + const remaining = list.filter( + (a) => getAgentModelId(a) !== selectedAgentModelId, + ); + setSelectedId( + remaining.length > 0 ? getAgentModelId(remaining[0]) : null, + ); } catch (e) { NotificationsManager.fromBackend("Failed to delete agent"); } finally { @@ -452,21 +469,24 @@ export default function AgentBuilderView({ ) : ( <> - {agentModels.map((agent) => ( - - ))} + {agentModels.map((agent) => { + const id = getAgentModelId(agent); + return ( + + ); + })}