mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
fix(ui): block an auto-router save on any dry-run rejection, message or not
The gate read the verdict's error as well as its valid flag, so a rejection that carried no message fell through to the write and came back as the raw 400 the dry-run exists to replace. valid is derived from error server side today, which is why nothing had hit it, but the two arrive as independent fields and the UI should not depend on that derivation. Both forms now call one dryRunRejection, which gates on valid alone and falls back to its own message when the backend sent none. A transport failure still fails open as valid, leaving the write gate authoritative. Reported by Bugbot.
This commit is contained in:
parent
eb79ba4349
commit
5c6d2a8627
4 changed files with 41 additions and 4 deletions
|
|
@ -41,6 +41,7 @@ import {
|
|||
getPlanModeTierError,
|
||||
getSemanticConfigError,
|
||||
getTierLabelsError,
|
||||
dryRunRejection,
|
||||
} from "./build_complexity_router_config";
|
||||
import { activeTierName, activeTierRows, getCustomTierRowsError, resolveComplexityDefaultModel } from "./tier_rows";
|
||||
import { tierRowLabel } from "./complexity_router_tiers";
|
||||
|
|
@ -405,9 +406,10 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({
|
|||
complexityRouterConfigPayload as unknown as Record<string, unknown>,
|
||||
requiresTeamScope ? form.getValues("team_id") : undefined,
|
||||
);
|
||||
if (!serverVerdict.valid && serverVerdict.error) {
|
||||
const dryRunError = dryRunRejection(serverVerdict);
|
||||
if (dryRunError) {
|
||||
setShowValidationErrors(true);
|
||||
toast.fromError(serverVerdict.error);
|
||||
toast.fromError(dryRunError);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
getTierLabelsError,
|
||||
hydrateTierLabels,
|
||||
BuildComplexityRouterConfigParams,
|
||||
dryRunRejection,
|
||||
} from "./build_complexity_router_config";
|
||||
import { CUSTOM_TIER_RESTRICTIONS, activeTierRows } from "./tier_rows";
|
||||
|
||||
|
|
@ -958,3 +959,24 @@ describe("hydrateCustomTierSet", () => {
|
|||
expect(hydrated?.fallback_tier_id).toBe(hydrated?.tiers[0].id);
|
||||
});
|
||||
});
|
||||
|
||||
describe("dryRunRejection", () => {
|
||||
it("blocks the save on a rejection whose message is missing, which the write would return as a raw 400", () => {
|
||||
// The verdict's two fields arrive independently, so a rejection carrying no message must still
|
||||
// stop the save rather than fall through to the write endpoint.
|
||||
expect(dryRunRejection({ valid: false })).toBe("The proxy rejected this auto-router configuration");
|
||||
expect(dryRunRejection({ valid: false, error: null })).toBe("The proxy rejected this auto-router configuration");
|
||||
expect(dryRunRejection({ valid: false, error: " " })).toBe("The proxy rejected this auto-router configuration");
|
||||
});
|
||||
|
||||
it("surfaces the backend's own message when it sent one", () => {
|
||||
expect(dryRunRejection({ valid: false, error: "session_affinity cannot be combined with tier_definitions" })).toBe(
|
||||
"session_affinity cannot be combined with tier_definitions",
|
||||
);
|
||||
});
|
||||
|
||||
it("lets a valid verdict through, including the fail-open one a transport failure returns", () => {
|
||||
expect(dryRunRejection({ valid: true })).toBeNull();
|
||||
expect(dryRunRejection({ valid: true, error: null })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -124,6 +124,17 @@ export interface BuildComplexityRouterConfigParams {
|
|||
tierModelParams?: TierModelParamsByTier;
|
||||
}
|
||||
|
||||
/**
|
||||
* The message to surface when the dry-run rejects a save, or null to let it through.
|
||||
*
|
||||
* Gated on `valid` alone. The verdict's `valid` is derived from `error` server side today, but the
|
||||
* two arrive as independent fields, so reading `error` as the gate would let a rejection whose
|
||||
* message is missing or blank through to the write and back as a raw 400. A transport failure fails
|
||||
* open as `{valid: true}`, which this passes, leaving the write gate authoritative.
|
||||
*/
|
||||
export const dryRunRejection = (verdict: { valid: boolean; error?: string | null }): string | null =>
|
||||
verdict.valid ? null : verdict.error?.trim() || "The proxy rejected this auto-router configuration";
|
||||
|
||||
export interface TierDefinitionPayload {
|
||||
name: string;
|
||||
description?: string;
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import {
|
|||
hydrateCustomTierSet,
|
||||
hydratePlanModeMinTier,
|
||||
hydrateTierLabels,
|
||||
dryRunRejection,
|
||||
} from "../add_model/build_complexity_router_config";
|
||||
import { KeywordTierRule } from "../add_model/KeywordTierRules";
|
||||
import { DEFAULT_MATCH_THRESHOLD } from "../add_model/SemanticKeywordMatching";
|
||||
|
|
@ -562,9 +563,10 @@ const EditAutoRouterModal: React.FC<EditAutoRouterModalProps> = ({
|
|||
{ keywordTierRules, escalationKeywords, semanticMatchingEnabled, embeddingModel, matchThreshold },
|
||||
);
|
||||
const serverVerdict = await validateAutoRouterConfig(accessToken, updatedConfig, modelData?.model_info?.team_id);
|
||||
if (!serverVerdict.valid && serverVerdict.error) {
|
||||
const dryRunError = dryRunRejection(serverVerdict);
|
||||
if (dryRunError) {
|
||||
setShowValidationErrors(true);
|
||||
toast.fromError(serverVerdict.error);
|
||||
toast.fromError(dryRunError);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue