Merge pull request #13152 from betterthanbreakfast/bug-openmeter-user-id-from-token

[Bug]: Set user from token user_id for OpenMeter integration
This commit is contained in:
Krish Dholakia 2025-08-29 22:21:26 -07:00 • committed by GitHub
commit ead7e7259b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 209 additions and 3 deletions

View file

@ -66,8 +66,18 @@ class OpenMeterLogger(CustomLogger):
}
user_param = kwargs.get("user", None) # end-user passed in via 'user' param
# If no user provided directly, try to get it from token user_id
if user_param is None:
raise Exception("OpenMeter: user is required")
# Check if user_id is available from the API key metadata
litellm_params = kwargs.get("litellm_params", {})
metadata = litellm_params.get("metadata", {})
user_api_key_user_id = metadata.get("user_api_key_user_id", None)
if user_api_key_user_id is not None:
user_param = user_api_key_user_id
else:
raise Exception("OpenMeter: user is required")
# Ensure subject is always a string for OpenMeter API
subject = str(user_param)

View file

@ -1,7 +1,5 @@
import asyncio
import json
import os
import sys
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
@ -282,3 +280,201 @@ class TestOpenMeterIntegration:
result = logger._common_logic(kwargs, response_obj)
assert result["type"] == "custom_event_type"
def test_common_logic_user_from_token_user_id(self):
"""Test that _common_logic uses user_api_key_user_id when no user provided"""
logger = OpenMeterLogger()
kwargs = {
"model": "gpt-3.5-turbo",
"response_cost": 0.001,
"litellm_call_id": "test-call-id",
"litellm_params": {
"metadata": {
"user_api_key_user_id": "token-user-123"
}
}
# No "user" parameter - should use token user_id
}
response_obj = {
"id": "test-response-id",
"usage": {
"prompt_tokens": 10,
"completion_tokens": 5,
"total_tokens": 15
}
}
result = logger._common_logic(kwargs, response_obj)
# Verify user was set from token user_id
assert isinstance(result["subject"], str)
assert result["subject"] == "token-user-123"
assert result["data"]["model"] == "gpt-3.5-turbo"
def test_common_logic_direct_user_takes_priority_over_token(self):
"""Test that direct user parameter takes priority over token user_id"""
logger = OpenMeterLogger()
kwargs = {
"user": "direct-user-456", # Direct user should take priority
"model": "gpt-4",
"response_cost": 0.002,
"litellm_call_id": "test-call-id",
"litellm_params": {
"metadata": {
"user_api_key_user_id": "token-user-123" # This should be ignored
}
}
}
response_obj = {
"id": "test-response-id",
"usage": {
"prompt_tokens": 20,
"completion_tokens": 10,
"total_tokens": 30
}
}
result = logger._common_logic(kwargs, response_obj)
# Verify direct user takes priority
assert isinstance(result["subject"], str)
assert result["subject"] == "direct-user-456"
assert result["subject"] != "token-user-123"
def test_common_logic_missing_user_and_token_user_id(self):
"""Test that exception is raised when neither user nor token user_id available"""
logger = OpenMeterLogger()
kwargs = {
"model": "gpt-3.5-turbo",
"response_cost": 0.001,
"litellm_call_id": "test-call-id",
"litellm_params": {
"metadata": {
# No user_api_key_user_id
}
}
# No "user" parameter
}
response_obj = {"id": "test-response-id"}
with pytest.raises(Exception, match="OpenMeter: user is required"):
logger._common_logic(kwargs, response_obj)
def test_common_logic_token_user_id_none(self):
"""Test that exception is raised when token user_id is None"""
logger = OpenMeterLogger()
kwargs = {
"model": "gpt-3.5-turbo",
"response_cost": 0.001,
"litellm_call_id": "test-call-id",
"litellm_params": {
"metadata": {
"user_api_key_user_id": None # Explicitly None
}
}
}
response_obj = {"id": "test-response-id"}
with pytest.raises(Exception, match="OpenMeter: user is required"):
logger._common_logic(kwargs, response_obj)
def test_common_logic_no_metadata(self):
"""Test that exception is raised when no metadata is available"""
logger = OpenMeterLogger()
kwargs = {
"model": "gpt-3.5-turbo",
"response_cost": 0.001,
"litellm_call_id": "test-call-id",
# No litellm_params at all
}
response_obj = {"id": "test-response-id"}
with pytest.raises(Exception, match="OpenMeter: user is required"):
logger._common_logic(kwargs, response_obj)
def test_common_logic_integer_token_user_id(self):
"""Test that integer token user_id is converted to string"""
logger = OpenMeterLogger()
kwargs = {
"model": "gpt-4",
"response_cost": 0.003,
"litellm_call_id": "test-call-id",
"litellm_params": {
"metadata": {
"user_api_key_user_id": 12345 # Integer user_id
}
}
}
response_obj = {
"id": "test-response-id",
"usage": {
"prompt_tokens": 25,
"completion_tokens": 12,
"total_tokens": 37
}
}
result = logger._common_logic(kwargs, response_obj)
# Verify integer user_id is converted to string
assert isinstance(result["subject"], str)
assert result["subject"] == "12345"
@patch('litellm.integrations.openmeter.HTTPHandler')
def test_integration_token_user_id_scenario(self, mock_http_handler):
"""Integration test simulating the exact scenario that was failing"""
mock_post = MagicMock()
mock_http_handler.return_value.post = mock_post
logger = OpenMeterLogger()
# Simulate the exact scenario: request with token that has user_id but no direct user param
kwargs = {
"model": "gpt-3.5-turbo",
"messages": [{"role": "user", "content": "Hello"}],
"response_cost": 0.001,
"litellm_call_id": "test-integration-call-id",
"litellm_params": {
"metadata": {
"user_api_key_user_id": "user123-from-token",
"user_api_key": "hashed-key-abc",
"user_api_key_metadata": {}
}
}
# No "user" parameter - this was causing "OpenMeter: user is required" error
}
response_obj = {
"id": "chatcmpl-test123",
"usage": {
"prompt_tokens": 15,
"completion_tokens": 10,
"total_tokens": 25
}
}
# This should NOT raise "OpenMeter: user is required" anymore
logger.log_success_event(kwargs, response_obj, None, None)
# Verify HTTP call was made
mock_post.assert_called_once()
# Verify the data structure contains user from token
call_args = mock_post.call_args
data = json.loads(call_args[1]['data'])
assert data["subject"] == "user123-from-token"
assert isinstance(data["subject"], str)
assert data["data"]["model"] == "gpt-3.5-turbo"