mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
fix(ui): let admins change a model's team from the model edit page
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
9a715df212
commit
918fb506a6
3 changed files with 75 additions and 1 deletions
|
|
@ -24,6 +24,7 @@ import CacheControlInjectionPoints, {
|
|||
CACHE_CONTROL_TOOLTIP,
|
||||
type CacheControlInjectionPoint,
|
||||
} from "./add_model/cache_control_settings";
|
||||
import type { Team } from "./key_team_helpers/key_list";
|
||||
import type { CredentialItem } from "./networking";
|
||||
import NumericalInput from "./shared/numerical_input";
|
||||
import type { Tag } from "./tag_management/types";
|
||||
|
|
@ -103,6 +104,7 @@ export interface ModelEditFormValues {
|
|||
litellm_credential_name?: string;
|
||||
litellm_extra_params?: string;
|
||||
model_info?: string;
|
||||
team_id?: string;
|
||||
}
|
||||
|
||||
type ModelEditFieldName = keyof ModelEditFormValues;
|
||||
|
|
@ -139,6 +141,7 @@ const modelEditShape = {
|
|||
litellm_credential_name: textish,
|
||||
litellm_extra_params: textish,
|
||||
model_info: textish,
|
||||
team_id: textish,
|
||||
};
|
||||
|
||||
const isJson = (value: string): boolean => {
|
||||
|
|
@ -260,6 +263,7 @@ export const toModelEditFormValues = (localModelData: any, isWildcardModel: bool
|
|||
null,
|
||||
2,
|
||||
),
|
||||
team_id: localModelData.model_info?.team_id ?? undefined,
|
||||
});
|
||||
|
||||
const displayCost = (localModelData: any, field: TouchedPricingField): string => {
|
||||
|
|
@ -285,6 +289,7 @@ interface ModelInfoEditFormProps {
|
|||
tagsList: Record<string, Tag>;
|
||||
credentialsList: CredentialItem[];
|
||||
healthCheckModelOptions: { value: string; label: string }[];
|
||||
teams: Team[] | null;
|
||||
}
|
||||
|
||||
const Display: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
||||
|
|
@ -355,6 +360,7 @@ const ModelInfoEditForm: React.FC<ModelInfoEditFormProps> = ({
|
|||
tagsList,
|
||||
credentialsList,
|
||||
healthCheckModelOptions,
|
||||
teams,
|
||||
}) => {
|
||||
// Neither RHF's blur-based touchedFields nor its resettable dirtyFields matches antd's touched-on-change.
|
||||
const touchedRef = React.useRef<ReadonlySet<string>>(new Set<string>());
|
||||
|
|
@ -800,7 +806,36 @@ const ModelInfoEditForm: React.FC<ModelInfoEditFormProps> = ({
|
|||
|
||||
<div>
|
||||
<FieldLabel>Team ID</FieldLabel>
|
||||
<Display>{modelData.model_info.team_id || "Not Set"}</Display>
|
||||
{isEditing ? (
|
||||
<FormField control={form.control} name="team_id">
|
||||
{({ id, value, onChange, onBlur }) => {
|
||||
const items = (teams ?? []).map((team) => ({
|
||||
value: team.team_id,
|
||||
label: team.team_alias ? `${team.team_alias} (${team.team_id})` : team.team_id,
|
||||
}));
|
||||
return (
|
||||
<Select
|
||||
items={items}
|
||||
value={(value as string) ?? ""}
|
||||
onValueChange={(selected: string | null) => onChange(selected ?? "")}
|
||||
>
|
||||
<SelectTrigger id={id} className="w-full" onBlur={onBlur}>
|
||||
<SelectValue placeholder="Select a team" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{items.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}}
|
||||
</FormField>
|
||||
) : (
|
||||
<Display>{modelData.model_info.team_id || "Not Set"}</Display>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,11 @@ vi.mock("@/app/(dashboard)/hooks/models/useModelCostMap", () => ({
|
|||
useModelCostMap: (...args: any[]) => mockUseModelCostMap(...args),
|
||||
}));
|
||||
|
||||
const mockUseTeams = vi.fn();
|
||||
vi.mock("@/app/(dashboard)/hooks/teams/useTeams", () => ({
|
||||
useTeams: () => mockUseTeams(),
|
||||
}));
|
||||
|
||||
const mockUsePtuCostAttributionEnabled = vi.fn();
|
||||
vi.mock("@/app/(dashboard)/hooks/uiSettings/usePtuCostAttributionEnabled", () => ({
|
||||
usePtuCostAttributionEnabled: () => mockUsePtuCostAttributionEnabled(),
|
||||
|
|
@ -102,6 +107,7 @@ describe("ModelInfoView", () => {
|
|||
});
|
||||
vi.clearAllMocks();
|
||||
mockUsePtuCostAttributionEnabled.mockReturnValue(false);
|
||||
mockUseTeams.mockReturnValue({ data: undefined, isLoading: false, error: null });
|
||||
|
||||
mockUseModelsInfo.mockReturnValue({
|
||||
data: {
|
||||
|
|
@ -1564,6 +1570,32 @@ describe("ModelInfoView", () => {
|
|||
expect(payload.model_info).toMatchObject({ team_id: "team-7" });
|
||||
});
|
||||
|
||||
it("sends the team picked in the Team ID selector", async () => {
|
||||
mockUseTeams.mockReturnValue({
|
||||
data: [
|
||||
{ team_id: "team-1", team_alias: "alpha" },
|
||||
{ team_id: "team-2", team_alias: "beta" },
|
||||
],
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
const teamModel = {
|
||||
...defaultModelData,
|
||||
model_info: { ...defaultModelData.model_info, team_id: "team-1" },
|
||||
};
|
||||
mockUseModelsInfo.mockReturnValue({ data: { data: [teamModel] }, isLoading: false, error: null });
|
||||
mockModelInfoV1Call.mockResolvedValue({ data: [teamModel] });
|
||||
const user = userEvent.setup();
|
||||
await enterEditMode(user);
|
||||
|
||||
await user.click(screen.getByText("alpha (team-1)"));
|
||||
await user.click(await screen.findByText("beta (team-2)"));
|
||||
|
||||
const payload = await save(user);
|
||||
|
||||
expect(payload.model_info.team_id).toBe("team-2");
|
||||
});
|
||||
|
||||
it("sends the edited LiteLLM extra params", async () => {
|
||||
const user = userEvent.setup();
|
||||
await enterEditMode(user);
|
||||
|
|
|
|||
|
|
@ -420,6 +420,12 @@ export default function ModelInfoView({
|
|||
health_check_model: values.health_check_model,
|
||||
};
|
||||
}
|
||||
if (values.team_id) {
|
||||
updatedModelInfo = {
|
||||
...updatedModelInfo,
|
||||
team_id: values.team_id,
|
||||
};
|
||||
}
|
||||
updatedModelInfo = applyPtuModelInfo(updatedModelInfo, values, ptuCostAttributionEnabled);
|
||||
} catch (e) {
|
||||
toast.fromError("Invalid JSON in Model Info");
|
||||
|
|
@ -779,6 +785,7 @@ export default function ModelInfoView({
|
|||
tagsList={tagsList}
|
||||
credentialsList={credentialsList}
|
||||
healthCheckModelOptions={healthCheckModelOptions}
|
||||
teams={teams ?? null}
|
||||
/>
|
||||
) : (
|
||||
<p className="text-sm">Loading...</p>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue