Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/management_endpoints/user_agent_analytics_endpoints.py: 86%

267 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-10 12:01 +0000

1""" 

2User Agent Analytics Endpoints 

3 

4This module provides optimized endpoints for tracking user agent activity metrics including: 

5- Daily Active Users (DAU) by tags for configurable number of days 

6- Weekly Active Users (WAU) by tags for configurable number of weeks 

7- Monthly Active Users (MAU) by tags for configurable number of months 

8- Summary analytics by tags 

9 

10These endpoints use optimized single SQL queries with joins to efficiently calculate 

11user metrics from tag activity data and return time series for dashboard visualization. 

12""" 

13 

14from collections.abc import Mapping, Sequence 

15from datetime import datetime, timedelta 

16from typing import TYPE_CHECKING, Final, Protocol, TypeVar, overload 

17 

18from fastapi import APIRouter, Depends, HTTPException, Query 

19from pydantic import BaseModel, TypeAdapter 

20 

21if TYPE_CHECKING: 21 ↛ 22line 21 didn't jump to line 22 because the condition on line 21 was never true

22 from prisma.models import LiteLLM_DailyTagSpend as PrismaDailyTagSpendRow 

23 from prisma.models import LiteLLM_UserTable as PrismaUserRow 

24 from prisma.models import LiteLLM_VerificationToken as PrismaVerificationTokenRow 

25 

26 from litellm.proxy.utils import PrismaClient 

27 

28from litellm.proxy._types import CommonProxyErrors, UserAPIKeyAuth 

29from litellm.proxy.auth.user_api_key_auth import user_api_key_auth 

30from litellm.repositories.table_repositories import DailyTagSpendRepository 

31from litellm.repositories.user_repository import UserRepository 

32from litellm.repositories.verification_token_repository import ( 

33 VerificationTokenRepository, 

34) 

35 

36# Constants for analytics periods 

37MAX_DAYS: Final = 7 # Number of days to show in DAU analytics 

38MAX_WEEKS: Final = 7 # Number of weeks to show in WAU analytics 

39MAX_MONTHS: Final = 7 # Number of months to show in MAU analytics 

40MAX_TAGS: Final = 250 # Maximum number of distinct tags to return 

41 

42router: Final = APIRouter() 

43 

44 

45class TagActiveUsersResponse(BaseModel): 

46 """Response for tag active users metrics""" 

47 

48 tag: str 

49 active_users: int 

50 date: str # The specific date or period identifier 

51 period_start: str | None = None # For WAU/MAU, this will be the start of the period 

52 period_end: str | None = None # For WAU/MAU, this will be the end of the period 

53 

54 

55class ActiveUsersAnalyticsResponse(BaseModel): 

56 """Response for active users analytics""" 

57 

58 results: list[TagActiveUsersResponse] 

59 

60 

61class TagSummaryMetrics(BaseModel): 

62 """Summary metrics for a tag""" 

63 

64 tag: str 

65 unique_users: int 

66 total_requests: int 

67 successful_requests: int 

68 failed_requests: int 

69 total_tokens: int 

70 total_spend: float 

71 

72 

73class TagSummaryResponse(BaseModel): 

74 """Response for tag summary analytics""" 

75 

76 results: list[TagSummaryMetrics] 

77 

78 

79class DistinctTagResponse(BaseModel): 

80 """Response for distinct user agent tags""" 

81 

82 tag: str 

83 

84 

85class DistinctTagsResponse(BaseModel): 

86 """Response for all distinct user agent tags""" 

87 

88 results: list[DistinctTagResponse] 

89 

90 

91class PerUserMetrics(BaseModel): 

92 """Metrics for individual user""" 

93 

94 user_id: str 

95 user_email: str | None = None 

96 user_agent: str | None = None 

97 successful_requests: int = 0 

98 failed_requests: int = 0 

99 total_requests: int = 0 

100 total_tokens: int = 0 

101 spend: float = 0.0 

102 

103 

104class PerUserAnalyticsResponse(BaseModel): 

105 """Response for per-user analytics""" 

106 

107 results: list[PerUserMetrics] 

108 total_count: int 

109 page: int 

110 page_size: int 

111 total_pages: int 

112 

113 

114class _DistinctTagRow(BaseModel): 

115 tag: str 

116 

117 

118class _ActiveUsersRow(BaseModel): 

119 tag: str 

120 active_users: int 

121 date: str 

122 period_start: str | None = None 

123 period_end: str | None = None 

124 

125 

126class _TagSummaryRow(BaseModel): 

127 tag: str 

128 unique_users: int | None = None 

129 total_requests: float | int | str | None = None 

130 successful_requests: float | int | str | None = None 

131 failed_requests: float | int | str | None = None 

132 total_tokens: float | int | str | None = None 

133 total_spend: float | int | str | None = None 

134 

135 

136_DISTINCT_TAG_ROWS: Final = TypeAdapter(list[_DistinctTagRow]) 

137_ACTIVE_USERS_ROWS: Final = TypeAdapter(list[_ActiveUsersRow]) 

138_TAG_SUMMARY_ROWS: Final = TypeAdapter(list[_TagSummaryRow]) 

139 

140_RowT_co: Final = TypeVar("_RowT_co", covariant=True) 

141 

142if TYPE_CHECKING: 142 ↛ 144line 142 didn't jump to line 144 because the condition on line 142 was never true

143 

144 class _TableOps(Protocol[_RowT_co]): 

145 async def find_many(self, where: Mapping[str, object] | None = None) -> Sequence[_RowT_co]: ... 

146 

147 

148@overload 

149def _typed_table(repo: DailyTagSpendRepository) -> "_TableOps[PrismaDailyTagSpendRow]": ... 149 ↛ exitline 149 didn't return from function '_typed_table' because

150@overload 

151def _typed_table(repo: VerificationTokenRepository) -> "_TableOps[PrismaVerificationTokenRow]": ... 151 ↛ exitline 151 didn't return from function '_typed_table' because

152@overload 

153def _typed_table(repo: UserRepository) -> "_TableOps[PrismaUserRow]": ... 153 ↛ exitline 153 didn't return from function '_typed_table' because

154def _typed_table(repo: DailyTagSpendRepository | VerificationTokenRepository | UserRepository) -> object: 

155 return repo.table 

156 

157 

158async def _query_raw(prisma_client: "PrismaClient", sql_query: str, *params: object) -> object: 

159 return await prisma_client.db.query_raw(sql_query, *params) 

160 

161 

162@router.get( 

163 "/tag/distinct", 

164 response_model=DistinctTagsResponse, 

165 tags=["tag management", "user agent analytics"], 

166 dependencies=[Depends(user_api_key_auth)], 

167) 

168async def get_distinct_user_agent_tags( 

169 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), 

170): 

171 """ 

172 Get all distinct user agent tags up to a maximum of {MAX_TAGS} tags. 

173 

174 This endpoint returns all unique user agent tags found in the database, 

175 sorted by frequency of usage. 

176 

177 Returns: 

178 DistinctTagsResponse: List of distinct user agent tags 

179 """ 

180 from litellm.proxy.proxy_server import prisma_client 

181 

182 if prisma_client is None: 182 ↛ 183line 182 didn't jump to line 183 because the condition on line 182 was never true

183 raise HTTPException( 

184 status_code=500, 

185 detail={"error": CommonProxyErrors.db_not_connected_error.value}, 

186 ) 

187 

188 try: 

189 sql_query: Final = f""" 

190 SELECT  

191 dts.tag, 

192 COUNT(*) as usage_count 

193 FROM "LiteLLM_DailyTagSpend" dts 

194 WHERE dts.tag LIKE 'User-Agent:%' OR dts.tag NOT LIKE '%:%' 

195 GROUP BY dts.tag 

196 ORDER BY usage_count DESC 

197 LIMIT {MAX_TAGS} 

198 """ 

199 

200 db_response: Final = _DISTINCT_TAG_ROWS.validate_python(await _query_raw(prisma_client, sql_query)) 

201 

202 results: Final = [DistinctTagResponse(tag=row.tag) for row in db_response] 

203 

204 return DistinctTagsResponse(results=results) 

205 

206 except Exception as e: 

207 raise HTTPException( 

208 status_code=500, 

209 detail=f"Failed to fetch distinct user agent tags: {e}", 

210 ) 

211 

212 

213@router.get( 

214 "/tag/dau", 

215 response_model=ActiveUsersAnalyticsResponse, 

216 tags=["tag management", "user agent analytics"], 

217 dependencies=[Depends(user_api_key_auth)], 

218) 

219async def get_daily_active_users( 

220 tag_filter: str | None = Query( 

221 default=None, 

222 description="Filter by specific tag (optional)", 

223 ), 

224 tag_filters: list[str] | None = Query( 

225 default=None, 

226 description="Filter by multiple specific tags (optional, takes precedence over tag_filter)", 

227 ), 

228 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), 

229): 

230 """ 

231 Get Daily Active Users (DAU) by tags for the last {MAX_DAYS} days ending on UTC today + 1 day. 

232 

233 This endpoint efficiently calculates unique users per tag for each of the last {MAX_DAYS} days 

234 using a single optimized SQL query, perfect for dashboard time series visualization. 

235 

236 Args: 

237 tag_filter: Optional filter to specific tag (legacy) 

238 tag_filters: Optional filter to multiple specific tags (takes precedence over tag_filter) 

239 

240 Returns: 

241 ActiveUsersAnalyticsResponse: DAU data by tag for each of the last {MAX_DAYS} days 

242 """ 

243 from litellm.proxy.proxy_server import prisma_client 

244 

245 if prisma_client is None: 245 ↛ 246line 245 didn't jump to line 246 because the condition on line 245 was never true

246 raise HTTPException( 

247 status_code=500, 

248 detail={"error": CommonProxyErrors.db_not_connected_error.value}, 

249 ) 

250 

251 try: 

252 # Calculate end_date as UTC today + 1 day 

253 from datetime import timezone 

254 

255 end_dt = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1) 

256 end_date: Final = end_dt.strftime("%Y-%m-%d") 

257 

258 # Calculate date range (last MAX_DAYS days) 

259 start_dt: Final = end_dt - timedelta(days=MAX_DAYS) 

260 start_date: Final = start_dt.strftime("%Y-%m-%d") 

261 

262 # Build SQL query with optional tag filter(s) 

263 where_clause = "WHERE dts.date >= $1 AND dts.date <= $2 AND vt.user_id IS NOT NULL" 

264 params: Final = [start_date, end_date] 

265 

266 # Handle multiple tag filters (takes precedence over single tag filter) 

267 if tag_filters and len(tag_filters) > 0: 

268 tag_conditions: Final = [] 

269 for i, tag in enumerate(tag_filters): 

270 param_index = len(params) + 1 

271 tag_conditions.append(f"dts.tag = ${param_index}") 

272 params.append(tag) 

273 where_clause += f" AND ({' OR '.join(tag_conditions)})" 

274 elif tag_filter: 

275 where_clause += " AND dts.tag ILIKE $3" 

276 params.append(f"%{tag_filter}%") 

277 

278 sql_query: Final = f""" 

279 SELECT  

280 dts.tag, 

281 dts.date, 

282 COUNT(DISTINCT vt.user_id) as active_users 

283 FROM "LiteLLM_DailyTagSpend" dts 

284 INNER JOIN "LiteLLM_VerificationToken" vt ON dts.api_key = vt.token 

285 {where_clause} 

286 GROUP BY dts.tag, dts.date 

287 ORDER BY dts.date DESC, active_users DESC 

288 """ 

289 

290 db_response: Final = _ACTIVE_USERS_ROWS.validate_python(await _query_raw(prisma_client, sql_query, *params)) 

291 

292 results: Final = [ 

293 TagActiveUsersResponse(tag=row.tag, active_users=row.active_users, date=row.date) for row in db_response 

294 ] 

295 

296 return ActiveUsersAnalyticsResponse(results=results) 

297 

298 except Exception as e: 

299 raise HTTPException( 

300 status_code=500, 

301 detail=f"Failed to fetch DAU analytics: {e}", 

302 ) 

303 

304 

305@router.get( 

306 "/tag/wau", 

307 response_model=ActiveUsersAnalyticsResponse, 

308 tags=["tag management", "user agent analytics"], 

309 dependencies=[Depends(user_api_key_auth)], 

310) 

311async def get_weekly_active_users( 

312 tag_filter: str | None = Query( 

313 default=None, 

314 description="Filter by specific tag (optional)", 

315 ), 

316 tag_filters: list[str] | None = Query( 

317 default=None, 

318 description="Filter by multiple specific tags (optional, takes precedence over tag_filter)", 

319 ), 

320 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), 

321): 

322 """ 

323 Get Weekly Active Users (WAU) by tags for the last {MAX_WEEKS} weeks ending on UTC today + 1 day. 

324 

325 Shows week-by-week breakdown: 

326 - Week 1 (Jan 1): Earliest week (7 weeks ago) 

327 - Week 2 (Jan 8): Next week (6 weeks ago) 

328 - Week 3 (Jan 15): Next week (5 weeks ago) 

329 - ... and so on for {MAX_WEEKS} weeks total 

330 - Week 7: Most recent week ending on UTC today + 1 day 

331 

332 Args: 

333 tag_filter: Optional filter to specific tag (legacy) 

334 tag_filters: Optional filter to multiple specific tags (takes precedence over tag_filter) 

335 

336 Returns: 

337 ActiveUsersAnalyticsResponse: WAU data by tag for each of the last {MAX_WEEKS} weeks with descriptive week labels (e.g., "Week 1 (Jan 1)") 

338 """ 

339 from litellm.proxy.proxy_server import prisma_client 

340 

341 if prisma_client is None: 341 ↛ 342line 341 didn't jump to line 342 because the condition on line 341 was never true

342 raise HTTPException( 

343 status_code=500, 

344 detail={"error": CommonProxyErrors.db_not_connected_error.value}, 

345 ) 

346 

347 try: 

348 # Calculate end_date as UTC today + 1 day 

349 from datetime import timezone 

350 

351 end_dt = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1) 

352 end_date: Final = end_dt.strftime("%Y-%m-%d") 

353 

354 # Calculate date range for all weeks (49 days total) 

355 # Start from 48 days before end_date to cover exactly MAX_WEEKS complete weeks 

356 start_dt: Final = end_dt - timedelta(days=(MAX_WEEKS * 7 - 1)) # MAX_WEEKS weeks * 7 days - 1 

357 start_date: Final = start_dt.strftime("%Y-%m-%d") 

358 

359 # Build SQL query with optional tag filter(s) 

360 where_clause = "WHERE dts.date >= $1 AND dts.date <= $2 AND vt.user_id IS NOT NULL" 

361 params: Final = [start_date, end_date] 

362 

363 # Handle multiple tag filters (takes precedence over single tag filter) 

364 if tag_filters and len(tag_filters) > 0: 

365 tag_conditions: Final = [] 

366 for i, tag in enumerate(tag_filters): 

367 param_index = len(params) + 1 

368 tag_conditions.append(f"dts.tag = ${param_index}") 

369 params.append(tag) 

370 where_clause += f" AND ({' OR '.join(tag_conditions)})" 

371 elif tag_filter: 

372 where_clause += " AND dts.tag ILIKE $3" 

373 params.append(f"%{tag_filter}%") 

374 

375 # Use window function to group by weeks with clear week numbering 

376 sql_query: Final = f""" 

377 WITH weekly_data AS ( 

378 SELECT  

379 dts.tag, 

380 dts.date, 

381 vt.user_id, 

382 -- Calculate week number (0 = Week 1 most recent, 1 = Week 2, etc.) 

383 FLOOR((DATE '{end_date}' - dts.date::date) / 7) as week_offset 

384 FROM "LiteLLM_DailyTagSpend" dts 

385 INNER JOIN "LiteLLM_VerificationToken" vt ON dts.api_key = vt.token 

386 {where_clause} 

387 ) 

388 SELECT  

389 tag, 

390 COUNT(DISTINCT user_id) as active_users, 

391 -- Week identifier with month and day (Week 1 (earliest), Week 2, etc.) 

392 'Week ' || ({MAX_WEEKS} - week_offset)::text || ' (' ||  

393 TO_CHAR(DATE '{end_date}' - (week_offset * 7 || ' days')::interval - '6 days'::interval, 'Mon DD') || ')' as date, 

394 -- Calculate week start and end dates for each week 

395 (DATE '{end_date}' - (week_offset * 7 || ' days')::interval - '6 days'::interval)::text as period_start, 

396 (DATE '{end_date}' - (week_offset * 7 || ' days')::interval)::text as period_end, 

397 week_offset 

398 FROM weekly_data 

399 WHERE week_offset < {MAX_WEEKS} 

400 GROUP BY tag, week_offset 

401 ORDER BY week_offset DESC, active_users DESC 

402 """ 

403 

404 db_response: Final = _ACTIVE_USERS_ROWS.validate_python(await _query_raw(prisma_client, sql_query, *params)) 

405 

406 results: Final = [ 

407 TagActiveUsersResponse( 

408 tag=row.tag, 

409 active_users=row.active_users, 

410 date=row.date, # This will be "Week 1 (Jan 15)", "Week 2 (Jan 8)", etc. 

411 period_start=row.period_start, 

412 period_end=row.period_end, 

413 ) 

414 for row in db_response 

415 ] 

416 

417 return ActiveUsersAnalyticsResponse(results=results) 

418 

419 except Exception as e: 

420 raise HTTPException( 

421 status_code=500, 

422 detail=f"Failed to fetch WAU analytics: {e}", 

423 ) 

424 

425 

426@router.get( 

427 "/tag/mau", 

428 response_model=ActiveUsersAnalyticsResponse, 

429 tags=["tag management", "user agent analytics"], 

430 dependencies=[Depends(user_api_key_auth)], 

431) 

432async def get_monthly_active_users( 

433 tag_filter: str | None = Query( 

434 default=None, 

435 description="Filter by specific tag (optional)", 

436 ), 

437 tag_filters: list[str] | None = Query( 

438 default=None, 

439 description="Filter by multiple specific tags (optional, takes precedence over tag_filter)", 

440 ), 

441 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), 

442): 

443 """ 

444 Get Monthly Active Users (MAU) by tags for the last {MAX_MONTHS} months ending on UTC today + 1 day. 

445 

446 Shows month-by-month breakdown: 

447 - Month 1 (Nov): Earliest month (7 months ago, 30-day period) 

448 - Month 2 (Dec): Next month (6 months ago) 

449 - Month 3 (Jan): Next month (5 months ago) 

450 - ... and so on for {MAX_MONTHS} months total 

451 - Month 7: Most recent month ending on UTC today + 1 day 

452 

453 Args: 

454 tag_filter: Optional filter to specific tag (legacy) 

455 tag_filters: Optional filter to multiple specific tags (takes precedence over tag_filter) 

456 

457 Returns: 

458 ActiveUsersAnalyticsResponse: MAU data by tag for each of the last {MAX_MONTHS} months with descriptive month labels (e.g., "Month 1 (Nov)") 

459 """ 

460 from litellm.proxy.proxy_server import prisma_client 

461 

462 if prisma_client is None: 462 ↛ 463line 462 didn't jump to line 463 because the condition on line 462 was never true

463 raise HTTPException( 

464 status_code=500, 

465 detail={"error": CommonProxyErrors.db_not_connected_error.value}, 

466 ) 

467 

468 try: 

469 # Calculate end_date as UTC today + 1 day 

470 from datetime import timezone 

471 

472 end_dt = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1) 

473 end_date: Final = end_dt.strftime("%Y-%m-%d") 

474 

475 # Calculate date range for all months (210 days total) 

476 # Start from 209 days before end_date to cover exactly MAX_MONTHS complete months 

477 start_dt: Final = end_dt - timedelta(days=(MAX_MONTHS * 30 - 1)) # MAX_MONTHS months * 30 days - 1 

478 start_date: Final = start_dt.strftime("%Y-%m-%d") 

479 

480 # Build SQL query with optional tag filter(s) 

481 where_clause = "WHERE dts.date >= $1 AND dts.date <= $2 AND vt.user_id IS NOT NULL" 

482 params: Final = [start_date, end_date] 

483 

484 # Handle multiple tag filters (takes precedence over single tag filter) 

485 if tag_filters and len(tag_filters) > 0: 

486 tag_conditions: Final = [] 

487 for i, tag in enumerate(tag_filters): 

488 param_index = len(params) + 1 

489 tag_conditions.append(f"dts.tag = ${param_index}") 

490 params.append(tag) 

491 where_clause += f" AND ({' OR '.join(tag_conditions)})" 

492 elif tag_filter: 

493 where_clause += " AND dts.tag ILIKE $3" 

494 params.append(f"%{tag_filter}%") 

495 

496 # Use window function to group by months (30-day periods) with clear month numbering 

497 sql_query: Final = f""" 

498 WITH monthly_data AS ( 

499 SELECT  

500 dts.tag, 

501 dts.date, 

502 vt.user_id, 

503 -- Calculate month number (0 = Month 1 most recent, 1 = Month 2, etc.) 

504 FLOOR((DATE '{end_date}' - dts.date::date) / 30) as month_offset 

505 FROM "LiteLLM_DailyTagSpend" dts 

506 INNER JOIN "LiteLLM_VerificationToken" vt ON dts.api_key = vt.token 

507 {where_clause} 

508 ) 

509 SELECT  

510 tag, 

511 COUNT(DISTINCT user_id) as active_users, 

512 -- Month identifier with month name (Month 1 (earliest), Month 2, etc.) 

513 'Month ' || ({MAX_MONTHS} - month_offset)::text || ' (' ||  

514 TO_CHAR(DATE '{end_date}' - (month_offset * 30 || ' days')::interval - '29 days'::interval, 'Mon') || ')' as date, 

515 -- Calculate month start and end dates for each month 

516 (DATE '{end_date}' - (month_offset * 30 || ' days')::interval - '29 days'::interval)::text as period_start, 

517 (DATE '{end_date}' - (month_offset * 30 || ' days')::interval)::text as period_end, 

518 month_offset 

519 FROM monthly_data 

520 WHERE month_offset < {MAX_MONTHS} 

521 GROUP BY tag, month_offset 

522 ORDER BY month_offset DESC, active_users DESC 

523 """ 

524 

525 db_response: Final = _ACTIVE_USERS_ROWS.validate_python(await _query_raw(prisma_client, sql_query, *params)) 

526 

527 results: Final = [ 

528 TagActiveUsersResponse( 

529 tag=row.tag, 

530 active_users=row.active_users, 

531 date=row.date, # This will be "Month 1 (Jan)", "Month 2 (Dec)", etc. 

532 period_start=row.period_start, 

533 period_end=row.period_end, 

534 ) 

535 for row in db_response 

536 ] 

537 

538 return ActiveUsersAnalyticsResponse(results=results) 

539 

540 except Exception as e: 

541 raise HTTPException( 

542 status_code=500, 

543 detail=f"Failed to fetch MAU analytics: {e}", 

544 ) 

545 

546 

547@router.get( 

548 "/tag/summary", 

549 response_model=TagSummaryResponse, 

550 tags=["tag management", "user agent analytics"], 

551 dependencies=[Depends(user_api_key_auth)], 

552) 

553async def get_tag_summary( 

554 start_date: str = Query(description="Start date in YYYY-MM-DD format"), 

555 end_date: str = Query(description="End date in YYYY-MM-DD format"), 

556 tag_filter: str | None = Query( 

557 default=None, 

558 description="Filter by specific tag (optional)", 

559 ), 

560 tag_filters: list[str] | None = Query( 

561 default=None, 

562 description="Filter by multiple specific tags (optional, takes precedence over tag_filter)", 

563 ), 

564 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), 

565): 

566 """ 

567 Get summary analytics for tags including unique users, requests, tokens, and spend. 

568 

569 Args: 

570 start_date: Start date for the analytics period (YYYY-MM-DD) 

571 end_date: End date for the analytics period (YYYY-MM-DD) 

572 tag_filter: Optional filter to specific tag (legacy) 

573 tag_filters: Optional filter to multiple specific tags (takes precedence over tag_filter) 

574 

575 Returns: 

576 TagSummaryResponse: Summary analytics data by tag 

577 """ 

578 from litellm.proxy.proxy_server import prisma_client 

579 

580 if prisma_client is None: 580 ↛ 581line 580 didn't jump to line 581 because the condition on line 580 was never true

581 raise HTTPException( 

582 status_code=500, 

583 detail={"error": CommonProxyErrors.db_not_connected_error.value}, 

584 ) 

585 

586 try: 

587 # Validate date format 

588 datetime.strptime(start_date, "%Y-%m-%d") 

589 datetime.strptime(end_date, "%Y-%m-%d") 

590 

591 # Build SQL query with optional tag filter(s) 

592 where_clause = "WHERE dts.date >= $1 AND dts.date <= $2" 

593 params: Final = [start_date, end_date] 

594 

595 # Handle multiple tag filters (takes precedence over single tag filter) 

596 if tag_filters and len(tag_filters) > 0: 

597 tag_conditions: Final = [] 

598 for i, tag in enumerate(tag_filters): 

599 param_index = len(params) + 1 

600 tag_conditions.append(f"dts.tag = ${param_index}") 

601 params.append(tag) 

602 where_clause += f" AND ({' OR '.join(tag_conditions)})" 

603 elif tag_filter: 

604 where_clause += " AND dts.tag ILIKE $3" 

605 params.append(f"%{tag_filter}%") 

606 

607 sql_query: Final = f""" 

608 SELECT  

609 dts.tag, 

610 COUNT(DISTINCT vt.user_id) as unique_users, 

611 SUM(dts.api_requests) as total_requests, 

612 SUM(dts.successful_requests) as successful_requests, 

613 SUM(dts.failed_requests) as failed_requests, 

614 SUM(dts.prompt_tokens + dts.completion_tokens) as total_tokens, 

615 SUM(dts.spend) as total_spend 

616 FROM "LiteLLM_DailyTagSpend" dts 

617 LEFT JOIN "LiteLLM_VerificationToken" vt ON dts.api_key = vt.token 

618 {where_clause} 

619 GROUP BY dts.tag 

620 ORDER BY total_requests DESC 

621 """ 

622 

623 db_response: Final = _TAG_SUMMARY_ROWS.validate_python(await _query_raw(prisma_client, sql_query, *params)) 

624 

625 results: Final = [ 

626 TagSummaryMetrics( 

627 tag=row.tag, 

628 unique_users=row.unique_users or 0, 

629 total_requests=int(row.total_requests or 0), 

630 successful_requests=int(row.successful_requests or 0), 

631 failed_requests=int(row.failed_requests or 0), 

632 total_tokens=int(row.total_tokens or 0), 

633 total_spend=float(row.total_spend or 0.0), 

634 ) 

635 for row in db_response 

636 ] 

637 

638 return TagSummaryResponse(results=results) 

639 

640 except ValueError as e: 

641 raise HTTPException( 

642 status_code=400, 

643 detail=f"Invalid date format. Use YYYY-MM-DD: {e}", 

644 ) 

645 except Exception as e: 

646 raise HTTPException( 

647 status_code=500, 

648 detail=f"Failed to fetch tag summary analytics: {e}", 

649 ) 

650 

651 

652@router.get( 

653 "/tag/user-agent/per-user-analytics", 

654 response_model=PerUserAnalyticsResponse, 

655 tags=["tag management", "user agent analytics"], 

656 dependencies=[Depends(user_api_key_auth)], 

657) 

658async def get_per_user_analytics( 

659 tag_filter: str | None = Query( 

660 default=None, 

661 description="Filter by specific tag (optional)", 

662 ), 

663 tag_filters: list[str] | None = Query( 

664 default=None, 

665 description="Filter by multiple specific tags (optional, takes precedence over tag_filter)", 

666 ), 

667 page: int = Query(default=1, description="Page number for pagination", ge=1), 

668 page_size: int = Query(default=50, description="Items per page", ge=1, le=1000), 

669 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), 

670): 

671 """ 

672 Get per-user analytics including successful requests, tokens, and spend by individual users. 

673 

674 This endpoint provides usage metrics broken down by individual users based on their 

675 tag activity during the last 30 days ending on UTC today + 1 day. 

676 

677 Args: 

678 tag_filter: Optional filter to specific tag (legacy) 

679 tag_filters: Optional filter to multiple specific tags (takes precedence over tag_filter) 

680 page: Page number for pagination 

681 page_size: Number of items per page 

682 

683 Returns: 

684 PerUserAnalyticsResponse: Analytics data broken down by individual users for the last 30 days 

685 """ 

686 from litellm.proxy.proxy_server import prisma_client 

687 

688 if prisma_client is None: 688 ↛ 689line 688 didn't jump to line 689 because the condition on line 688 was never true

689 raise HTTPException( 

690 status_code=500, 

691 detail={"error": CommonProxyErrors.db_not_connected_error.value}, 

692 ) 

693 

694 try: 

695 # Calculate end_date as UTC today + 1 day 

696 from datetime import timezone 

697 

698 end_dt = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1) 

699 end_date: Final = end_dt.strftime("%Y-%m-%d") 

700 

701 # Calculate date range (last 30 days) 

702 start_dt: Final = end_dt - timedelta(days=30) 

703 start_date: Final = start_dt.strftime("%Y-%m-%d") 

704 

705 # Build where clause with date range 

706 where_clause: Final[dict[str, object]] = {"date": {"gte": start_date, "lte": end_date}} 

707 

708 # Add tag filtering if provided 

709 if tag_filters and len(tag_filters) > 0: 

710 where_clause["tag"] = {"in": tag_filters} 

711 elif tag_filter: 

712 where_clause["tag"] = {"contains": tag_filter} 

713 

714 # Get all tag records in the date range with optional tag filtering 

715 tag_records: Final = await _typed_table(DailyTagSpendRepository(prisma_client)).find_many(where=where_clause) 

716 

717 # Get unique api_keys 

718 api_keys: Final = set(record.api_key for record in tag_records if record.api_key) 

719 

720 if not api_keys: 

721 return PerUserAnalyticsResponse( 

722 results=[], 

723 total_count=0, 

724 page=page, 

725 page_size=page_size, 

726 total_pages=0, 

727 ) 

728 

729 # Lookup user_id for each api_key 

730 api_key_records: Final = await _typed_table(VerificationTokenRepository(prisma_client)).find_many( 

731 where={"token": {"in": list(api_keys)}} 

732 ) 

733 

734 # Create mapping from api_key to user_id 

735 api_key_to_user_id: Final = {record.token: record.user_id for record in api_key_records if record.user_id} 

736 

737 # Get user emails for the user_ids 

738 user_ids: Final = list(set(api_key_to_user_id.values())) 

739 user_records: Final = await _typed_table(UserRepository(prisma_client)).find_many( 

740 where={"user_id": {"in": user_ids}} 

741 ) 

742 

743 # Create mapping from user_id to user_email 

744 user_id_to_email: Final = {record.user_id: record.user_email for record in user_records} 

745 

746 # Aggregate metrics by user 

747 user_metrics: Final[dict[str, PerUserMetrics]] = {} 

748 

749 for record in tag_records: 

750 if record.api_key in api_key_to_user_id: 750 ↛ 751line 750 didn't jump to line 751 because the condition on line 750 was never true

751 user_id = api_key_to_user_id[record.api_key] 

752 tag = record.tag # Use the full tag as user_agent 

753 

754 if user_id not in user_metrics: 

755 user_metrics[user_id] = PerUserMetrics( 

756 user_id=user_id, 

757 user_email=user_id_to_email.get(user_id), 

758 user_agent=tag, 

759 ) 

760 else: 

761 # If tag is different, keep the first one or prioritize certain ones 

762 if tag and not user_metrics[user_id].user_agent: 

763 user_metrics[user_id].user_agent = tag 

764 

765 # Aggregate metrics 

766 user_metrics[user_id].successful_requests += record.successful_requests or 0 

767 user_metrics[user_id].failed_requests += record.failed_requests or 0 

768 user_metrics[user_id].total_requests += record.api_requests or 0 

769 # Calculate total_tokens from prompt_tokens + completion_tokens 

770 prompt_tokens = record.prompt_tokens or 0 

771 completion_tokens = record.completion_tokens or 0 

772 user_metrics[user_id].total_tokens += int(prompt_tokens + completion_tokens) 

773 user_metrics[user_id].spend += record.spend or 0.0 

774 

775 # Convert to list and sort by successful requests (descending) 

776 results: Final = sorted( 

777 list(user_metrics.values()), 

778 key=lambda x: x.successful_requests, 

779 reverse=True, 

780 ) 

781 

782 # Apply pagination 

783 total_count: Final = len(results) 

784 total_pages: Final = (total_count + page_size - 1) // page_size 

785 start_idx: Final = (page - 1) * page_size 

786 end_idx: Final = start_idx + page_size 

787 paginated_results: Final = results[start_idx:end_idx] 

788 

789 return PerUserAnalyticsResponse( 

790 results=paginated_results, 

791 total_count=total_count, 

792 page=page, 

793 page_size=page_size, 

794 total_pages=total_pages, 

795 ) 

796 

797 except Exception as e: 

798 raise HTTPException( 

799 status_code=500, 

800 detail=f"Failed to fetch per-user analytics: {e}", 

801 )