mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-03 02:22:24 +00:00
[Fix] Team-based model name corruption on PATCH
For team-scoped ("Team-BYOK") models, the backend stores an internal
routing key `model_name_{team_id}_{uuid}` in the `model_name` column and
the user-facing name in `model_info.team_public_model_name`. The
internal name was leaking into `/v1/model/info` and `/v2/model/info`
responses. The UI bound the model edit form to that field, so any
non-rename edit (e.g. changing TPM) would PATCH the model with the
internal name as `model_name`. The PATCH endpoint then treated it as
a new public name, overwriting `team_public_model_name` and rewriting
the team's `models[]` list with the mangled string.
Translate `model_name` to the public name in API responses for
team-scoped rows; the DB column and router index continue to use the
internal name as the routing key. Harden `_get_public_model_name` to
detect a no-op or internal-shape `model_name` in the patch and fall
through to the existing `team_public_model_name` instead of treating
it as a rename. Standardize `AgentBuilderView` selection on
`model_info.id` so cross-team agents with the same public name don't
collide.
This commit is contained in:
parent
a67b7a7e87
commit
c96d47192b
3 changed files with 95 additions and 31 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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<AgentModel[]> => {
|
||||
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({
|
|||
</div>
|
||||
) : (
|
||||
<>
|
||||
{agentModels.map((agent) => (
|
||||
<button
|
||||
key={agent.model_name}
|
||||
type="button"
|
||||
onClick={() => setSelectedId(agent.model_name)}
|
||||
className={`mb-1 w-full rounded-md border-l-2 px-3 py-2 text-left text-sm transition-colors ${
|
||||
selectedId === agent.model_name
|
||||
? "border-blue-500 bg-blue-50 text-blue-800"
|
||||
: "border-transparent hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
<div className="font-medium truncate">{agent.model_name}</div>
|
||||
<div className="text-[10px] text-gray-500 truncate">litellm_agent</div>
|
||||
</button>
|
||||
))}
|
||||
{agentModels.map((agent) => {
|
||||
const id = getAgentModelId(agent);
|
||||
return (
|
||||
<button
|
||||
key={id ?? agent.model_name}
|
||||
type="button"
|
||||
onClick={() => id && setSelectedId(id)}
|
||||
className={`mb-1 w-full rounded-md border-l-2 px-3 py-2 text-left text-sm transition-colors ${
|
||||
selectedId === id
|
||||
? "border-blue-500 bg-blue-50 text-blue-800"
|
||||
: "border-transparent hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
<div className="font-medium truncate">{agent.model_name}</div>
|
||||
<div className="text-[10px] text-gray-500 truncate">litellm_agent</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAddAgent}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue