feat: replace JSON textarea with model dropdown editor for team_member_model_max_budget

This commit is contained in:
Ishaan Jaffer 2026-04-24 16:26:30 -07:00
parent e5c33ca695
commit f844ff664e
No known key found for this signature in database

View file

@ -5,6 +5,7 @@ import TeamSSOSettings from "@/components/TeamSSOSettings";
import { isProxyAdminRole } from "@/utils/roles";
import {
InfoCircleOutlined,
MinusCircleOutlined,
PlusOutlined,
TeamOutlined,
ReloadOutlined,
@ -21,6 +22,7 @@ import {
Flex,
Form,
Input,
InputNumber,
Layout,
Modal,
Pagination,
@ -172,6 +174,127 @@ const getOrganizationAlias = (
return organization?.organization_alias || organizationId;
};
const BUDGET_DURATION_OPTIONS = [
{ label: "No reset", value: "" },
{ label: "Daily", value: "24h" },
{ label: "Weekly", value: "7d" },
{ label: "Monthly", value: "30d" },
{ label: "Yearly", value: "365d" },
];
interface ModelBudgetRow {
id: string;
model: string;
max_budget: number | null;
budget_duration: string;
}
type ModelBudgetValue = Record<string, { max_budget: number; budget_duration?: string }>;
interface ModelBudgetEditorProps {
value?: ModelBudgetValue;
onChange?: (val: ModelBudgetValue | undefined) => void;
availableModels: string[];
}
const ModelBudgetEditor: React.FC<ModelBudgetEditorProps> = ({ value, onChange, availableModels }) => {
const [rows, setRows] = useState<ModelBudgetRow[]>(() => {
if (!value || Object.keys(value).length === 0) return [];
return Object.entries(value).map(([model, cfg]) => ({
id: Math.random().toString(36).slice(2),
model,
max_budget: cfg.max_budget ?? null,
budget_duration: cfg.budget_duration ?? "",
}));
});
const notifyChange = (newRows: ModelBudgetRow[]) => {
if (!onChange) return;
const filled = newRows.filter((r) => r.model);
if (filled.length === 0) {
onChange(undefined);
return;
}
const obj: ModelBudgetValue = {};
for (const r of filled) {
obj[r.model] = {
max_budget: r.max_budget ?? 0,
...(r.budget_duration ? { budget_duration: r.budget_duration } : {}),
};
}
onChange(obj);
};
const addRow = () => {
const newRows = [
...rows,
{ id: Math.random().toString(36).slice(2), model: "", max_budget: null, budget_duration: "" },
];
setRows(newRows);
};
const updateRow = (id: string, field: keyof ModelBudgetRow, val: any) => {
const newRows = rows.map((r) => (r.id === id ? { ...r, [field]: val } : r));
setRows(newRows);
notifyChange(newRows);
};
const removeRow = (id: string) => {
const newRows = rows.filter((r) => r.id !== id);
setRows(newRows);
notifyChange(newRows);
};
return (
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
{rows.map((row) => (
<div key={row.id} style={{ display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" }}>
<Select
style={{ width: 200 }}
placeholder="Select model"
value={row.model || undefined}
onChange={(val) => updateRow(row.id, "model", val)}
showSearch
options={availableModels.map((m) => ({ label: m, value: m }))}
filterOption={(input, opt) =>
(opt?.label as string ?? "").toLowerCase().includes(input.toLowerCase())
}
/>
<InputNumber
style={{ width: 140 }}
placeholder="Max budget"
min={0}
precision={4}
prefix="$"
value={row.max_budget}
onChange={(val) => updateRow(row.id, "max_budget", val)}
/>
<Select
style={{ width: 130 }}
value={row.budget_duration}
onChange={(val) => updateRow(row.id, "budget_duration", val)}
options={BUDGET_DURATION_OPTIONS}
/>
<Button
type="text"
danger
icon={<MinusCircleOutlined />}
onClick={() => removeRow(row.id)}
/>
</div>
))}
<Button
type="dashed"
onClick={addRow}
icon={<PlusOutlined />}
style={{ width: "fit-content" }}
>
Add model limit
</Button>
</div>
);
};
// @deprecated
const Teams: React.FC<TeamProps> = ({
teams,
@ -563,21 +686,6 @@ const Teams: React.FC<TeamProps> = ({
delete formValues.allowed_agents_and_groups;
}
// Parse team_member_model_max_budget from JSON string
if (formValues.team_member_model_max_budget) {
if (typeof formValues.team_member_model_max_budget === "string") {
if (formValues.team_member_model_max_budget.trim() === "") {
delete formValues.team_member_model_max_budget;
} else {
try {
formValues.team_member_model_max_budget = JSON.parse(formValues.team_member_model_max_budget);
} catch (e) {
throw new Error("Failed to parse team member model budget: " + e);
}
}
}
}
// Add model_aliases if any are defined
if (Object.keys(modelAliases).length > 0) {
formValues.model_aliases = modelAliases;
@ -1242,9 +1350,9 @@ const Teams: React.FC<TeamProps> = ({
<Form.Item
label="Default Member Model Budget"
name="team_member_model_max_budget"
tooltip='Optional. Set independent spend limits per model for each team member. Each member gets their own independent budget per model. Format: {"model-name": {"max_budget": 10.0}}'
tooltip="Optional. Set per-model spend limits for every team member. Each member gets their own independent budget per model."
>
<TextInput placeholder='{"gpt-3.5-turbo": {"max_budget": 10.0}}' />
<ModelBudgetEditor availableModels={userModels} />
</Form.Item>
<Accordion