From 16abb004a5cccc6190b0f0da38096730d0aeecdb Mon Sep 17 00:00:00 2001 From: Yassin Kortam Date: Thu, 15 Oct 2026 15:13:14 -0700 Subject: [PATCH] feat(ui): set a search tool's cost per query from the Admin UI (#46723) Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../CreateSearchTools.integration.test.tsx | 34 ++++++++++- .../_components/CreateSearchTools.tsx | 23 +++++++ .../_components/SearchToolView.test.tsx | 26 ++++++++ .../_components/SearchToolView.tsx | 11 ++++ .../SearchTools.integration.test.tsx | 61 ++++++++++++++++++- .../search-tools/_components/SearchTools.tsx | 21 +++++++ .../_components/searchToolPayload.test.ts | 17 ++++++ .../_components/searchToolPayload.ts | 2 + 8 files changed, 193 insertions(+), 2 deletions(-) diff --git a/ui/litellm-dashboard/src/app/(dashboard)/search-tools/_components/CreateSearchTools.integration.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/search-tools/_components/CreateSearchTools.integration.test.tsx index dac43885da4..6d796b2d4eb 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/search-tools/_components/CreateSearchTools.integration.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/search-tools/_components/CreateSearchTools.integration.test.tsx @@ -54,6 +54,7 @@ describe("CreateSearchTools submit payload", () => { fireEvent.change(screen.getByLabelText(/Search Tool Name/), { target: { value: "my-search" } }); await pickProvider(user, "Perplexity AI"); fireEvent.change(screen.getByLabelText(/API Key/), { target: { value: "sk-secret" } }); + fireEvent.change(screen.getByLabelText(/Cost per Query/), { target: { value: "0.05" } }); fireEvent.change(screen.getByLabelText(/Description/), { target: { value: "finds things" } }); await user.click(screen.getByRole("button", { name: "Add Search Tool" })); @@ -65,14 +66,45 @@ describe("CreateSearchTools submit payload", () => { litellm_params: { search_provider: "perplexity", api_key: "sk-secret", + input_cost_per_query: 0.05, }, search_tool_info: { description: "finds things" }, }); expect(JSON.stringify(payload)).toBe( - '{"search_tool_name":"my-search","litellm_params":{"search_provider":"perplexity","api_key":"sk-secret"},"search_tool_info":{"description":"finds things"}}', + '{"search_tool_name":"my-search","litellm_params":{"search_provider":"perplexity","api_key":"sk-secret","input_cost_per_query":0.05},"search_tool_info":{"description":"finds things"}}', ); }); + it("sends a zero cost per query as 0, not as an omitted field", async () => { + const user = userEvent.setup(); + renderModal(); + await screen.findByLabelText(/Search Tool Name/); + + fireEvent.change(screen.getByLabelText(/Search Tool Name/), { target: { value: "free-search" } }); + await pickProvider(user, "Tavily Search"); + fireEvent.change(screen.getByLabelText(/Cost per Query/), { target: { value: "0" } }); + await user.click(screen.getByRole("button", { name: "Add Search Tool" })); + + await waitFor(() => expect(networking.createSearchTool).toHaveBeenCalledTimes(1)); + expect(JSON.stringify(vi.mocked(networking.createSearchTool).mock.calls[0][1])).toBe( + '{"search_tool_name":"free-search","litellm_params":{"search_provider":"tavily","input_cost_per_query":0}}', + ); + }); + + it("rejects a negative cost per query before anything is sent", async () => { + const user = userEvent.setup(); + renderModal(); + await screen.findByLabelText(/Search Tool Name/); + + fireEvent.change(screen.getByLabelText(/Search Tool Name/), { target: { value: "neg-search" } }); + await pickProvider(user, "Tavily Search"); + fireEvent.change(screen.getByLabelText(/Cost per Query/), { target: { value: "-1" } }); + await user.click(screen.getByRole("button", { name: "Add Search Tool" })); + + expect(await screen.findByText("Cost per query cannot be negative")).toBeInTheDocument(); + expect(networking.createSearchTool).not.toHaveBeenCalled(); + }); + it("omits untouched optional fields from the wire body instead of sending empty strings", async () => { const user = userEvent.setup(); renderModal(); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/search-tools/_components/CreateSearchTools.tsx b/ui/litellm-dashboard/src/app/(dashboard)/search-tools/_components/CreateSearchTools.tsx index f777113c2d8..b42484dbe71 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/search-tools/_components/CreateSearchTools.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/search-tools/_components/CreateSearchTools.tsx @@ -71,6 +71,7 @@ const createSearchToolShape = { .pipe(z.string({ error: "Please select a search provider" }).min(1, "Please select a search provider")), api_key: z.string().optional(), description: z.string().optional(), + input_cost_per_query: z.number().nonnegative("Cost per query cannot be negative").optional(), }; const createSearchToolSchema = z.object(createSearchToolShape); @@ -267,6 +268,28 @@ const CreateSearchTool: React.FC = ({ )} + + {({ ref, value, onChange, ...field }) => ( + onChange(event.target.value === "" ? undefined : event.target.valueAsNumber)} + placeholder="e.g., 0.007" + className="rounded-lg" + /> + )} + + {({ ref, value, ...field }) => (