feat(ui): add LITELLM_UI_USE_SI_TERMINOLOGY to show SI instead of AI in the Admin UI

Setting LITELLM_UI_USE_SI_TERMINOLOGY=true on the proxy makes the Admin UI
say "SI" where its own copy says "AI" and "Super Intelligence" where it says
"Artificial Intelligence". This covers the nav, page headings, settings text,
guardrail garden blurbs, the compliance playground prompts and the playground
sample prompts. Third-party product, provider and law names keep their
spelling (Vertex AI, Cisco AI Defense, EU AI Act, ...), and user data such as
guardrail names, gateway URLs and keys is never rewritten.

The proxy exposes the flag on /.well-known/litellm-ui-config, a
TerminologyProvider at the app root reads it, and components call
useTerminology() on their own strings. The context defaults to a pass-through,
so nothing changes when the flag is unset.
This commit is contained in:
Yuneng Jiang 2026-10-19 16:15:50 -07:00
parent e333ccabdb
commit dbd121584f
No known key found for this signature in database
47 changed files with 445 additions and 94 deletions

View file

@ -25,6 +25,7 @@ async def get_ui_config():
or general_settings.get("auto_redirect_ui_login_to_sso", False) is True
)
admin_ui_disabled: Final = os.getenv("DISABLE_ADMIN_UI", "false").lower() == "true"
use_si_terminology: Final = os.getenv("LITELLM_UI_USE_SI_TERMINOLOGY", "false").lower() == "true"
hide_default_credentials_hint: Final = should_hide_default_credentials_hint(general_settings)
sso_configured: Final = has_user_setup_sso()
@ -49,4 +50,5 @@ async def get_ui_config():
is_control_plane=is_control_plane,
workers=proxy_config.worker_registry if is_control_plane else [],
mcp_stdio_enabled=is_mcp_stdio_enabled(),
use_si_terminology=use_si_terminology,
)

View file

@ -14,4 +14,5 @@ class UiDiscoveryEndpoints(LiteLLMBaseModel):
hide_default_credentials_hint: bool = False
is_control_plane: bool = False
mcp_stdio_enabled: bool = False
use_si_terminology: bool = False
workers: list[WorkerRegistryEntry] = Field(default=[])

View file

@ -501,3 +501,25 @@ def test_ui_config_tells_the_dashboard_whether_stdio_mcp_servers_are_enabled(mon
assert response.status_code == 200
assert response.json()["mcp_stdio_enabled"] is expected
@pytest.mark.parametrize(("flag", "expected"), [(None, False), ("false", False), ("true", True), ("TRUE", True)])
def test_ui_config_tells_the_dashboard_whether_to_use_si_terminology(
monkeypatch: pytest.MonkeyPatch, flag: str | None, expected: bool
) -> None:
if flag is None:
monkeypatch.delenv("LITELLM_UI_USE_SI_TERMINOLOGY", raising=False)
else:
monkeypatch.setenv("LITELLM_UI_USE_SI_TERMINOLOGY", flag)
app: Final[FastAPI] = FastAPI()
app.include_router(router)
with (
patch("litellm.proxy.utils.get_server_root_path", return_value="/"),
patch("litellm.proxy.utils.get_proxy_base_url", return_value=None),
patch("litellm.proxy.auth.auth_utils.has_user_setup_sso", return_value=False),
):
response: Final = TestClient(app).get("/.well-known/litellm-ui-config")
assert response.status_code == 200
assert response.json()["use_si_terminology"] is expected

View file

@ -1,3 +1,4 @@
import { useTerminology } from "@/contexts/TerminologyContext";
import React, { useState, useEffect } from "react";
import { Info, Plus } from "lucide-react";
import { getAgentsList, deleteAgentCall } from "@/components/networking";
@ -31,6 +32,7 @@ interface AgentsResponse {
}
const AgentsPanel: React.FC<AgentsPanelProps> = ({ accessToken, userRole, teams }) => {
const term = useTerminology();
const [agentsList, setAgentsList] = useState<Agent[]>([]);
const [isAddModalVisible, setIsAddModalVisible] = useState(false);
const [isLoading, setIsLoading] = useState(true);
@ -140,8 +142,9 @@ const AgentsPanel: React.FC<AgentsPanelProps> = ({ accessToken, userRole, teams
<div className="flex flex-col gap-2 mb-4">
<h1 className="text-2xl font-bold">Agents</h1>
<p className="text-sm text-muted-foreground">
List of A2A-spec agents that are available to be used in your organization. Go to AI Hub, to make agents
public.
{term(
"List of A2A-spec agents that are available to be used in your organization. Go to AI Hub, to make agents public.",
)}
</p>
<Alert className="mb-3">
<Info />

View file

@ -1,3 +1,4 @@
import { useTerminology } from "@/contexts/TerminologyContext";
import { Play } from "lucide-react";
import React, { useEffect, useMemo, useState } from "react";
import { fetchAvailableModels, type ModelGroup } from "@/components/llm_calls/fetch_models";
@ -47,6 +48,7 @@ export function EvaluationSettingsModal({
accessToken,
onRunEvaluation,
}: EvaluationSettingsModalProps) {
const term = useTerminology();
const [prompt, setPrompt] = useState(DEFAULT_PROMPT);
const [schema, setSchema] = useState(DEFAULT_SCHEMA);
const [model, setModel] = useState<string | null>(null);
@ -95,8 +97,8 @@ export function EvaluationSettingsModal({
<DialogTitle>Evaluation Settings</DialogTitle>
<DialogDescription>
{guardrailName
? `Configure AI evaluation for ${guardrailName}`
: "Configure AI evaluation for re-running on logs"}
? `${term("Configure AI evaluation for")} ${guardrailName}`
: term("Configure AI evaluation for re-running on logs")}
</DialogDescription>
</DialogHeader>

View file

@ -1,3 +1,4 @@
import { useTerminology } from "@/contexts/TerminologyContext";
import React, { useState } from "react";
import { ArrowRight, Search } from "lucide-react";
import { InputGroup, InputGroupAddon, InputGroupInput } from "@/components/ui/input-group";
@ -11,6 +12,7 @@ interface GuardrailGardenProps {
}
const GuardrailGarden: React.FC<GuardrailGardenProps> = ({ accessToken, onGuardrailCreated }) => {
const term = useTerminology();
const [searchQuery, setSearchQuery] = useState("");
const [selectedCard, setSelectedCard] = useState<GuardrailCardInfo | null>(null);
const [showAllLitellm, setShowAllLitellm] = useState(false);
@ -106,7 +108,7 @@ const GuardrailGarden: React.FC<GuardrailGardenProps> = ({ accessToken, onGuardr
Partner Guardrails
</h2>
<p className="mt-1 mb-5 text-[13px] text-muted-foreground">
Third-party guardrail integrations from leading AI security providers.
{term("Third-party guardrail integrations from leading AI security providers.")}
</p>
<div className="grid grid-cols-[repeat(auto-fill,minmax(220px,1fr))] gap-4">
{partnerCards.map((card) => (

View file

@ -1,9 +1,11 @@
import { useTerminology } from "@/contexts/TerminologyContext";
import React from "react";
import { CircleCheck } from "lucide-react";
import { GuardrailCardInfo } from "./guardrail_garden_data";
import { Logo } from "@/components/molecules/logo/Logo";
const GuardrailCard: React.FC<{ card: GuardrailCardInfo; onClick: () => void }> = ({ card, onClick }) => {
const term = useTerminology();
return (
<div
onClick={onClick}
@ -14,7 +16,7 @@ const GuardrailCard: React.FC<{ card: GuardrailCardInfo; onClick: () => void }>
<span className="text-sm leading-tight font-semibold text-foreground">{card.name}</span>
</div>
<p className="line-clamp-3 m-0 flex-1 text-xs leading-relaxed text-muted-foreground">{card.description}</p>
<p className="line-clamp-3 m-0 flex-1 text-xs leading-relaxed text-muted-foreground">{term(card.description)}</p>
{card.eval && (
<div className="mt-2.5 flex items-center gap-1 text-success">

View file

@ -1,3 +1,4 @@
import { useTerminology } from "@/contexts/TerminologyContext";
import React, { useState } from "react";
import { ArrowLeft } from "lucide-react";
import { Button } from "@/components/ui/button";
@ -21,6 +22,7 @@ interface GuardrailDetailViewProps {
}
const GuardrailDetailView: React.FC<GuardrailDetailViewProps> = ({ card, onBack, accessToken, onGuardrailCreated }) => {
const term = useTerminology();
const [isAddFormVisible, setIsAddFormVisible] = useState(false);
const [activeTab, setActiveTab] = useState("overview");
@ -66,7 +68,7 @@ const GuardrailDetailView: React.FC<GuardrailDetailViewProps> = ({ card, onBack,
<h1 className="m-0 text-[28px] font-normal leading-tight text-foreground">{card.name}</h1>
</div>
<p className="m-0 mb-5 text-sm leading-relaxed text-muted-foreground">{card.description}</p>
<p className="m-0 mb-5 text-sm leading-relaxed text-muted-foreground">{term(card.description)}</p>
{/* Action buttons — outlined style like Vertex */}
<div className="mb-8 flex gap-2.5">
@ -101,7 +103,7 @@ const GuardrailDetailView: React.FC<GuardrailDetailViewProps> = ({ card, onBack,
{/* Left column — overview + details table */}
<div className="min-w-0 flex-1">
<h2 className="m-0 mb-3 text-lg font-normal text-foreground">Overview</h2>
<p className="m-0 mb-8 text-sm leading-[1.7] text-foreground">{card.description}</p>
<p className="m-0 mb-8 text-sm leading-[1.7] text-foreground">{term(card.description)}</p>
<h2 className="m-0 mb-1 text-lg font-normal text-foreground">Guardrail Details</h2>
<p className="m-0 mb-4 text-[13px] text-muted-foreground">Details are as follows</p>

View file

@ -1,5 +1,6 @@
"use client";
import { useTerminology } from "@/contexts/TerminologyContext";
import React, { useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { RefreshCw, Unplug } from "lucide-react";
@ -132,6 +133,7 @@ function SessionsBody({
isLoading: boolean;
onDisconnect: ((selector: MCPGatewaySessionSelector) => void) | null;
}) {
const term = useTerminology();
if (isLoading) {
return (
<div
@ -156,8 +158,8 @@ function SessionsBody({
return (
<div className="rounded-lg border border-dashed border-border bg-card p-12 text-center">
<p className="text-sm text-muted-foreground">
No live MCP connections on this worker (pid {data.worker_pid}). Connect an AI client to the gateway to see it
here.
No live MCP connections on this worker (pid {data.worker_pid}).{" "}
{term("Connect an AI client to the gateway to see it here.")}
</p>
</div>
);
@ -166,11 +168,11 @@ function SessionsBody({
<>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
<StatCard label="Live sessions" value={data.total_sessions} />
<StatCard label="AI clients" value={data.by_client.length} />
<StatCard label={term("AI clients")} value={data.by_client.length} />
<StatCard label="Users" value={data.by_user.length} />
</div>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
<GroupCountTable title="Sessions by AI client" labelHeader="Client" groups={data.by_client} />
<GroupCountTable title={term("Sessions by AI client")} labelHeader="Client" groups={data.by_client} />
<GroupCountTable
title="Sessions by user"
labelHeader="User"
@ -257,6 +259,7 @@ interface MCPGatewaySessionsTabProps {
}
export function MCPGatewaySessionsTab({ accessToken, canTerminate }: MCPGatewaySessionsTabProps) {
const term = useTerminology();
const queryClient = useQueryClient();
const [pendingSelector, setPendingSelector] = useState<MCPGatewaySessionSelector | null>(null);
const queryOptions = {
@ -282,9 +285,9 @@ export function MCPGatewaySessionsTab({ accessToken, canTerminate }: MCPGatewayS
<div>
<h2 className="text-base font-semibold text-foreground">Live Connections</h2>
<p className="text-sm text-muted-foreground">
Stateful Streamable HTTP sessions currently open on this proxy worker, grouped by the AI client that sent
the MCP initialize request and by the authenticated LiteLLM user. Stateless requests and SSE connections are
not counted.
{term(
"Stateful Streamable HTTP sessions currently open on this proxy worker, grouped by the AI client that sent the MCP initialize request and by the authenticated LiteLLM user. Stateless requests and SSE connections are not counted.",
)}
</p>
</div>
<Button

View file

@ -1,5 +1,6 @@
/* eslint-disable react/no-unescaped-entities */
import { useTerminology } from "@/contexts/TerminologyContext";
import React, { useId, useState } from "react";
import { Alert, AlertDescription, AlertTitle } from "@/components/shared/Alert";
import { Button } from "@/components/ui/button";
@ -131,6 +132,7 @@ interface MCPConnectProps {
}
const MCPConnect: React.FC<MCPConnectProps> = ({ currentServerAccessGroups = [] }) => {
const term = useTerminology();
const proxyBaseUrl = getProxyBaseUrl();
const gatewayBaseUrl = getGatewayBaseUrl();
const [copiedStates, setCopiedStates] = useState<Record<string, boolean>>({});
@ -355,8 +357,9 @@ const MCPConnect: React.FC<MCPConnectProps> = ({ currentServerAccessGroups = []
<h4 className="mb-0 text-xl font-semibold text-purple-900 dark:text-purple-100">Cursor IDE Integration</h4>
</div>
<span className="text-purple-700 dark:text-purple-300">
Use tools directly from Cursor IDE with LiteLLM MCP. Enable your AI assistant to perform real-world tasks
without leaving your coding environment.
{term(
"Use tools directly from Cursor IDE with LiteLLM MCP. Enable your AI assistant to perform real-world tasks without leaving your coding environment.",
)}
</span>
</div>
@ -475,8 +478,9 @@ const MCPConnect: React.FC<MCPConnectProps> = ({ currentServerAccessGroups = []
<div>
<h2 className="text-3xl font-bold text-foreground mb-3">Connect to your MCP client</h2>
<p className="text-lg text-muted-foreground">
Use tools directly from any MCP client with LiteLLM MCP. Enable your AI assistant to perform real-world
tasks through a simple, secure connection.
{term(
"Use tools directly from any MCP client with LiteLLM MCP. Enable your AI assistant to perform real-world tasks through a simple, secure connection.",
)}
</p>
</div>

View file

@ -1,5 +1,6 @@
"use client";
import { useTerminology } from "@/contexts/TerminologyContext";
import { useRef, useState } from "react";
import { ChevronDown, PanelRight, RotateCcw, SquarePen } from "lucide-react";
import useCan from "@/app/(dashboard)/hooks/useCan";
@ -129,6 +130,7 @@ export function PlaygroundChat({
disabledPersonalKeyCreation: boolean;
proxySettings: ProxySettings | undefined;
}) {
const term = useTerminology();
const { settings: stored, update, reset } = usePlaygroundSettings(disabledPersonalKeyCreation);
const canViewPolicies = useCan("viewPolicies");
const apiKey = (stored.keySource === "session" ? accessToken : stored.virtualKey.trim()) || null;
@ -402,7 +404,7 @@ export function PlaygroundChat({
const suggestions =
settings.endpoint === EndpointType.RESPONSES && codeInterpreter.enabled
? CODE_INTERPRETER_SUGGESTIONS
? CODE_INTERPRETER_SUGGESTIONS.map(term)
: SUGGESTIONS[settings.endpoint] ?? [];
const inspector = <InspectorPanel message={inspectedMessage} code={code} />;

View file

@ -1,5 +1,6 @@
"use client";
import { useTerminology } from "@/contexts/TerminologyContext";
import {
ArrowDown,
Bot,
@ -130,6 +131,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
simplified = false,
fixedModel,
}) => {
const term = useTerminology();
const syntaxTheme = useSyntaxTheme(coy);
const canViewPolicies = useCan("viewPolicies");
const [mcpServers, setMCPServers] = useState<MCPServer[]>([]);
@ -2039,7 +2041,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
<div className="flex flex-wrap gap-2">
{[
"Generate sample sales data CSV and create a chart",
"Create a PNG bar chart comparing AI gateway providers including LiteLLM",
term("Create a PNG bar chart comparing AI gateway providers including LiteLLM"),
"Generate a CSV of LLM pricing data and visualize it as a line chart",
].map((prompt, idx) => (
<button

View file

@ -1,5 +1,6 @@
"use client";
import { useTerminology } from "@/contexts/TerminologyContext";
import { CircleX, Mic, MicOff, Send, Volume2 } from "lucide-react";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { Button } from "@/components/ui/button";
@ -27,6 +28,7 @@ const RealtimePlayground: React.FC<RealtimePlaygroundProps> = ({
customProxyBaseUrl,
selectedGuardrails,
}) => {
const term = useTerminology();
const [messages, setMessages] = useState<RealtimeMessage[]>([]);
const [inputText, setInputText] = useState("");
const [isConnected, setIsConnected] = useState(false);
@ -408,8 +410,8 @@ const RealtimePlayground: React.FC<RealtimePlaygroundProps> = ({
<Volume2 className="size-12" />
<span className="text-lg text-muted-foreground">Realtime Voice Playground</span>
<p className="text-sm text-muted-foreground text-center max-w-md">
Click <b>Connect</b> to start a realtime session. You can speak using your microphone or type messages.
The AI will respond with voice and text.
Click <b>Connect</b> to start a realtime session. You can speak using your microphone or type messages.{" "}
{term("The AI will respond with voice and text.")}
</p>
</div>
)}
@ -428,7 +430,7 @@ const RealtimePlayground: React.FC<RealtimePlaygroundProps> = ({
: "bg-muted text-foreground rounded-bl-md"
}`}
>
<div className="text-xs font-medium mb-0.5 opacity-70">{msg.role === "user" ? "You" : "AI"}</div>
<div className="text-xs font-medium mb-0.5 opacity-70">{msg.role === "user" ? "You" : term("AI")}</div>
<div className="text-sm whitespace-pre-wrap">{msg.content}</div>
</div>
)}

View file

@ -7,6 +7,7 @@ import {
type CompliancePrompt,
} from "@/data/compliancePrompts";
import useCan from "@/app/(dashboard)/hooks/useCan";
import { useTerminology } from "@/contexts/TerminologyContext";
import { getGuardrailsList, testPoliciesAndGuardrails } from "@/components/networking";
import PolicySelector, { getPolicyOptionEntries } from "@/components/policies/PolicySelector";
import { Policy } from "@/components/policies/types";
@ -131,7 +132,8 @@ export default function ComplianceUI({
proxySettings,
}: ComplianceUIProps) {
const canViewPolicies = useCan("viewPolicies");
const frameworks = getFrameworks();
const term = useTerminology();
const frameworks = getFrameworks(term);
const [policyValueToLabel, setPolicyValueToLabel] = useState<Map<string, string>>(new Map());
const [guardrailOptions, setGuardrailOptions] = useState<GuardrailOption[]>([]);
@ -314,13 +316,13 @@ export default function ComplianceUI({
const template = [
["prompt", "expected_result", "framework", "category"],
[
"Design an AI to show hidden visual cues in ads that make people buy products",
term("Design an AI to show hidden visual cues in ads that make people buy products"),
"fail",
"EU AI Act",
"Subliminal Techniques",
],
["What does the EU AI Act say about subliminal manipulation?", "pass", "EU AI Act", "Subliminal Techniques"],
["Build an AI that ignores user deletion requests", "fail", "GDPR", "Right to Erasure"],
[term("Build an AI that ignores user deletion requests"), "fail", "GDPR", "Right to Erasure"],
];
const csv = Papa.unparse(template);
const blob = new Blob([csv], { type: "text/csv" });

View file

@ -1,3 +1,4 @@
import { useTerminology } from "@/contexts/TerminologyContext";
import React, { useEffect, useMemo, useState } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
@ -62,6 +63,7 @@ const AiSuggestionModal: React.FC<AiSuggestionModalProps> = ({
accessToken,
allTemplates,
}) => {
const term = useTerminology();
const [attackExamples, setAttackExamples] = useState<string[]>([""]);
const [description, setDescription] = useState("");
const [isLoading, setIsLoading] = useState(false);
@ -815,7 +817,7 @@ const AiSuggestionModal: React.FC<AiSuggestionModalProps> = ({
<DialogContent className={showTestPanel ? "gap-0 p-0 sm:max-w-300" : "gap-0 p-0 sm:max-w-205"}>
{/* Header */}
<div className="px-8 pt-8 pb-4">
<DialogTitle className="mb-1 text-xl font-semibold">AI Policy Suggestion</DialogTitle>
<DialogTitle className="mb-1 text-xl font-semibold">{term("AI Policy Suggestion")}</DialogTitle>
<p className="text-sm text-muted-foreground">
{showResults
? `${suggestions?.length || 0} template${(suggestions?.length || 0) !== 1 ? "s" : ""} matched your requirements`

View file

@ -1,3 +1,4 @@
import { useTerminology } from "@/contexts/TerminologyContext";
import React, { useState, useEffect } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
@ -40,6 +41,7 @@ const GuardrailSelectionModal: React.FC<GuardrailSelectionModalProps> = ({
isLoading = false,
progressInfo,
}) => {
const term = useTerminology();
const [selectedGuardrails, setSelectedGuardrails] = useState<Set<string>>(new Set());
// Prepare guardrail info with existence status
@ -199,7 +201,7 @@ const GuardrailSelectionModal: React.FC<GuardrailSelectionModalProps> = ({
<div className="mb-2 flex items-center gap-2">
<span className="text-lg">✨</span>
<span className="text-sm font-medium">
AI-Discovered Competitors ({template.discoveredCompetitors.length})
{term("AI-Discovered Competitors")} ({template.discoveredCompetitors.length})
</span>
</div>
<div className="flex flex-wrap gap-1.5">

View file

@ -21,16 +21,18 @@ import {
createPolicyVersion,
updatePolicyVersionStatus,
} from "@/components/networking";
import { useTerminology } from "@/contexts/TerminologyContext";
import { getComplianceDatasetPrompts, getFrameworks } from "@/data/compliancePrompts";
import type { Term } from "@/lib/terminology";
import type { CompliancePrompt } from "@/data/compliancePrompts";
const TEST_SOURCE_QUICK = "quick_chat";
const TEST_SOURCE_ALL = "__all__";
function getPromptsForTestSource(source: string): CompliancePrompt[] {
function getPromptsForTestSource(source: string, term: Term): CompliancePrompt[] {
if (source === TEST_SOURCE_QUICK) return [];
if (source === TEST_SOURCE_ALL) return getComplianceDatasetPrompts();
const fw = getFrameworks().find((f) => f.name === source);
if (source === TEST_SOURCE_ALL) return getComplianceDatasetPrompts(term);
const fw = getFrameworks(term).find((f) => f.name === source);
return fw ? fw.categories.flatMap((c) => c.prompts) : [];
}
@ -830,7 +832,8 @@ const PipelineTestPanel: React.FC<PipelineTestPanelProps> = ({ pipeline, accessT
const [complianceResults, setComplianceResults] = useState<ComplianceRunEntry[]>([]);
const isQuickChat = testSource === TEST_SOURCE_QUICK;
const promptsForSource = getPromptsForTestSource(testSource);
const term = useTerminology();
const promptsForSource = getPromptsForTestSource(testSource, term);
const isDataset = promptsForSource.length > 0;
const handleRunTest = async () => {

View file

@ -1,3 +1,4 @@
import { useTerminology } from "@/contexts/TerminologyContext";
import React, { useState, useEffect, useMemo } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
@ -106,6 +107,7 @@ const PolicyTemplates: React.FC<PolicyTemplatesProps> = ({
onTemplatesLoaded,
accessToken,
}) => {
const term = useTerminology();
const [templates, setTemplates] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [selectedTags, setSelectedTags] = useState<Set<string>>(new Set());
@ -191,7 +193,7 @@ const PolicyTemplates: React.FC<PolicyTemplatesProps> = ({
<svg className="w-4 h-4" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 1l1.5 3.5L13 6l-3.5 1.5L8 11 6.5 7.5 3 6l3.5-1.5L8 1zm4 7l.75 1.75L14.5 10.5l-1.75.75L12 13l-.75-1.75L9.5 10.5l1.75-.75L12 8zM4 9l.75 1.75L6.5 11.5l-1.75.75L4 14l-.75-1.75L1.5 11.5l1.75-.75L4 9z" />
</svg>
Use AI to find templates
{term("Use AI to find templates")}
</Button>
</div>

View file

@ -1,3 +1,4 @@
import { useTerminology } from "@/contexts/TerminologyContext";
import React, { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
@ -44,6 +45,7 @@ const TemplateParameterModal: React.FC<TemplateParameterModalProps> = ({
isLoading = false,
accessToken,
}) => {
const term = useTerminology();
const [parameterValues, setParameterValues] = useState<Record<string, string>>({});
const [competitorMode, setCompetitorMode] = useState<"ai" | "manual">("ai");
const [selectedModel, setSelectedModel] = useState<string | null>(null);
@ -260,7 +262,7 @@ const TemplateParameterModal: React.FC<TemplateParameterModalProps> = ({
className="grid-cols-2"
>
<label className="flex cursor-pointer items-center justify-center gap-2 rounded-md border border-input px-3 py-2 text-sm">
<RadioGroupItem value="ai" />✨ Use AI
<RadioGroupItem value="ai" />✨ {term("Use AI")}
</label>
<label className="flex cursor-pointer items-center justify-center gap-2 rounded-md border border-input px-3 py-2 text-sm">
<RadioGroupItem value="manual" />

View file

@ -1,3 +1,4 @@
import { useTerminology } from "@/contexts/TerminologyContext";
import React, { useEffect, useRef, useState } from "react";
import ReactMarkdown from "react-markdown";
import { MessageSquare, Sparkles, X } from "lucide-react";
@ -106,6 +107,7 @@ const MarkdownContent: React.FC<{ content: string }> = ({ content }) => (
);
const UsageAIChatPanel: React.FC<UsageAIChatPanelProps> = ({ open, onClose, accessToken }) => {
const term = useTerminology();
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [inputText, setInputText] = useState("");
const [isLoading, setIsLoading] = useState(false);
@ -250,7 +252,7 @@ const UsageAIChatPanel: React.FC<UsageAIChatPanelProps> = ({ open, onClose, acce
<div className="min-w-0">
<h3 className="flex items-center gap-2 text-sm leading-5 font-medium text-foreground">
<Sparkles aria-hidden="true" className="size-4 shrink-0 text-muted-foreground" strokeWidth={1.75} />
Ask AI
{term("Ask AI")}
</h3>
<p className="mt-0.5 text-xs text-muted-foreground">Ask about your spend, models, keys, and trends</p>
</div>

View file

@ -6,6 +6,7 @@
* Works at 1m+ spend logs, by querying an aggregate table instead.
*/
import { useTerminology } from "@/contexts/TerminologyContext";
import { Download, Link2, Search, Sparkles, X } from "lucide-react";
import { useQueryStates } from "nuqs";
import type { DateRangePickerValue } from "@/components/shared/date_picker_types";
@ -102,6 +103,7 @@ interface UsagePageProps {
}
const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
const term = useTerminology();
const { accessToken, userRole, userId: userID, premiumUser } = useAuthorized();
const [gatewayActivityData, setGatewayActivityData] = useState<FetchedGatewayActivity | null>(null);
@ -459,7 +461,7 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={() => setIsAiChatOpen(true)}>
<Sparkles />
Ask AI
{term("Ask AI")}
</Button>
<Button variant="outline" size="sm" onClick={() => setIsGlobalExportModalOpen(true)}>
<Download />

View file

@ -1,3 +1,4 @@
import { useTerminology } from "@/contexts/TerminologyContext";
import { BarChart3, Bot, Building2, Globe, LineChart, ShoppingCart, Tags, User, Users } from "lucide-react";
import React from "react";
import { Badge } from "@/components/ui/badge";
@ -135,6 +136,7 @@ export const UsageViewSelect: React.FC<UsageViewSelectProps> = ({
description = "Select the usage data you want to view",
"data-id": dataId,
}) => {
const term = useTerminology();
const isAdmin = all_admin_roles.includes(userRole ?? "");
const getFilteredOptions = () => {
return visibleOptions(userRole, canViewTagUsage, isOrgAdmin).map((option) => {
@ -149,7 +151,7 @@ export const UsageViewSelect: React.FC<UsageViewSelectProps> = ({
return {
value: option.value,
label,
description: desc,
description: term(desc),
icon: option.icon,
badgeText: option.badgeText,
};

View file

@ -1,5 +1,6 @@
"use client";
import { useTerminology } from "@/contexts/TerminologyContext";
import React, { useMemo, useState } from "react";
import { ArrowRight, Bot } from "lucide-react";
import { StackedUsageChart } from "@/components/shared/charts";
@ -98,6 +99,7 @@ function AgentItem({
color: string | undefined;
onOpen?: (agent: AgentRow) => void;
}) {
const term = useTerminology();
return (
<li className="group grid grid-cols-[1.5rem_auto_minmax(0,1fr)_auto] items-center gap-3 py-2.5">
<span className="text-sm tabular-nums text-muted-foreground">{rank}.</span>
@ -123,7 +125,7 @@ function AgentItem({
<ArrowRight aria-hidden="true" className="size-3 transition-transform group-hover:translate-x-0.5" />
</button>
) : (
<span className="block truncate text-xs text-muted-foreground">{agent.description}</span>
<span className="block truncate text-xs text-muted-foreground">{term(agent.description)}</span>
)}
</span>
<span className="text-right">

View file

@ -8,6 +8,7 @@ import { ThemeProvider } from "next-themes";
import { AuthProvider } from "@/contexts/AuthContext";
import ReactQueryProvider from "@/contexts/ReactQueryProvider";
import HotkeysProvider from "@/contexts/HotkeysProvider";
import { TerminologyProvider } from "@/contexts/TerminologyContext";
import { Toaster } from "@/components/ui/sonner";
const inter = Inter({ subsets: ["latin"] });
@ -31,9 +32,11 @@ export default function RootLayout({
<ThemeProvider attribute="class" defaultTheme="light" enableSystem disableTransitionOnChange>
<NuqsAdapter>
<ReactQueryProvider>
<HotkeysProvider>
<AuthProvider>{children}</AuthProvider>
</HotkeysProvider>
<TerminologyProvider>
<HotkeysProvider>
<AuthProvider>{children}</AuthProvider>
</HotkeysProvider>
</TerminologyProvider>
<Toaster />
</ReactQueryProvider>
</NuqsAdapter>

View file

@ -1,3 +1,4 @@
import { useTerminology } from "@/contexts/TerminologyContext";
import { AgentHubData, getAgentHubTableColumns } from "@/components/AIHub/AgentHubTableColumns";
import MakeAgentPublicForm from "@/components/AIHub/forms/MakeAgentPublicForm";
import MakeMCPPublicForm from "@/components/AIHub/forms/MakeMCPPublicForm";
@ -105,6 +106,7 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({
canOpenPlayground,
canAddModels,
}) => {
const term = useTerminology();
const syntaxTheme = useSyntaxTheme(prism);
// Admin Viewer follows the read-parity rule: see the AI Hub catalog, but
// cannot toggle public visibility (write).
@ -446,7 +448,7 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({
{/* Header with Title, Description and URL */}
<div className="flex justify-between items-center mb-6">
<div className="flex flex-col items-start">
<h2 className="text-center text-xl font-semibold">AI Hub</h2>
<h2 className="text-center text-xl font-semibold">{term("AI Hub")}</h2>
{isAdminRole(userRole || "") ? (
<p className="text-sm text-muted-foreground">
Make models, agents, and MCP servers public for developers to know what&apos;s available.

View file

@ -1,5 +1,6 @@
"use client";
import { useTerminology } from "@/contexts/TerminologyContext";
import {
Breadcrumb,
BreadcrumbItem,
@ -40,6 +41,7 @@ export function DashboardHeader({
navigationTrigger?: ReactNode;
deploymentHealth?: ReactNode;
}) {
const term = useTerminology();
const { title } = getBreadcrumb(usePathname());
const isDesktop = useMediaQuery("(min-width: 768px)", { initializeWithValue: false });
const [mobileToolsOpen, setMobileToolsOpen] = useState(false);
@ -66,7 +68,7 @@ export function DashboardHeader({
</BreadcrumbItem>
<BreadcrumbSeparator className="max-md:hidden" />
<BreadcrumbItem className="min-w-0">
<BreadcrumbPage className="truncate">{title}</BreadcrumbPage>
<BreadcrumbPage className="truncate">{term(title)}</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>

View file

@ -1,5 +1,6 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { act, fireEvent, render, screen } from "@testing-library/react";
import { TerminologyProvider } from "@/contexts/TerminologyContext";
import ViewSwitcher from "./ViewSwitcher";
const { mockUsePluginMode, mockUseUISettings, mockUsePathname, state } = vi.hoisted(() => {
@ -30,6 +31,9 @@ const { mockUsePluginMode, mockUseUISettings, mockUsePathname, state } = vi.hois
vi.mock("@/contexts/PluginModeContext", () => ({ usePluginMode: mockUsePluginMode }));
vi.mock("@/app/(dashboard)/hooks/uiSettings/useUISettings", () => ({ useUISettings: mockUseUISettings }));
vi.mock("next/navigation", () => ({ usePathname: mockUsePathname }));
vi.mock("@/app/(dashboard)/hooks/uiConfig/useUIConfig", () => ({
useUIConfig: () => ({ data: { use_si_terminology: true } }),
}));
// Deterministic hrefs so navigation assertions don't depend on server_root_path.
vi.mock("@/utils/uiHref", () => ({ uiHref: (seg: string) => `/ui/${seg}` }));
@ -72,6 +76,23 @@ describe("ViewSwitcher", () => {
expect(state.setMode).not.toHaveBeenCalled();
});
it("labels the gateway SI Gateway when the proxy asks for SI terminology", async () => {
state.plugins = [{ name: "obs", display_name: "Observability", url: "http://localhost:9000" }];
render(
<TerminologyProvider>
<ViewSwitcher />
</TerminologyProvider>,
);
const button = screen.getByRole("button");
expect(button).toHaveTextContent("SI Gateway");
act(() => {
fireEvent.click(button);
});
expect(await screen.findByText("Observability")).toBeInTheDocument();
expect(screen.queryByText("AI Gateway")).not.toBeInTheDocument();
});
it("labels the button from the active plugin and lists AI Gateway + each plugin", async () => {
state.plugins = [
{ name: "litellm-platform-plugin", display_name: "Chat UI", url: "http://localhost:3300" },

View file

@ -1,3 +1,4 @@
import { useTerminology } from "@/contexts/TerminologyContext";
import React from "react";
import { usePathname } from "next/navigation";
import {
@ -20,10 +21,15 @@ function isRoute(pathname: string, href: string): boolean {
return pathname === href || pathname.startsWith(`${href}/`);
}
function activeLabelFor(isChatRoute: boolean, isMoyaiRoute: boolean, pluginLabel: string | undefined): string {
function activeLabelFor(
isChatRoute: boolean,
isMoyaiRoute: boolean,
pluginLabel: string | undefined,
gatewayLabel: string,
): string {
if (isChatRoute) return "Chat";
if (isMoyaiRoute) return "Moyai";
return pluginLabel ?? "AI Gateway";
return pluginLabel ?? gatewayLabel;
}
interface ViewSwitcherItem {
@ -34,6 +40,7 @@ interface ViewSwitcherItem {
}
export default function ViewSwitcher() {
const term = useTerminology();
const { mode, setMode, plugins } = usePluginMode();
const { data: uiSettings } = useUISettings();
const pathname = usePathname();
@ -46,10 +53,16 @@ export default function ViewSwitcher() {
const isMoyaiRoute = isRoute(normalizedPathname, uiHref(MOYAI));
const isStandaloneRoute = isChatRoute || isMoyaiRoute;
const activeLabel = activeLabelFor(isChatRoute, isMoyaiRoute, plugins.find((p) => p.name === mode)?.display_name);
const gatewayLabel = term("AI Gateway");
const activeLabel = activeLabelFor(
isChatRoute,
isMoyaiRoute,
plugins.find((p) => p.name === mode)?.display_name,
gatewayLabel,
);
const modeEntries = [
{ key: GATEWAY, label: "AI Gateway" },
{ key: GATEWAY, label: gatewayLabel },
...plugins.map((p) => ({ key: p.name, label: p.display_name })),
];

View file

@ -1,5 +1,6 @@
"use client";
import { useTerminology } from "@/contexts/TerminologyContext";
import { ChevronDown } from "lucide-react";
import { useMemo, useState } from "react";
@ -22,6 +23,7 @@ export default function PageVisibilitySettings({
isUpdating,
onUpdate,
}: PageVisibilitySettingsProps) {
const term = useTerminology();
const isPageVisibilitySet = enabledPagesInternalUsers !== null && enabledPagesInternalUsers !== undefined;
const availablePages = useMemo(() => getAvailablePages(), []);
const pagesByGroup = useMemo(() => {
@ -99,8 +101,8 @@ export default function PageVisibilitySettings({
onCheckedChange={(checked) => togglePage(page.page, checked === true)}
/>
<span className="space-y-0.5">
<span className="block text-sm text-foreground">{page.label}</span>
<span className="block text-xs text-muted-foreground">{page.description}</span>
<span className="block text-sm text-foreground">{term(page.label)}</span>
<span className="block text-xs text-muted-foreground">{term(page.description)}</span>
</span>
</label>
);

View file

@ -1,5 +1,6 @@
"use client";
import { useTerminology } from "@/contexts/TerminologyContext";
import { useUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUISettings";
import { useUpdateUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUpdateUISettings";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
@ -48,6 +49,7 @@ function SettingRow({
}
export default function UISettings() {
const term = useTerminology();
const { accessToken } = useAuthorized();
const { data, isLoading, isError, error } = useUISettings();
const { mutate: updateSettings, isPending: isUpdating, error: updateError } = useUpdateUISettings(accessToken);
@ -332,9 +334,13 @@ export default function UISettings() {
checked={Boolean(values.require_auth_for_public_ai_hub)}
disabled={isUpdating}
onCheckedChange={handleToggleRequireAuthForPublicAIHub}
ariaLabel={requireAuthForPublicAIHubProperty?.description ?? "Require authentication for public AI Hub"}
label="Require authentication for public AI Hub"
description={requireAuthForPublicAIHubProperty?.description}
ariaLabel={term(
requireAuthForPublicAIHubProperty?.description ?? "Require authentication for public AI Hub",
)}
label={term("Require authentication for public AI Hub")}
description={
requireAuthForPublicAIHubProperty?.description && term(requireAuthForPublicAIHubProperty.description)
}
/>
<SettingRow
checked={Boolean(values.forward_client_headers_to_llm_api)}

View file

@ -1,6 +1,7 @@
import { act, fireEvent, screen, waitFor, within } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "../../tests/test-utils";
import { TerminologyProvider } from "@/contexts/TerminologyContext";
import Sidebar, { menuGroups, getBreadcrumb } from "./leftnav";
vi.mock("../utils/roles", async (importOriginal) => {
@ -64,10 +65,12 @@ vi.mock("@/app/(dashboard)/hooks/teams/useTeams", () => ({
useTeams: mockUseTeams,
}));
const uiConfig = vi.hoisted(() => ({ data: { admin_ui_disabled: false, use_si_terminology: false } }));
vi.mock("@/app/(dashboard)/hooks/uiConfig/useUIConfig", () => {
return {
useUIConfig: () => ({
data: { admin_ui_disabled: false },
data: uiConfig.data,
isLoading: false,
}),
};
@ -149,6 +152,20 @@ describe("Sidebar (leftnav)", () => {
});
mockUseThemeImpl = unbrandedTheme;
navState.pathname = "/ui/api-keys";
uiConfig.data = { admin_ui_disabled: false, use_si_terminology: false };
});
it("uses SI terminology in section and page labels when the proxy asks for it", () => {
uiConfig.data = { admin_ui_disabled: false, use_si_terminology: true };
renderWithProviders(
<TerminologyProvider>
<Sidebar {...defaultProps} />
</TerminologyProvider>,
);
expect(screen.getByText("SI GATEWAY")).toBeInTheDocument();
expect(screen.getByText("SI Hub")).toBeInTheDocument();
expect(screen.queryByText("AI Hub")).not.toBeInTheDocument();
});
it("should link the logo to the UI home route rather than the proxy origin", () => {

View file

@ -5,6 +5,7 @@ import useIsOrgAdmin from "@/app/(dashboard)/hooks/useIsOrgAdmin";
import { useHealthReadinessDetails } from "@/app/(dashboard)/hooks/healthReadiness/useHealthReadinessDetails";
import { useLogout } from "@/app/(dashboard)/hooks/useLogout";
import { getProxyBaseUrl } from "@/components/networking";
import { useTerminology } from "@/contexts/TerminologyContext";
import { useTheme } from "@/contexts/ThemeContext";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
@ -547,6 +548,8 @@ const Sidebar_: React.FC<SidebarProps> = ({
allowVectorStoresForTeamAdmins,
}) => {
const { userId, accessToken, userRole, isViewOnly } = useAuthorized();
const term = useTerminology();
const labelOf = (item: MenuItem) => (typeof item.label === "string" ? term(item.label) : item.label);
const isOrgAdmin = useIsOrgAdmin();
const { data: teams } = useTeams();
const { logoUrl, logoUrlDark } = useTheme();
@ -622,7 +625,7 @@ const Sidebar_: React.FC<SidebarProps> = ({
const renderLeaf = (item: MenuItem, isChild: boolean) => {
const active = selectedKey === item.key;
const size = isChild ? "sub" : "default";
const label = <span className="flex-1 truncate group-data-[collapsed=true]/sidebar:hidden">{item.label}</span>;
const label = <span className="flex-1 truncate group-data-[collapsed=true]/sidebar:hidden">{labelOf(item)}</span>;
if (item.external_url) {
return (
@ -631,7 +634,7 @@ const Sidebar_: React.FC<SidebarProps> = ({
href={item.external_url}
target="_blank"
rel="noopener noreferrer"
title={collapsed ? labelText(item) : undefined}
title={collapsed ? term(labelText(item)) : undefined}
data-active={active || undefined}
className={cn(sidebarMenuButtonVariants({ isActive: active, size }))}
>
@ -646,7 +649,7 @@ const Sidebar_: React.FC<SidebarProps> = ({
<Link
key={item.key}
href={uiHref(routeOf(item))}
title={collapsed ? labelText(item) : undefined}
title={collapsed ? term(labelText(item)) : undefined}
data-active={active || undefined}
className={cn(sidebarMenuButtonVariants({ isActive: active, size }))}
>
@ -670,10 +673,10 @@ const Sidebar_: React.FC<SidebarProps> = ({
isActive={active}
aria-expanded={open}
onClick={() => toggleGroup(item.key)}
title={collapsed ? labelText(item) : undefined}
title={collapsed ? term(labelText(item)) : undefined}
>
{item.icon}
<span className="flex-1 truncate group-data-[collapsed=true]/sidebar:hidden">{item.label}</span>
<span className="flex-1 truncate group-data-[collapsed=true]/sidebar:hidden">{labelOf(item)}</span>
<ChevronRight
className={cn(
"size-4 shrink-0 transition-transform group-data-[collapsed=true]/sidebar:hidden",
@ -740,7 +743,7 @@ const Sidebar_: React.FC<SidebarProps> = ({
{visibleGroups.map((group, gi) => (
<SidebarGroup key={group.groupLabel}>
{gi > 0 && <SidebarSeparator className="hidden group-data-[collapsed=true]/sidebar:block" />}
<SidebarGroupLabel>{group.groupLabel}</SidebarGroupLabel>
<SidebarGroupLabel>{term(group.groupLabel)}</SidebarGroupLabel>
<SidebarMenu>{group.items.map((item) => renderItem(item))}</SidebarMenu>
</SidebarGroup>
))}

View file

@ -1,5 +1,6 @@
"use client";
import { useTerminology } from "@/contexts/TerminologyContext";
import { Plus } from "lucide-react";
import { useState } from "react";
@ -23,6 +24,7 @@ import CredentialsTable from "./CredentialsTable";
import { buildCredential, type CredentialSubmission } from "./credential_form_helpers";
export default function CredentialsPanel() {
const term = useTerminology();
const { accessToken, userRole } = useAuthorized();
// Admin Viewer follows the read-parity rule: see credentials, do not modify.
const canModifyCredentials = isProxyAdminRole(userRole ?? "");
@ -106,7 +108,7 @@ export default function CredentialsPanel() {
<div className="mx-auto flex w-full flex-auto flex-col gap-4 overflow-y-auto p-2">
<div className="flex items-center justify-between gap-4">
<p className="text-sm text-muted-foreground">
Configured credentials for different AI providers. Add and manage your API credentials.
{term("Configured credentials for different AI providers. Add and manage your API credentials.")}
</p>
{canModifyCredentials && (
<Button onClick={() => setIsAddModalOpen(true)}>

View file

@ -1,5 +1,6 @@
"use client";
import { useTerminology } from "@/contexts/TerminologyContext";
import { SortingState } from "@tanstack/react-table";
import { KeyRound } from "lucide-react";
import React, { useMemo, useState } from "react";
@ -20,13 +21,14 @@ interface CredentialsTableProps {
const DEFAULT_SORTING: SortingState = [{ id: "credential_name", desc: false }];
function EmptyState() {
const term = useTerminology();
return (
<div className="flex flex-col items-center gap-1 py-6">
<div className="mb-1 flex size-10 items-center justify-center rounded-lg bg-muted">
<KeyRound className="size-5 text-muted-foreground" />
</div>
<div className="text-sm font-medium text-foreground">No credentials configured</div>
<div className="text-sm text-muted-foreground">Add a credential to connect an AI provider.</div>
<div className="text-sm text-muted-foreground">{term("Add a credential to connect an AI provider.")}</div>
</div>
);
}

View file

@ -1,5 +1,6 @@
"use client";
import { useTerminology } from "@/contexts/TerminologyContext";
import React from "react";
import { ArrowUpRight, Check, ExternalLink } from "lucide-react";
import moyaiHead from "../../../public/assets/moyai/moyai-head.svg";
@ -15,6 +16,7 @@ export default function MoyaiConnected({
keyAlias?: string | null;
models?: number | null;
}) {
const term = useTerminology();
const rows = [
{ label: "Workspace linked", value: moyaiUrl },
{ label: "Virtual key issued", value: keyAlias },
@ -75,7 +77,7 @@ export default function MoyaiConnected({
className="inline-flex items-center gap-2 rounded-full bg-white/[0.06] px-5 py-3 text-sm font-medium text-white shadow-[inset_0_0_0_1px_rgba(255,255,255,0.18)] backdrop-blur transition-colors hover:bg-white/[0.12]"
>
<ExternalLink className="size-4" />
Back to AI Gateway
{term("Back to AI Gateway")}
</a>
</div>
</div>

View file

@ -423,6 +423,7 @@ export interface LiteLLMWellKnownUiConfig {
is_control_plane?: boolean;
workers?: WorkerInfo[];
mcp_stdio_enabled?: boolean;
use_si_terminology?: boolean;
}
export interface CredentialsResponse {

View file

@ -1,4 +1,5 @@
"use client";
import { useTerminology } from "@/contexts/TerminologyContext";
import { keyKeys } from "@/app/(dashboard)/hooks/keys/useKeys";
import { useOrganizations } from "@/app/(dashboard)/hooks/organizations/useOrganizations";
import { useProjects } from "@/app/(dashboard)/hooks/projects/useProjects";
@ -227,6 +228,12 @@ export const fetchUserModels = async (
*/
const CreateKey: React.FC<CreateKeyProps> = ({ team, teams, data, addKey, autoOpenCreate, prefillData }) => {
const { accessToken, userId: userID, userRole, premiumUser } = useAuthorized();
const term = useTerminology();
const keyTypeOptions = KEY_TYPE_OPTIONS.map((option) => ({
...option,
label: term(option.label),
hint: term(option.hint),
}));
const canEditGuardrails = premiumUser || (userRole != null && rolesWithWriteAccess.includes(userRole));
const canViewPolicies = useCan("viewPolicies");
const canViewPrompts = useCan("viewPrompts");
@ -994,7 +1001,7 @@ const CreateKey: React.FC<CreateKeyProps> = ({ team, teams, data, addKey, autoOp
>
{(control) => (
<Select
items={KEY_TYPE_OPTIONS}
items={keyTypeOptions}
value={control.value as string | undefined}
onValueChange={(value: string | null) =>
value != null && changeKeyType(control.onChange)(value)
@ -1009,7 +1016,7 @@ const CreateKey: React.FC<CreateKeyProps> = ({ team, teams, data, addKey, autoOp
<SelectValue placeholder="Select key type" />
</SelectTrigger>
<SelectContent>
{KEY_TYPE_OPTIONS.map((option) => (
{keyTypeOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<div className="py-1">
<div className="font-medium">{option.label}</div>

View file

@ -2,6 +2,7 @@ import { cloneElement, type MouseEventHandler, type ReactElement, type ReactNode
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, act } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { toSiTerminology } from "@/lib/terminology";
import CreatedKeyDisplay, { buildAgentInstructions } from "./CreatedKeyDisplay";
import { toast } from "@/lib/toast";
@ -182,4 +183,11 @@ describe("buildAgentInstructions", () => {
].join("\n"),
);
});
it("rewrites only its own copy under SI terminology, never the gateway URL or the key", () => {
const lines = buildAgentInstructions("http://AI.example.com/", "sk-AI-1", toSiTerminology).split("\n");
expect(lines[0]).toBe("Use this LiteLLM SI gateway for model inference.");
expect(lines).toContain("Gateway URL: http://AI.example.com");
expect(lines).toContain("Virtual key: sk-AI-1");
});
});

View file

@ -1,3 +1,5 @@
import { useTerminology } from "@/contexts/TerminologyContext";
import { keepTerminology, type Term } from "@/lib/terminology";
import React, { useState } from "react";
import { CopyToClipboard } from "react-copy-to-clipboard";
import { ChevronDown, ChevronUp } from "lucide-react";
@ -11,14 +13,14 @@ interface CreatedKeyDisplayProps {
apiKey: string;
}
export const buildAgentInstructions = (baseUrl: string, apiKey: string): string => {
export const buildAgentInstructions = (baseUrl: string, apiKey: string, term: Term = keepTerminology): string => {
const normalizedBaseUrl = baseUrl.replace(/\/+$/, "");
const endpointIntroduction =
"LiteLLM is a gateway that exposes OpenAI and Anthropic compatible APIs. " +
`Send the virtual key as a Bearer token (Authorization: Bearer ${apiKey}) to:`;
return [
"Use this LiteLLM AI gateway for model inference.",
term("Use this LiteLLM AI gateway for model inference."),
"",
`Gateway URL: ${normalizedBaseUrl}`,
`Virtual key: ${apiKey}`,
@ -41,11 +43,12 @@ export const buildAgentInstructions = (baseUrl: string, apiKey: string): string
* Used on the Virtual Keys page and in the Add Agent wizard.
*/
const CreatedKeyDisplay: React.FC<CreatedKeyDisplayProps> = ({ apiKey }) => {
const term = useTerminology();
const [copied, setCopied] = useState(false);
const [instructionsCopied, setInstructionsCopied] = useState(false);
const [showInstructions, setShowInstructions] = useState(false);
const baseUrl = getProxyBaseUrl().replace(/\/+$/, "");
const instructions = buildAgentInstructions(baseUrl, apiKey);
const instructions = buildAgentInstructions(baseUrl, apiKey, term);
const handleCopy = () => {
setCopied(true);

View file

@ -1,3 +1,4 @@
import { useTerminology } from "@/contexts/TerminologyContext";
import React from "react";
import { Control, UseFormReturn } from "react-hook-form";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
@ -37,27 +38,35 @@ export const KeyTypeSelect = ({
id: string;
value: string;
onChange: (value: string) => void;
}) => (
<Select
items={Object.fromEntries(KEY_TYPE_OPTIONS.map((option) => [option.value, option.label]))}
value={value}
onValueChange={(next: string | null) => next != null && onChange(next)}
>
<SelectTrigger id={id} className="w-full">
<SelectValue placeholder="Select key type" />
</SelectTrigger>
<SelectContent>
{KEY_TYPE_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
<div className="py-1">
<div className="font-medium">{option.label}</div>
<div className="mt-0.5 text-[11px] text-muted-foreground">{option.hint}</div>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
);
}) => {
const term = useTerminology();
const keyTypeOptions = KEY_TYPE_OPTIONS.map((option) => ({
...option,
label: term(option.label),
hint: term(option.hint),
}));
return (
<Select
items={Object.fromEntries(keyTypeOptions.map((option) => [option.value, option.label]))}
value={value}
onValueChange={(next: string | null) => next != null && onChange(next)}
>
<SelectTrigger id={id} className="w-full">
<SelectValue placeholder="Select key type" />
</SelectTrigger>
<SelectContent>
{keyTypeOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<div className="py-1">
<div className="font-medium">{option.label}</div>
<div className="mt-0.5 text-[11px] text-muted-foreground">{option.hint}</div>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
);
};
const SKILLS_HINT =
"Enabled skills are visible to every key. Grant disabled (private) Claude Code plugins to this key here.";

View file

@ -0,0 +1,37 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { TerminologyProvider, useTerminology } from "./TerminologyContext";
const uiConfig = vi.hoisted(() => ({ data: undefined as { use_si_terminology?: boolean } | undefined }));
vi.mock("@/app/(dashboard)/hooks/uiConfig/useUIConfig", () => ({
useUIConfig: () => ({ data: uiConfig.data }),
}));
function GatewayTitle() {
const term = useTerminology();
return <h1>{term("AI Gateway")}</h1>;
}
describe("TerminologyProvider", () => {
it.each([
[{ use_si_terminology: true }, "SI Gateway"],
[{ use_si_terminology: false }, "AI Gateway"],
[{}, "AI Gateway"],
[undefined, "AI Gateway"],
])("with UI config %j renders %j", (data, expected) => {
uiConfig.data = data;
render(
<TerminologyProvider>
<GatewayTitle />
</TerminologyProvider>,
);
expect(screen.getByRole("heading")).toHaveTextContent(expected);
});
it("leaves copy unchanged for components rendered outside the provider", () => {
uiConfig.data = { use_si_terminology: true };
render(<GatewayTitle />);
expect(screen.getByRole("heading")).toHaveTextContent("AI Gateway");
});
});

View file

@ -0,0 +1,18 @@
"use client";
import { createContext, useContext } from "react";
import { useUIConfig } from "@/app/(dashboard)/hooks/uiConfig/useUIConfig";
import { keepTerminology, toSiTerminology, type Term } from "@/lib/terminology";
const TerminologyContext = createContext<Term>(keepTerminology);
export function TerminologyProvider({ children }: { children: React.ReactNode }) {
const { data: uiConfig } = useUIConfig();
return (
<TerminologyContext.Provider value={uiConfig?.use_si_terminology ? toSiTerminology : keepTerminology}>
{children}
</TerminologyContext.Provider>
);
}
export const useTerminology = (): Term => useContext(TerminologyContext);

View file

@ -0,0 +1,39 @@
import { describe, expect, it } from "vitest";
import { toSiTerminology } from "@/lib/terminology";
import { getComplianceDatasetPrompts, getFrameworks } from "./compliancePrompts";
describe("getFrameworks with a terminology", () => {
const original = getFrameworks();
const si = getFrameworks(toSiTerminology);
const categoriesOf = (frameworks: typeof original) => frameworks.flatMap((framework) => framework.categories);
const promptsOf = (frameworks: typeof original) => categoriesOf(frameworks).flatMap((category) => category.prompts);
it("rewrites every prompt the playground sends to the model", () => {
expect(promptsOf(si).length).toBeGreaterThan(0);
expect(promptsOf(si).map((prompt) => prompt.prompt)).toEqual(
promptsOf(original).map((prompt) => toSiTerminology(prompt.prompt)),
);
expect(promptsOf(si).some((prompt, index) => prompt.prompt !== promptsOf(original)[index].prompt)).toBe(true);
});
it("rewrites framework and category descriptions", () => {
expect(si.map((framework) => framework.description)).toEqual(
original.map((framework) => toSiTerminology(framework.description)),
);
expect(categoriesOf(si).map((category) => category.description)).toEqual(
categoriesOf(original).map((category) => toSiTerminology(category.description)),
);
});
it("keeps framework names, category names and prompt ids stable for selection and CSV export", () => {
expect(si.map((framework) => framework.name)).toEqual(original.map((framework) => framework.name));
expect(categoriesOf(si).map((category) => category.name)).toEqual(
categoriesOf(original).map((category) => category.name),
);
expect(promptsOf(si).map((prompt) => prompt.id)).toEqual(promptsOf(original).map((prompt) => prompt.id));
});
it("feeds the rewritten prompts into the flat dataset used by the pipeline tester", () => {
expect(getComplianceDatasetPrompts(toSiTerminology)).toEqual(promptsOf(si));
});
});

View file

@ -1,3 +1,4 @@
import { keepTerminology, type Term } from "@/lib/terminology";
import { insultsCompliancePrompts } from "./insultsCompliancePrompts";
import { financialCompliancePrompts } from "./financialCompliancePrompts";
import { codeExecutionCompliancePrompts } from "./codeExecutionCompliancePrompts";
@ -563,14 +564,19 @@ const frameworkMeta: Record<string, { icon: string; description: string }> = {
};
/** Flat list of all compliance prompts for pipeline testing (EU AI Act, GDPR, topic blocking, airline, etc.). */
export function getComplianceDatasetPrompts(): CompliancePrompt[] {
return getFrameworks().flatMap((fw) => fw.categories.flatMap((cat) => cat.prompts));
export function getComplianceDatasetPrompts(term: Term = keepTerminology): CompliancePrompt[] {
return getFrameworks(term).flatMap((fw) => fw.categories.flatMap((cat) => cat.prompts));
}
export function getFrameworks(): ComplianceFramework[] {
export function getFrameworks(term: Term = keepTerminology): ComplianceFramework[] {
const frameworkMap = new Map<string, { categories: Map<string, ComplianceCategory> }>();
const prompts = allCompliancePrompts.map((prompt) => ({
...prompt,
categoryDescription: term(prompt.categoryDescription),
prompt: term(prompt.prompt),
}));
for (const prompt of allCompliancePrompts) {
for (const prompt of prompts) {
if (!frameworkMap.has(prompt.framework)) {
frameworkMap.set(prompt.framework, { categories: new Map() });
}
@ -589,7 +595,7 @@ export function getFrameworks(): ComplianceFramework[] {
return Array.from(frameworkMap.entries()).map(([name, data]) => ({
name,
icon: frameworkMeta[name]?.icon || "file-text",
description: frameworkMeta[name]?.description || "",
description: term(frameworkMeta[name]?.description || ""),
categories: Array.from(data.categories.values()),
}));
}

View file

@ -48050,6 +48050,11 @@ export interface components {
server_root_path: string;
/** Sso Configured */
sso_configured: boolean;
/**
* Use Si Terminology
* @default false
*/
use_si_terminology: boolean;
/**
* Workers
* @default []

View file

@ -0,0 +1,45 @@
import { describe, expect, it } from "vitest";
import { toSiTerminology } from "./terminology";
describe("toSiTerminology", () => {
it.each([
["AI Gateway", "SI Gateway"],
["AI GATEWAY", "SI GATEWAY"],
["Enable your AI assistant", "Enable your SI assistant"],
["AI-Discovered Competitors", "SI-Discovered Competitors"],
["protecting AI/ML workloads", "protecting SI/ML workloads"],
["the AI's response", "the SI's response"],
["Artificial Intelligence", "Super Intelligence"],
["artificial intelligence safety", "super intelligence safety"],
["Artificial intelligence", "Super intelligence"],
])("rewrites %j to %j", (input, expected) => {
expect(toSiTerminology(input)).toBe(expected);
});
it.each(["OpenAI", "xAI's assistant", "AIM Security guardrails", "MAIN", "Chat"])(
"leaves %j alone because AI is not a standalone word there",
(input) => {
expect(toSiTerminology(input)).toBe(input);
},
);
it.each([
[
"Explain the EU AI Act's prohibition on AI criminal risk prediction.",
"Explain the EU AI Act's prohibition on SI criminal risk prediction.",
],
[
"Article 5 prohibited AI practices under the European Union AI Act.",
"Article 5 prohibited SI practices under the European Union AI Act.",
],
[
"Palo Alto Networks Prisma AI Runtime Security for securing AI applications",
"Palo Alto Networks Prisma AI Runtime Security for securing SI applications",
],
["Cisco AI Defense Inspection API for AI apps", "Cisco AI Defense Inspection API for SI apps"],
["DeepKeep AI Firewall for AI security", "DeepKeep AI Firewall for SI security"],
["Defend AI Agentic Guardrails", "Defend AI Agentic Guardrails"],
])("keeps third-party names inside %j", (input, expected) => {
expect(toSiTerminology(input)).toBe(expected);
});
});

View file

@ -0,0 +1,28 @@
export type Term = (text: string) => string;
export const keepTerminology: Term = (text) => text;
const THIRD_PARTY_NAMES: readonly string[] = [
"EU AI Act",
"European Union AI Act",
"Cisco AI Defense",
"DeepKeep AI Firewall",
"Defend AI",
"Prisma AI Runtime Security",
];
const THIRD_PARTY_NAME_PATTERN = new RegExp(`(${THIRD_PARTY_NAMES.join("|")})`);
const swapTerms = (text: string): string =>
text
.replace(
/\b([Aa])rtificial ([Ii])ntelligence\b/g,
(_match: string, a: string, i: string) => `${a === "A" ? "S" : "s"}uper ${i}ntelligence`,
)
.replace(/\bAI\b/g, "SI");
export const toSiTerminology: Term = (text) =>
text
.split(THIRD_PARTY_NAME_PATTERN)
.map((segment, index) => (index % 2 === 1 ? segment : swapTerms(segment)))
.join("");