mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-03 02:22:24 +00:00
feat(ui): select Laya for OSS classification (#43768)
This commit is contained in:
parent
b21e44cbf9
commit
b1e0e9e84b
24 changed files with 280 additions and 150 deletions
|
|
@ -85,7 +85,8 @@ describe("autoRouterRows", () => {
|
|||
|
||||
it.each([
|
||||
["llm", "LLM Classifier"],
|
||||
["jev", "JEV Classifier"],
|
||||
["jev", "OSS Classifier"],
|
||||
["oss_classifier", "OSS Classifier"],
|
||||
])("labels a router using the %s classifier", (classifierType, label) => {
|
||||
const row = toAutoRouterRow(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -57,7 +57,8 @@ const dedupe = (models: string[]): string[] => Array.from(new Set(models));
|
|||
|
||||
const COMPLEXITY_TYPE_LABELS: Record<string, string> = {
|
||||
llm: "LLM Classifier",
|
||||
jev: "JEV Classifier",
|
||||
jev: "OSS Classifier",
|
||||
oss_classifier: "OSS Classifier",
|
||||
capability: "Capability",
|
||||
llm_v2: "Fuse v2",
|
||||
heuristic_first: "Heuristic first",
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ export const AutoRouterLimits = () => {
|
|||
<PopoverContent align="end" className="w-96 max-w-[calc(100vw-2rem)] gap-3">
|
||||
<PopoverTitle>Routing and customization limits</PopoverTitle>
|
||||
<p className="text-xs leading-5 text-muted-foreground">
|
||||
Rule-based, Complexity, and Jev are unlimited with built-in settings. Choose or change tier models freely.
|
||||
Rule-based, Complexity, and OSS are unlimited with built-in settings. Choose or change tier models freely.
|
||||
Customization allowances are shared across this proxy.
|
||||
</p>
|
||||
<dl className="space-y-2 text-xs">
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ describe("Auto-router classifier selection", () => {
|
|||
llm: "LLM",
|
||||
heuristic_first: "LLM",
|
||||
hybrid: "LLM",
|
||||
jev: "Jev",
|
||||
jev: "OSS Classifier",
|
||||
}[classifier_type];
|
||||
expect(screen.getByRole("radio", { name: new RegExp(`^${family}$`) })).toBeChecked();
|
||||
fireEvent.click(screen.getByRole("radio", { name: new RegExp(`^${family}$`) }));
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
type ClassifierType,
|
||||
type ComplexityRouterConfigValue,
|
||||
} from "./ComplexityRouterConfig";
|
||||
import { defaultJevClassifierConfig, normalizeJevClassifierConfig } from "./jev_classifier_config";
|
||||
import { transitionClassifierType } from "./classifier_type_transition";
|
||||
import { isForecastClassifier } from "./forecast_classifier_config";
|
||||
import {
|
||||
|
|
@ -148,6 +149,14 @@ const AutoRouterClassifierTabs: React.FC<AutoRouterClassifierTabsProps> = ({ val
|
|||
if (next === "llm") changeType("llm");
|
||||
if (next === "jev") changeType("jev");
|
||||
};
|
||||
const changeProvider = (provider: unknown) => {
|
||||
if (provider !== "jev" && provider !== "laya") return;
|
||||
const defaults = defaultJevClassifierConfig(provider);
|
||||
onChange({
|
||||
...value,
|
||||
jev_classifier_config: { ...defaults, ...value.jev_classifier_config, provider, model: defaults.model },
|
||||
});
|
||||
};
|
||||
const approachLabels: Partial<Record<ClassifierType, string>> = { capability: "Capability", llm_v2: "Fuse v2" };
|
||||
const approachDescription: Partial<Record<ClassifierType, string>> = {
|
||||
capability: "Use the efficient model when it is likely to succeed",
|
||||
|
|
@ -164,7 +173,7 @@ const AutoRouterClassifierTabs: React.FC<AutoRouterClassifierTabsProps> = ({ val
|
|||
{[
|
||||
{ value: "heuristics", label: "Heuristics", description: "Classify locally, with no API call" },
|
||||
{ value: "llm", label: "LLM", description: "Use a judge model to choose a solver" },
|
||||
{ value: "jev", label: "Jev", description: "Use TypeSafe System One Choice to choose a tier" },
|
||||
{ value: "jev", label: "OSS Classifier", description: "Use Jev or Laya to choose a tier" },
|
||||
].map((option) => (
|
||||
<Label
|
||||
key={option.value}
|
||||
|
|
@ -189,6 +198,25 @@ const AutoRouterClassifierTabs: React.FC<AutoRouterClassifierTabsProps> = ({ val
|
|||
))}
|
||||
</RadioGroup>
|
||||
</fieldset>
|
||||
{family === "jev" && (
|
||||
<fieldset className="space-y-2">
|
||||
<legend className="text-sm font-medium">OSS provider</legend>
|
||||
<RadioGroup
|
||||
value={normalizeJevClassifierConfig(value.jev_classifier_config).provider}
|
||||
onValueChange={changeProvider}
|
||||
className="flex gap-6"
|
||||
>
|
||||
<Label>
|
||||
<RadioGroupItem value="jev" />
|
||||
Jev
|
||||
</Label>
|
||||
<Label>
|
||||
<RadioGroupItem value="laya" />
|
||||
Laya
|
||||
</Label>
|
||||
</RadioGroup>
|
||||
</fieldset>
|
||||
)}
|
||||
{family === "custom" && (
|
||||
<p className="text-sm text-muted-foreground">This router uses a custom classifier plugin</p>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -642,8 +642,8 @@ const ClassificationMethodConfig: React.FC<ClassificationMethodConfigProps> = ({
|
|||
/>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Number of prior user turns sent to the classifier provider, excluding tool output and harness reminders.
|
||||
LLM and Jev default to 3 turns; Jev sends them to the configured TypeSafe endpoint. Set to 0 to omit
|
||||
conversation history. The current message and selected system text are still sent.
|
||||
LLM and OSS classifiers default to 3 turns. Set to 0 to omit conversation history. The current message and
|
||||
selected system text are still sent.
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -54,8 +54,8 @@ const ClassifierTypeRadios: React.FC<ClassifierTypeRadiosProps> = ({ value, clas
|
|||
<Label className="items-start font-normal leading-normal">
|
||||
<RadioGroupItem value="jev" className="mt-0.5" />
|
||||
<span>
|
||||
<strong className="font-semibold">Jev Classifier</strong>{" "}
|
||||
<span className="text-muted-foreground">uses TypeSafe System One Choice to decide the tier</span>
|
||||
<strong className="font-semibold">OSS Classifier</strong>{" "}
|
||||
<span className="text-muted-foreground">uses Jev or Laya to decide the tier</span>
|
||||
</span>
|
||||
</Label>
|
||||
<SimpleTooltip content={scorerLockedReason}>
|
||||
|
|
|
|||
|
|
@ -237,7 +237,7 @@ const TierSetToolbar: React.FC<{
|
|||
{editing && (
|
||||
<span className="block mt-1 text-xs text-muted-foreground">
|
||||
Add or remove tiers to define your own set. Every custom tier needs a definition the classifier routes on, and
|
||||
an edited set requires the LLM or Jev classification method
|
||||
an edited set requires the LLM or OSS classification method
|
||||
</span>
|
||||
)}
|
||||
{editing && keywordRulesError && (
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useState } from "react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { fireEvent, renderWithProviders, screen } from "../../../tests/test-utils";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
|
|
@ -96,49 +97,65 @@ function Form() {
|
|||
|
||||
describe("JEV classifier editor", () => {
|
||||
afterEach(() => vi.mocked(useAuthorized).mockReset());
|
||||
it("uses built-in JEV without a license and preserves custom tiers and context through reload", () => {
|
||||
renderWithProviders(<Form />);
|
||||
expect(screen.getByLabelText("Judge model")).toBeInTheDocument();
|
||||
expect(screen.getByText("Reasoning Effort")).toBeInTheDocument();
|
||||
expect(screen.getByText("Classifier Prompt")).toBeInTheDocument();
|
||||
expect(screen.getByRole("switch", { name: "Use images for classification" })).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole("radio", { name: /Jev Classifier/ }));
|
||||
expect(screen.getByRole("radio", { name: /^Jev Classifier/ })).toBeChecked();
|
||||
expect(screen.getByLabelText("Jev Model")).toHaveValue("jev-latest");
|
||||
expect(screen.getByLabelText("Jev Instructions")).toBeEnabled();
|
||||
expect(screen.queryByLabelText("Judge model")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Reasoning Effort")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Classifier Prompt")).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("switch", { name: "Use images for classification" })).not.toBeInTheDocument();
|
||||
fireEvent.change(screen.getByLabelText("Jev Model"), { target: { value: "jev-test" } });
|
||||
fireEvent.change(screen.getByLabelText("Jev Timeout (ms)"), { target: { value: "4200" } });
|
||||
fireEvent.change(screen.getByLabelText("Context Window Size"), { target: { value: "6" } });
|
||||
fireEvent.change(screen.getByLabelText("Circuit breaker cooldown (seconds)"), { target: { value: "50" } });
|
||||
fireEvent.click(screen.getByRole("switch", { name: "Classifier circuit breaker" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Customize tiers" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Save and reload" }));
|
||||
expect(screen.getByRole("radio", { name: /Jev Classifier/ })).toBeChecked();
|
||||
expect(screen.getByLabelText("Jev Model")).toHaveValue("jev-test");
|
||||
expect(screen.getByLabelText("Jev Timeout (ms)")).toHaveValue(4200);
|
||||
expect(screen.getByLabelText("Context Window Size")).toHaveValue("6");
|
||||
expect(screen.getByRole("switch", { name: "Classifier circuit breaker" })).not.toBeChecked();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Probe current config" }));
|
||||
expect(testAutoRouterRouting).toHaveBeenCalledWith(
|
||||
"token",
|
||||
expect.objectContaining({
|
||||
complexity_router_config: expect.objectContaining({
|
||||
classifier_type: "jev",
|
||||
jev_classifier_config: {
|
||||
model: "jev-test",
|
||||
timeout_ms: 4200,
|
||||
circuit_breaker_enabled: false,
|
||||
circuit_breaker_cooldown_seconds: 50,
|
||||
},
|
||||
tiers: expect.objectContaining({ QUICK: ["fast"] }),
|
||||
it.each(["jev", "laya"] as const)(
|
||||
"preserves %s, custom tiers and context through save, reload and probe",
|
||||
async (provider) => {
|
||||
renderWithProviders(<Form />);
|
||||
expect(screen.getByLabelText("Judge model")).toBeInTheDocument();
|
||||
expect(screen.getByText("Reasoning Effort")).toBeInTheDocument();
|
||||
expect(screen.getByText("Classifier Prompt")).toBeInTheDocument();
|
||||
expect(screen.getByRole("switch", { name: "Use images for classification" })).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole("radio", { name: /^OSS Classifier$/ }));
|
||||
expect(screen.getByRole("radio", { name: /^OSS Classifier$/ })).toBeChecked();
|
||||
expect(screen.getByLabelText("Classifier Model")).toHaveValue("jev-latest");
|
||||
expect(screen.getByLabelText("Classifier Instructions")).toBeEnabled();
|
||||
expect(screen.queryByLabelText("Judge model")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Reasoning Effort")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Classifier Prompt")).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("switch", { name: "Use images for classification" })).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole("radio", { name: "Laya" }));
|
||||
expect(screen.getByLabelText("Classifier Model")).toHaveTextContent("english");
|
||||
fireEvent.click(screen.getByRole("radio", { name: "Jev" }));
|
||||
expect(screen.getByLabelText("Classifier Model")).toHaveValue("jev-latest");
|
||||
if (provider === "laya") {
|
||||
fireEvent.click(screen.getByRole("radio", { name: "Laya" }));
|
||||
await userEvent.click(screen.getByLabelText("Classifier Model"));
|
||||
await userEvent.click(screen.getByRole("option", { name: "multilingual" }));
|
||||
} else {
|
||||
fireEvent.change(screen.getByLabelText("Classifier Model"), { target: { value: "jev-test" } });
|
||||
}
|
||||
fireEvent.change(screen.getByLabelText("Classifier Timeout (ms)"), { target: { value: "4200" } });
|
||||
fireEvent.change(screen.getByLabelText("Context Window Size"), { target: { value: "6" } });
|
||||
fireEvent.change(screen.getByLabelText("Circuit breaker cooldown (seconds)"), { target: { value: "50" } });
|
||||
fireEvent.click(screen.getByRole("switch", { name: "Classifier circuit breaker" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Customize tiers" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Save and reload" }));
|
||||
expect(screen.getByRole("radio", { name: /^OSS Classifier$/ })).toBeChecked();
|
||||
expect(screen.getByRole("radio", { name: provider === "laya" ? "Laya" : "Jev" })).toBeChecked();
|
||||
if (provider === "laya") expect(screen.getByLabelText("Classifier Model")).toHaveTextContent("multilingual");
|
||||
else expect(screen.getByLabelText("Classifier Model")).toHaveValue("jev-test");
|
||||
expect(screen.getByLabelText("Classifier Timeout (ms)")).toHaveValue(4200);
|
||||
expect(screen.getByLabelText("Context Window Size")).toHaveValue("6");
|
||||
expect(screen.getByRole("switch", { name: "Classifier circuit breaker" })).not.toBeChecked();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Probe current config" }));
|
||||
expect(testAutoRouterRouting).toHaveBeenCalledWith(
|
||||
"token",
|
||||
expect.objectContaining({
|
||||
complexity_router_config: expect.objectContaining({
|
||||
classifier_type: "oss_classifier",
|
||||
opensource_classifier_config: {
|
||||
provider,
|
||||
model: provider === "laya" ? "multilingual" : "jev-test",
|
||||
timeout_ms: 4200,
|
||||
circuit_breaker_enabled: false,
|
||||
circuit_breaker_cooldown_seconds: 50,
|
||||
},
|
||||
tiers: expect.objectContaining({ QUICK: ["fast"] }),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("allows licensed instructions and can restore built-in instructions", () => {
|
||||
const authorized = useAuthorized();
|
||||
|
|
@ -152,10 +169,10 @@ describe("JEV classifier editor", () => {
|
|||
return <JevEditor value={value} onChange={setValue} />;
|
||||
};
|
||||
renderWithProviders(<LicensedForm />);
|
||||
expect(screen.getByLabelText("Jev Instructions")).toBeEnabled();
|
||||
fireEvent.change(screen.getByLabelText("Jev Instructions"), { target: { value: "New instructions" } });
|
||||
expect(screen.getByLabelText("Jev Instructions")).toHaveValue("New instructions");
|
||||
fireEvent.click(screen.getByRole("button", { name: "Restore built-in Jev instructions" }));
|
||||
expect(screen.getByLabelText("Jev Instructions")).toHaveValue("");
|
||||
expect(screen.getByLabelText("Classifier Instructions")).toBeEnabled();
|
||||
fireEvent.change(screen.getByLabelText("Classifier Instructions"), { target: { value: "New instructions" } });
|
||||
expect(screen.getByLabelText("Classifier Instructions")).toHaveValue("New instructions");
|
||||
fireEvent.click(screen.getByRole("button", { name: "Restore built-in instructions" }));
|
||||
expect(screen.getByLabelText("Classifier Instructions")).toHaveValue("");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ import { Label } from "@/components/ui/label";
|
|||
import { Textarea } from "@/components/ui/textarea";
|
||||
import ClassifierCircuitBreakerConfig from "./ClassifierCircuitBreakerConfig";
|
||||
import type { ComplexityRouterConfigValue } from "./ComplexityRouterConfig";
|
||||
import { defaultJevClassifierConfig } from "./jev_classifier_config";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { defaultJevClassifierConfig, LAYA_MODELS } from "./jev_classifier_config";
|
||||
|
||||
export default function JevClassifierConfig({
|
||||
value,
|
||||
|
|
@ -17,20 +18,38 @@ export default function JevClassifierConfig({
|
|||
}) {
|
||||
const id = useId();
|
||||
const config = value.jev_classifier_config ?? defaultJevClassifierConfig();
|
||||
const isLaya = config.provider === "laya";
|
||||
const update = (patch: Partial<typeof config>) =>
|
||||
onChange({ ...value, jev_classifier_config: { ...config, ...patch } });
|
||||
|
||||
return (
|
||||
<div className="mt-4 space-y-3">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Uses TypeSafe System One Choice evaluation with your configured tiers
|
||||
{isLaya
|
||||
? "Uses Laya with your configured tiers. Set LAYA_API_BASE on the gateway to connect your Laya server."
|
||||
: "Uses TypeSafe System One Choice evaluation with your configured tiers"}
|
||||
</p>
|
||||
<div>
|
||||
<Label htmlFor={`${id}-model`}>Jev Model</Label>
|
||||
<Input id={`${id}-model`} value={config.model} onChange={(event) => update({ model: event.target.value })} />
|
||||
<Label htmlFor={`${id}-model`}>Classifier Model</Label>
|
||||
{isLaya ? (
|
||||
<Select value={config.model} onValueChange={(model) => model && update({ model })}>
|
||||
<SelectTrigger id={`${id}-model`} className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{LAYA_MODELS.map((model) => (
|
||||
<SelectItem key={model} value={model}>
|
||||
{model}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<Input id={`${id}-model`} value={config.model} onChange={(event) => update({ model: event.target.value })} />
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor={`${id}-timeout`}>Jev Timeout (ms)</Label>
|
||||
<Label htmlFor={`${id}-timeout`}>Classifier Timeout (ms)</Label>
|
||||
<Input
|
||||
id={`${id}-timeout`}
|
||||
type="number"
|
||||
|
|
@ -50,7 +69,7 @@ export default function JevClassifierConfig({
|
|||
}
|
||||
/>
|
||||
<div>
|
||||
<Label htmlFor={`${id}-instructions`}>Jev Instructions</Label>
|
||||
<Label htmlFor={`${id}-instructions`}>Classifier Instructions</Label>
|
||||
<AutoRouterAllowanceNote
|
||||
feature="tier_or_classifier_prompt"
|
||||
label="Custom instructions share the custom-tier allowance"
|
||||
|
|
@ -63,11 +82,11 @@ export default function JevClassifierConfig({
|
|||
/>
|
||||
{config.instructions && (
|
||||
<Button variant="outline" type="button" onClick={() => update({ instructions: undefined })}>
|
||||
Restore built-in Jev instructions
|
||||
Restore built-in instructions
|
||||
</Button>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Built-in Jev is available without a license and uses the shipped tier criteria
|
||||
Built-in OSS classification is available without a license and uses the shipped tier criteria
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -107,10 +107,10 @@ describe("JEV network probes", () => {
|
|||
expect(JSON.parse(String(routingCall?.[1]?.body))).toEqual(expectedRequest);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(5);
|
||||
expect(screen.getAllByTestId("test-status-success")).toHaveLength(4);
|
||||
expect(screen.getByRole("status", { name: "Jev connection" })).toHaveTextContent(
|
||||
expect(screen.getByRole("status", { name: "OSS classifier connection" })).toHaveTextContent(
|
||||
cause === "jev_classifier"
|
||||
? "Jev classification succeeded"
|
||||
: `Jev was not reached successfully (routing cause: ${cause})`,
|
||||
? "OSS classification succeeded"
|
||||
: `OSS classifier was not reached successfully (routing cause: ${cause})`,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
@ -131,7 +131,7 @@ describe("JEV network probes", () => {
|
|||
);
|
||||
fireEvent.change(screen.getByTestId("auto-router-routing-test-prompt"), { target: { value: "Hello" } });
|
||||
fireEvent.click(screen.getByTestId("auto-router-routing-test-send"));
|
||||
expect(await screen.findByText("JEV classifier")).toBeInTheDocument();
|
||||
expect(await screen.findByText("OSS classifier")).toBeInTheDocument();
|
||||
expect(screen.getByText("jev-latest")).toBeInTheDocument();
|
||||
expect(screen.getByText("80.0%")).toBeInTheDocument();
|
||||
expect(screen.getByText("SIMPLE: 80.0%")).toBeInTheDocument();
|
||||
|
|
|
|||
|
|
@ -274,13 +274,13 @@ describe("AddAutoRouterTab", () => {
|
|||
mockFetchAvailableModels.mockResolvedValue(ALL_FAMILY_MODELS);
|
||||
renderWithProviders(<Harness />);
|
||||
await user.click(await screen.findByRole("button", { name: "Choose models for me" }));
|
||||
await user.click(screen.getByRole("radio", { name: "Jev" }));
|
||||
await user.click(screen.getByRole("radio", { name: "OSS Classifier" }));
|
||||
await waitFor(() =>
|
||||
expect(apiClient.post).toHaveBeenLastCalledWith(
|
||||
"/auto_router/availability",
|
||||
expect.objectContaining({
|
||||
body: expect.objectContaining({
|
||||
complexity_router_config: expect.objectContaining({ classifier_type: "jev" }),
|
||||
complexity_router_config: expect.objectContaining({ classifier_type: "oss_classifier" }),
|
||||
}),
|
||||
}),
|
||||
),
|
||||
|
|
@ -301,13 +301,13 @@ describe("AddAutoRouterTab", () => {
|
|||
expect(within(screen.getByRole("alert")).getByRole("link", { name: "Talk to our team" })).toBeVisible();
|
||||
await user.click(screen.getByRole("button", { name: "Restore defaults" }));
|
||||
await waitFor(() => expect(screen.queryByRole("alert")).not.toBeInTheDocument());
|
||||
expect(screen.getByRole("radio", { name: "Jev" })).toBeChecked();
|
||||
expect(screen.getByRole("radio", { name: "OSS Classifier" })).toBeChecked();
|
||||
await waitFor(() => expect(screen.getByRole("button", { name: "Add Auto Router" })).toBeEnabled());
|
||||
await user.click(screen.getByRole("button", { name: "Add Auto Router" }));
|
||||
await waitFor(() => expect(handleAddAutoRouterSubmit).toHaveBeenCalled());
|
||||
const saved = vi.mocked(handleAddAutoRouterSubmit).mock.calls.at(-1)?.[0].complexity_router_config;
|
||||
expect(saved).not.toHaveProperty("tier_definitions");
|
||||
expect(saved?.classifier_type).toBe("jev");
|
||||
expect(saved?.classifier_type).toBe("oss_classifier");
|
||||
expect(Object.keys(saved?.tiers ?? {})).toEqual(["SIMPLE", "MEDIUM", "COMPLEX", "REASONING"]);
|
||||
expect(saved?.tiers).toEqual(initialRequest.complexity_router_config.tiers);
|
||||
});
|
||||
|
|
@ -317,7 +317,7 @@ describe("AddAutoRouterTab", () => {
|
|||
mockFetchAvailableModels.mockResolvedValue(ALL_FAMILY_MODELS);
|
||||
renderWithProviders(<Harness />);
|
||||
await user.click(await screen.findByRole("button", { name: "Choose models for me" }));
|
||||
await user.click(screen.getByRole("radio", { name: "Jev" }));
|
||||
await user.click(screen.getByRole("radio", { name: "OSS Classifier" }));
|
||||
fireEvent.change(screen.getByLabelText("Auto Router Name"), { target: { value: "checked-router" } });
|
||||
await waitFor(() => expect(screen.getByRole("button", { name: "Add Auto Router" })).toBeEnabled());
|
||||
let complete: ((result: unknown) => void) | undefined;
|
||||
|
|
@ -357,17 +357,22 @@ describe("AddAutoRouterTab", () => {
|
|||
expect(screen.getByRole("button", { name: "Routing approach" })).toHaveTextContent("Complexity");
|
||||
});
|
||||
|
||||
it.each(["LLM", "Jev"])("keeps %s and the frequency when choosing models automatically", async (family) => {
|
||||
mockFetchAvailableModels.mockResolvedValue(ALL_FAMILY_MODELS);
|
||||
renderWithProviders(<Harness />);
|
||||
const automatic = await screen.findByRole("button", { name: "Choose models for me" });
|
||||
await userEvent.click(screen.getByRole("radio", { name: family }));
|
||||
await selectAutoRouterOption("How often to classify", "Every new user message");
|
||||
await userEvent.click(automatic);
|
||||
expect(screen.getByRole("radio", { name: family })).toBeChecked();
|
||||
expect(screen.getByRole("combobox", { name: "How often to classify" })).toHaveTextContent("Every new user message");
|
||||
expect(screen.getByRole("button", { name: "Advanced settings" })).toHaveAttribute("aria-expanded", "false");
|
||||
});
|
||||
it.each(["LLM", "OSS Classifier"])(
|
||||
"keeps %s and the frequency when choosing models automatically",
|
||||
async (family) => {
|
||||
mockFetchAvailableModels.mockResolvedValue(ALL_FAMILY_MODELS);
|
||||
renderWithProviders(<Harness />);
|
||||
const automatic = await screen.findByRole("button", { name: "Choose models for me" });
|
||||
await userEvent.click(screen.getByRole("radio", { name: family }));
|
||||
await selectAutoRouterOption("How often to classify", "Every new user message");
|
||||
await userEvent.click(automatic);
|
||||
expect(screen.getByRole("radio", { name: family })).toBeChecked();
|
||||
expect(screen.getByRole("combobox", { name: "How often to classify" })).toHaveTextContent(
|
||||
"Every new user message",
|
||||
);
|
||||
expect(screen.getByRole("button", { name: "Advanced settings" })).toHaveAttribute("aria-expanded", "false");
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["Capability", "Fuse v2"])(
|
||||
"creates %s from its dedicated tab without complexity templates",
|
||||
|
|
@ -1902,7 +1907,7 @@ describe("preset catalog fetch states", () => {
|
|||
|
||||
await waitFor(() => expect(handleAddAutoRouterSubmit).toHaveBeenCalledOnce());
|
||||
expect(vi.mocked(handleAddAutoRouterSubmit).mock.calls[0][0].complexity_router_config).toMatchObject({
|
||||
classifier_type: "jev",
|
||||
classifier_type: "oss_classifier",
|
||||
classifier_context_per_turn_chars: 450,
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ const AutoRouterConnectionTest: React.FC<AutoRouterConnectionTestProps> = ({
|
|||
? { status: "success" }
|
||||
: {
|
||||
status: "error",
|
||||
error: `Jev was not reached successfully (routing cause: ${decision.cause ?? "unknown"})`,
|
||||
error: `OSS classifier was not reached successfully (routing cause: ${decision.cause ?? "unknown"})`,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
|
@ -91,11 +91,11 @@ const AutoRouterConnectionTest: React.FC<AutoRouterConnectionTestProps> = ({
|
|||
classifier probe includes its reasoning effort override.
|
||||
</p>
|
||||
{jevRequest && (
|
||||
<div role="status" aria-label="Jev connection" className="rounded-lg border p-3 text-sm">
|
||||
<strong>Jev Classifier</strong>
|
||||
<div role="status" aria-label="OSS classifier connection" className="rounded-lg border p-3 text-sm">
|
||||
<strong>OSS Classifier</strong>
|
||||
<p>
|
||||
{jevResult.status === "pending" && "Testing Jev classification"}
|
||||
{jevResult.status === "success" && "Jev classification succeeded"}
|
||||
{jevResult.status === "pending" && "Testing OSS classification"}
|
||||
{jevResult.status === "success" && "OSS classification succeeded"}
|
||||
{jevResult.status === "error" && jevResult.error}
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -33,20 +33,20 @@ describe("buildAutoRouterRoutingTestRequest", () => {
|
|||
const expectedRequest = {
|
||||
prompt: JEV_CONNECTION_TEST_PROMPT,
|
||||
complexity_router_config: {
|
||||
classifier_type: "jev",
|
||||
classifier_type: "oss_classifier",
|
||||
tiers: CONFIG.tiers,
|
||||
jev_classifier_config: defaultJevClassifierConfig(),
|
||||
opensource_classifier_config: defaultJevClassifierConfig(),
|
||||
},
|
||||
saved_model_id: "saved-id",
|
||||
};
|
||||
expect(request).toEqual(expectedRequest);
|
||||
expect(request?.complexity_router_config.jev_classifier_config).not.toHaveProperty("api_key");
|
||||
expect(request?.complexity_router_config.jev_classifier_config).not.toHaveProperty("api_base");
|
||||
expect(request?.complexity_router_config.opensource_classifier_config).not.toHaveProperty("api_key");
|
||||
expect(request?.complexity_router_config.opensource_classifier_config).not.toHaveProperty("api_base");
|
||||
});
|
||||
it.each(["object", "json"])("probes saved JEV %s configuration with custom tiers and team context", (format) => {
|
||||
it.each(["object", "json"])("probes saved Laya %s configuration with custom tiers and team context", (format) => {
|
||||
const config = {
|
||||
classifier_type: "jev",
|
||||
jev_classifier_config: { model: "jev-test", timeout_ms: 900 },
|
||||
classifier_type: "oss_classifier",
|
||||
opensource_classifier_config: { provider: "laya", model: "english", timeout_ms: 900 },
|
||||
tiers: { QUICK: ["fast"], DEEP: ["strong"] },
|
||||
tier_definitions: { QUICK: "Simple questions", DEEP: "Complex questions" },
|
||||
fallback_tier: "DEEP",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { AutoRouterRoutingTestRequest } from "../networking";
|
||||
import { ComplexityRouterConfigPayload } from "./build_complexity_router_config";
|
||||
import { z } from "zod";
|
||||
import { jevClassifierConfigSchema } from "./jev_classifier_config";
|
||||
import { hydrateOssClassifier, jevClassifierConfigSchema, normalizeJevClassifierConfig } from "./jev_classifier_config";
|
||||
|
||||
export const JEV_CONNECTION_TEST_PROMPT = "What is 2 plus 2?";
|
||||
|
||||
|
|
@ -23,16 +23,23 @@ export const buildSavedJevConnectionTestRequest = (
|
|||
: rawConfig;
|
||||
const result = z
|
||||
.object({
|
||||
classifier_type: z.literal("jev"),
|
||||
classifier_type: z.enum(["jev", "oss_classifier"]),
|
||||
tiers: z.record(z.unknown()),
|
||||
jev_classifier_config: jevClassifierConfigSchema.default({}),
|
||||
jev_classifier_config: jevClassifierConfigSchema.optional(),
|
||||
opensource_classifier_config: jevClassifierConfigSchema.optional(),
|
||||
})
|
||||
.passthrough()
|
||||
.safeParse(parsed);
|
||||
if (!result.success) return undefined;
|
||||
const { jev_classifier_config, opensource_classifier_config, ...config } = result.data;
|
||||
const classifier = hydrateOssClassifier({ ...config, jev_classifier_config, opensource_classifier_config });
|
||||
return {
|
||||
prompt: JEV_CONNECTION_TEST_PROMPT,
|
||||
complexity_router_config: result.data,
|
||||
complexity_router_config: {
|
||||
...config,
|
||||
classifier_type: "oss_classifier",
|
||||
opensource_classifier_config: normalizeJevClassifierConfig(classifier.jev_classifier_config),
|
||||
},
|
||||
saved_model_id: savedModelId,
|
||||
...(teamId && { team_id: teamId }),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ describe("buildComplexityRouterConfig", () => {
|
|||
it.each([
|
||||
{ model: "" },
|
||||
{ model: " " },
|
||||
{ provider: "laya" as const, model: "unsupported" },
|
||||
{ timeout_ms: 0 },
|
||||
{ timeout_ms: 1.5 },
|
||||
{ timeout_ms: Number.NaN },
|
||||
|
|
@ -75,15 +76,16 @@ describe("buildComplexityRouterConfig", () => {
|
|||
classifier_type: "jev",
|
||||
jev_classifier_config: { model: "jev-latest", timeout_ms: 3000, ...patch },
|
||||
}),
|
||||
).toBe("Enter a JEV model, a positive whole-number timeout and a positive cooldown");
|
||||
).toBe("Enter a valid classifier model, a positive whole-number timeout and a positive cooldown");
|
||||
});
|
||||
|
||||
it.each([false, true])("serializes JEV with shared context and no LLM config, custom tiers: %s", (custom) => {
|
||||
it.each([false, true])("serializes Laya with shared context and no LLM config, custom tiers: %s", (custom) => {
|
||||
const params: BuildComplexityRouterConfigParams = {
|
||||
...baseParams,
|
||||
classifierType: "jev",
|
||||
jevClassifierConfig: {
|
||||
model: "jev-test",
|
||||
provider: "laya",
|
||||
model: "english",
|
||||
timeout_ms: 4500,
|
||||
instructions: " Choose the configured tier ",
|
||||
circuit_breaker_enabled: false,
|
||||
|
|
@ -108,15 +110,17 @@ describe("buildComplexityRouterConfig", () => {
|
|||
}),
|
||||
};
|
||||
const config = buildComplexityRouterConfig(params);
|
||||
expect(config.classifier_type).toBe("jev");
|
||||
expect(config.classifier_type).toBe("oss_classifier");
|
||||
const expectedJevConfig = {
|
||||
model: "jev-test",
|
||||
provider: "laya",
|
||||
model: "english",
|
||||
timeout_ms: 4500,
|
||||
instructions: "Choose the configured tier",
|
||||
circuit_breaker_enabled: false,
|
||||
circuit_breaker_cooldown_seconds: 12.5,
|
||||
};
|
||||
expect(config.jev_classifier_config).toEqual(expectedJevConfig);
|
||||
expect(config.opensource_classifier_config).toEqual(expectedJevConfig);
|
||||
expect(config).not.toHaveProperty("jev_classifier_config");
|
||||
expect(config.classifier_context_window_size).toBe(4);
|
||||
expect(config.classifier_context_budget_chars).toBe(2000);
|
||||
expect(config.classifier_context_per_turn_chars).toBe(450);
|
||||
|
|
@ -139,15 +143,15 @@ describe("buildComplexityRouterConfig", () => {
|
|||
classifierType: "jev",
|
||||
jevClassifierConfig: { model: "jev-latest", timeout_ms: 3000, instructions: " " },
|
||||
});
|
||||
expect(jev.jev_classifier_config).toEqual({ model: "jev-latest", timeout_ms: 3000 });
|
||||
expect(jev.opensource_classifier_config).toEqual({ provider: "jev", model: "jev-latest", timeout_ms: 3000 });
|
||||
const llmParams: BuildComplexityRouterConfigParams = {
|
||||
...baseParams,
|
||||
classifierType: "llm",
|
||||
classifierLlmConfig: { model: "judge", timeout_ms: 1000 },
|
||||
jevClassifierConfig: jev.jev_classifier_config,
|
||||
jevClassifierConfig: jev.opensource_classifier_config,
|
||||
};
|
||||
const llm = buildComplexityRouterConfig(llmParams);
|
||||
expect(llm).not.toHaveProperty("jev_classifier_config");
|
||||
expect(llm).not.toHaveProperty("opensource_classifier_config");
|
||||
});
|
||||
|
||||
it("forwards preset references and explicit overrides without materializing absent text on create", () => {
|
||||
|
|
|
|||
|
|
@ -153,12 +153,13 @@ export interface StoredComplexityRouterConfig {
|
|||
heuristic_first_max_tier?: unknown;
|
||||
hybrid_boundary_margin?: unknown;
|
||||
tier_labels?: unknown;
|
||||
classifier_type?: ClassifierType;
|
||||
classifier_type?: ClassifierType | "oss_classifier";
|
||||
heuristic_v2_success_threshold?: unknown;
|
||||
capability_classifier_config?: unknown;
|
||||
llm_v2_config?: unknown;
|
||||
classifier_llm_config?: ClassifierLLMConfig;
|
||||
jev_classifier_config?: unknown;
|
||||
opensource_classifier_config?: unknown;
|
||||
classifier_context_window_size?: unknown;
|
||||
classifier_context_budget_chars?: unknown;
|
||||
classifier_context_per_turn_chars?: unknown;
|
||||
|
|
@ -283,12 +284,13 @@ export interface ComplexityRouterConfigPayload {
|
|||
default_model?: string;
|
||||
plan_mode_min_tier?: string;
|
||||
tier_labels?: ComplexityTierLabels;
|
||||
classifier_type: ClassifierType;
|
||||
classifier_type: ClassifierType | "oss_classifier";
|
||||
heuristic_v2_success_threshold?: number;
|
||||
capability_classifier_config?: CapabilitySettings;
|
||||
llm_v2_config?: FuseSettings;
|
||||
classifier_llm_config?: ClassifierLLMConfig;
|
||||
jev_classifier_config?: JevClassifierConfig;
|
||||
opensource_classifier_config?: JevClassifierConfig;
|
||||
classifier_context_window_size?: number;
|
||||
classifier_context_budget_chars?: number;
|
||||
classifier_context_per_turn_chars?: number;
|
||||
|
|
@ -438,7 +440,9 @@ export const getClassifierModelError = (
|
|||
): string | null => {
|
||||
if (effectiveClassifierType(config) === "jev") {
|
||||
const parsed = jevClassifierConfigSchema.safeParse(config.jev_classifier_config ?? {});
|
||||
return parsed.success ? null : "Enter a JEV model, a positive whole-number timeout and a positive cooldown";
|
||||
return parsed.success
|
||||
? null
|
||||
: "Enter a valid classifier model, a positive whole-number timeout and a positive cooldown";
|
||||
}
|
||||
if (!usesLlmClassifier(effectiveClassifierType(config)) || config.classifier_llm_config?.model) return null;
|
||||
return config.custom_tier_set
|
||||
|
|
@ -498,7 +502,7 @@ export const customTierWireFields = (
|
|||
tiers: Object.fromEntries(rows.map((row) => [activeTierName(row), row.models])),
|
||||
tier_definitions: tierDefinitionsFromRows(rows),
|
||||
...(fallback && { fallback_tier: activeTierName(fallback) }),
|
||||
classifier_type: classifierType === "jev" ? "jev" : "llm",
|
||||
classifier_type: classifierType === "jev" ? "oss_classifier" : "llm",
|
||||
// Rebuilt from the fields an edited tier set allows. The backend rejects system_prompt and
|
||||
// classification_rubric beside tier_definitions, and both live inside this object rather than at
|
||||
// the top level the omit list covers. The opening instructions ride classification_prompt below.
|
||||
|
|
@ -769,8 +773,8 @@ export const buildComplexityRouterConfig = ({
|
|||
...(defaultModel?.trim() && { default_model: defaultModel }),
|
||||
...(planModeMinTier?.trim() && { plan_mode_min_tier: planModeMinTier }),
|
||||
...(cleanedTierLabels && { tier_labels: cleanedTierLabels }),
|
||||
classifier_type: classifierType,
|
||||
...(effectiveType === "jev" && { jev_classifier_config: normalizeJevClassifierConfig(jevClassifierConfig) }),
|
||||
classifier_type: classifierType === "jev" ? "oss_classifier" : classifierType,
|
||||
...(effectiveType === "jev" && { opensource_classifier_config: normalizeJevClassifierConfig(jevClassifierConfig) }),
|
||||
...(heuristicV2SuccessThreshold !== undefined && {
|
||||
heuristic_v2_success_threshold: heuristicV2SuccessThreshold,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
import { z } from "zod";
|
||||
import type { ClassifierType } from "./classifier_types";
|
||||
|
||||
export const LAYA_MODELS = ["english", "multilingual", "typed-decisions"] as const;
|
||||
|
||||
const jevClassifierConfigFields = {
|
||||
model: z.string().trim().min(1).default("jev-latest"),
|
||||
provider: z.preprocess((value) => (value === "typesafe" ? "jev" : value), z.enum(["jev", "laya"]).optional()),
|
||||
model: z.string().trim().min(1).optional(),
|
||||
timeout_ms: z.number().int().positive().default(3000),
|
||||
instructions: z
|
||||
.string()
|
||||
|
|
@ -11,15 +15,39 @@ const jevClassifierConfigFields = {
|
|||
circuit_breaker_cooldown_seconds: z.number().finite().positive().optional(),
|
||||
};
|
||||
|
||||
export const jevClassifierConfigSchema = z.object(jevClassifierConfigFields);
|
||||
export const jevClassifierConfigSchema = z
|
||||
.object(jevClassifierConfigFields)
|
||||
.transform((config) => ({
|
||||
...config,
|
||||
model: config.model ?? (config.provider === "laya" ? "english" : "jev-latest"),
|
||||
}))
|
||||
.refine((config) => config.provider !== "laya" || LAYA_MODELS.some((model) => model === config.model), {
|
||||
message: "Select a supported Laya model",
|
||||
path: ["model"],
|
||||
});
|
||||
|
||||
export type JevClassifierConfig = z.infer<typeof jevClassifierConfigSchema>;
|
||||
|
||||
export const defaultJevClassifierConfig = (): JevClassifierConfig => jevClassifierConfigSchema.parse({});
|
||||
export const defaultJevClassifierConfig = (provider: "jev" | "laya" = "jev"): JevClassifierConfig =>
|
||||
jevClassifierConfigSchema.parse({ provider });
|
||||
|
||||
export const hydrateOssClassifier = (config: {
|
||||
classifier_type?: ClassifierType | "oss_classifier";
|
||||
opensource_classifier_config?: unknown;
|
||||
jev_classifier_config?: unknown;
|
||||
}): { classifier_type: ClassifierType; jev_classifier_config?: JevClassifierConfig } => ({
|
||||
classifier_type: config.classifier_type === "oss_classifier" ? "jev" : config.classifier_type ?? "heuristic",
|
||||
jev_classifier_config:
|
||||
config.classifier_type === "oss_classifier" || config.classifier_type === "jev"
|
||||
? jevClassifierConfigSchema.safeParse(config.opensource_classifier_config ?? config.jev_classifier_config ?? {})
|
||||
.data ?? defaultJevClassifierConfig()
|
||||
: undefined,
|
||||
});
|
||||
|
||||
export const normalizeJevClassifierConfig = (
|
||||
config: JevClassifierConfig = defaultJevClassifierConfig(),
|
||||
): JevClassifierConfig => ({
|
||||
provider: config.provider ?? "jev",
|
||||
model: config.model.trim(),
|
||||
timeout_ms: config.timeout_ms,
|
||||
...(config.instructions?.trim() && { instructions: config.instructions.trim() }),
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ const hydratedState: KeywordMatchingState = {
|
|||
};
|
||||
|
||||
describe("buildUpdatedComplexityRouterConfig keyword matching", () => {
|
||||
it.each([false, true])("omits masked JEV credentials from dashboard saves, edited: %s", (edited) => {
|
||||
it.each([false, true])("omits masked Jev credentials from legacy/canonical saves, edited: %s", (edited) => {
|
||||
const stored = {
|
||||
classifier_type: "jev" as const,
|
||||
tiers: FORM_VALUE.tiers,
|
||||
|
|
@ -60,7 +60,15 @@ describe("buildUpdatedComplexityRouterConfig keyword matching", () => {
|
|||
api_base: "https://jev.example.com",
|
||||
},
|
||||
};
|
||||
const hydrated = hydrateComplexityRouterConfig(stored, undefined);
|
||||
const source = edited
|
||||
? {
|
||||
...stored,
|
||||
classifier_type: "oss_classifier" as const,
|
||||
jev_classifier_config: undefined,
|
||||
opensource_classifier_config: { ...stored.jev_classifier_config, provider: "typesafe" },
|
||||
}
|
||||
: stored;
|
||||
const hydrated = hydrateComplexityRouterConfig(source, undefined);
|
||||
expect(hydrated.jev_classifier_config).not.toHaveProperty("api_key");
|
||||
expect(hydrated.jev_classifier_config).not.toHaveProperty("api_base");
|
||||
const value = edited
|
||||
|
|
@ -69,8 +77,9 @@ describe("buildUpdatedComplexityRouterConfig keyword matching", () => {
|
|||
jev_classifier_config: { model: "jev-updated", timeout_ms: 8100, instructions: "" },
|
||||
}
|
||||
: hydrated;
|
||||
const saved = buildUpdatedComplexityRouterConfig(stored, value);
|
||||
expect(saved.jev_classifier_config).toEqual({
|
||||
const saved = buildUpdatedComplexityRouterConfig(source, value);
|
||||
expect(saved.opensource_classifier_config).toEqual({
|
||||
provider: "jev",
|
||||
...(edited
|
||||
? { model: "jev-updated", timeout_ms: 8100 }
|
||||
: { model: "jev-configured", timeout_ms: 6100, instructions: "Existing instructions" }),
|
||||
|
|
@ -78,7 +87,7 @@ describe("buildUpdatedComplexityRouterConfig keyword matching", () => {
|
|||
for (const classifierType of ["llm", "heuristic"] as const) {
|
||||
expect(
|
||||
buildUpdatedComplexityRouterConfig(saved, transitionClassifierType(value, classifierType)),
|
||||
).not.toHaveProperty("jev_classifier_config");
|
||||
).not.toHaveProperty("opensource_classifier_config");
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -94,19 +103,21 @@ describe("buildUpdatedComplexityRouterConfig keyword matching", () => {
|
|||
tiers: FORM_VALUE.tiers,
|
||||
};
|
||||
const saved = buildUpdatedComplexityRouterConfig(stored, hydrateComplexityRouterConfig(stored, undefined));
|
||||
expect(saved.jev_classifier_config).toEqual({
|
||||
expect(saved.opensource_classifier_config).toEqual({
|
||||
provider: "jev",
|
||||
model: "jev-configured",
|
||||
timeout_ms: 6100,
|
||||
circuit_breaker_enabled: false,
|
||||
});
|
||||
});
|
||||
it.each([false, true])("round trips JEV settings and preserves unmanaged fields, custom: %s", (custom) => {
|
||||
it.each([false, true])("round trips Laya settings and preserves unmanaged fields, custom: %s", (custom) => {
|
||||
const stored = {
|
||||
...(custom ? storedCustomConfig() : STORED),
|
||||
classifier_llm_config: { model: "stale-judge", timeout_ms: 3000 },
|
||||
classifier_type: "jev" as const,
|
||||
jev_classifier_config: {
|
||||
model: "jev-test",
|
||||
classifier_type: "oss_classifier" as const,
|
||||
opensource_classifier_config: {
|
||||
provider: "laya" as const,
|
||||
model: "english",
|
||||
timeout_ms: 4100,
|
||||
instructions: "Judge the request",
|
||||
circuit_breaker_enabled: false,
|
||||
|
|
@ -121,12 +132,12 @@ describe("buildUpdatedComplexityRouterConfig keyword matching", () => {
|
|||
const hydrated = hydrateComplexityRouterConfig(stored, undefined);
|
||||
expect(effectiveClassifierType(hydrated)).toBe("jev");
|
||||
expect(hydrated.classifier_llm_config).toBeUndefined();
|
||||
expect(hydrated.jev_classifier_config).toEqual(stored.jev_classifier_config);
|
||||
expect(hydrated.jev_classifier_config).toEqual(stored.opensource_classifier_config);
|
||||
expect(hydrated.classifier_context_per_turn_chars).toBe(450);
|
||||
const saved = buildUpdatedComplexityRouterConfig(stored, hydrated);
|
||||
const expectedSavedConfig = {
|
||||
classifier_type: "jev",
|
||||
jev_classifier_config: stored.jev_classifier_config,
|
||||
classifier_type: "oss_classifier",
|
||||
opensource_classifier_config: stored.opensource_classifier_config,
|
||||
classifier_context_window_size: 7,
|
||||
classifier_context_budget_chars: 9000,
|
||||
classifier_context_per_turn_chars: 450,
|
||||
|
|
@ -135,12 +146,13 @@ describe("buildUpdatedComplexityRouterConfig keyword matching", () => {
|
|||
};
|
||||
expect(saved).toMatchObject(expectedSavedConfig);
|
||||
expect(saved).not.toHaveProperty("classifier_llm_config");
|
||||
expect(saved).not.toHaveProperty("jev_classifier_config");
|
||||
const reloaded = hydrateComplexityRouterConfig(saved, undefined);
|
||||
expect(reloaded.jev_classifier_config).toEqual(hydrated.jev_classifier_config);
|
||||
expect(reloaded.classifier_context_per_turn_chars).toBe(450);
|
||||
expect(effectiveClassifierType(reloaded)).toBe("jev");
|
||||
const llm = buildUpdatedComplexityRouterConfig(saved, transitionClassifierType(reloaded, "llm"));
|
||||
expect(llm).not.toHaveProperty("jev_classifier_config");
|
||||
expect(llm).not.toHaveProperty("opensource_classifier_config");
|
||||
});
|
||||
|
||||
it.each([0, 0.92, 1])("hydrates and saves a success threshold of %s without changing the artifact", (threshold) => {
|
||||
|
|
@ -883,6 +895,7 @@ describe("managed keys survive an untouched open-and-save", () => {
|
|||
"fallback_tier",
|
||||
"hybrid_boundary_margin",
|
||||
"jev_classifier_config",
|
||||
"opensource_classifier_config",
|
||||
"classifier_plugin_timeout_ms",
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ export const MANAGED_COMPLEXITY_ROUTER_KEYS = new Set([
|
|||
"llm_v2_config",
|
||||
"classifier_llm_config",
|
||||
"jev_classifier_config",
|
||||
"opensource_classifier_config",
|
||||
"classifier_context_window_size",
|
||||
"classifier_context_budget_chars",
|
||||
"classifier_context_include_assistant_turns",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { defaultJevClassifierConfig, jevClassifierConfigSchema } from "../add_model/jev_classifier_config";
|
||||
import { hydrateOssClassifier } from "../add_model/jev_classifier_config";
|
||||
import { capabilitySettingsSchema, fuseSettingsSchema } from "../add_model/forecast_classifier_config";
|
||||
import type { StoredComplexityRouterConfig } from "../add_model/build_complexity_router_config";
|
||||
import {
|
||||
|
|
@ -54,6 +54,7 @@ export const hydrateComplexityRouterConfig = (
|
|||
parsedConfig: StoredComplexityRouterConfig,
|
||||
complexityRouterDefaultModel: string | null | undefined,
|
||||
): ComplexityRouterConfigValue => {
|
||||
const classifier = hydrateOssClassifier(parsedConfig);
|
||||
const builtIn = hydrateBuiltInTiers(parsedConfig.tiers, parsedConfig.enable_non_reasoning_tier);
|
||||
const { tiers: hydratedTiers, enable_non_reasoning_tier } = builtIn;
|
||||
const custom_tier_set = hydrateCustomTierSet(parsedConfig);
|
||||
|
|
@ -70,19 +71,14 @@ export const hydrateComplexityRouterConfig = (
|
|||
default_model: hydratePinnedDefaultModel(parsedConfig.default_model, complexityRouterDefaultModel, activeTiers),
|
||||
plan_mode_min_tier: hydratePlanModeMinTier(parsedConfig.plan_mode_min_tier, custom_tier_set),
|
||||
tier_labels: hydrateTierLabels(parsedConfig.tier_labels),
|
||||
classifier_type: parsedConfig.classifier_type || "heuristic",
|
||||
...classifier,
|
||||
heuristic_v2_success_threshold:
|
||||
typeof parsedConfig.heuristic_v2_success_threshold === "number"
|
||||
? parsedConfig.heuristic_v2_success_threshold
|
||||
: undefined,
|
||||
capability_classifier_config: capabilitySettingsSchema.safeParse(parsedConfig.capability_classifier_config).data,
|
||||
llm_v2_config: fuseSettingsSchema.safeParse(parsedConfig.llm_v2_config).data,
|
||||
classifier_llm_config: parsedConfig.classifier_type === "jev" ? undefined : parsedConfig.classifier_llm_config,
|
||||
jev_classifier_config:
|
||||
parsedConfig.classifier_type === "jev"
|
||||
? jevClassifierConfigSchema.safeParse(parsedConfig.jev_classifier_config ?? {}).data ??
|
||||
defaultJevClassifierConfig()
|
||||
: undefined,
|
||||
classifier_llm_config: classifier.classifier_type === "jev" ? undefined : parsedConfig.classifier_llm_config,
|
||||
classifier_context_window_size:
|
||||
typeof parsedConfig.classifier_context_window_size === "number"
|
||||
? parsedConfig.classifier_context_window_size
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ function describeCause(decision: RoutingDecision): string {
|
|||
case "llm_classifier":
|
||||
return classifierModel ? `LLM classifier (${classifierModel})` : "LLM classifier";
|
||||
case "jev_classifier":
|
||||
return "JEV classifier";
|
||||
return "OSS classifier";
|
||||
case "literal_keyword_match":
|
||||
case "keyword":
|
||||
return matchedKeyword ? `Keyword match: "${matchedKeyword}"` : "Keyword match";
|
||||
|
|
|
|||
|
|
@ -859,23 +859,28 @@ describe("autorouter_presets", () => {
|
|||
});
|
||||
|
||||
describe("buildPresetPrefill", () => {
|
||||
it("preserves JEV settings and drops inactive classifier settings when prefilling", () => {
|
||||
it("preserves Laya settings and drops inactive classifier settings when prefilling", () => {
|
||||
const config = {
|
||||
tiers: { SIMPLE: ["fast"], MEDIUM: [], COMPLEX: [], REASONING: [] },
|
||||
classifier_type: "jev" as const,
|
||||
classifier_type: "oss_classifier" as const,
|
||||
classification_mode: "every_request" as const,
|
||||
session_affinity: false,
|
||||
deployment_affinity: true,
|
||||
modality_routing: false,
|
||||
modality_pin_override: false,
|
||||
jev_classifier_config: { model: "jev-test", timeout_ms: 4000, circuit_breaker_enabled: false },
|
||||
opensource_classifier_config: {
|
||||
provider: "laya" as const,
|
||||
model: "english",
|
||||
timeout_ms: 4000,
|
||||
circuit_breaker_enabled: false,
|
||||
},
|
||||
classifier_llm_config: { model: "stale-judge", timeout_ms: 6000 },
|
||||
classifier_context_window_size: 6,
|
||||
};
|
||||
const prefill = buildPresetPrefill(config, groupsOnly(["fast"]));
|
||||
const expectedJevConfig = {
|
||||
classifier_type: "jev",
|
||||
jev_classifier_config: config.jev_classifier_config,
|
||||
jev_classifier_config: config.opensource_classifier_config,
|
||||
classifier_context_window_size: 6,
|
||||
classifier_llm_config: undefined,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { hydrateOssClassifier } from "@/components/add_model/jev_classifier_config";
|
||||
import {
|
||||
ComplexityRouterConfigPayload,
|
||||
hydrateTierLabels,
|
||||
|
|
@ -302,6 +303,7 @@ export const buildPresetPrefill = (
|
|||
config: ComplexityRouterConfigPayload,
|
||||
availability: ModelAvailability,
|
||||
): PresetPrefill => {
|
||||
const classifier = hydrateOssClassifier(config);
|
||||
const resolve = (model: string): string => resolveAvailableModel(model, availability) ?? model;
|
||||
const resolveTier = (models: string[]): string[] => models.map(resolve);
|
||||
// Params key on the model name the preset spells while every tier entry is rewritten to the
|
||||
|
|
@ -334,11 +336,10 @@ export const buildPresetPrefill = (
|
|||
},
|
||||
tier_model_params: resolveParamKeys(hydrateTierModelParams(config.tiers, config.tier_model_configs)),
|
||||
tier_labels: hydrateTierLabels(config.tier_labels),
|
||||
classifier_type: config.classifier_type,
|
||||
...classifier,
|
||||
heuristic_v2_success_threshold: config.heuristic_v2_success_threshold,
|
||||
jev_classifier_config: config.classifier_type === "jev" ? config.jev_classifier_config : undefined,
|
||||
classifier_llm_config:
|
||||
config.classifier_type !== "jev" && config.classifier_llm_config
|
||||
classifier.classifier_type !== "jev" && config.classifier_llm_config
|
||||
? { ...config.classifier_llm_config, model: resolve(config.classifier_llm_config.model) }
|
||||
: undefined,
|
||||
classifier_context_window_size: config.classifier_context_window_size,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue