diff --git a/ui/litellm-dashboard/src/components/common_components/MetadataKeyValueFields.test.tsx b/ui/litellm-dashboard/src/components/common_components/MetadataKeyValueFields.test.tsx index 3d9f4d8de15..0ccb2346450 100644 --- a/ui/litellm-dashboard/src/components/common_components/MetadataKeyValueFields.test.tsx +++ b/ui/litellm-dashboard/src/components/common_components/MetadataKeyValueFields.test.tsx @@ -307,6 +307,28 @@ describe("MetadataKeyValueFields with a declared schema", () => { expect(screen.getByTestId("metadata-schema-label")).toBeInTheDocument(); }); + it("should keep duplicate keys in a free-form row", async () => { + const user = userEvent.setup(); + const onFinish = vi.fn(); + render(); + + expect(await screen.findByTestId("metadata-schema-label")).toHaveTextContent("Cost Center"); + await user.click(screen.getByRole("button", { name: /add key-value pair/i })); + fireEvent.change(screen.getByPlaceholderText("Key"), { target: { value: "cost_center" } }); + + expect(screen.getAllByPlaceholderText("Key")).toHaveLength(1); + expect(screen.getByPlaceholderText("Key")).toHaveValue("cost_center"); + expect(screen.getAllByLabelText("Remove key-value pair")).toHaveLength(1); + expect(screen.getAllByTestId("metadata-schema-label")).toHaveLength(1); + + await user.click(screen.getByRole("button", { name: "Save" })); + + await waitFor(() => { + expect(screen.getByText("Duplicate key")).toBeInTheDocument(); + }); + expect(onFinish).not.toHaveBeenCalled(); + }); + it("should show a skeleton instead of the editor while the schema is loading", () => { render(); diff --git a/ui/litellm-dashboard/src/components/common_components/MetadataKeyValueFields.tsx b/ui/litellm-dashboard/src/components/common_components/MetadataKeyValueFields.tsx index 1d4cdc81adf..07f50cf9f61 100644 --- a/ui/litellm-dashboard/src/components/common_components/MetadataKeyValueFields.tsx +++ b/ui/litellm-dashboard/src/components/common_components/MetadataKeyValueFields.tsx @@ -2,6 +2,7 @@ import { CircleMinus, Plus } from "lucide-react"; import React, { useEffect, useRef } from "react"; import { useFieldArray, + useWatch, type Control, type FieldArrayPath, type FieldPath, @@ -87,6 +88,10 @@ const MetadataKeyValueFields = ({ schemaLoading = false, }: MetadataKeyValueFieldsProps) => { const { fields, append, remove } = useFieldArray({ control, name }); + const watchedPairs = (useWatch({ control, name: name as unknown as FieldPath }) ?? []) as readonly ( + | Partial + | undefined + )[]; const seededRef = useRef(false); const schemaLabelsByKey = new Map(schemaFields.map((field) => [field.key, field.label || field.key])); @@ -114,10 +119,22 @@ const MetadataKeyValueFields = ({ ); } + const getSchemaLabel = (index: number): string | undefined => { + const key = watchedPairs[index]?.key; + if ( + key === undefined || + !schemaLabelsByKey.has(key) || + watchedPairs.findIndex((pair) => pair?.key === key) !== index + ) { + return undefined; + } + return schemaLabelsByKey.get(key); + }; + return ( <> {fields.map((field, index) => { - const schemaLabel = schemaLabelsByKey.get((field as unknown as Partial).key ?? ""); + const schemaLabel = getSchemaLabel(index); return (
{schemaLabel === undefined ? (