Merge pull request #33581 from BerriAI/litellm_lit4478_anthropic_auto_cache_ui

feat(ui): configure Anthropic automatic prompt caching from the Admin UI
This commit is contained in:
tin-berri 2026-07-17 23:15:58 -07:00 committed by GitHub
commit 3ba5266ab3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 467 additions and 42 deletions

View file

@ -1517,6 +1517,12 @@ LITELLM_SETTINGS_SAFE_DB_OVERRIDES = [
"cost_discount_config",
"cost_margin_config",
"budget_exceeded_throttle_percentage",
# Every field editable from the Admin UI (proxy_server._GENERAL_SETTINGS_UI_LITELLM_FIELDS)
# must be listed here so a DB write from one worker overrides the live litellm attribute on
# the others when config reloads; otherwise peer workers stay on their startup value.
# test_general_settings_ui_fields_are_db_overridable enforces that pairing.
"enable_anthropic_prompt_caching",
"anthropic_prompt_caching_ttl",
]
SPECIAL_LITELLM_AUTH_TOKEN = ["ui-token"]
DEFAULT_MANAGEMENT_OBJECT_IN_MEMORY_CACHE_TTL = int(os.getenv("DEFAULT_MANAGEMENT_OBJECT_IN_MEMORY_CACHE_TTL", 60))

View file

@ -1011,10 +1011,10 @@ class LiteLLM_ObjectPermissionBase(LiteLLMPydanticObjectBase):
mcp_tool_search_enabled: Optional[bool] = None
from litellm.models.team import BudgetLimitEntry as BudgetLimitEntry # noqa: E402
from litellm.types.object_permission import ( # noqa: E402
ObjectPermissionDict as ObjectPermissionDict,
)
from litellm.models.team import BudgetLimitEntry as BudgetLimitEntry # noqa: E402
class GenerateRequestBase(LiteLLMPydanticObjectBase):
@ -2122,6 +2122,8 @@ class ConfigList(LiteLLMPydanticObjectBase):
field_default_value: Any
premium_field: bool = False
nested_fields: Optional[List[FieldDetail]] = None # For nested dictionary or Pydantic fields
field_options: Optional[list[str]] = None # Allowed values, for field_type == "Select"
field_tab: Optional[str] = None # Admin UI sub-tab this field renders under; None groups it with the rest
class UserHeaderMapping(LiteLLMPydanticObjectBase):

View file

@ -28,6 +28,7 @@ from typing import (
Optional,
Set,
Tuple,
TypedDict,
Union,
cast,
get_args,
@ -39,6 +40,7 @@ import anyio
import websockets
import websockets.exceptions
from pydantic import BaseModel, Json, JsonValue
from typing_extensions import NotRequired, assert_never
from litellm._uuid import uuid
from litellm.constants import (
@ -363,15 +365,15 @@ from litellm.proxy.management_endpoints.cache_settings_endpoints import (
from litellm.proxy.management_endpoints.callback_management_endpoints import (
router as callback_management_endpoints_router,
)
from litellm.proxy.management_endpoints.coordination_redis_endpoints import (
get_persisted_coordination_redis_settings,
router as coordination_redis_settings_router,
)
from litellm.proxy.management_endpoints.common_utils import (
_user_has_admin_privileges,
_user_has_admin_view,
admin_can_invite_user,
)
from litellm.proxy.management_endpoints.coordination_redis_endpoints import (
get_persisted_coordination_redis_settings,
router as coordination_redis_settings_router,
)
from litellm.proxy.management_endpoints.cost_tracking_settings import (
router as cost_tracking_settings_router,
)
@ -14828,7 +14830,17 @@ async def get_config_general_settings(
)
_GENERAL_SETTINGS_UI_LITELLM_FIELDS: dict[str, dict[str, str]] = {
GeneralSettingsUILiteLLMValue = Union[float, bool, str, None]
class GeneralSettingsUILiteLLMFieldSpec(TypedDict):
type: Literal["Float", "Boolean", "Select"]
description: str
options: NotRequired[tuple[str, ...]]
tab: NotRequired[str] # Admin UI sub-tab this field renders under; None groups it with the rest
_GENERAL_SETTINGS_UI_LITELLM_FIELDS: dict[str, GeneralSettingsUILiteLLMFieldSpec] = {
"budget_exceeded_throttle_percentage": {
"type": "Float",
"description": (
@ -14837,18 +14849,60 @@ _GENERAL_SETTINGS_UI_LITELLM_FIELDS: dict[str, dict[str, str]] = {
"over-budget keys."
),
},
"enable_anthropic_prompt_caching": {
"type": "Boolean",
"tab": "prompt_caching",
"description": (
"Auto-adds cache_control to the system prompt and trailing turn for supported Anthropic "
"and Bedrock Claude models. The cache is shared across callers on the same upstream credentials."
),
},
"anthropic_prompt_caching_ttl": {
"type": "Select",
"options": ("5m", "1h"),
"tab": "prompt_caching",
"description": "Empty uses Anthropic's 5m default. 1h suits long sessions but doubles the cache write cost.",
},
}
def _validate_general_settings_ui_litellm_value(field_name: str, value: Any) -> Optional[float]:
def _general_settings_ui_litellm_default(
field_type: Literal["Float", "Boolean", "Select"],
) -> GeneralSettingsUILiteLLMValue:
"""The value a field falls back to when it is cleared or reset."""
return False if field_type == "Boolean" else None
def _validate_general_settings_ui_litellm_value(field_name: str, value: Any) -> GeneralSettingsUILiteLLMValue:
spec = _GENERAL_SETTINGS_UI_LITELLM_FIELDS[field_name]
field_type = spec["type"]
if value is None or value == "":
return None
if isinstance(value, bool) or not isinstance(value, (int, float)) or not (0 < float(value) <= 1):
raise HTTPException(
status_code=400,
detail={"error": f"{field_name} must be a number in (0, 1] or empty"},
)
return float(value)
return _general_settings_ui_litellm_default(field_type)
match field_type:
case "Boolean":
if not isinstance(value, bool):
raise HTTPException(
status_code=400,
detail={"error": f"{field_name} must be true or false"},
)
return value
case "Select":
options = spec.get("options", ())
if value not in options:
raise HTTPException(
status_code=400,
detail={"error": f"{field_name} must be one of: {', '.join(options)}, or empty"},
)
return cast(str, value) # cast-ok: membership in options proves it is one of the option strings
case "Float":
if isinstance(value, bool) or not isinstance(value, (int, float)) or not (0 < float(value) <= 1):
raise HTTPException(
status_code=400,
detail={"error": f"{field_name} must be a number in (0, 1] or empty"},
)
return float(value)
case _:
assert_never(field_type)
async def _persist_general_settings_ui_litellm_field(
@ -14869,11 +14923,12 @@ async def _persist_general_settings_ui_litellm_field(
async def _reset_general_settings_ui_litellm_field(field_name: str, user_api_key_dict: UserAPIKeyAuth) -> dict:
config = await proxy_config.get_config()
before_value = config.get("litellm_settings", {}).get(field_name)
setattr(litellm, field_name, None)
default_value = _general_settings_ui_litellm_default(_GENERAL_SETTINGS_UI_LITELLM_FIELDS[field_name]["type"])
setattr(litellm, field_name, default_value)
if "litellm_settings" in config:
config["litellm_settings"].pop(field_name, None)
await proxy_config.save_config(new_config=config)
asyncio.create_task(create_config_audit_log(field_name, "deleted", before_value, None, user_api_key_dict))
asyncio.create_task(create_config_audit_log(field_name, "deleted", before_value, default_value, user_api_key_dict))
return {"message": f"Field {field_name} reset", "status": "success"}
@ -15041,11 +15096,12 @@ async def get_config_list(
else {}
)
for litellm_field_name, spec in _GENERAL_SETTINGS_UI_LITELLM_FIELDS.items():
current_value: Optional[float] = getattr(litellm, litellm_field_name, None)
current_value: GeneralSettingsUILiteLLMValue = getattr(litellm, litellm_field_name, None)
default_value = _general_settings_ui_litellm_default(spec["type"])
stored_in_db_litellm: Optional[bool]
if litellm_field_name in db_litellm_settings:
stored_in_db_litellm = True
elif current_value is not None:
elif current_value != default_value:
stored_in_db_litellm = False
else:
stored_in_db_litellm = None
@ -15056,7 +15112,9 @@ async def get_config_list(
field_description=spec["description"],
field_value=current_value,
stored_in_db=stored_in_db_litellm,
field_default_value=None,
field_default_value=default_value,
field_options=list(spec.get("options", ())) or None,
field_tab=spec.get("tab"),
nested_fields=None,
)
)

View file

@ -8984,6 +8984,261 @@ async def test_update_config_field_throttle_persists_to_litellm_settings(monkeyp
assert saved["litellm_settings"]["budget_exceeded_throttle_percentage"] == 0.1
def test_get_config_list_includes_anthropic_prompt_caching_fields(monkeypatch):
"""The auto prompt caching flag and its ttl are litellm_settings globals surfaced on the
General Settings table, so an admin can turn caching on without hand-writing config. The
ttl is a Select and must ship its allowed values, or the table renders no editor for it."""
import types
from unittest.mock import AsyncMock, MagicMock
from fastapi.testclient import TestClient
import litellm.proxy.proxy_server as ps
from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth
from litellm.proxy.proxy_server import app
mock_prisma = MagicMock()
mock_config_table = MagicMock()
mock_config_table.find_first = AsyncMock(return_value=None)
mock_prisma.db = types.SimpleNamespace(litellm_config=mock_config_table)
monkeypatch.setattr(ps, "prisma_client", mock_prisma)
monkeypatch.setattr(litellm, "enable_anthropic_prompt_caching", True)
monkeypatch.setattr(litellm, "anthropic_prompt_caching_ttl", "1h")
app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth(
user_id="admin", user_role=LitellmUserRoles.PROXY_ADMIN
)
try:
client = TestClient(app)
resp = client.get("/config/list", params={"config_type": "general_settings"})
assert resp.status_code == 200, resp.text
fields = {item["field_name"]: item for item in resp.json()}
assert fields["enable_anthropic_prompt_caching"]["field_type"] == "Boolean"
assert fields["enable_anthropic_prompt_caching"]["field_value"] is True
assert fields["anthropic_prompt_caching_ttl"]["field_type"] == "Select"
assert fields["anthropic_prompt_caching_ttl"]["field_value"] == "1h"
assert fields["anthropic_prompt_caching_ttl"]["field_options"] == ["5m", "1h"]
# Both caching fields carry their sub-tab so the Admin UI can render them on a
# dedicated Prompt Caching tab, while ungrouped fields stay on General.
assert fields["enable_anthropic_prompt_caching"]["field_tab"] == "prompt_caching"
assert fields["anthropic_prompt_caching_ttl"]["field_tab"] == "prompt_caching"
assert fields["budget_exceeded_throttle_percentage"]["field_tab"] is None
finally:
app.dependency_overrides.clear()
def test_general_settings_ui_fields_are_db_overridable():
"""Every field the Admin UI can edit is a `litellm.<attr>` set via setattr on the handling
worker (`_persist_general_settings_ui_litellm_field`). Unless it is also in
LITELLM_SETTINGS_SAFE_DB_OVERRIDES, a config reload on a peer worker merges the DB value but
never applies it to the live attribute, so peer workers stay on their startup value.
This invariant is the guard against the two registries drifting: adding a UI-editable field
without enrolling it in the DB-override allowlist silently breaks cross-worker propagation.
"""
from litellm.constants import LITELLM_SETTINGS_SAFE_DB_OVERRIDES
from litellm.proxy.proxy_server import _GENERAL_SETTINGS_UI_LITELLM_FIELDS
missing = set(_GENERAL_SETTINGS_UI_LITELLM_FIELDS) - set(LITELLM_SETTINGS_SAFE_DB_OVERRIDES)
assert not missing, (
f"UI-editable litellm_settings fields missing from LITELLM_SETTINGS_SAFE_DB_OVERRIDES: {sorted(missing)}. "
"Add them, or they will not propagate to other workers when changed from the UI."
)
@pytest.mark.parametrize(
"field_name, db_value",
[
("enable_anthropic_prompt_caching", True),
("anthropic_prompt_caching_ttl", "1h"),
],
)
def test_prompt_caching_settings_propagate_on_config_reload(monkeypatch, field_name, db_value):
"""A UI toggle on one worker persists to the DB; a peer worker picks it up only when the
config reload applies the safe-override allowlist. Regression for the fields being absent
from that allowlist, which left peer workers stale."""
import litellm.proxy.proxy_server as ps
# peer worker booted with the opposite/absent value
monkeypatch.setattr(litellm, field_name, False if isinstance(db_value, bool) else None)
pc = ps.ProxyConfig()
pc._update_config_fields(
current_config={"litellm_settings": {}},
param_name="litellm_settings",
db_param_value={field_name: db_value},
)
assert getattr(litellm, field_name) == db_value
def test_get_config_list_marks_untouched_prompt_caching_flag_as_not_set(monkeypatch):
"""The flag defaults to False rather than None, so a plain 'is not None' check would
report the default as 'In Config' and imply an admin had set it."""
import types
from unittest.mock import AsyncMock, MagicMock
from fastapi.testclient import TestClient
import litellm.proxy.proxy_server as ps
from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth
from litellm.proxy.proxy_server import app
mock_prisma = MagicMock()
mock_config_table = MagicMock()
mock_config_table.find_first = AsyncMock(return_value=None)
mock_prisma.db = types.SimpleNamespace(litellm_config=mock_config_table)
monkeypatch.setattr(ps, "prisma_client", mock_prisma)
monkeypatch.setattr(litellm, "enable_anthropic_prompt_caching", False)
app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth(
user_id="admin", user_role=LitellmUserRoles.PROXY_ADMIN
)
try:
client = TestClient(app)
resp = client.get("/config/list", params={"config_type": "general_settings"})
fields = {item["field_name"]: item for item in resp.json()}
assert fields["enable_anthropic_prompt_caching"]["stored_in_db"] is None
finally:
app.dependency_overrides.clear()
@pytest.mark.parametrize(
"field_name, field_value",
[
("enable_anthropic_prompt_caching", True),
("enable_anthropic_prompt_caching", False),
("anthropic_prompt_caching_ttl", "5m"),
("anthropic_prompt_caching_ttl", "1h"),
],
)
@pytest.mark.asyncio
async def test_update_config_field_prompt_caching_persists_to_litellm_settings(monkeypatch, field_name, field_value):
"""Toggling either row must set litellm.<attr> live and persist under litellm_settings,
so the running proxy caches immediately and still does after a restart."""
from unittest.mock import MagicMock
import litellm.proxy.proxy_server as ps
from litellm.proxy._types import (
ConfigFieldUpdate,
LitellmUserRoles,
UserAPIKeyAuth,
)
from litellm.proxy.proxy_server import update_config_general_settings
saved: dict = {}
async def fake_get_config():
return {"litellm_settings": {}}
async def fake_save_config(new_config=None):
saved.update(new_config or {})
monkeypatch.setattr(ps.proxy_config, "get_config", fake_get_config)
monkeypatch.setattr(ps.proxy_config, "save_config", fake_save_config)
monkeypatch.setattr(ps, "prisma_client", MagicMock())
monkeypatch.setattr(litellm, "store_audit_logs", False)
monkeypatch.setattr(litellm, field_name, None)
admin = UserAPIKeyAuth(api_key="k", user_id="a", user_role=LitellmUserRoles.PROXY_ADMIN)
await update_config_general_settings(
data=ConfigFieldUpdate(field_name=field_name, field_value=field_value, config_type="general_settings"),
user_api_key_dict=admin,
)
assert getattr(litellm, field_name) == field_value
assert saved["litellm_settings"][field_name] == field_value
@pytest.mark.parametrize(
"field_name, bad_value",
[
("enable_anthropic_prompt_caching", "yes"),
("enable_anthropic_prompt_caching", 1),
("anthropic_prompt_caching_ttl", "10m"),
("anthropic_prompt_caching_ttl", "1H"),
("anthropic_prompt_caching_ttl", 3600),
],
)
@pytest.mark.asyncio
async def test_update_config_field_prompt_caching_rejects_invalid(monkeypatch, field_name, bad_value):
"""An unsupported ttl must be refused here rather than reaching Anthropic verbatim."""
from unittest.mock import MagicMock
from fastapi import HTTPException
import litellm.proxy.proxy_server as ps
from litellm.proxy._types import (
ConfigFieldUpdate,
LitellmUserRoles,
UserAPIKeyAuth,
)
from litellm.proxy.proxy_server import update_config_general_settings
async def fake_get_config():
return {"litellm_settings": {}}
monkeypatch.setattr(ps.proxy_config, "get_config", fake_get_config)
monkeypatch.setattr(ps, "prisma_client", MagicMock())
monkeypatch.setattr(litellm, field_name, None)
admin = UserAPIKeyAuth(api_key="k", user_id="a", user_role=LitellmUserRoles.PROXY_ADMIN)
with pytest.raises(HTTPException) as exc:
await update_config_general_settings(
data=ConfigFieldUpdate(field_name=field_name, field_value=bad_value, config_type="general_settings"),
user_api_key_dict=admin,
)
assert exc.value.status_code == 400
assert getattr(litellm, field_name) is None
@pytest.mark.parametrize(
"field_name, expected_default",
[
("enable_anthropic_prompt_caching", False),
("anthropic_prompt_caching_ttl", None),
("budget_exceeded_throttle_percentage", None),
],
)
@pytest.mark.asyncio
async def test_reset_config_field_restores_type_default(monkeypatch, field_name, expected_default):
"""Reset must restore each field's own default. Blanket None would leave the boolean flag
set to None, which is not a bool and would read as neither on nor off."""
from unittest.mock import MagicMock
import litellm.proxy.proxy_server as ps
from litellm.proxy._types import (
ConfigFieldDelete,
LitellmUserRoles,
UserAPIKeyAuth,
)
from litellm.proxy.proxy_server import delete_config_general_settings
saved: dict = {}
async def fake_get_config():
return {"litellm_settings": {field_name: "stale"}}
async def fake_save_config(new_config=None):
saved.update(new_config or {})
monkeypatch.setattr(ps.proxy_config, "get_config", fake_get_config)
monkeypatch.setattr(ps.proxy_config, "save_config", fake_save_config)
monkeypatch.setattr(ps, "prisma_client", MagicMock())
monkeypatch.setattr(litellm, "store_audit_logs", False)
monkeypatch.setattr(litellm, field_name, "stale")
admin = UserAPIKeyAuth(api_key="k", user_id="a", user_role=LitellmUserRoles.PROXY_ADMIN)
await delete_config_general_settings(
data=ConfigFieldDelete(field_name=field_name, config_type="general_settings"),
user_api_key_dict=admin,
)
assert getattr(litellm, field_name) is expected_default
assert field_name not in saved["litellm_settings"]
@pytest.mark.parametrize("bad_value", [0, -0.1, 1.5, True])
@pytest.mark.asyncio
async def test_update_config_field_throttle_rejects_invalid(monkeypatch, bad_value):

View file

@ -1079,7 +1079,7 @@
},
"src/app/(dashboard)/router-settings/_components/general_settings.tsx": {
"no-nested-ternary": {
"count": 3
"count": 1
},
"no-restricted-imports": {
"count": 2

View file

@ -7,6 +7,7 @@ import {
TableHeaderCell,
TableCell,
TableBody,
Title,
Text,
Button,
Icon,
@ -14,13 +15,18 @@ import {
} from "@tremor/react";
import { TabPanel, TabPanels, TabGroup, TabList, Tab } from "@tremor/react";
import { getGeneralSettingsCall, updateConfigFieldSetting, deleteConfigFieldSetting } from "@/components/networking";
import { InputNumber } from "antd";
import { InputNumber, Select as AntdSelect } from "antd";
import { TrashIcon } from "@heroicons/react/outline";
import { StatusBadge } from "@/components/shared/table_cells";
import RouterSettings from "@/components/router_settings";
import Fallbacks from "@/components/Settings/RouterSettings/Fallbacks/Fallbacks";
import RoutingGroups from "@/components/routing_groups";
const PROMPT_CACHING_TAB = "prompt_caching";
const ENABLE_ANTHROPIC_PROMPT_CACHING = "enable_anthropic_prompt_caching";
const ANTHROPIC_PROMPT_CACHING_TTL = "anthropic_prompt_caching_ttl";
interface GeneralSettingsPageProps {
accessToken: string | null;
userRole: string | null;
@ -33,8 +39,117 @@ interface generalSettingsItem {
field_value: any;
field_description: string;
stored_in_db: boolean | null;
field_options?: string[] | null;
field_tab?: string | null;
}
const SettingValueEditor: React.FC<{
setting: generalSettingsItem;
onChange: (fieldName: string, newValue: any) => void;
}> = ({ setting, onChange }) => {
if (setting.field_type === "Integer") {
return (
<InputNumber
step={1}
value={setting.field_value}
onChange={(newValue) => onChange(setting.field_name, newValue)}
/>
);
}
if (setting.field_type === "Boolean") {
return (
<Switch
checked={setting.field_value === true || setting.field_value === "true"}
onChange={(checked) => onChange(setting.field_name, checked)}
/>
);
}
if (setting.field_type === "Float") {
return (
<InputNumber
min={0}
max={1}
step={0.05}
value={setting.field_value}
onChange={(newValue) => onChange(setting.field_name, newValue)}
/>
);
}
if (setting.field_type === "Select") {
return (
<AntdSelect
allowClear
style={{ minWidth: "8rem" }}
placeholder="Default"
value={setting.field_value || undefined}
options={(setting.field_options ?? []).map((option) => ({ label: option, value: option }))}
onChange={(newValue) => onChange(setting.field_name, newValue ?? "")}
/>
);
}
return null;
};
const PromptCachingPanel: React.FC<{
accessToken: string;
settings: generalSettingsItem[];
onChange: (fieldName: string, newValue: any) => void;
}> = ({ accessToken, settings, onChange }) => {
const enableSetting = settings.find((s) => s.field_name === ENABLE_ANTHROPIC_PROMPT_CACHING);
const ttlSetting = settings.find((s) => s.field_name === ANTHROPIC_PROMPT_CACHING_TTL);
// The two rows come from the same registry the General tab reads; if they
// are not loaded yet there is nothing to render.
if (!enableSetting) {
return null;
}
const enabled = enableSetting.field_value === true || enableSetting.field_value === "true";
// Apply immediately: a toggle and a dropdown are direct controls, so there is
// no separate Update button. Clearing the ttl resets it to the provider default.
const persist = (fieldName: string, value: any) => {
onChange(fieldName, value);
if (value === "" || value === null || value === undefined) {
deleteConfigFieldSetting(accessToken, fieldName);
} else {
updateConfigFieldSetting(accessToken, fieldName, value);
}
};
return (
<Card>
<Title>Prompt Caching</Title>
<div className="mt-6 flex items-start justify-between gap-8">
<div className="max-w-2xl">
<Text className="font-medium">Automatic Anthropic prompt caching</Text>
<p className="mt-1 text-xs text-gray-500">{enableSetting.field_description}</p>
</div>
<Switch checked={enabled} onChange={(checked) => persist(ENABLE_ANTHROPIC_PROMPT_CACHING, checked)} />
</div>
{ttlSetting && (
<div className="mt-6 flex items-start justify-between gap-8">
<div className="max-w-2xl">
<Text className={`font-medium ${enabled ? "" : "text-gray-400"}`}>Cache lifetime (TTL)</Text>
<p className="mt-1 text-xs text-gray-500">{ttlSetting.field_description}</p>
</div>
<AntdSelect
allowClear
disabled={!enabled}
style={{ minWidth: "10rem" }}
placeholder="5m (default)"
value={ttlSetting.field_value || undefined}
options={(ttlSetting.field_options ?? []).map((option) => ({ label: option, value: option }))}
onChange={(newValue) => persist(ANTHROPIC_PROMPT_CACHING_TTL, newValue ?? "")}
/>
</div>
)}
</Card>
);
};
const GeneralSettings: React.FC<GeneralSettingsPageProps> = ({ accessToken, userRole, userID }) => {
const [generalSettings, setGeneralSettings] = useState<generalSettingsItem[]>([]);
@ -108,6 +223,7 @@ const GeneralSettings: React.FC<GeneralSettingsPageProps> = ({ accessToken, user
<Tab value="1">Loadbalancing</Tab>
<Tab value="2">Routing Groups</Tab>
<Tab value="3">Fallbacks</Tab>
<Tab value="5">Prompt Caching</Tab>
<Tab value="4">General</Tab>
</TabList>
<TabPanels className="px-8 py-6">
@ -120,6 +236,9 @@ const GeneralSettings: React.FC<GeneralSettingsPageProps> = ({ accessToken, user
<TabPanel>
<Fallbacks accessToken={accessToken} userRole={userRole} userID={userID} />
</TabPanel>
<TabPanel>
<PromptCachingPanel accessToken={accessToken} settings={generalSettings} onChange={handleInputChange} />
</TabPanel>
<TabPanel>
<Card>
<Table>
@ -133,7 +252,7 @@ const GeneralSettings: React.FC<GeneralSettingsPageProps> = ({ accessToken, user
</TableHead>
<TableBody>
{generalSettings
.filter((value) => value.field_type !== "TypedDictionary")
.filter((value) => value.field_type !== "TypedDictionary" && value.field_tab !== PROMPT_CACHING_TAB)
.map((value, index) => (
<TableRow key={index}>
<TableCell>
@ -150,26 +269,7 @@ const GeneralSettings: React.FC<GeneralSettingsPageProps> = ({ accessToken, user
</p>
</TableCell>
<TableCell>
{value.field_type == "Integer" ? (
<InputNumber
step={1}
value={value.field_value}
onChange={(newValue) => handleInputChange(value.field_name, newValue)}
/>
) : value.field_type == "Boolean" ? (
<Switch
checked={value.field_value === true || value.field_value === "true"}
onChange={(checked) => handleInputChange(value.field_name, checked)}
/>
) : value.field_type == "Float" ? (
<InputNumber
min={0}
max={1}
step={0.05}
value={value.field_value}
onChange={(newValue) => handleInputChange(value.field_name, newValue)}
/>
) : null}
<SettingValueEditor setting={value} onChange={handleInputChange} />
</TableCell>
<TableCell>
{value.stored_in_db == true ? (

View file

@ -22711,6 +22711,10 @@ export interface components {
field_description: string;
/** Field Name */
field_name: string;
/** Field Options */
field_options?: string[] | null;
/** Field Tab */
field_tab?: string | null;
/** Field Type */
field_type: string;
/** Field Value */