mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-29 01:42:19 +00:00
Merge 1a13c23f25 into e26a6450c8
This commit is contained in:
commit
41d914e647
6 changed files with 186 additions and 15 deletions
|
|
@ -0,0 +1,37 @@
|
|||
import { fireEvent, renderWithProviders, screen } from "../../../tests/test-utils";
|
||||
import { vi } from "vitest";
|
||||
import AdaptiveRoutingConfig from "./AdaptiveRoutingConfig";
|
||||
import { ComplexityRouterConfigValue } from "./ComplexityRouterConfig";
|
||||
|
||||
const adaptiveValue: ComplexityRouterConfigValue = {
|
||||
adaptive: true,
|
||||
adaptive_eligible: "all",
|
||||
tier_distance_penalty: 0.2,
|
||||
};
|
||||
|
||||
describe("AdaptiveRoutingConfig", () => {
|
||||
it("keeps the tier distance penalty empty while it is being edited, then commits the new penalty", () => {
|
||||
const onChange = vi.fn();
|
||||
renderWithProviders(<AdaptiveRoutingConfig value={adaptiveValue} onChange={onChange} />);
|
||||
|
||||
const input = screen.getByRole("spinbutton");
|
||||
fireEvent.change(input, { target: { value: "" } });
|
||||
|
||||
expect(input).toHaveValue(null);
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
|
||||
fireEvent.change(input, { target: { value: "0.5" } });
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith({ ...adaptiveValue, tier_distance_penalty: 0.5 });
|
||||
});
|
||||
|
||||
it("restores the committed tier distance penalty after an empty field loses focus", () => {
|
||||
renderWithProviders(<AdaptiveRoutingConfig value={adaptiveValue} onChange={vi.fn()} />);
|
||||
|
||||
const input = screen.getByRole("spinbutton");
|
||||
fireEvent.change(input, { target: { value: "" } });
|
||||
fireEvent.blur(input);
|
||||
|
||||
expect(input).toHaveValue(0.2);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { NumberInput } from "@/components/shared/NumberInput";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
|
|
@ -42,8 +42,8 @@ const AdaptiveRoutingConfig: React.FC<AdaptiveRoutingConfigProps> = ({ value, on
|
|||
onChange({ ...value, adaptive_eligible: eligible });
|
||||
};
|
||||
|
||||
const handleTierDistancePenaltyChange = (penalty: number | null) => {
|
||||
onChange({ ...value, tier_distance_penalty: penalty ?? DEFAULT_TIER_DISTANCE_PENALTY });
|
||||
const handleTierDistancePenaltyChange = (penalty: number) => {
|
||||
onChange({ ...value, tier_distance_penalty: penalty });
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -120,12 +120,9 @@ const AdaptiveRoutingConfig: React.FC<AdaptiveRoutingConfigProps> = ({ value, on
|
|||
{adaptiveEligible === "all" && (
|
||||
<div>
|
||||
<strong className="mb-1 block font-semibold">Tier Distance Penalty</strong>
|
||||
<Input
|
||||
type="number"
|
||||
<NumberInput
|
||||
value={tierDistancePenalty}
|
||||
onChange={(event) =>
|
||||
handleTierDistancePenaltyChange(event.target.value === "" ? null : event.target.valueAsNumber)
|
||||
}
|
||||
onValueChange={handleTierDistancePenaltyChange}
|
||||
min={0}
|
||||
step={0.1}
|
||||
className="w-full"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { renderWithProviders, screen } from "../../../tests/test-utils";
|
||||
import { fireEvent, renderWithProviders, screen } from "../../../tests/test-utils";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { vi } from "vitest";
|
||||
import SemanticKeywordMatching from "./SemanticKeywordMatching";
|
||||
|
|
@ -44,6 +44,23 @@ describe("SemanticKeywordMatching", () => {
|
|||
expect(screen.getByText("An embedding model is required")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("keeps the minimum match score empty while it is being edited, then commits the new score", () => {
|
||||
const onMatchThresholdChange = vi.fn();
|
||||
renderWithProviders(
|
||||
<SemanticKeywordMatching {...baseProps} onMatchThresholdChange={onMatchThresholdChange} matchThreshold={0.5} />,
|
||||
);
|
||||
|
||||
const input = screen.getByRole("spinbutton");
|
||||
fireEvent.change(input, { target: { value: "" } });
|
||||
|
||||
expect(input).toHaveValue(null);
|
||||
expect(onMatchThresholdChange).not.toHaveBeenCalled();
|
||||
|
||||
fireEvent.change(input, { target: { value: "0.8" } });
|
||||
|
||||
expect(onMatchThresholdChange).toHaveBeenCalledWith(0.8);
|
||||
});
|
||||
|
||||
it("hides the validation error once an embedding model is set", () => {
|
||||
renderWithProviders(
|
||||
<SemanticKeywordMatching {...baseProps} showValidationErrors={true} embeddingModel="voyage-3-5" />,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Info } from "lucide-react";
|
||||
import { SimpleTooltip } from "@/components/ui/tooltip";
|
||||
import { SearchSelect } from "@/components/shared/SearchSelect";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { NumberInput } from "@/components/shared/NumberInput";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import React from "react";
|
||||
import { ModelGroup } from "@/components/llm_calls/fetch_models";
|
||||
|
|
@ -74,12 +74,9 @@ const SemanticKeywordMatching: React.FC<SemanticKeywordMatchingProps> = ({
|
|||
</div>
|
||||
<div>
|
||||
<span className="mb-1 block text-sm font-medium">Minimum match score</span>
|
||||
<Input
|
||||
type="number"
|
||||
<NumberInput
|
||||
value={matchThreshold}
|
||||
onChange={(event) =>
|
||||
onMatchThresholdChange(event.target.value === "" ? DEFAULT_MATCH_THRESHOLD : event.target.valueAsNumber)
|
||||
}
|
||||
onValueChange={onMatchThresholdChange}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.05}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,87 @@
|
|||
import { useState } from "react";
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { vi } from "vitest";
|
||||
import { NumberInput } from "./NumberInput";
|
||||
|
||||
const STORED_VALUE = 3;
|
||||
|
||||
const StatefulHarness = () => {
|
||||
const [value, setValue] = useState(STORED_VALUE);
|
||||
return (
|
||||
<>
|
||||
<NumberInput value={value} onValueChange={setValue} min={0} />
|
||||
<output>{value}</output>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
describe("NumberInput", () => {
|
||||
it("should report the typed number", () => {
|
||||
const onValueChange = vi.fn();
|
||||
render(<NumberInput value={3} onValueChange={onValueChange} />);
|
||||
|
||||
fireEvent.change(screen.getByRole("spinbutton"), { target: { value: "7" } });
|
||||
|
||||
expect(onValueChange).toHaveBeenCalledWith(7);
|
||||
});
|
||||
|
||||
it("should commit nothing while the field holds no number", () => {
|
||||
const onValueChange = vi.fn();
|
||||
render(<NumberInput value={3} onValueChange={onValueChange} />);
|
||||
|
||||
fireEvent.change(screen.getByRole("spinbutton"), { target: { value: "" } });
|
||||
|
||||
expect(onValueChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should stay empty once cleared, keeping the stored value untouched", () => {
|
||||
render(<StatefulHarness />);
|
||||
const input = screen.getByRole("spinbutton");
|
||||
|
||||
fireEvent.change(input, { target: { value: "" } });
|
||||
|
||||
expect(input).toHaveValue(null);
|
||||
expect(screen.getByRole("status")).toHaveTextContent(String(STORED_VALUE));
|
||||
});
|
||||
|
||||
it("should accept a fresh number typed into the cleared field", () => {
|
||||
render(<StatefulHarness />);
|
||||
const input = screen.getByRole("spinbutton");
|
||||
|
||||
fireEvent.change(input, { target: { value: "" } });
|
||||
fireEvent.change(input, { target: { value: "5" } });
|
||||
|
||||
expect(input).toHaveValue(5);
|
||||
expect(screen.getByRole("status")).toHaveTextContent("5");
|
||||
});
|
||||
|
||||
it("should show the stored value again once the cleared field is blurred", () => {
|
||||
render(<StatefulHarness />);
|
||||
const input = screen.getByRole("spinbutton");
|
||||
|
||||
fireEvent.change(input, { target: { value: "" } });
|
||||
fireEvent.blur(input);
|
||||
|
||||
expect(input).toHaveValue(STORED_VALUE);
|
||||
});
|
||||
|
||||
it("should follow the parent value after the draft is committed", () => {
|
||||
const { rerender } = render(<NumberInput value={3} onValueChange={vi.fn()} />);
|
||||
const input = screen.getByRole("spinbutton");
|
||||
|
||||
fireEvent.change(input, { target: { value: "" } });
|
||||
fireEvent.blur(input);
|
||||
rerender(<NumberInput value={9} onValueChange={vi.fn()} />);
|
||||
|
||||
expect(input).toHaveValue(9);
|
||||
});
|
||||
|
||||
it("should call a caller-supplied blur handler", () => {
|
||||
const onBlur = vi.fn();
|
||||
render(<NumberInput value={3} onValueChange={vi.fn()} onBlur={onBlur} />);
|
||||
|
||||
fireEvent.blur(screen.getByRole("spinbutton"));
|
||||
|
||||
expect(onBlur).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
36
ui/litellm-dashboard/src/components/shared/NumberInput.tsx
Normal file
36
ui/litellm-dashboard/src/components/shared/NumberInput.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import * as React from "react";
|
||||
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
type NumberInputProps = Omit<React.ComponentProps<"input">, "type" | "value" | "onChange"> & {
|
||||
value: number;
|
||||
onValueChange: (value: number) => void;
|
||||
};
|
||||
|
||||
const NumberInput = React.forwardRef<HTMLInputElement, NumberInputProps>(
|
||||
({ value, onValueChange, onBlur, ...props }, ref) => {
|
||||
const [draft, setDraft] = React.useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<Input
|
||||
{...props}
|
||||
ref={ref}
|
||||
type="number"
|
||||
value={draft ?? String(value)}
|
||||
onChange={(event) => {
|
||||
setDraft(event.target.value);
|
||||
if (!Number.isNaN(event.target.valueAsNumber)) {
|
||||
onValueChange(event.target.valueAsNumber);
|
||||
}
|
||||
}}
|
||||
onBlur={(event) => {
|
||||
setDraft(null);
|
||||
onBlur?.(event);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
NumberInput.displayName = "NumberInput";
|
||||
|
||||
export { NumberInput };
|
||||
Loading…
Add table
Reference in a new issue