Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { InfoCircleOutlined } from "@ant-design/icons";
import { Select as AntdSelect, Card, InputNumber, Radio, Space, Tooltip, Typography } from "antd";
import { Select as AntdSelect, Card, InputNumber, Radio, Space, Switch, Tooltip, Typography } from "antd";
import React from "react";
import {
ClassifierType,
Expand Down Expand Up @@ -47,6 +47,8 @@ const ClassificationMethodConfig: React.FC<ClassificationMethodConfigProps> = ({
classifierType === "llm"
? value.classifier_context_per_turn_chars ?? DEFAULT_CLASSIFIER_CONTEXT_PER_TURN_CHARS
: undefined,
classifier_context_include_assistant_turns:
classifierType === "llm" ? value.classifier_context_include_assistant_turns : undefined,
};
onChange(nextValue);
};
Expand Down Expand Up @@ -85,6 +87,13 @@ const ClassificationMethodConfig: React.FC<ClassificationMethodConfigProps> = ({
});
};

const handleClassifierContextIncludeAssistantTurnsChange = (includeAssistantTurns: boolean) => {
onChange({
...value,
classifier_context_include_assistant_turns: includeAssistantTurns,
});
};

return (
<>
<Radio.Group
Expand Down Expand Up @@ -170,6 +179,26 @@ const ClassificationMethodConfig: React.FC<ClassificationMethodConfigProps> = ({
Prior turns longer than this are truncated.
</Text>
</div>
<div>
<div className="flex items-center gap-2 mb-1">
<Switch
checked={value.classifier_context_include_assistant_turns ?? false}
onChange={handleClassifierContextIncludeAssistantTurnsChange}
size="small"
aria-label="Include Assistant Turns"
/>
Comment thread
greptile-apps[bot] marked this conversation as resolved.
<Text strong>Include Assistant Turns</Text>
<Tooltip title="Off by default. Enabling it changes tier decisions, and therefore spend, for an existing router, and sends assistant text to the classifier model, which may be a different provider than the routed model.">
<InfoCircleOutlined className="text-gray-400" />
</Tooltip>
</div>
<Text type="secondary" style={{ fontSize: 12 }}>
Let the classifier read the assistant&apos;s replies, so difficulty the model stated rather than the user
stays visible: a plan the assistant calls complex, approved with &quot;yes&quot;, is classified on the
work being approved. Context Window Size then counts the last N turns across both roles rather than the
last N user turns.
</Text>
</div>
</div>
)}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
{ model_group: "gpt-3.5-turbo", mode: "chat" },
{ model_group: "claude-3-opus", mode: "chat" },
{ model_group: "text-embedding-3-small", mode: "embedding" },
] as any[];

Check warning on line 11 in ui/litellm-dashboard/src/components/add_model/ComplexityRouterConfig.test.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type

const defaultValue: ComplexityRouterConfigValue = {
tiers: {
Expand Down Expand Up @@ -147,6 +147,57 @@
expect(within(perTurnCharsSection).getByDisplayValue("200")).toBeInTheDocument();
});

it("should show the assistant-turns switch with its configured value when classifier_type is llm", () => {
const llmValue: ComplexityRouterConfigValue = {
...defaultValue,
classifier_type: "llm",
classifier_llm_config: { model: "gpt-3.5-turbo", timeout_ms: 750 },
classifier_context_include_assistant_turns: true,
};
renderWithProviders(<ComplexityRouterConfig modelInfo={mockModelInfo} value={llmValue} onChange={vi.fn()} />);

fireEvent.click(screen.getByText("Advanced: Classification Method"));

expect(screen.getByText("Include Assistant Turns")).toBeInTheDocument();
expect(screen.getByRole("switch", { name: "Include Assistant Turns" })).toBeChecked();
});

it("should render the assistant-turns switch off when it is not set", () => {
const llmValue: ComplexityRouterConfigValue = {
...defaultValue,
classifier_type: "llm",
classifier_llm_config: { model: "gpt-3.5-turbo", timeout_ms: 3000 },
};
renderWithProviders(<ComplexityRouterConfig modelInfo={mockModelInfo} value={llmValue} onChange={vi.fn()} />);

fireEvent.click(screen.getByText("Advanced: Classification Method"));

expect(screen.getByRole("switch", { name: "Include Assistant Turns" })).not.toBeChecked();
});

it("should hide the assistant-turns switch when classifier_type is heuristic", () => {
renderWithProviders(<ComplexityRouterConfig modelInfo={mockModelInfo} value={defaultValue} onChange={vi.fn()} />);
fireEvent.click(screen.getByText("Advanced: Classification Method"));
expect(screen.queryByText("Include Assistant Turns")).not.toBeInTheDocument();
});

it("should call onChange when the assistant-turns switch is toggled", () => {
const onChange = vi.fn();
const llmValue: ComplexityRouterConfigValue = {
...defaultValue,
classifier_type: "llm",
classifier_llm_config: { model: "gpt-3.5-turbo", timeout_ms: 3000 },
};
renderWithProviders(<ComplexityRouterConfig modelInfo={mockModelInfo} value={llmValue} onChange={onChange} />);

fireEvent.click(screen.getByText("Advanced: Classification Method"));
fireEvent.click(screen.getByRole("switch", { name: "Include Assistant Turns" }));

expect(onChange).toHaveBeenCalledWith(
expect.objectContaining({ classifier_context_include_assistant_turns: true }),
);
});

it("should hide classifier context fields when classifier_type is heuristic", () => {
renderWithProviders(<ComplexityRouterConfig modelInfo={mockModelInfo} value={defaultValue} onChange={vi.fn()} />);
fireEvent.click(screen.getByText("Advanced: Classification Method"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export interface ComplexityRouterConfigValue {
classifier_llm_config?: ClassifierLLMConfig;
classifier_context_window_size?: number;
classifier_context_per_turn_chars?: number;
classifier_context_include_assistant_turns?: boolean;
adaptive?: boolean;
adaptive_weights?: AdaptiveRouterWeights;
tier_distance_penalty?: number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
useEffect(() => {
const fetchModelAccessGroups = async () => {
const response = await modelAvailableCall(accessToken, "", "", false, null, true, true);
setModelAccessGroups(response["data"].map((model: any) => model["id"]));

Check warning on line 73 in ui/litellm-dashboard/src/components/add_model/add_auto_router_tab.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type
};
fetchModelAccessGroups();
}, [accessToken]);
Expand Down Expand Up @@ -101,6 +101,7 @@
classifier_llm_config: classifierLlmConfig,
classifier_context_window_size: classifierContextWindowSize,
classifier_context_per_turn_chars: classifierContextPerTurnChars,
classifier_context_include_assistant_turns: classifierContextIncludeAssistantTurns,
adaptive = false,
adaptive_weights: adaptiveWeights = DEFAULT_ADAPTIVE_WEIGHTS,
tier_distance_penalty: tierDistancePenalty = DEFAULT_TIER_DISTANCE_PENALTY,
Expand Down Expand Up @@ -128,9 +129,9 @@
return;
}

const defaultModel = tiers.MEDIUM[0] || tiers.SIMPLE[0] || tiers.COMPLEX[0] || tiers.REASONING[0];

Check warning on line 132 in ui/litellm-dashboard/src/components/add_model/add_auto_router_tab.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Boolean expression combines 4 conditions; extract it into a named variable

form.setFieldsValue({

Check warning on line 134 in ui/litellm-dashboard/src/components/add_model/add_auto_router_tab.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Object literal with 4 properties passed inline as an argument; assign it to a named variable first
custom_llm_provider: "auto_router",
model: name,
api_key: "not_required_for_auto_router",
Expand All @@ -146,6 +147,7 @@
classifierLlmConfig,
classifierContextWindowSize,
classifierContextPerTurnChars,
classifierContextIncludeAssistantTurns,
customTechnicalKeywords,
keywordTierRules,
semanticMatchingEnabled,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
classifierLlmConfig: undefined,
classifierContextWindowSize: undefined,
classifierContextPerTurnChars: undefined,
classifierContextIncludeAssistantTurns: undefined,
customTechnicalKeywords: [],
keywordTierRules: [],
semanticMatchingEnabled: false,
Expand Down Expand Up @@ -200,7 +201,7 @@
});

it("omits adaptive fields when adaptive is disabled even if weights linger in state", () => {
const config = buildComplexityRouterConfig({

Check warning on line 204 in ui/litellm-dashboard/src/components/add_model/build_complexity_router_config.test.ts

View workflow job for this annotation

GitHub Actions / frontend-lint

Object literal with 5 properties passed inline as an argument; assign it to a named variable first
...baseParams,
adaptive: false,
adaptiveWeights: { quality: 0.9, cost: 0.1 },
Expand All @@ -224,7 +225,7 @@
});

it("includes tier_distance_penalty when adaptive is enabled with eligible='all'", () => {
const config = buildComplexityRouterConfig({

Check warning on line 228 in ui/litellm-dashboard/src/components/add_model/build_complexity_router_config.test.ts

View workflow job for this annotation

GitHub Actions / frontend-lint

Object literal with 5 properties passed inline as an argument; assign it to a named variable first
...baseParams,
adaptive: true,
adaptiveWeights: { quality: 0.6, cost: 0.4 },
Expand All @@ -238,7 +239,7 @@
});

it("omits tier_distance_penalty when eligible='classified_tier', since the penalty doesn't apply there", () => {
const config = buildComplexityRouterConfig({

Check warning on line 242 in ui/litellm-dashboard/src/components/add_model/build_complexity_router_config.test.ts

View workflow job for this annotation

GitHub Actions / frontend-lint

Object literal with 5 properties passed inline as an argument; assign it to a named variable first
...baseParams,
adaptive: true,
adaptiveWeights: { quality: 0.6, cost: 0.4 },
Expand Down Expand Up @@ -318,3 +319,35 @@
).toBeNull();
});
});

describe("buildComplexityRouterConfig assistant turns", () => {
const llmParams: BuildComplexityRouterConfigParams = {
...baseParams,
classifierType: "llm",
classifierLlmConfig: { model: "gpt-4o-mini", timeout_ms: 3000 },
};

it("emits the field when the LLM classifier is selected", () => {
const config = buildComplexityRouterConfig({ ...llmParams, classifierContextIncludeAssistantTurns: true });
expect(config.classifier_context_include_assistant_turns).toBe(true);
});

it("emits the switch turned off, since false is a choice the operator made and not an absent value", () => {
const config = buildComplexityRouterConfig({ ...llmParams, classifierContextIncludeAssistantTurns: false });
expect(config.classifier_context_include_assistant_turns).toBe(false);
});

it("omits it when classifier_type is heuristic even if a value lingers in state", () => {
const config = buildComplexityRouterConfig({
...baseParams,
classifierType: "heuristic",
classifierContextIncludeAssistantTurns: true,
});
expect(config.classifier_context_include_assistant_turns).toBeUndefined();
});

it("omits it when unset, leaving the backend default", () => {
const config = buildComplexityRouterConfig(llmParams);
expect(config.classifier_context_include_assistant_turns).toBeUndefined();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface BuildComplexityRouterConfigParams {
classifierLlmConfig: ClassifierLLMConfig | undefined;
classifierContextWindowSize: number | undefined;
classifierContextPerTurnChars: number | undefined;
classifierContextIncludeAssistantTurns: boolean | undefined;
customTechnicalKeywords: string[];
keywordTierRules: KeywordTierRule[];
semanticMatchingEnabled: boolean;
Expand All @@ -33,6 +34,7 @@ export interface ComplexityRouterConfigPayload {
classifier_llm_config?: ClassifierLLMConfig;
classifier_context_window_size?: number;
classifier_context_per_turn_chars?: number;
classifier_context_include_assistant_turns?: boolean;
custom_technical_keywords?: string[];
keyword_tier_rules?: { keywords: string[]; tier: KeywordTierRule["tier"] }[];
semantic_keyword_matching?: boolean;
Expand Down Expand Up @@ -75,6 +77,7 @@ export const buildComplexityRouterConfig = ({
classifierLlmConfig,
classifierContextWindowSize,
classifierContextPerTurnChars,
classifierContextIncludeAssistantTurns,
customTechnicalKeywords,
keywordTierRules,
semanticMatchingEnabled,
Expand Down Expand Up @@ -103,6 +106,10 @@ export const buildComplexityRouterConfig = ({
classifierContextPerTurnChars !== undefined && {
classifier_context_per_turn_chars: classifierContextPerTurnChars,
}),
...(classifierType === "llm" &&
classifierContextIncludeAssistantTurns !== undefined && {
classifier_context_include_assistant_turns: classifierContextIncludeAssistantTurns,
}),
...(customTechnicalKeywords.length > 0 && { custom_technical_keywords: customTechnicalKeywords }),
...(cleanedKeywordTierRules.length > 0 && { keyword_tier_rules: cleanedKeywordTierRules }),
escalation_keywords: cleanedEscalationKeywords,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,51 @@ describe("buildUpdatedComplexityRouterConfig classifier context window", () => {
expect(result.classifier_context_per_turn_chars).toBeUndefined();
});
});

const STORED_ASSISTANT_CTX = {
tiers: { SIMPLE: ["gpt-4o-mini"], MEDIUM: [], COMPLEX: [], REASONING: [] },
classifier_type: "llm",
classifier_llm_config: { model: "gpt-4o-mini", timeout_ms: 3000 },
classifier_context_include_assistant_turns: true,
};

describe("buildUpdatedComplexityRouterConfig assistant turns", () => {
const formBase = {
tiers: STORED_ASSISTANT_CTX.tiers,
classifier_type: "llm" as const,
classifier_llm_config: STORED_ASSISTANT_CTX.classifier_llm_config,
};

it("round-trips an untouched edit without changing the value", () => {
const result = buildUpdatedComplexityRouterConfig(STORED_ASSISTANT_CTX, {
...formBase,
classifier_context_include_assistant_turns: true,
});
expect(result.classifier_context_include_assistant_turns).toBe(true);
});

it("persists turning assistant turns back off", () => {
// The off case is the one a preserved-config fallback would silently lose, since false and
// "absent" look alike to a truthiness check.
const result = buildUpdatedComplexityRouterConfig(STORED_ASSISTANT_CTX, {
...formBase,
classifier_context_include_assistant_turns: false,
});
expect(result.classifier_context_include_assistant_turns).toBe(false);
});

it("omits it when classifier_type is heuristic even if a value lingers in state", () => {
const result = buildUpdatedComplexityRouterConfig(STORED_ASSISTANT_CTX, {
tiers: STORED_ASSISTANT_CTX.tiers,
classifier_type: "heuristic" as const,
classifier_context_include_assistant_turns: true,
});
expect(result.classifier_context_include_assistant_turns).toBeUndefined();
});

it("does not resurrect a stale stored value once the form's own value is unset", () => {
// A MANAGED key: the form wins over the stored config, never falls back to it.
const result = buildUpdatedComplexityRouterConfig(STORED_ASSISTANT_CTX, formBase);
expect(result.classifier_context_include_assistant_turns).toBeUndefined();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,62 @@ describe("EditAutoRouterModal classifier context window", () => {
expect(savedConfig().classifier_context_window_size).toBe(8);
});
});

describe("EditAutoRouterModal assistant turns", () => {
beforeEach(() => {
modelPatchUpdateCall.mockClear();
});

const STORED_CONFIG = {
tiers: { SIMPLE: ["gpt-4o-mini"], MEDIUM: ["gpt-4o-mini"], COMPLEX: ["gpt-4o-mini"], REASONING: ["gpt-4o-mini"] },
classifier_type: "llm",
classifier_llm_config: { model: "gpt-4o-mini", timeout_ms: 3000 },
classifier_context_include_assistant_turns: true,
};

const renderModal = () =>
renderWithProviders(
<EditAutoRouterModal
isVisible
onCancel={vi.fn()}
onSuccess={vi.fn()}
modelData={{
...MODEL_DATA,
litellm_params: { ...MODEL_DATA.litellm_params, complexity_router_config: STORED_CONFIG },
}}
accessToken="token"
userRole="Admin"
/>,
);

// The create and edit stacks share the rendered control but duplicate the serializer, the
// hydrator and the managed-key set, so a field wired into only one of them fails here and
// nowhere else: the payload-builder unit tests are handed a form value assembled by hand.
it("shows the stored value and preserves it through an untouched open-and-save", async () => {
const user = userEvent.setup();
renderModal();

await user.click(await screen.findByText("Advanced: Classification Method"));
await screen.findByText("Include Assistant Turns");
expect(screen.getByRole("switch", { name: "Include Assistant Turns" })).toBeChecked();

await user.click(screen.getByRole("button", { name: /save changes/i }));

await waitFor(() => expect(modelPatchUpdateCall).toHaveBeenCalled());
expect(savedConfig().classifier_context_include_assistant_turns).toBe(true);
});

it("persists turning assistant turns off", async () => {
const user = userEvent.setup();
renderModal();

await user.click(await screen.findByText("Advanced: Classification Method"));
await screen.findByText("Include Assistant Turns");
await user.click(screen.getByRole("switch", { name: "Include Assistant Turns" }));

await user.click(screen.getByRole("button", { name: /save changes/i }));

await waitFor(() => expect(modelPatchUpdateCall).toHaveBeenCalled());
expect(savedConfig().classifier_context_include_assistant_turns).toBe(false);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
interface EditAutoRouterModalProps {
isVisible: boolean;
onCancel: () => void;
onSuccess: (updatedModel: any) => void;

Check warning on line 23 in ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type
modelData: any;

Check warning on line 24 in ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type
accessToken: string;
userRole: string;
}
Expand All @@ -35,6 +35,7 @@
"classifier_llm_config",
"classifier_context_window_size",
"classifier_context_per_turn_chars",
"classifier_context_include_assistant_turns",
"adaptive",
"adaptive_weights",
"tier_distance_penalty",
Expand Down Expand Up @@ -96,6 +97,10 @@
value.classifier_context_per_turn_chars !== undefined && {
classifier_context_per_turn_chars: value.classifier_context_per_turn_chars,
}),
...(value.classifier_type === "llm" &&
value.classifier_context_include_assistant_turns !== undefined && {
classifier_context_include_assistant_turns: value.classifier_context_include_assistant_turns,
}),
...(customTechnicalKeywords &&
customTechnicalKeywords.length > 0 && {
custom_technical_keywords: customTechnicalKeywords,
Expand Down Expand Up @@ -137,7 +142,7 @@
const [modelInfo, setModelInfo] = useState<ModelGroup[]>([]);
const [showCustomDefaultModel, setShowCustomDefaultModel] = useState<boolean>(false);
const [showCustomEmbeddingModel, setShowCustomEmbeddingModel] = useState<boolean>(false);
const [routerConfig, setRouterConfig] = useState<any>(null);

Check warning on line 145 in ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type
const [customTechnicalKeywords, setCustomTechnicalKeywords] = useState<string[]>([]);
const [keywordTierRules, setKeywordTierRules] = useState<KeywordTierRule[]>([]);
const [escalationKeywords, setEscalationKeywords] = useState<string[]>([]);
Expand Down Expand Up @@ -209,6 +214,10 @@
typeof parsedConfig.classifier_context_per_turn_chars === "number"
? parsedConfig.classifier_context_per_turn_chars
: undefined,
classifier_context_include_assistant_turns:
typeof parsedConfig.classifier_context_include_assistant_turns === "boolean"
? parsedConfig.classifier_context_include_assistant_turns
: undefined,
adaptive: parsedConfig.adaptive || false,
adaptive_weights: parsedConfig.adaptive_weights,
tier_distance_penalty: parsedConfig.tier_distance_penalty,
Expand Down
Loading