mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
feat(proxy): let team admins edit tpm_limit when a proxy admin enables it
tpm_limit is the first field in the team-admin allow-list registry. The team settings tab gives a team admin a form with only the enabled fields and sends only those on save, and UI Settings labels the checkbox the same way
This commit is contained in:
parent
d233043b05
commit
a44a58e91a
13 changed files with 291 additions and 28 deletions
|
|
@ -19,8 +19,8 @@ from litellm.proxy._types import (
|
|||
|
||||
TEAM_ADMIN_EDITABLE_TEAM_FIELDS_SETTING: Final = "team_admin_editable_team_fields"
|
||||
|
||||
# TODO(LIT-5722): stays empty until each field's value-diff and dashboard wiring lands, one field per PR
|
||||
SUPPORTED_TEAM_ADMIN_EDITABLE_TEAM_FIELDS: Final[frozenset[str]] = frozenset()
|
||||
# TODO(LIT-5722): add the remaining team settings one per PR, each with its value-diff tests and dashboard field
|
||||
SUPPORTED_TEAM_ADMIN_EDITABLE_TEAM_FIELDS: Final[frozenset[str]] = frozenset({"tpm_limit"})
|
||||
|
||||
_FIELD_LIST: Final = TypeAdapter(list[str])
|
||||
_JSON_OBJECT: Final = TypeAdapter(dict[str, object])
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ pytestmark = pytest.mark.asyncio(loop_scope="session")
|
|||
# update_team and denials are the handler's 403, never the route gate's 401.
|
||||
# Only PROXY_ADMIN and an ORG_ADMIN of the team's org pass: a team admin is
|
||||
# admitted by _resolve_team_access but then refused because no team field is
|
||||
# enabled for team admins (team_admin_editable_team_fields ships empty).
|
||||
# enabled for team admins (team_admin_editable_team_fields defaults to empty).
|
||||
MARKER_ALIAS = "behavior-pin-update-marker-alias"
|
||||
|
||||
_MATRIX = [
|
||||
|
|
|
|||
|
|
@ -80,8 +80,8 @@ client = TestClient(app)
|
|||
def _team_admin_may_edit(*fields: str):
|
||||
"""Let team admins change ``fields`` on /team/update for the duration of the block.
|
||||
|
||||
The registry ships empty (LIT-5722 adds fields one PR at a time), so tests that exercise the
|
||||
gates layered underneath the allow-list widen it here instead of asserting the early 403."""
|
||||
The registry only lists the fields shipped so far (LIT-5722 adds them one PR at a time), so tests that
|
||||
exercise the gates layered underneath the allow-list widen it here instead of asserting the early 403."""
|
||||
with (
|
||||
patch( # test-quality-ok: the registry is a module constant update_team reads directly; no seam to inject
|
||||
"litellm.proxy.management_endpoints.team_endpoints.SUPPORTED_TEAM_ADMIN_EDITABLE_TEAM_FIELDS",
|
||||
|
|
@ -15048,6 +15048,36 @@ async def test_update_team_team_admin_echoing_unpermitted_fields_unchanged_is_al
|
|||
assert prisma.db.litellm_teamtable.update.called
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_team_team_admin_changes_tpm_limit_once_a_proxy_admin_enables_it(
|
||||
disable_audit_logging_for_mocked_team,
|
||||
):
|
||||
"""tpm_limit is the first field a proxy admin can open to team admins; every other field stays admin-only."""
|
||||
import contextlib
|
||||
|
||||
with contextlib.ExitStack() as stack:
|
||||
prisma = _wire_update_team(stack, {})
|
||||
stack.enter_context(
|
||||
patch("litellm.proxy.proxy_server.general_settings", {"team_admin_editable_team_fields": ["tpm_limit"]}) # test-quality-ok: update_team reads general_settings as a proxy_server module global
|
||||
)
|
||||
await update_team(
|
||||
data=UpdateTeamRequest(team_id="test_team_id", tpm_limit=5000),
|
||||
http_request=_update_request_stub(),
|
||||
user_api_key_dict=_TEAM_ADMIN_CALLER,
|
||||
)
|
||||
with pytest.raises(ProxyException) as refused:
|
||||
await update_team(
|
||||
data=UpdateTeamRequest(team_id="test_team_id", tpm_limit=6000, rpm_limit=10),
|
||||
http_request=_update_request_stub(),
|
||||
user_api_key_dict=_TEAM_ADMIN_CALLER,
|
||||
)
|
||||
|
||||
assert prisma.db.litellm_teamtable.update.await_count == 1
|
||||
assert prisma.db.litellm_teamtable.update.call_args.kwargs["data"]["tpm_limit"] == 5000
|
||||
assert str(refused.value.code) == "403"
|
||||
assert "'rpm_limit'" in str(refused.value.message)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_team_org_admin_is_not_filtered_by_the_team_admin_field_list(
|
||||
disable_audit_logging_for_mocked_team,
|
||||
|
|
|
|||
|
|
@ -3321,11 +3321,7 @@ class TestTeamAdminEditableTeamFieldsSetting:
|
|||
|
||||
def test_patch_persists_and_syncs_the_list_to_general_settings(self, monkeypatch):
|
||||
mock_prisma = self._as_proxy_admin(monkeypatch)
|
||||
monkeypatch.setattr(
|
||||
"litellm.proxy.ui_crud_endpoints.proxy_setting_endpoints.SUPPORTED_TEAM_ADMIN_EDITABLE_TEAM_FIELDS",
|
||||
frozenset({"tpm_limit", "rpm_limit"}),
|
||||
)
|
||||
general_settings: dict = {"team_admin_editable_team_fields": ["rpm_limit"]}
|
||||
general_settings: dict = {"team_admin_editable_team_fields": []}
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.general_settings", general_settings)
|
||||
|
||||
try:
|
||||
|
|
@ -3373,7 +3369,7 @@ class TestTeamAdminEditableTeamFieldsSetting:
|
|||
field_schema = data["field_schema"]["properties"]["team_admin_editable_team_fields"]
|
||||
assert field_schema["type"] == "array"
|
||||
assert field_schema["items"]["type"] == "string"
|
||||
assert isinstance(field_schema["items"]["enum"], list)
|
||||
assert "tpm_limit" in field_schema["items"]["enum"]
|
||||
|
||||
|
||||
class TestSyncUiSettingsToGeneralSettings:
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ describe("TeamAdminEditableFieldsSettings", () => {
|
|||
expect(screen.queryByRole("checkbox")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders one checkbox per supported field, checked for the enabled ones", () => {
|
||||
it("renders one checkbox per supported field, checked for the enabled ones and named by the field's form label when the dashboard has one", () => {
|
||||
renderWithProviders(
|
||||
<TeamAdminEditableFieldsSettings
|
||||
editableFields={["tpm_limit"]}
|
||||
|
|
@ -35,7 +35,7 @@ describe("TeamAdminEditableFieldsSettings", () => {
|
|||
expect(screen.getByText("1 field enabled")).toBeInTheDocument();
|
||||
expect(screen.getByText("Fields a team admin may change")).toBeInTheDocument();
|
||||
expect(screen.getByRole("checkbox", { name: "max_budget" })).not.toBeChecked();
|
||||
expect(screen.getByRole("checkbox", { name: "tpm_limit" })).toBeChecked();
|
||||
expect(screen.getByRole("checkbox", { name: "Tokens per minute Limit (TPM)" })).toBeChecked();
|
||||
});
|
||||
|
||||
it("saves the list with the field added when an unchecked field is ticked", async () => {
|
||||
|
|
@ -67,7 +67,7 @@ describe("TeamAdminEditableFieldsSettings", () => {
|
|||
/>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("checkbox", { name: "tpm_limit" }));
|
||||
await user.click(screen.getByRole("checkbox", { name: "Tokens per minute Limit (TPM)" }));
|
||||
|
||||
expect(onUpdate).toHaveBeenCalledWith({ team_admin_editable_team_fields: ["max_budget"] });
|
||||
});
|
||||
|
|
@ -84,7 +84,7 @@ describe("TeamAdminEditableFieldsSettings", () => {
|
|||
/>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("checkbox", { name: "tpm_limit" }));
|
||||
await user.click(screen.getByRole("checkbox", { name: "Tokens per minute Limit (TPM)" }));
|
||||
|
||||
expect(onUpdate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { teamAdminFieldLabel } from "@/components/team/teamAdminEditAccess";
|
||||
|
||||
interface TeamAdminEditableFieldsSettingsProps {
|
||||
editableFields: readonly string[];
|
||||
|
|
@ -53,7 +54,7 @@ export default function TeamAdminEditableFieldsSettings({
|
|||
disabled={isUpdating}
|
||||
onCheckedChange={(checked) => toggleField(field, checked === true)}
|
||||
/>
|
||||
<span className="text-sm text-foreground">{field}</span>
|
||||
<span className="text-sm text-foreground">{teamAdminFieldLabel(field)}</span>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -187,7 +187,7 @@ describe("UISettings", () => {
|
|||
expect(screen.getByText("Team settings fields a team admin may change")).toBeInTheDocument();
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByRole("checkbox", { name: "tpm_limit" }));
|
||||
fireEvent.click(screen.getByRole("checkbox", { name: "Tokens per minute Limit (TPM)" }));
|
||||
});
|
||||
|
||||
expect(mutateMock).toHaveBeenCalledWith(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import { fireEvent, renderWithProviders, screen, waitFor } from "@/../tests/test-utils";
|
||||
|
||||
import TeamAdminSettingsForm from "./TeamAdminSettingsForm";
|
||||
|
||||
const renderForm = (editableFields: ReadonlySet<string>, overrides: { isSaving?: boolean } = {}) => {
|
||||
const onSave = vi.fn().mockResolvedValue(undefined);
|
||||
const onCancel = vi.fn();
|
||||
renderWithProviders(
|
||||
<TeamAdminSettingsForm
|
||||
initialValues={{ tpm_limit: 1000 }}
|
||||
editableFields={editableFields}
|
||||
isSaving={overrides.isSaving ?? false}
|
||||
onCancel={onCancel}
|
||||
onSave={onSave}
|
||||
/>,
|
||||
);
|
||||
return { onSave, onCancel };
|
||||
};
|
||||
|
||||
describe("TeamAdminSettingsForm", () => {
|
||||
it("shows the team's current TPM limit when the proxy lets team admins edit it", () => {
|
||||
renderForm(new Set(["tpm_limit"]));
|
||||
|
||||
expect(screen.getByLabelText("Tokens per minute Limit (TPM)")).toHaveValue(1000);
|
||||
});
|
||||
|
||||
it("hides the TPM limit when the proxy has not enabled it for team admins", () => {
|
||||
renderForm(new Set(["max_budget"]));
|
||||
|
||||
expect(screen.queryByLabelText("Tokens per minute Limit (TPM)")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("saves the new TPM limit and nothing else", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { onSave } = renderForm(new Set(["tpm_limit"]));
|
||||
|
||||
fireEvent.change(screen.getByLabelText("Tokens per minute Limit (TPM)"), { target: { value: "5000" } });
|
||||
await user.click(screen.getByRole("button", { name: /save changes/i }));
|
||||
|
||||
await waitFor(() => expect(onSave).toHaveBeenCalledWith({ tpm_limit: 5000 }));
|
||||
});
|
||||
|
||||
it("saves a cleared TPM limit as no limit", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { onSave } = renderForm(new Set(["tpm_limit"]));
|
||||
|
||||
fireEvent.change(screen.getByLabelText("Tokens per minute Limit (TPM)"), { target: { value: "" } });
|
||||
await user.click(screen.getByRole("button", { name: /save changes/i }));
|
||||
|
||||
await waitFor(() => expect(onSave).toHaveBeenCalledWith({ tpm_limit: null }));
|
||||
});
|
||||
|
||||
it("closes without saving on cancel", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { onSave, onCancel } = renderForm(new Set(["tpm_limit"]));
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Cancel" }));
|
||||
|
||||
expect(onCancel).toHaveBeenCalledTimes(1);
|
||||
expect(onSave).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("locks both buttons while a save is in flight", () => {
|
||||
renderForm(new Set(["tpm_limit"]), { isSaving: true });
|
||||
|
||||
expect(screen.getByRole("button", { name: "Cancel" })).toBeDisabled();
|
||||
expect(screen.getByRole("button", { name: /save changes/i })).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
"use client";
|
||||
|
||||
import { Save } from "lucide-react";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { FormField } from "@/components/shared/form/FormField";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { FieldGroup } from "@/components/ui/field";
|
||||
import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner";
|
||||
import { useZodForm } from "@/lib/forms/useZodForm";
|
||||
|
||||
import NumericalInput from "../shared/numerical_input";
|
||||
import {
|
||||
teamAdminFieldLabel,
|
||||
teamAdminSettingsChanges,
|
||||
type TeamAdminSettingsChanges,
|
||||
type TeamAdminSettingsValues,
|
||||
} from "./teamAdminEditAccess";
|
||||
|
||||
const teamAdminSettingsSchema = z.object({
|
||||
tpm_limit: z.union([z.string(), z.number()]).nullish(),
|
||||
});
|
||||
|
||||
interface TeamAdminSettingsFormProps {
|
||||
initialValues: TeamAdminSettingsValues;
|
||||
editableFields: ReadonlySet<string>;
|
||||
isSaving: boolean;
|
||||
onCancel: () => void;
|
||||
onSave: (changes: TeamAdminSettingsChanges) => Promise<void>;
|
||||
}
|
||||
|
||||
export default function TeamAdminSettingsForm({
|
||||
initialValues,
|
||||
editableFields,
|
||||
isSaving,
|
||||
onCancel,
|
||||
onSave,
|
||||
}: TeamAdminSettingsFormProps) {
|
||||
const form = useZodForm(teamAdminSettingsSchema, { defaultValues: initialValues });
|
||||
const submit = form.handleSubmit((values) => onSave(teamAdminSettingsChanges(values, editableFields)));
|
||||
|
||||
return (
|
||||
<form onSubmit={(event) => void submit(event)}>
|
||||
<FieldGroup>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
A proxy admin chose which settings team admins can change. Ask a proxy admin to change anything else.
|
||||
</p>
|
||||
{editableFields.has("tpm_limit") && (
|
||||
<FormField control={form.control} name="tpm_limit" label={teamAdminFieldLabel("tpm_limit")}>
|
||||
{({ ref, value, ...field }) => <NumericalInput {...field} ref={ref} value={value ?? ""} step={1} />}
|
||||
</FormField>
|
||||
)}
|
||||
</FieldGroup>
|
||||
|
||||
<div className="mt-6 flex items-center justify-end gap-2">
|
||||
<Button type="button" variant="outline" onClick={onCancel} disabled={isSaving}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={isSaving}>
|
||||
{isSaving ? <UiLoadingSpinner className="size-4" /> : <Save className="size-4" />}
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
|
@ -1889,18 +1889,32 @@ describe("TeamInfoView", () => {
|
|||
expect(screen.getByRole("button", { name: /edit settings/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("opens the form for a team admin once the proxy reports an enabled field", async () => {
|
||||
it("gives a team admin only the fields the proxy enabled and sends only those on save", async () => {
|
||||
const user = userEvent.setup({ delay: null });
|
||||
vi.mocked(networking.teamInfoCall).mockResolvedValue(
|
||||
createMockTeamData({ caller_edit_access: { kind: "team_admin", editable_fields: ["tpm_limit"] } }),
|
||||
createMockTeamData({
|
||||
tpm_limit: 1000,
|
||||
caller_edit_access: { kind: "team_admin", editable_fields: ["tpm_limit"] },
|
||||
}),
|
||||
);
|
||||
vi.mocked(networking.teamUpdateCall).mockResolvedValue({ data: {}, team_id: "123" } as any);
|
||||
|
||||
renderWithProviders(<TeamInfoView {...teamAdminProps} />);
|
||||
|
||||
await user.click(await screen.findByRole("tab", { name: "Settings" }));
|
||||
await user.click(await screen.findByRole("button", { name: /edit settings/i }));
|
||||
|
||||
expect(await screen.findByLabelText("Team Name")).toBeInTheDocument();
|
||||
const tpmInput = await screen.findByLabelText("Tokens per minute Limit (TPM)");
|
||||
expect(tpmInput).toHaveValue(1000);
|
||||
expect(screen.queryByLabelText("Team Name")).not.toBeInTheDocument();
|
||||
expect(screen.queryByLabelText("Requests per minute Limit (RPM)")).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.change(tpmInput, { target: { value: "5000" } });
|
||||
await user.click(screen.getByRole("button", { name: /save changes/i }));
|
||||
|
||||
await waitFor(() => expect(networking.teamUpdateCall).toHaveBeenCalledTimes(1));
|
||||
expect(vi.mocked(networking.teamUpdateCall).mock.calls[0][1]).toStrictEqual({ team_id: "123", tpm_limit: 5000 });
|
||||
expect(toast.success).toHaveBeenCalledWith("Team settings updated successfully");
|
||||
expect(toast.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -53,7 +53,9 @@ import {
|
|||
parseTeamEditAccess,
|
||||
TEAM_ADMIN_EDITING_DISABLED_DESCRIPTION,
|
||||
TEAM_ADMIN_EDITING_DISABLED_TITLE,
|
||||
type TeamAdminSettingsChanges,
|
||||
} from "./teamAdminEditAccess";
|
||||
import TeamAdminSettingsForm from "./TeamAdminSettingsForm";
|
||||
import { copyToClipboard as utilCopyToClipboard } from "../../utils/dataUtils";
|
||||
import AccessGroupSelector from "../common_components/AccessGroupSelector";
|
||||
import BudgetDurationDropdown, { NEVER_RESETS_BUDGET_DURATION } from "../common_components/budget_duration_dropdown";
|
||||
|
|
@ -862,6 +864,27 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
setMemberToDelete(null);
|
||||
};
|
||||
|
||||
const persistTeamUpdate = async (token: string, updateData: Record<string, unknown>) => {
|
||||
await teamUpdateCall(token, updateData);
|
||||
queryClient.invalidateQueries({ queryKey: organizationKeys.all });
|
||||
|
||||
toast.success("Team settings updated successfully");
|
||||
setIsEditing(false);
|
||||
fetchTeamInfo();
|
||||
};
|
||||
|
||||
const saveTeamAdminSettings = async (changes: TeamAdminSettingsChanges) => {
|
||||
if (!accessToken) return;
|
||||
setIsTeamSaving(true);
|
||||
try {
|
||||
await persistTeamUpdate(accessToken, { team_id: teamId, ...changes });
|
||||
} catch (error) {
|
||||
console.error("Error updating team:", error);
|
||||
} finally {
|
||||
setIsTeamSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTeamUpdate = async (values: any) => {
|
||||
try {
|
||||
if (!accessToken) return;
|
||||
|
|
@ -1112,12 +1135,7 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
}
|
||||
}
|
||||
|
||||
await teamUpdateCall(accessToken, updateData);
|
||||
queryClient.invalidateQueries({ queryKey: organizationKeys.all });
|
||||
|
||||
toast.success("Team settings updated successfully");
|
||||
setIsEditing(false);
|
||||
fetchTeamInfo();
|
||||
await persistTeamUpdate(accessToken, updateData);
|
||||
} catch (error) {
|
||||
console.error("Error updating team:", error);
|
||||
} finally {
|
||||
|
|
@ -1135,6 +1153,17 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
|
||||
const { team_info: info } = teamData;
|
||||
|
||||
const teamAdminSettingsEditor =
|
||||
teamEditAccess.kind === "team_admin" ? (
|
||||
<TeamAdminSettingsForm
|
||||
initialValues={{ tpm_limit: info.tpm_limit }}
|
||||
editableFields={teamEditAccess.editableFields}
|
||||
isSaving={isTeamSaving}
|
||||
onCancel={() => setIsEditing(false)}
|
||||
onSave={saveTeamAdminSettings}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
const inheritedMcpServers = computeInheritedGrants(
|
||||
info.access_group_mcp_server_ids,
|
||||
info.access_group_details,
|
||||
|
|
@ -1347,8 +1376,8 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
)}
|
||||
</div>
|
||||
|
||||
{isEditing && isGuardrailsLoading ? (
|
||||
<div className="p-4">Loading...</div>
|
||||
{isEditing && (teamAdminSettingsEditor !== null || isGuardrailsLoading) ? (
|
||||
teamAdminSettingsEditor ?? <div className="p-4">Loading...</div>
|
||||
) : isEditing ? (
|
||||
<TooltipProvider>
|
||||
<form onSubmit={(event) => void form.handleSubmit(onTeamUpdateSubmit)(event)}>
|
||||
|
|
|
|||
|
|
@ -4,8 +4,40 @@ import {
|
|||
parseSupportedTeamAdminEditableFields,
|
||||
parseTeamAdminEditableFields,
|
||||
parseTeamEditAccess,
|
||||
teamAdminFieldLabel,
|
||||
teamAdminSettingsChanges,
|
||||
} from "./teamAdminEditAccess";
|
||||
|
||||
describe("teamAdminFieldLabel", () => {
|
||||
it("names tpm_limit the way the team settings form does", () => {
|
||||
expect(teamAdminFieldLabel("tpm_limit")).toBe("Tokens per minute Limit (TPM)");
|
||||
});
|
||||
|
||||
it("falls back to the raw field name for a field the dashboard has no label for", () => {
|
||||
expect(teamAdminFieldLabel("max_budget")).toBe("max_budget");
|
||||
});
|
||||
});
|
||||
|
||||
describe("teamAdminSettingsChanges", () => {
|
||||
const tpmEnabled = new Set(["tpm_limit"]);
|
||||
|
||||
it.each([
|
||||
["a typed number string", "5000", 5000],
|
||||
["a stored number", 1200, 1200],
|
||||
["zero", "0", 0],
|
||||
["an emptied input", "", null],
|
||||
["whitespace", " ", null],
|
||||
["no stored limit", null, null],
|
||||
["an unset value", undefined, null],
|
||||
])("sends tpm_limit for %s", (_label, tpm_limit, expected) => {
|
||||
expect(teamAdminSettingsChanges({ tpm_limit }, tpmEnabled)).toStrictEqual({ tpm_limit: expected });
|
||||
});
|
||||
|
||||
it("leaves tpm_limit out when the proxy did not enable it for team admins", () => {
|
||||
expect(teamAdminSettingsChanges({ tpm_limit: "5000" }, new Set(["max_budget"]))).toStrictEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseTeamAdminEditableFields", () => {
|
||||
it("returns the configured list", () => {
|
||||
expect(parseTeamAdminEditableFields({ team_admin_editable_team_fields: ["tpm_limit", "rpm_limit"] })).toEqual([
|
||||
|
|
|
|||
|
|
@ -39,6 +39,29 @@ export const parseSupportedTeamAdminEditableFields = (uiSettingsFieldSchema: unk
|
|||
return items.success ? fieldListSchema.parse(items.data.enum) : [];
|
||||
};
|
||||
|
||||
const TEAM_ADMIN_FIELD_LABELS: ReadonlyMap<string, string> = new Map([["tpm_limit", "Tokens per minute Limit (TPM)"]]);
|
||||
|
||||
export const teamAdminFieldLabel = (field: string): string => TEAM_ADMIN_FIELD_LABELS.get(field) ?? field;
|
||||
|
||||
export interface TeamAdminSettingsValues {
|
||||
readonly tpm_limit?: string | number | null;
|
||||
}
|
||||
|
||||
export interface TeamAdminSettingsChanges {
|
||||
readonly tpm_limit?: number | null;
|
||||
}
|
||||
|
||||
const numberOrNull = (value: string | number | null | undefined): number | null => {
|
||||
if (value === null || value === undefined || String(value).trim() === "") return null;
|
||||
const parsed = Number(value);
|
||||
return Number.isNaN(parsed) ? null : parsed;
|
||||
};
|
||||
|
||||
export const teamAdminSettingsChanges = (
|
||||
values: TeamAdminSettingsValues,
|
||||
editableFields: ReadonlySet<string>,
|
||||
): TeamAdminSettingsChanges => (editableFields.has("tpm_limit") ? { tpm_limit: numberOrNull(values.tpm_limit) } : {});
|
||||
|
||||
export const parseTeamEditAccess = (callerEditAccess: unknown): TeamEditAccess => {
|
||||
const parsed = callerEditAccessSchema.safeParse(callerEditAccess);
|
||||
if (!parsed.success) return { kind: "none" };
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue