mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-11 22:51:28 +00:00
Merge pull request #19971 from BerriAI/litellm_v2_model_info_sorting_fix
[Fix] Sorting for /v2/model/info
This commit is contained in:
commit
bc23a97e14
2 changed files with 128 additions and 18 deletions
|
|
@ -11,7 +11,7 @@ import sys
|
|||
import time
|
||||
import traceback
|
||||
import warnings
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
|
|
@ -8055,6 +8055,48 @@ async def _apply_search_filter_to_models(
|
|||
return filtered_models, search_total_count
|
||||
|
||||
|
||||
def _normalize_datetime_for_sorting(dt: Any) -> Optional[datetime]:
|
||||
"""
|
||||
Normalize a datetime value to a timezone-aware UTC datetime for sorting.
|
||||
|
||||
This function handles:
|
||||
- None values: returns None
|
||||
- String values: parses ISO format strings and converts to UTC-aware datetime
|
||||
- Datetime objects: converts naive datetimes to UTC-aware, and aware datetimes to UTC
|
||||
|
||||
Args:
|
||||
dt: Datetime value (None, str, or datetime object)
|
||||
|
||||
Returns:
|
||||
UTC-aware datetime object, or None if input is None or cannot be parsed
|
||||
"""
|
||||
if dt is None:
|
||||
return None
|
||||
|
||||
if isinstance(dt, str):
|
||||
try:
|
||||
# Handle ISO format strings, including 'Z' suffix
|
||||
dt_str = dt.replace("Z", "+00:00") if dt.endswith("Z") else dt
|
||||
parsed_dt = datetime.fromisoformat(dt_str)
|
||||
# Ensure it's UTC-aware
|
||||
if parsed_dt.tzinfo is None:
|
||||
parsed_dt = parsed_dt.replace(tzinfo=timezone.utc)
|
||||
else:
|
||||
parsed_dt = parsed_dt.astimezone(timezone.utc)
|
||||
return parsed_dt
|
||||
except (ValueError, AttributeError):
|
||||
return None
|
||||
|
||||
if isinstance(dt, datetime):
|
||||
# If naive, assume UTC and make it aware
|
||||
if dt.tzinfo is None:
|
||||
return dt.replace(tzinfo=timezone.utc)
|
||||
# If aware, convert to UTC
|
||||
return dt.astimezone(timezone.utc)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _sort_models(
|
||||
all_models: List[Dict[str, Any]],
|
||||
sort_by: Optional[str],
|
||||
|
|
@ -8084,26 +8126,18 @@ def _sort_models(
|
|||
|
||||
elif sort_by == "created_at":
|
||||
created_at = model_info.get("created_at")
|
||||
if created_at is None:
|
||||
normalized_dt = _normalize_datetime_for_sorting(created_at)
|
||||
if normalized_dt is None:
|
||||
# Put None values at the end for asc, at the start for desc
|
||||
return (datetime.max if not reverse else datetime.min)
|
||||
if isinstance(created_at, str):
|
||||
try:
|
||||
return datetime.fromisoformat(created_at.replace("Z", "+00:00"))
|
||||
except (ValueError, AttributeError):
|
||||
return datetime.min if not reverse else datetime.max
|
||||
return created_at
|
||||
return (datetime.max.replace(tzinfo=timezone.utc) if not reverse else datetime.min.replace(tzinfo=timezone.utc))
|
||||
return normalized_dt
|
||||
|
||||
elif sort_by == "updated_at":
|
||||
updated_at = model_info.get("updated_at")
|
||||
if updated_at is None:
|
||||
return (datetime.max if not reverse else datetime.min)
|
||||
if isinstance(updated_at, str):
|
||||
try:
|
||||
return datetime.fromisoformat(updated_at.replace("Z", "+00:00"))
|
||||
except (ValueError, AttributeError):
|
||||
return datetime.min if not reverse else datetime.max
|
||||
return updated_at
|
||||
normalized_dt = _normalize_datetime_for_sorting(updated_at)
|
||||
if normalized_dt is None:
|
||||
return (datetime.max.replace(tzinfo=timezone.utc) if not reverse else datetime.min.replace(tzinfo=timezone.utc))
|
||||
return normalized_dt
|
||||
|
||||
elif sort_by == "costs":
|
||||
input_cost = model_info.get("input_cost_per_token", 0) or 0
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import os
|
|||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
from unittest.mock import AsyncMock, MagicMock, mock_open, patch
|
||||
|
|
@ -1049,6 +1049,82 @@ async def test_get_config_from_file(tmp_path, monkeypatch):
|
|||
assert result == test_config
|
||||
|
||||
|
||||
def test_normalize_datetime_for_sorting():
|
||||
"""
|
||||
Test the _normalize_datetime_for_sorting function.
|
||||
Tests various scenarios: None values, ISO format strings, datetime objects (naive and aware).
|
||||
"""
|
||||
from litellm.proxy.proxy_server import _normalize_datetime_for_sorting
|
||||
|
||||
# Test Case 1: None value
|
||||
assert _normalize_datetime_for_sorting(None) is None
|
||||
|
||||
# Test Case 2: ISO format string with 'Z' suffix
|
||||
dt_str_z = "2024-01-15T10:30:00Z"
|
||||
result = _normalize_datetime_for_sorting(dt_str_z)
|
||||
assert result is not None
|
||||
assert isinstance(result, datetime)
|
||||
assert result.tzinfo == timezone.utc
|
||||
assert result.year == 2024
|
||||
assert result.month == 1
|
||||
assert result.day == 15
|
||||
assert result.hour == 10
|
||||
assert result.minute == 30
|
||||
|
||||
# Test Case 3: ISO format string without 'Z' suffix (naive)
|
||||
dt_str_naive = "2024-01-15T10:30:00"
|
||||
result = _normalize_datetime_for_sorting(dt_str_naive)
|
||||
assert result is not None
|
||||
assert isinstance(result, datetime)
|
||||
assert result.tzinfo == timezone.utc
|
||||
|
||||
# Test Case 4: ISO format string with timezone offset
|
||||
dt_str_tz = "2024-01-15T10:30:00+05:00"
|
||||
result = _normalize_datetime_for_sorting(dt_str_tz)
|
||||
assert result is not None
|
||||
assert isinstance(result, datetime)
|
||||
assert result.tzinfo == timezone.utc
|
||||
# Should convert from +05:00 to UTC (subtract 5 hours)
|
||||
assert result.hour == 5 # 10:30 - 5 hours = 5:30 UTC
|
||||
|
||||
# Test Case 5: Naive datetime object
|
||||
naive_dt = datetime(2024, 1, 15, 10, 30, 0)
|
||||
result = _normalize_datetime_for_sorting(naive_dt)
|
||||
assert result is not None
|
||||
assert isinstance(result, datetime)
|
||||
assert result.tzinfo == timezone.utc
|
||||
assert result.year == 2024
|
||||
assert result.month == 1
|
||||
assert result.day == 15
|
||||
|
||||
# Test Case 6: Timezone-aware datetime object (non-UTC)
|
||||
from datetime import timedelta
|
||||
aware_dt = datetime(2024, 1, 15, 10, 30, 0, tzinfo=timezone(timedelta(hours=5)))
|
||||
result = _normalize_datetime_for_sorting(aware_dt)
|
||||
assert result is not None
|
||||
assert isinstance(result, datetime)
|
||||
assert result.tzinfo == timezone.utc
|
||||
# Should convert from +05:00 to UTC
|
||||
assert result.hour == 5
|
||||
|
||||
# Test Case 7: UTC-aware datetime object
|
||||
utc_dt = datetime(2024, 1, 15, 10, 30, 0, tzinfo=timezone.utc)
|
||||
result = _normalize_datetime_for_sorting(utc_dt)
|
||||
assert result is not None
|
||||
assert isinstance(result, datetime)
|
||||
assert result.tzinfo == timezone.utc
|
||||
assert result == utc_dt
|
||||
|
||||
# Test Case 8: Invalid string format
|
||||
invalid_str = "not-a-date"
|
||||
result = _normalize_datetime_for_sorting(invalid_str)
|
||||
assert result is None
|
||||
|
||||
# Test Case 9: Invalid type (should return None)
|
||||
result = _normalize_datetime_for_sorting(12345)
|
||||
assert result is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_proxy_budget_to_db_only_creates_user_no_keys():
|
||||
"""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue