- {ids.map((id) => (
+ {items.map(({ id, name }) => (
- {id}
+ {name ? (
+
+ {name}
+
+ ) : (
+ {id}
+ )}
))}
@@ -34,6 +48,23 @@ function ResourceList({ ids, emptyMessage }: { ids: string[]; emptyMessage: stri
);
}
+function ResourceBadge({
+ resource: { id, name },
+ href,
+ fallback,
+}: {
+ resource: AccessGroupResource;
+ href: string;
+ fallback: (id: string) => string;
+}) {
+ const badge = (
+
+ {name ?? fallback(id)}
+
+ );
+ return name ?
{badge} : badge;
+}
+
export function AccessGroupDetail({ accessGroupId, onBack }: AccessGroupDetailProps) {
const { data: accessGroup, isLoading } = useAccessGroupDetails(accessGroupId);
const [isEditModalVisible, setIsEditModalVisible] = useState(false);
@@ -61,14 +92,14 @@ export function AccessGroupDetail({ accessGroupId, onBack }: AccessGroupDetailPr
);
}
- const modelIds = accessGroup.access_model_names ?? [];
- const mcpServerIds = accessGroup.access_mcp_server_ids ?? [];
- const agentIds = accessGroup.access_agent_ids ?? [];
- const keyIds = accessGroup.assigned_key_ids ?? [];
- const teamIds = accessGroup.assigned_team_ids ?? [];
+ const models = accessGroup.access_model_names.map((id) => ({ id, name: null }));
+ const mcpServers = accessGroup.access_mcp_servers;
+ const agents = accessGroup.access_agents;
+ const keys = accessGroup.assigned_keys;
+ const teams = accessGroup.assigned_teams;
- const displayedKeys = showAllKeys ? keyIds : keyIds.slice(0, MAX_PREVIEW);
- const displayedTeams = showAllTeams ? teamIds : teamIds.slice(0, MAX_PREVIEW);
+ const displayedKeys = showAllKeys ? keys : keys.slice(0, MAX_PREVIEW);
+ const displayedTeams = showAllTeams ? teams : teams.slice(0, MAX_PREVIEW);
return (
@@ -129,23 +160,21 @@ export function AccessGroupDetail({ accessGroupId, onBack }: AccessGroupDetailPr
Attached Keys
- {keyIds.length}
+ {keys.length}
- {keyIds.length > MAX_PREVIEW && (
+ {keys.length > MAX_PREVIEW && (
)}
- {keyIds.length > 0 ? (
+ {keys.length > 0 ? (
- {displayedKeys.map((id) => (
-
- {id.length > 20 ? `${id.slice(0, 10)}...${id.slice(-6)}` : id}
-
+ {displayedKeys.map((key) => (
+
))}
) : (
@@ -159,23 +188,21 @@ export function AccessGroupDetail({ accessGroupId, onBack }: AccessGroupDetailPr
Attached Teams
- {teamIds.length}
+ {teams.length}
- {teamIds.length > MAX_PREVIEW && (
+ {teams.length > MAX_PREVIEW && (
)}
- {teamIds.length > 0 ? (
+ {teams.length > 0 ? (
- {displayedTeams.map((id) => (
-
- {id}
-
+ {displayedTeams.map((team) => (
+ id} />
))}
) : (
@@ -192,27 +219,27 @@ export function AccessGroupDetail({ accessGroupId, onBack }: AccessGroupDetailPr
Models
- {modelIds.length}
+ {models.length}
MCP Servers
- {mcpServerIds.length}
+ {mcpServers.length}
Agents
- {agentIds.length}
+ {agents.length}
-
+
-
+
-
+
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/access-groups/_components/AccessGroupsModal/AccessGroupEditModal.integration.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/access-groups/_components/AccessGroupsModal/AccessGroupEditModal.integration.test.tsx
index 2e65be36796..bd77ad8e897 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/access-groups/_components/AccessGroupsModal/AccessGroupEditModal.integration.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/access-groups/_components/AccessGroupsModal/AccessGroupEditModal.integration.test.tsx
@@ -42,6 +42,10 @@ const accessGroup: AccessGroupResponse = {
access_agent_ids: ["agent-1"],
assigned_team_ids: [],
assigned_key_ids: [],
+ access_mcp_servers: [{ id: "srv-1", name: "Server One" }],
+ access_agents: [{ id: "agent-1", name: "Agent One" }],
+ assigned_teams: [],
+ assigned_keys: [],
created_at: "2024-01-01T00:00:00Z",
created_by: "user-1",
updated_at: "2024-01-02T00:00:00Z",
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/access-groups/_components/AccessGroupsPage.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/access-groups/_components/AccessGroupsPage.test.tsx
index 63ff0f4100f..12d3d773c1f 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/access-groups/_components/AccessGroupsPage.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/access-groups/_components/AccessGroupsPage.test.tsx
@@ -15,6 +15,10 @@ const mockAccessGroups: AccessGroupResponse[] = [
access_agent_ids: ["a1"],
assigned_team_ids: [],
assigned_key_ids: [],
+ access_mcp_servers: [{ id: "s1", name: "Server One" }],
+ access_agents: [{ id: "a1", name: "Agent One" }],
+ assigned_teams: [],
+ assigned_keys: [],
created_at: "2024-01-15T10:00:00Z",
created_by: "user-1",
updated_at: "2024-01-20T12:00:00Z",
@@ -29,6 +33,10 @@ const mockAccessGroups: AccessGroupResponse[] = [
access_agent_ids: [],
assigned_team_ids: [],
assigned_key_ids: [],
+ access_mcp_servers: [],
+ access_agents: [],
+ assigned_teams: [],
+ assigned_keys: [],
created_at: "2024-01-10T09:00:00Z",
created_by: null,
updated_at: "2024-01-12T11:00:00Z",
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/accessGroups/useAccessGroups.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/accessGroups/useAccessGroups.test.ts
index b15ea4491e9..14cae5b1c1f 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/accessGroups/useAccessGroups.test.ts
+++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/accessGroups/useAccessGroups.test.ts
@@ -46,6 +46,10 @@ const mockAccessGroups: AccessGroupResponse[] = [
access_agent_ids: [],
assigned_team_ids: [],
assigned_key_ids: [],
+ access_mcp_servers: [],
+ access_agents: [],
+ assigned_teams: [],
+ assigned_keys: [],
created_at: "2025-01-01T00:00:00Z",
created_by: "user-1",
updated_at: "2025-01-01T00:00:00Z",
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/accessGroups/useAccessGroups.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/accessGroups/useAccessGroups.ts
index 9f306c21459..b251c019187 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/accessGroups/useAccessGroups.ts
+++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/accessGroups/useAccessGroups.ts
@@ -3,23 +3,11 @@ import { createQueryKeys } from "../common/queryKeysFactory";
import { getProxyBaseUrl, getGlobalLitellmHeaderName, deriveErrorMessage, handleError } from "@/components/networking";
import { all_admin_roles } from "@/utils/roles";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
+import type { components } from "@/lib/http/schema";
// ── Types ────────────────────────────────────────────────────────────────────
-export interface AccessGroupResponse {
- access_group_id: string;
- access_group_name: string;
- description: string | null;
- access_model_names: string[];
- access_mcp_server_ids: string[];
- access_agent_ids: string[];
- assigned_team_ids: string[];
- assigned_key_ids: string[];
- created_at: string;
- created_by: string | null;
- updated_at: string;
- updated_by: string | null;
-}
+export type AccessGroupResponse = components["schemas"]["AccessGroupResponse"];
// ── Query keys (shared across access-group hooks) ────────────────────────────
diff --git a/ui/litellm-dashboard/src/components/add_model/build_complexity_router_config.test.ts b/ui/litellm-dashboard/src/components/add_model/build_complexity_router_config.test.ts
index edb32c6717f..1b5bb9e72eb 100644
--- a/ui/litellm-dashboard/src/components/add_model/build_complexity_router_config.test.ts
+++ b/ui/litellm-dashboard/src/components/add_model/build_complexity_router_config.test.ts
@@ -592,6 +592,12 @@ describe("classifier prompt and fallback", () => {
timeout_ms: 1,
});
});
+
+ it.each([{}, { system_prompt: "x" }])("normalizeClassifierLlmConfig carries vision through %o", (extra) => {
+ const base = { model: "m", timeout_ms: 1, ...extra };
+ const vision = { enabled: true, max_images: 2 };
+ expect(normalizeClassifierLlmConfig({ ...base, vision })).toEqual({ ...base, vision });
+ });
});
describe("tier labels", () => {
diff --git a/ui/litellm-dashboard/src/components/add_model/build_complexity_router_config.ts b/ui/litellm-dashboard/src/components/add_model/build_complexity_router_config.ts
index 1a395c38779..d7974484970 100644
--- a/ui/litellm-dashboard/src/components/add_model/build_complexity_router_config.ts
+++ b/ui/litellm-dashboard/src/components/add_model/build_complexity_router_config.ts
@@ -1,4 +1,7 @@
import { KeywordTierRule } from "./KeywordTierRules";
+
+type ClassifierLLMConfigWire = ClassifierLLMConfig & { vision?: { enabled?: boolean; max_images?: number } };
+
import type { ModelGroup } from "../llm_calls/fetch_models";
import {
type CustomTierSet,
@@ -61,7 +64,8 @@ export const normalizeClassifierLlmConfig = ({
reasoning_effort,
classification_rubric,
system_prompt,
-}: ClassifierLLMConfig): ClassifierLLMConfig =>
+ vision,
+}: ClassifierLLMConfigWire): ClassifierLLMConfigWire =>
system_prompt?.trim()
? {
model,
@@ -69,6 +73,7 @@ export const normalizeClassifierLlmConfig = ({
...(circuit_breaker_enabled !== undefined && { circuit_breaker_enabled }),
...(circuit_breaker_cooldown_seconds !== undefined && { circuit_breaker_cooldown_seconds }),
...(reasoning_effort && { reasoning_effort }),
+ ...(vision && { vision }),
system_prompt,
}
: {
@@ -78,6 +83,7 @@ export const normalizeClassifierLlmConfig = ({
...(circuit_breaker_cooldown_seconds !== undefined && { circuit_breaker_cooldown_seconds }),
...(reasoning_effort && { reasoning_effort }),
...(classification_rubric && { classification_rubric }),
+ ...(vision && { vision }),
};
interface ScorerKnobInputs {
@@ -324,7 +330,7 @@ export const getSemanticConfigError = ({
};
interface CustomTierWireFieldInputs {
- classifierLlmConfig: ClassifierLLMConfig | undefined;
+ classifierLlmConfig: ClassifierLLMConfigWire | undefined;
planModeMinTierId: string | undefined;
classificationPrompt: string | undefined;
classificationExamples: string | undefined;
@@ -356,6 +362,7 @@ export const customTierWireFields = (
circuit_breaker_cooldown_seconds: classifierLlmConfig.circuit_breaker_cooldown_seconds,
}),
...(classifierLlmConfig.reasoning_effort && { reasoning_effort: classifierLlmConfig.reasoning_effort }),
+ ...(classifierLlmConfig.vision && { vision: classifierLlmConfig.vision }),
},
}),
session_affinity: false,
diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts
index 6f60905d4c1..427e5deb555 100644
--- a/ui/litellm-dashboard/src/lib/http/schema.d.ts
+++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts
@@ -22764,22 +22764,40 @@ export interface components {
/** Spend */
spend?: number | null;
};
+ /**
+ * AccessGroupResource
+ * @description A resource referenced by an access group. `name` is null when the id no longer resolves or has no alias.
+ */
+ AccessGroupResource: {
+ /** Id */
+ id: string;
+ /** Name */
+ name: string | null;
+ };
/** AccessGroupResponse */
AccessGroupResponse: {
/** Access Agent Ids */
access_agent_ids: string[];
+ /** Access Agents */
+ access_agents: components["schemas"]["AccessGroupResource"][];
/** Access Group Id */
access_group_id: string;
/** Access Group Name */
access_group_name: string;
/** Access Mcp Server Ids */
access_mcp_server_ids: string[];
+ /** Access Mcp Servers */
+ access_mcp_servers: components["schemas"]["AccessGroupResource"][];
/** Access Model Names */
access_model_names: string[];
/** Assigned Key Ids */
assigned_key_ids: string[];
+ /** Assigned Keys */
+ assigned_keys: components["schemas"]["AccessGroupResource"][];
/** Assigned Team Ids */
assigned_team_ids: string[];
+ /** Assigned Teams */
+ assigned_teams: components["schemas"]["AccessGroupResource"][];
/**
* Created At
* Format: date-time
@@ -25305,6 +25323,30 @@ export interface components {
* @default 3000
*/
timeout_ms: number;
+ /** @description Whether the classifier sees images on the request, and how many */
+ vision?: components["schemas"]["ClassifierVisionConfig"];
+ };
+ /**
+ * ClassifierVisionConfig
+ * @description Whether the LLM classifier sees the images on the request it is classifying.
+ *
+ * Off by default because images cost far more than the text ask they arrive with, and the
+ * classifier runs on every request. A turn whose complexity lives in the image ("what is wrong in
+ * this stack trace screenshot") is invisible to a text-only classifier, which is what this buys.
+ */
+ ClassifierVisionConfig: {
+ /**
+ * Enabled
+ * @description Forward image content to the classifier. Requires a classifier model declared supports_vision, on the deployment's model_info or in the model cost map; images stay stripped otherwise, so a classifier that cannot read them is never sent one. Declare model_info.supports_vision on the deployment to enable a model the cost map does not describe. Only inline data: URIs are forwarded. A request whose images are http(s) URLs still classifies on its text alone, because some providers fetch such a URL from the proxy rather than the provider, which would let a caller aim a proxy-side request at an address of their choosing.
+ * @default false
+ */
+ enabled: boolean;
+ /**
+ * Max Images
+ * @description How many images from the newest user turn to forward, in wire order. Bounds the added cost of a turn that attaches many images. Images on earlier turns are never forwarded.
+ * @default 1
+ */
+ max_images: number;
};
/**
* CloudZeroExportRequest