Skip to content

Commit 6bc4b46

Browse files
Merge pull request #25156 from BerriAI/litellm_ryan-apr-4
Litellm ryan apr 4
2 parents 8ecbf75 + e87f6ca commit 6bc4b46

5 files changed

Lines changed: 266 additions & 71 deletions

File tree

litellm/proxy/public_endpoints/provider_create_fields.json

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,36 @@
406406
"field_type": "password",
407407
"options": null,
408408
"default_value": null
409+
},
410+
{
411+
"key": "tenant_id",
412+
"label": "Tenant ID",
413+
"placeholder": "Enter your Azure AD tenant ID",
414+
"tooltip": "Entra ID (Service Principal) auth. Provide tenant id, client id, and client secret together as an alternative to api key.",
415+
"required": false,
416+
"field_type": "text",
417+
"options": null,
418+
"default_value": null
419+
},
420+
{
421+
"key": "client_id",
422+
"label": "Client ID",
423+
"placeholder": "Enter your Service Principal client ID",
424+
"tooltip": "Entra ID (Service Principal) auth. Provide tenant id, client id, and client secret together as an alternative to api key.",
425+
"required": false,
426+
"field_type": "text",
427+
"options": null,
428+
"default_value": null
429+
},
430+
{
431+
"key": "client_secret",
432+
"label": "Client Secret",
433+
"placeholder": "Enter your Service Principal client secret",
434+
"tooltip": "Entra ID (Service Principal) auth. Provide tenant id, client id, and client secret together as an alternative to api key.",
435+
"required": false,
436+
"field_type": "password",
437+
"options": null,
438+
"default_value": null
409439
}
410440
],
411441
"default_model_placeholder": "azure/my-deployment"

tests/test_litellm/proxy/public_endpoints/test_public_endpoints.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,34 @@ def test_watsonx_provider_fields():
110110
assert "zen_api_key" in field_keys
111111

112112

113+
def test_azure_provider_fields_include_entra_id():
114+
"""Azure provider must expose Entra ID (Service Principal) credential fields so
115+
the UI can input tenant_id / client_id / client_secret as an alternative to api_key."""
116+
app = FastAPI()
117+
app.include_router(router)
118+
client = TestClient(app)
119+
120+
response = client.get("/public/providers/fields")
121+
providers = response.json()
122+
123+
azure = next((p for p in providers if p["provider"] == "Azure"), None)
124+
assert azure is not None
125+
126+
fields_by_key = {f["key"]: f for f in azure["credential_fields"]}
127+
# API-key auth still supported
128+
assert "api_key" in fields_by_key
129+
# Entra ID fields
130+
assert "tenant_id" in fields_by_key
131+
assert "client_id" in fields_by_key
132+
assert "client_secret" in fields_by_key
133+
# client_secret must be masked in the UI
134+
assert fields_by_key["client_secret"]["field_type"] == "password"
135+
# Entra ID is an alternative to api_key, so none of these are individually required
136+
assert fields_by_key["tenant_id"]["required"] is False
137+
assert fields_by_key["client_id"]["required"] is False
138+
assert fields_by_key["client_secret"]["required"] is False
139+
140+
113141
def test_public_model_hub_with_healthy_model():
114142
"""Test that health information is populated for a healthy model"""
115143
app = FastAPI()

ui/litellm-dashboard/src/components/EntityUsageExport/utils.test.ts

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,17 @@ vi.mock("papaparse", () => ({
3030
}));
3131

3232
describe("EntityUsageExport utils", () => {
33+
// Entity keys match team_ids because that's how the backend shapes team exports
34+
// (breakdown.entities is keyed by team_id). The fix under test uses the entity key
35+
// directly for display, so the key_alias/team_id in api_key_breakdown metadata is
36+
// no longer consulted — it's retained here only to mirror real payload shape.
3337
const mockSpendData: EntitySpendData = {
3438
results: [
3539
{
3640
date: "2025-01-01",
3741
breakdown: {
3842
entities: {
39-
entity1: {
43+
"team-1": {
4044
metrics: {
4145
spend: 10.5,
4246
api_requests: 100,
@@ -64,7 +68,7 @@ describe("EntityUsageExport utils", () => {
6468
},
6569
},
6670
},
67-
entity2: {
71+
"team-2": {
6872
metrics: {
6973
spend: 20.3,
7074
api_requests: 200,
@@ -99,7 +103,7 @@ describe("EntityUsageExport utils", () => {
99103
date: "2025-01-02",
100104
breakdown: {
101105
entities: {
102-
entity1: {
106+
"team-1": {
103107
metrics: {
104108
spend: 15.2,
105109
api_requests: 150,
@@ -184,22 +188,24 @@ describe("EntityUsageExport utils", () => {
184188
expect(entity1?.metrics.cache_creation_input_tokens).toBe(75);
185189
});
186190

187-
it("should use key alias when available", () => {
191+
it("should use entity key as alias when no team alias map is provided", () => {
192+
// Non-team exports (tags, orgs, customers, …) pass no teamAliasMap.
193+
// For teams, this is also the fallback when a team is missing from the map.
188194
const result = getEntityBreakdown(mockSpendData);
189195
const entity1 = result.find((e) => e.metadata.id === "team-1");
190196

191-
expect(entity1?.metadata.alias).toBe("alias-1");
197+
expect(entity1?.metadata.alias).toBe("team-1");
192198
});
193199

194-
it("should use team alias map when key alias is not available", () => {
200+
it("should use team alias map to resolve alias from entity key", () => {
195201
const spendDataWithoutAlias: EntitySpendData = {
196202
...mockSpendData,
197203
results: [
198204
{
199205
date: "2025-01-01",
200206
breakdown: {
201207
entities: {
202-
entity1: {
208+
"team-1": {
203209
metrics: {
204210
spend: 10.5,
205211
api_requests: 100,
@@ -299,7 +305,7 @@ describe("EntityUsageExport utils", () => {
299305
date: "2025-01-01",
300306
breakdown: {
301307
entities: {
302-
entity1: {
308+
"team-1": {
303309
metrics: {
304310
spend: 10.5,
305311
api_requests: 100,
@@ -379,15 +385,17 @@ describe("EntityUsageExport utils", () => {
379385
}
380386
});
381387

382-
it("should use dash when team id is not available", () => {
383-
const spendDataWithoutTeamId: EntitySpendData = {
388+
it("should fall back to the entity key when there is no team alias mapping", () => {
389+
// e.g. tag/org/customer exports where teamAliasMap has no entry for the entity,
390+
// or a team that isn't in the alias map — the entity key itself is the label.
391+
const spendDataWithoutAlias: EntitySpendData = {
384392
...mockSpendData,
385393
results: [
386394
{
387395
date: "2025-01-01",
388396
breakdown: {
389397
entities: {
390-
entity1: {
398+
"my-tag": {
391399
metrics: {
392400
spend: 10.5,
393401
api_requests: 100,
@@ -406,11 +414,11 @@ describe("EntityUsageExport utils", () => {
406414
metadata: mockSpendData.metadata,
407415
};
408416

409-
const result = generateDailyData(spendDataWithoutTeamId, "Team");
417+
const result = generateDailyData(spendDataWithoutAlias, "Tag");
410418
const entry = result[0];
411419

412-
expect(entry["Team ID"]).toBe("-");
413-
expect(entry["Team"]).toBe("-");
420+
expect(entry["Tag ID"]).toBe("my-tag");
421+
expect(entry["Tag"]).toBe("my-tag");
414422
});
415423

416424
it("should format spend values correctly", () => {
@@ -471,7 +479,7 @@ describe("EntityUsageExport utils", () => {
471479
date: "2025-01-01",
472480
breakdown: {
473481
entities: {
474-
entity1: {
482+
"team-1": {
475483
metrics: {
476484
spend: 10.5,
477485
api_requests: 100,
@@ -514,7 +522,7 @@ describe("EntityUsageExport utils", () => {
514522
},
515523
},
516524
},
517-
entity2: {
525+
"team-2": {
518526
metrics: {
519527
spend: 20.3,
520528
api_requests: 200,
@@ -549,7 +557,7 @@ describe("EntityUsageExport utils", () => {
549557
date: "2025-01-02",
550558
breakdown: {
551559
entities: {
552-
entity1: {
560+
"team-1": {
553561
metrics: {
554562
spend: 15.2,
555563
api_requests: 150,
@@ -979,7 +987,7 @@ describe("EntityUsageExport utils", () => {
979987
date: "2025-01-01",
980988
breakdown: {
981989
entities: {
982-
entity1: {
990+
"team-1": {
983991
metrics: {
984992
spend: 10.5,
985993
api_requests: 100,

ui/litellm-dashboard/src/components/EntityUsageExport/utils.ts

Lines changed: 30 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,16 @@ import type { DateRangePickerValue } from "@tremor/react";
33
import Papa from "papaparse";
44
import type { EntityBreakdown, EntitySpendData, EntityType, ExportMetadata, ExportScope } from "./types";
55

6-
// Helper function to extract team_id from api_key_breakdown
7-
const extractTeamIdFromApiKeyBreakdown = (apiKeyBreakdown: Record<string, any> | undefined): string | null => {
8-
if (!apiKeyBreakdown) return null;
9-
10-
// Look through all API keys to find the first non-null team_id
11-
for (const apiKeyData of Object.values(apiKeyBreakdown)) {
12-
const teamId = (apiKeyData as any)?.metadata?.team_id;
13-
if (teamId) {
14-
return teamId;
15-
}
16-
}
17-
return null;
18-
};
6+
// Resolve display name for an entity. For teams the teamAliasMap provides
7+
// a human-readable alias; for every other entity type the entity key itself
8+
// (tag name, org id, customer id, …) is already the correct label.
9+
const resolveEntityDisplay = (
10+
entity: string,
11+
teamAliasMap: Record<string, string>,
12+
): { id: string; alias: string } => ({
13+
id: entity,
14+
alias: teamAliasMap[entity] || entity,
15+
});
1916

2017
// Mirrors backend SpendMetrics fields (litellm/types/activity_tracking.py).
2118
// If the backend adds a field, add it here too.
@@ -68,18 +65,7 @@ export const getEntityBreakdown = (
6865

6966
spendData.results.forEach((day) => {
7067
Object.entries(resolveEntities(day.breakdown)).forEach(([entity, data]: [string, any]) => {
71-
// Extract team_id from api_key_breakdown metadata (not data.metadata which is empty)
72-
const teamId = extractTeamIdFromApiKeyBreakdown(data.api_key_breakdown) || entity;
73-
// Extract key_alias from the first API key that has one
74-
const apiKeyBreakdown = data.api_key_breakdown || {};
75-
let keyAlias: string | null = null;
76-
for (const apiKeyData of Object.values(apiKeyBreakdown)) {
77-
const alias = (apiKeyData as any)?.metadata?.key_alias;
78-
if (alias) {
79-
keyAlias = alias;
80-
break;
81-
}
82-
}
68+
const { id, alias } = resolveEntityDisplay(entity, teamAliasMap);
8369

8470
if (!entitySpend[entity]) {
8571
entitySpend[entity] = {
@@ -95,8 +81,8 @@ export const getEntityBreakdown = (
9581
cache_creation_input_tokens: 0,
9682
},
9783
metadata: {
98-
alias: keyAlias || teamAliasMap[teamId] || entity,
99-
id: teamId,
84+
alias,
85+
id,
10086
},
10187
};
10288
}
@@ -124,14 +110,12 @@ export const generateDailyData = (
124110

125111
spendData.results.forEach((day) => {
126112
Object.entries(resolveEntities(day.breakdown)).forEach(([entity, data]: [string, any]) => {
127-
// Extract team_id from api_key_breakdown metadata (not data.metadata which is empty)
128-
const teamId = extractTeamIdFromApiKeyBreakdown(data.api_key_breakdown);
129-
const teamAlias = teamId ? teamAliasMap[teamId] || null : null;
113+
const { id, alias } = resolveEntityDisplay(entity, teamAliasMap);
130114

131115
dailyBreakdown.push({
132116
Date: day.date,
133-
[entityLabel]: teamAlias || "-",
134-
[`${entityLabel} ID`]: teamId || "-",
117+
[entityLabel]: alias,
118+
[`${entityLabel} ID`]: id,
135119
"Spend ($)": formatNumberWithCommas(data.metrics.spend, 4),
136120
Requests: data.metrics.api_requests,
137121
"Successful Requests": data.metrics.successful_requests,
@@ -151,12 +135,12 @@ export const generateDailyWithKeysData = (
151135
entityLabel: string,
152136
teamAliasMap: Record<string, string> = {},
153137
): any[] => {
154-
// Aggregate by unique (Date, Team ID, Key ID) combination to prevent duplicates
138+
// Aggregate by unique (Date, Entity ID, Key ID) combination to prevent duplicates
155139
const aggregatedData: {
156140
[key: string]: {
157141
Date: string;
158-
teamId: string;
159-
teamAlias: string | null;
142+
entityId: string;
143+
entityAlias: string;
160144
keyId: string;
161145
keyAlias: string | null;
162146
metrics: {
@@ -173,23 +157,22 @@ export const generateDailyWithKeysData = (
173157

174158
spendData.results.forEach((day) => {
175159
Object.entries(resolveEntities(day.breakdown)).forEach(([entity, data]: [string, any]) => {
160+
const { id: entityId, alias: entityAlias } = resolveEntityDisplay(entity, teamAliasMap);
176161
const apiKeyBreakdown = data.api_key_breakdown || {};
177162

178163
// Iterate through each API key in the breakdown
179164
Object.entries(apiKeyBreakdown).forEach(([keyId, keyData]: [string, any]) => {
180165
const keyAlias = keyData?.metadata?.key_alias || null;
181-
const teamId = keyData?.metadata?.team_id || entity;
182-
const teamAlias = teamId ? teamAliasMap[teamId] || null : null;
183166

184-
// Create unique key for aggregation: Date_TeamID_KeyID
185-
const uniqueKey = `${day.date}_${teamId}_${keyId}`;
167+
// Create unique key for aggregation: Date_EntityID_KeyID
168+
const uniqueKey = `${day.date}_${entityId}_${keyId}`;
186169

187170
if (!aggregatedData[uniqueKey]) {
188-
// First time seeing this (Date, Team ID, Key ID) combination
171+
// First time seeing this (Date, Entity ID, Key ID) combination
189172
aggregatedData[uniqueKey] = {
190173
Date: day.date,
191-
teamId,
192-
teamAlias,
174+
entityId,
175+
entityAlias,
193176
keyId,
194177
keyAlias,
195178
metrics: {
@@ -219,8 +202,8 @@ export const generateDailyWithKeysData = (
219202
// Convert aggregated data to array format
220203
const dailyKeyBreakdown = Object.values(aggregatedData).map((item) => ({
221204
Date: item.Date,
222-
[entityLabel]: item.teamAlias || "-",
223-
[`${entityLabel} ID`]: item.teamId || "-",
205+
[entityLabel]: item.entityAlias,
206+
[`${entityLabel} ID`]: item.entityId,
224207
"Key Alias": item.keyAlias || "-",
225208
"Key ID": item.keyId,
226209
"Spend ($)": formatNumberWithCommas(item.metrics.spend, 4),
@@ -273,16 +256,13 @@ export const generateDailyWithModelsData = (
273256
});
274257

275258
Object.entries(dailyEntityModels).forEach(([entity, models]) => {
276-
const entityData = resolveEntities(day.breakdown)[entity];
277-
// Extract team_id from api_key_breakdown metadata (not entityData.metadata which is empty)
278-
const teamId = extractTeamIdFromApiKeyBreakdown(entityData?.api_key_breakdown);
279-
const teamAlias = teamId ? teamAliasMap[teamId] || null : null;
259+
const { id, alias } = resolveEntityDisplay(entity, teamAliasMap);
280260

281261
Object.entries(models).forEach(([model, metrics]: [string, any]) => {
282262
dailyModelBreakdown.push({
283263
Date: day.date,
284-
[entityLabel]: teamAlias || "-",
285-
[`${entityLabel} ID`]: teamId || "-",
264+
[entityLabel]: alias,
265+
[`${entityLabel} ID`]: id,
286266
Model: model,
287267
"Spend ($)": formatNumberWithCommas(metrics.spend, 4),
288268
Requests: metrics.requests,

0 commit comments

Comments
 (0)