fix(ui): wire up cache type selectors and semantic round-trip

Backend derivation, field/type definitions, index state, and lint baseline for the Cache Type / Redis Deployment Type split.
This commit is contained in:
Devin AI 2026-07-09 14:20:55 +00:00
parent 3b89c594fa
commit 8dcc631dbb
7 changed files with 86 additions and 58 deletions

View file

@ -287,7 +287,9 @@ async def get_cache_settings(
# Derive redis_type for UI based on settings
# UI uses redis_type to show/hide fields, backend only stores 'type'
if decrypted_settings.get("type") == "redis":
if decrypted_settings.get("type") == "redis-semantic":
decrypted_settings["redis_type"] = "semantic"
elif decrypted_settings.get("type") == "redis":
if decrypted_settings.get("redis_startup_nodes"):
decrypted_settings["redis_type"] = "cluster"
elif decrypted_settings.get("sentinel_nodes"):

View file

@ -275,6 +275,38 @@ async def test_get_cache_settings_masks_password_bearing_url():
assert response.current_values["namespace"] == "ns"
@pytest.mark.asyncio
@pytest.mark.parametrize(
"stored_settings, expected_redis_type",
[
({"type": "redis-semantic", "host": "localhost"}, "semantic"),
({"type": "redis", "redis_startup_nodes": [{"host": "h", "port": "7001"}]}, "cluster"),
({"type": "redis", "sentinel_nodes": [["localhost", 26379]]}, "sentinel"),
({"type": "redis", "host": "localhost"}, "node"),
],
)
async def test_get_cache_settings_derives_redis_type_for_ui(stored_settings, expected_redis_type):
"""The UI's Cache Type/Redis Deployment Type selectors round-trip on reload: a
redis-semantic config must surface as 'semantic' (not fall back to node),
and redis topologies must resolve to cluster/sentinel/node."""
cache_row = MagicMock()
cache_row.cache_settings = json.dumps(stored_settings)
mock_prisma = MagicMock()
mock_prisma.db.litellm_cacheconfig.find_unique = AsyncMock(return_value=cache_row)
proxy_config = MagicMock()
proxy_config._decrypt_db_variables = MagicMock(side_effect=lambda variables_dict: dict(variables_dict))
with (
patch("litellm.proxy.proxy_server.prisma_client", mock_prisma),
patch("litellm.proxy.proxy_server.proxy_config", proxy_config),
):
response = await get_cache_settings(user_api_key_dict=_admin_auth())
assert response.current_values["redis_type"] == expected_redis_type
class TestCacheSettingsManager:
"""Tests for CacheSettingsManager class"""

View file

@ -45,11 +45,6 @@
"count": 1
}
},
"src/app/(dashboard)/caching/components/cache_settings/RedisTypeSelector.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"src/app/(dashboard)/caching/components/cache_settings/index.tsx": {
"no-restricted-imports": {
"count": 1

View file

@ -1,12 +0,0 @@
import { describe, expect, it } from "vitest";
import RedisTypeSelector from "./RedisTypeSelector";
import { render } from "@testing-library/react";
describe("RedisTypeSelector", () => {
it("should render the component", () => {
const { getAllByText } = render(
<RedisTypeSelector redisType="redis" redisTypeDescriptions={{}} onTypeChange={() => {}} />,
);
expect(getAllByText(/Redis/i).length).toBeGreaterThan(0);
});
});

View file

@ -1,27 +0,0 @@
import React from "react";
import { Select, SelectItem } from "@tremor/react";
interface RedisTypeSelectorProps {
redisType: string;
redisTypeDescriptions: Readonly<Record<string, string>>;
onTypeChange: (type: string) => void;
}
const RedisTypeSelector: React.FC<RedisTypeSelectorProps> = ({ redisType, redisTypeDescriptions, onTypeChange }) => {
return (
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700">Redis Type</label>
<Select value={redisType} onValueChange={onTypeChange}>
<SelectItem value="node">Node (Single Instance)</SelectItem>
<SelectItem value="cluster">Cluster</SelectItem>
<SelectItem value="sentinel">Sentinel</SelectItem>
<SelectItem value="semantic">Semantic</SelectItem>
</Select>
<p className="text-xs text-gray-500">
{redisTypeDescriptions[redisType] || "Select the type of Redis deployment you're using"}
</p>
</div>
);
};
export default RedisTypeSelector;

View file

@ -4,6 +4,10 @@ export type CacheFieldType = "string" | "password" | "integer" | "float" | "bool
export type RedisType = "node" | "cluster" | "sentinel" | "semantic";
export type CacheMode = "standard" | "semantic";
export type RedisDeploymentType = "node" | "cluster" | "sentinel";
export type CacheSection = "connection" | "cluster" | "sentinel" | "semantic" | "ssl" | "cacheManagement" | "gcp";
export type CacheFieldRule = NonNullable<FormItemProps["rules"]>[number];
@ -19,13 +23,30 @@ export interface CacheField {
readonly rules?: CacheFieldRule[];
}
export const REDIS_TYPES: readonly RedisType[] = ["node", "cluster", "sentinel", "semantic"];
export const CACHE_MODES: readonly CacheMode[] = ["standard", "semantic"];
export const REDIS_TYPE_DESCRIPTIONS: Readonly<Record<RedisType, string>> = {
export const REDIS_DEPLOYMENT_TYPES: readonly RedisDeploymentType[] = ["node", "cluster", "sentinel"];
export const CACHE_MODE_LABELS: Readonly<Record<CacheMode, string>> = {
standard: "Standard (exact match)",
semantic: "Semantic (similarity-based)",
};
export const CACHE_MODE_DESCRIPTIONS: Readonly<Record<CacheMode, string>> = {
standard: "Exact-match caching that reuses a response only when a request matches a cached one exactly",
semantic: "Embedding-based caching that reuses responses for semantically similar prompts (single-node Redis only)",
};
export const REDIS_DEPLOYMENT_LABELS: Readonly<Record<RedisDeploymentType, string>> = {
node: "Node (Single Instance)",
cluster: "Cluster",
sentinel: "Sentinel",
};
export const REDIS_DEPLOYMENT_DESCRIPTIONS: Readonly<Record<RedisDeploymentType, string>> = {
node: "Standard Redis node/single instance",
cluster: "Redis Cluster mode for high availability and horizontal scaling",
sentinel: "Redis Sentinel mode for high availability with automatic failover",
semantic: "Semantic caching that reuses responses for similar prompts",
};
const portRule: CacheFieldRule = {

View file

@ -4,10 +4,10 @@ import { Form } from "antd";
import { getCacheSettingsCall, testCacheConnectionCall, updateCacheSettingsCall } from "@/components/networking";
import { fetchAvailableModels, ModelGroup } from "@/components/llm_calls/fetch_models";
import NotificationsManager from "@/components/molecules/notifications_manager";
import RedisTypeSelector from "./RedisTypeSelector";
import CacheTypeSelector from "./CacheTypeSelector";
import CacheFieldSection from "./CacheFieldSection";
import { EmbeddingModelOption } from "./CacheFormField";
import { REDIS_TYPES, REDIS_TYPE_DESCRIPTIONS, RedisType } from "./cacheSettingsFields";
import { CacheMode, REDIS_DEPLOYMENT_TYPES, RedisDeploymentType, RedisType } from "./cacheSettingsFields";
import { buildCachePayload, buildInitialValues, CacheFormValues } from "./cacheSettingsUtils";
interface CacheSettingsProps {
@ -16,12 +16,26 @@ interface CacheSettingsProps {
userID: string | null;
}
const toRedisType = (value: unknown): RedisType =>
REDIS_TYPES.includes(value as RedisType) ? (value as RedisType) : "node";
interface CacheSelection {
cacheMode: CacheMode;
deploymentType: RedisDeploymentType;
}
const toCacheSelection = (value: unknown): CacheSelection => {
if (value === "semantic") {
return { cacheMode: "semantic", deploymentType: "node" };
}
const deploymentType = REDIS_DEPLOYMENT_TYPES.includes(value as RedisDeploymentType)
? (value as RedisDeploymentType)
: "node";
return { cacheMode: "standard", deploymentType };
};
const CacheSettings: React.FC<CacheSettingsProps> = ({ accessToken }) => {
const [form] = Form.useForm<CacheFormValues>();
const [redisType, setRedisType] = useState<RedisType>("node");
const [cacheMode, setCacheMode] = useState<CacheMode>("standard");
const [deploymentType, setDeploymentType] = useState<RedisDeploymentType>("node");
const redisType: RedisType = cacheMode === "semantic" ? "semantic" : deploymentType;
const [embeddingModels, setEmbeddingModels] = useState<EmbeddingModelOption[]>([]);
const [isTesting, setIsTesting] = useState<boolean>(false);
const [isSaving, setIsSaving] = useState<boolean>(false);
@ -34,7 +48,9 @@ const CacheSettings: React.FC<CacheSettingsProps> = ({ accessToken }) => {
const data = (await getCacheSettingsCall(accessToken)) as { current_values?: Record<string, unknown> };
const currentValues = data.current_values ?? {};
form.setFieldsValue(buildInitialValues(currentValues));
setRedisType(toRedisType(currentValues.redis_type));
const selection = toCacheSelection(currentValues.redis_type);
setCacheMode(selection.cacheMode);
setDeploymentType(selection.deploymentType);
} catch (error) {
console.error("Failed to load cache settings:", error);
NotificationsManager.fromBackend("Failed to load cache settings");
@ -132,10 +148,11 @@ const CacheSettings: React.FC<CacheSettingsProps> = ({ accessToken }) => {
<p className="text-xs text-gray-500 mt-1">Configure Redis cache for LiteLLM</p>
</div>
<RedisTypeSelector
redisType={redisType}
redisTypeDescriptions={REDIS_TYPE_DESCRIPTIONS}
onTypeChange={(type) => setRedisType(toRedisType(type))}
<CacheTypeSelector
cacheMode={cacheMode}
deploymentType={deploymentType}
onCacheModeChange={setCacheMode}
onDeploymentTypeChange={setDeploymentType}
/>
<div className="pt-4 border-t border-gray-200">