from unittest import mock import pytest from mlflow.exceptions import MlflowException from mlflow.gateway.config import EndpointConfig from mlflow.gateway.providers.base import PassthroughAction from mlflow.gateway.providers.litellm import LiteLLMAdapter, LiteLLMProvider from mlflow.gateway.schemas import chat, embeddings TEST_MESSAGE = "This is a test" def chat_config(): return { "name": "chat", "endpoint_type": "llm/v1/chat", "model": { "provider": "litellm", "name": "claude-3-5-sonnet-20241022", "config": { "litellm_auth_config": {"api_key": "test-key"}, }, }, } def chat_config_with_api_base(): return { "name": "chat", "endpoint_type": "llm/v1/chat", "model": { "provider": "litellm", "name": "custom-model", "config": { "litellm_auth_config": { "api_key": "test-key", "api_base": "https://custom-api.example.com", }, }, }, } def chat_config_with_provider(): return { "name": "chat", "endpoint_type": "llm/v1/chat", "model": { "provider": "litellm", "name": "claude-3-5-sonnet-20241022", "config": { "litellm_provider": "anthropic", "litellm_auth_config": {"api_key": "test-key"}, }, }, } def embeddings_config(): return { "name": "embeddings", "endpoint_type": "llm/v1/embeddings", "model": { "provider": "litellm", "name": "text-embedding-3-small", "config": { "litellm_auth_config": {"api_key": "test-key"}, }, }, } def mock_litellm_chat_response(): """Create a mock LiteLLM chat response object.""" response = mock.MagicMock() response.id = "litellm-chat-id" response.object = "chat.completion" response.created = 1234567890 response.model = "claude-3-5-sonnet-20241022" choice = mock.MagicMock() choice.index = 0 choice.message = mock.MagicMock() choice.message.role = "assistant" choice.message.content = TEST_MESSAGE choice.message.tool_calls = None choice.finish_reason = "stop" response.choices = [choice] response.usage = mock.MagicMock() response.usage.prompt_tokens = 10 response.usage.completion_tokens = 20 response.usage.total_tokens = 30 response.usage.prompt_tokens_details = None return response def mock_litellm_embeddings_response(): """Create a mock LiteLLM embeddings response object.""" response = mock.MagicMock() response.model = "text-embedding-3-small" data = mock.MagicMock() data.__getitem__ = lambda self, key: [0.1, 0.2, 0.3] if key == "embedding" else None response.data = [data] response.usage = mock.MagicMock() response.usage.prompt_tokens = 5 response.usage.total_tokens = 5 return response def test_litellm_not_installed(): config = chat_config() with mock.patch("importlib.util.find_spec", return_value=None): with pytest.raises(MlflowException, match="pip install litellm"): LiteLLMProvider(EndpointConfig(**config)) @pytest.mark.asyncio async def test_chat(): config = chat_config() mock_response = mock_litellm_chat_response() with mock.patch("litellm.acompletion", return_value=mock_response) as mock_completion: provider = LiteLLMProvider(EndpointConfig(**config)) payload = { "messages": [{"role": "user", "content": TEST_MESSAGE}], "temperature": 0.7, "max_tokens": 100, } response = await provider.chat(chat.RequestPayload(**payload)) assert response.id == "litellm-chat-id" assert response.object == "chat.completion" assert response.model == "claude-3-5-sonnet-20241022" assert len(response.choices) == 1 assert response.choices[0].message.content == TEST_MESSAGE assert response.usage.prompt_tokens == 10 assert response.usage.completion_tokens == 20 assert response.usage.total_tokens == 30 # Verify litellm was called with correct parameters mock_completion.assert_called_once() call_kwargs = mock_completion.call_args[1] assert call_kwargs["model"] == "claude-3-5-sonnet-20241022" assert call_kwargs["messages"] == [{"role": "user", "content": TEST_MESSAGE}] assert call_kwargs["temperature"] == 0.7 assert call_kwargs["max_tokens"] == 100 assert call_kwargs["api_key"] == "test-key" @pytest.mark.asyncio async def test_chat_with_api_base(): config = chat_config_with_api_base() mock_response = mock_litellm_chat_response() with mock.patch("litellm.acompletion", return_value=mock_response) as mock_completion: provider = LiteLLMProvider(EndpointConfig(**config)) payload = {"messages": [{"role": "user", "content": TEST_MESSAGE}]} await provider.chat(chat.RequestPayload(**payload)) # Verify API base is passed call_kwargs = mock_completion.call_args[1] assert call_kwargs["api_base"] == "https://custom-api.example.com" @pytest.mark.asyncio async def test_chat_with_provider_prefix(): config = chat_config_with_provider() mock_response = mock_litellm_chat_response() with mock.patch("litellm.acompletion", return_value=mock_response) as mock_completion: provider = LiteLLMProvider(EndpointConfig(**config)) payload = {"messages": [{"role": "user", "content": TEST_MESSAGE}]} await provider.chat(chat.RequestPayload(**payload)) # Verify model name includes provider prefix call_kwargs = mock_completion.call_args[1] assert call_kwargs["model"] == "anthropic/claude-3-5-sonnet-20241022" @pytest.mark.asyncio async def test_chat_stream(): config = chat_config() # Create mock streaming chunks async def mock_stream(): chunk1 = mock.MagicMock() chunk1.id = "chunk-1" chunk1.object = "chat.completion.chunk" chunk1.created = 1234567890 chunk1.model = "claude-3-5-sonnet-20241022" chunk1.usage = None choice1 = mock.MagicMock() choice1.index = 0 choice1.delta = mock.MagicMock(spec=["role", "content"]) choice1.delta.role = "assistant" choice1.delta.content = "Hello" choice1.finish_reason = None chunk1.choices = [choice1] yield chunk1 chunk2 = mock.MagicMock() chunk2.id = "chunk-2" chunk2.object = "chat.completion.chunk" chunk2.created = 1234567890 chunk2.model = "claude-3-5-sonnet-20241022" chunk2.usage = None choice2 = mock.MagicMock() choice2.index = 0 choice2.delta = mock.MagicMock(spec=["content"]) choice2.delta.content = " world" choice2.finish_reason = "stop" chunk2.choices = [choice2] yield chunk2 # Final usage-only chunk (litellm emits this when include_usage is set). chunk3 = mock.MagicMock() chunk3.id = "chunk-3" chunk3.object = "chat.completion.chunk" chunk3.created = 1234567890 chunk3.model = "claude-3-5-sonnet-20241022" chunk3.choices = [] chunk3.usage = mock.MagicMock() chunk3.usage.prompt_tokens = 10 chunk3.usage.completion_tokens = 20 chunk3.usage.total_tokens = 30 chunk3.usage.prompt_tokens_details = None yield chunk3 with mock.patch("litellm.acompletion", return_value=mock_stream()) as mock_completion: provider = LiteLLMProvider(EndpointConfig(**config), enable_tracing=True) payload = { "messages": [{"role": "user", "content": "Hello"}], "stream": True, } chunks = [chunk async for chunk in provider.chat_stream(chat.RequestPayload(**payload))] assert len(chunks) == 3 assert chunks[0].choices[0].delta.content == "Hello" assert chunks[1].choices[0].delta.content == " world" assert chunks[1].choices[0].finish_reason == "stop" # Final chunk forwards token usage instead of content. assert chunks[2].usage.prompt_tokens == 10 assert chunks[2].usage.completion_tokens == 20 assert chunks[2].usage.total_tokens == 30 # No cache details reported by the provider => field is omitted. assert chunks[2].usage.prompt_tokens_details is None # Verify stream parameter was set call_kwargs = mock_completion.call_args[1] assert call_kwargs["stream"] is True assert call_kwargs["stream_options"]["include_usage"] is True @pytest.mark.asyncio async def test_chat_stream_forwards_zero_token_usage(): config = chat_config() async def mock_stream(): chunk = mock.MagicMock() chunk.id = "chunk-1" chunk.object = "chat.completion.chunk" chunk.created = 1234567890 chunk.model = "claude-3-5-sonnet-20241022" chunk.choices = [] chunk.usage = mock.MagicMock() chunk.usage.prompt_tokens = 0 chunk.usage.completion_tokens = 0 chunk.usage.total_tokens = 0 chunk.usage.prompt_tokens_details = None yield chunk with mock.patch("litellm.acompletion", return_value=mock_stream()): provider = LiteLLMProvider(EndpointConfig(**config), enable_tracing=True) payload = {"messages": [{"role": "user", "content": "Hi"}], "stream": True} chunks = [chunk async for chunk in provider.chat_stream(chat.RequestPayload(**payload))] # A valid all-zero usage payload must still be forwarded, not dropped. assert chunks[-1].usage.total_tokens == 0 @pytest.mark.asyncio async def test_chat_stream_forwards_cached_tokens(): config = chat_config() async def mock_stream(): chunk = mock.MagicMock() chunk.id = "chunk-1" chunk.object = "chat.completion.chunk" chunk.created = 1234567890 chunk.model = "claude-3-5-sonnet-20241022" chunk.choices = [] chunk.usage = mock.MagicMock() chunk.usage.prompt_tokens = 100 chunk.usage.completion_tokens = 10 chunk.usage.total_tokens = 110 chunk.usage.prompt_tokens_details = mock.MagicMock() chunk.usage.prompt_tokens_details.cached_tokens = 80 yield chunk with mock.patch("litellm.acompletion", return_value=mock_stream()): provider = LiteLLMProvider(EndpointConfig(**config), enable_tracing=True) payload = {"messages": [{"role": "user", "content": "Hi"}], "stream": True} chunks = [chunk async for chunk in provider.chat_stream(chat.RequestPayload(**payload))] # Cache-read tokens (a subset of prompt_tokens) are forwarded so clients can price cache hits. assert chunks[-1].usage.prompt_tokens == 100 assert chunks[-1].usage.prompt_tokens_details.cached_tokens == 80 @pytest.mark.asyncio async def test_embeddings(): config = embeddings_config() mock_response = mock_litellm_embeddings_response() with mock.patch("litellm.aembedding", return_value=mock_response) as mock_embedding: provider = LiteLLMProvider(EndpointConfig(**config)) payload = {"input": "Hello world"} response = await provider.embeddings(embeddings.RequestPayload(**payload)) assert response.model == "text-embedding-3-small" assert len(response.data) == 1 assert response.data[0].embedding == [0.1, 0.2, 0.3] assert response.usage.prompt_tokens == 5 assert response.usage.total_tokens == 5 # Verify litellm was called with correct parameters mock_embedding.assert_called_once() call_kwargs = mock_embedding.call_args[1] assert call_kwargs["model"] == "text-embedding-3-small" assert call_kwargs["input"] == "Hello world" assert call_kwargs["api_key"] == "test-key" @pytest.mark.asyncio async def test_embeddings_batch(): config = embeddings_config() # Create mock response for batch response = mock.MagicMock() response.model = "text-embedding-3-small" data1 = mock.MagicMock() data1.__getitem__ = lambda self, key: [0.1, 0.2, 0.3] if key == "embedding" else None data2 = mock.MagicMock() data2.__getitem__ = lambda self, key: [0.4, 0.5, 0.6] if key == "embedding" else None response.data = [data1, data2] response.usage = mock.MagicMock() response.usage.prompt_tokens = 10 response.usage.total_tokens = 10 with mock.patch("litellm.aembedding", return_value=response): provider = LiteLLMProvider(EndpointConfig(**config)) payload = {"input": ["Hello", "World"]} response_payload = await provider.embeddings(embeddings.RequestPayload(**payload)) assert len(response_payload.data) == 2 assert response_payload.data[0].embedding == [0.1, 0.2, 0.3] assert response_payload.data[1].embedding == [0.4, 0.5, 0.6] def test_adapter_chat_to_model(): config = EndpointConfig(**chat_config()) payload = { "messages": [{"role": "user", "content": TEST_MESSAGE}], "temperature": 0.7, } result = LiteLLMAdapter.chat_to_model(payload, config) assert result["model"] == "claude-3-5-sonnet-20241022" assert result["messages"] == [{"role": "user", "content": TEST_MESSAGE}] assert result["temperature"] == 0.7 def test_adapter_embeddings_to_model(): config = EndpointConfig(**embeddings_config()) payload = {"input": TEST_MESSAGE} result = LiteLLMAdapter.embeddings_to_model(payload, config) assert result["model"] == "text-embedding-3-small" assert result["input"] == TEST_MESSAGE def test_adapter_chat_to_model_with_provider(): config = EndpointConfig(**chat_config_with_provider()) payload = { "messages": [{"role": "user", "content": TEST_MESSAGE}], "temperature": 0.7, } result = LiteLLMAdapter.chat_to_model(payload, config) assert result["model"] == "anthropic/claude-3-5-sonnet-20241022" assert result["messages"] == [{"role": "user", "content": TEST_MESSAGE}] assert result["temperature"] == 0.7 def test_adapter_model_to_chat(): config = EndpointConfig(**chat_config()) resp = { "id": "test-id", "object": "chat.completion", "created": 1234567890, "model": "test-model", "choices": [ { "index": 0, "message": {"role": "assistant", "content": TEST_MESSAGE, "tool_calls": None}, "finish_reason": "stop", } ], "usage": {"prompt_tokens": 10, "completion_tokens": 20, "total_tokens": 30}, } result = LiteLLMAdapter.model_to_chat(resp, config) assert result.id == "test-id" assert result.model == "test-model" assert len(result.choices) == 1 assert result.choices[0].message.content == TEST_MESSAGE assert result.usage.prompt_tokens == 10 def test_adapter_model_to_embeddings(): config = EndpointConfig(**embeddings_config()) resp = { "data": [{"embedding": [0.1, 0.2, 0.3], "index": 0}], "model": "test-model", "usage": {"prompt_tokens": 5, "total_tokens": 5}, } result = LiteLLMAdapter.model_to_embeddings(resp, config) assert result.model == "test-model" assert len(result.data) == 1 assert result.data[0].embedding == [0.1, 0.2, 0.3] assert result.data[0].index == 0 assert result.usage.prompt_tokens == 5 # Passthrough tests def mock_response_with_model_dump(): """Create a mock response object that supports model_dump().""" response = mock.MagicMock() response.model_dump.return_value = { "id": "test-response-id", "output": "Test response output", "model": "test-model", } return response @pytest.mark.asyncio async def test_passthrough_openai_responses(): config = chat_config() mock_response = mock_response_with_model_dump() with mock.patch("litellm.aresponses", return_value=mock_response) as mock_aresponses: provider = LiteLLMProvider(EndpointConfig(**config)) payload = {"input": "Hello, world!"} result = await provider.passthrough( PassthroughAction.OPENAI_RESPONSES, payload, headers=None, ) assert result["id"] == "test-response-id" assert result["output"] == "Test response output" mock_aresponses.assert_called_once() call_kwargs = mock_aresponses.call_args[1] assert call_kwargs["model"] == "claude-3-5-sonnet-20241022" assert call_kwargs["input"] == "Hello, world!" @pytest.mark.asyncio async def test_passthrough_openai_responses_streaming(): config = chat_config() async def mock_stream(): for i in range(2): chunk = mock.MagicMock() chunk.model_dump.return_value = {"chunk": i, "content": f"part{i}"} yield chunk with mock.patch("litellm.aresponses", return_value=mock_stream()): provider = LiteLLMProvider(EndpointConfig(**config)) payload = {"input": "Hello, world!", "stream": True} result = await provider.passthrough( PassthroughAction.OPENAI_RESPONSES, payload, headers=None, ) chunks = [chunk async for chunk in result] assert len(chunks) == 2 assert b"data:" in chunks[0] @pytest.mark.asyncio async def test_passthrough_anthropic_messages(): config = chat_config_with_provider() mock_response = mock.MagicMock() mock_response.model_dump.return_value = { "id": "msg-test-id", "type": "message", "role": "assistant", "content": [{"type": "text", "text": "Hello!"}], } with mock.patch( "litellm.anthropic.messages.acreate", return_value=mock_response ) as mock_acreate: provider = LiteLLMProvider(EndpointConfig(**config)) payload = { "messages": [{"role": "user", "content": "Hello"}], "max_tokens": 100, } result = await provider.passthrough( PassthroughAction.ANTHROPIC_MESSAGES, payload, headers=None, ) assert result["id"] == "msg-test-id" assert result["type"] == "message" mock_acreate.assert_called_once() call_kwargs = mock_acreate.call_args[1] assert call_kwargs["model"] == "anthropic/claude-3-5-sonnet-20241022" assert call_kwargs["max_tokens"] == 100 @pytest.mark.asyncio async def test_passthrough_anthropic_messages_streaming(): config = chat_config_with_provider() async def mock_stream(): # LiteLLM returns raw SSE bytes for Anthropic streaming yield b'event: message_start\ndata: {"type":"message_start"}\n\n' yield b'event: content_block_delta\ndata: {"type":"content_block_delta","delta":{"text":"Hello"}}\n\n' # noqa: E501 yield b'event: message_stop\ndata: {"type":"message_stop"}\n\n' with mock.patch( "litellm.anthropic.messages.acreate", return_value=mock_stream() ) as mock_acreate: provider = LiteLLMProvider(EndpointConfig(**config)) payload = { "messages": [{"role": "user", "content": "Hello"}], "max_tokens": 100, "stream": True, } result = await provider.passthrough( PassthroughAction.ANTHROPIC_MESSAGES, payload, headers=None, ) chunks = [chunk async for chunk in result] assert len(chunks) == 3 assert b"message_start" in chunks[0] assert b"content_block_delta" in chunks[1] assert b"message_stop" in chunks[2] mock_acreate.assert_called_once() call_kwargs = mock_acreate.call_args[1] assert call_kwargs["stream"] is True assert call_kwargs["model"] == "anthropic/claude-3-5-sonnet-20241022" @pytest.mark.asyncio async def test_passthrough_gemini_generate_content(): config = chat_config() mock_response = mock.MagicMock() mock_response.model_dump.return_value = { "candidates": [{"content": {"parts": [{"text": "Generated content"}]}}], } with mock.patch( "litellm.google_genai.agenerate_content", return_value=mock_response ) as mock_agenerate: provider = LiteLLMProvider(EndpointConfig(**config)) payload = { "contents": [{"parts": [{"text": "Generate something"}]}], } result = await provider.passthrough( PassthroughAction.GEMINI_GENERATE_CONTENT, payload, headers=None, ) assert "candidates" in result mock_agenerate.assert_called_once() @pytest.mark.asyncio async def test_passthrough_gemini_stream_generate_content(): config = chat_config() async def mock_stream(): for i in range(2): chunk = mock.MagicMock() chunk.model_dump.return_value = { "candidates": [{"content": {"parts": [{"text": f"chunk{i}"}]}}] } yield chunk # agenerate_content is called with stream=True for streaming with mock.patch("litellm.google_genai.agenerate_content", return_value=mock_stream()): provider = LiteLLMProvider(EndpointConfig(**config)) payload = { "contents": [{"parts": [{"text": "Generate something"}]}], } result = provider._passthrough_gemini_stream_generate_content({ "model": "claude-3-5-sonnet-20241022", **payload, }) chunks = [chunk async for chunk in result] assert len(chunks) == 2 assert chunks[0].model_dump() == { "candidates": [{"content": {"parts": [{"text": "chunk0"}]}}] } @pytest.mark.asyncio async def test_passthrough_openai_chat(): config = chat_config() mock_response = mock_response_with_model_dump() mock_response.model_dump.return_value = { "id": "chatcmpl-test", "object": "chat.completion", "choices": [{"message": {"role": "assistant", "content": "Hello!"}}], } with mock.patch("litellm.acompletion", return_value=mock_response) as mock_completion: provider = LiteLLMProvider(EndpointConfig(**config)) payload = { "messages": [{"role": "user", "content": "Hello"}], } result = await provider.passthrough( PassthroughAction.OPENAI_CHAT, payload, headers=None, ) assert result["id"] == "chatcmpl-test" assert result["object"] == "chat.completion" mock_completion.assert_called_once() @pytest.mark.asyncio async def test_passthrough_openai_embeddings(): config = embeddings_config() mock_response = mock.MagicMock() mock_response.model_dump.return_value = { "object": "list", "data": [{"embedding": [0.1, 0.2, 0.3], "index": 0}], "model": "text-embedding-3-small", } with mock.patch("litellm.aembedding", return_value=mock_response) as mock_embedding: provider = LiteLLMProvider(EndpointConfig(**config)) payload = {"input": "Hello, world!"} result = await provider.passthrough( PassthroughAction.OPENAI_EMBEDDINGS, payload, headers=None, ) assert result["object"] == "list" assert len(result["data"]) == 1 mock_embedding.assert_called_once() def test_response_to_dict_with_model_dump(): config = chat_config() provider = LiteLLMProvider(EndpointConfig(**config)) response = mock.MagicMock() response.model_dump.return_value = {"key": "value"} result = provider._response_to_dict(response) assert result == {"key": "value"} def test_response_to_dict_with_dict_input(): config = chat_config() provider = LiteLLMProvider(EndpointConfig(**config)) result = provider._response_to_dict({"key": "value"}) assert result == {"key": "value"} def test_response_to_dict_with_unknown_type_raises(): config = chat_config() provider = LiteLLMProvider(EndpointConfig(**config)) with pytest.raises(TypeError, match="Unexpected response type"): provider._response_to_dict("string value") # Token extraction tests def test_litellm_extract_passthrough_token_usage_openai_format(): provider = LiteLLMProvider(EndpointConfig(**chat_config())) result = { "id": "chatcmpl-123", "usage": { "prompt_tokens": 10, "completion_tokens": 20, "total_tokens": 30, }, } token_usage = provider._extract_passthrough_token_usage(PassthroughAction.OPENAI_CHAT, result) assert token_usage == { "input_tokens": 10, "output_tokens": 20, "total_tokens": 30, } def test_litellm_extract_passthrough_token_usage_anthropic_format(): provider = LiteLLMProvider(EndpointConfig(**chat_config())) result = { "id": "msg_123", "usage": { "input_tokens": 100, "output_tokens": 50, }, } token_usage = provider._extract_passthrough_token_usage( PassthroughAction.ANTHROPIC_MESSAGES, result ) assert token_usage == { "input_tokens": 100, "output_tokens": 50, "total_tokens": 150, } def test_litellm_extract_passthrough_token_usage_gemini_format(): provider = LiteLLMProvider(EndpointConfig(**chat_config())) result = { "candidates": [{"content": {"parts": [{"text": "Hello"}]}}], "usageMetadata": { "promptTokenCount": 10, "candidatesTokenCount": 20, "totalTokenCount": 30, }, } token_usage = provider._extract_passthrough_token_usage( PassthroughAction.GEMINI_GENERATE_CONTENT, result ) assert token_usage == { "input_tokens": 10, "output_tokens": 20, "total_tokens": 30, } def test_litellm_extract_passthrough_token_usage_gemini_with_cached_tokens(): provider = LiteLLMProvider(EndpointConfig(**chat_config())) result = { "candidates": [{"content": {"parts": [{"text": "Hello"}]}}], "usageMetadata": { "promptTokenCount": 10, "candidatesTokenCount": 20, "totalTokenCount": 30, "cachedContentTokenCount": 5, }, } token_usage = provider._extract_passthrough_token_usage( PassthroughAction.GEMINI_GENERATE_CONTENT, result ) assert token_usage == { "input_tokens": 10, "output_tokens": 20, "total_tokens": 30, "cache_read_input_tokens": 5, } def test_litellm_extract_passthrough_token_usage_no_usage(): provider = LiteLLMProvider(EndpointConfig(**chat_config())) result = {"id": "chatcmpl-123", "choices": []} token_usage = provider._extract_passthrough_token_usage(PassthroughAction.OPENAI_CHAT, result) assert token_usage is None def test_litellm_extract_streaming_token_usage_openai_format(): provider = LiteLLMProvider(EndpointConfig(**chat_config())) chunk = ( b'data: {"id":"chatcmpl-123","usage":' b'{"prompt_tokens":10,"completion_tokens":20,"total_tokens":30}}\n' ) result = provider._extract_streaming_token_usage(chunk) assert result == { "input_tokens": 10, "output_tokens": 20, "total_tokens": 30, } def test_litellm_extract_streaming_token_usage_anthropic_message_start(): provider = LiteLLMProvider(EndpointConfig(**chat_config())) chunk = b'data: {"type":"message_start","message":{"usage":{"input_tokens":100}}}\n' result = provider._extract_streaming_token_usage(chunk) assert result == {"input_tokens": 100} def test_litellm_extract_streaming_token_usage_anthropic_message_delta(): provider = LiteLLMProvider(EndpointConfig(**chat_config())) chunk = b'data: {"type":"message_delta","usage":{"output_tokens":50}}\n' result = provider._extract_streaming_token_usage(chunk) # Method only returns chunk's usage; total is calculated by _stream_passthrough_with_usage assert result == {"output_tokens": 50} def test_litellm_extract_streaming_token_usage_gemini_format(): provider = LiteLLMProvider(EndpointConfig(**chat_config())) chunk = ( b'data: {"candidates":[{"content":{}}],"usageMetadata":' b'{"promptTokenCount":10,"candidatesTokenCount":20,"totalTokenCount":30}}\n' ) result = provider._extract_streaming_token_usage(chunk) assert result == { "input_tokens": 10, "output_tokens": 20, "total_tokens": 30, } def test_litellm_extract_streaming_token_usage_gemini_with_cached_tokens(): provider = LiteLLMProvider(EndpointConfig(**chat_config())) chunk = ( b'data: {"candidates":[{"content":{}}],"usageMetadata":' b'{"promptTokenCount":10,"candidatesTokenCount":20,"totalTokenCount":30,"cachedContentTokenCount":5}}\n' ) result = provider._extract_streaming_token_usage(chunk) assert result == { "input_tokens": 10, "output_tokens": 20, "total_tokens": 30, "cache_read_input_tokens": 5, } def test_litellm_extract_streaming_token_usage_empty_chunk(): provider = LiteLLMProvider(EndpointConfig(**chat_config())) chunk = b"" result = provider._extract_streaming_token_usage(chunk) assert result == {} def test_litellm_extract_streaming_token_usage_done_chunk(): provider = LiteLLMProvider(EndpointConfig(**chat_config())) chunk = b"data: [DONE]\n" result = provider._extract_streaming_token_usage(chunk) assert result == {} def test_litellm_extract_streaming_token_usage_invalid_json(): provider = LiteLLMProvider(EndpointConfig(**chat_config())) chunk = b"data: {invalid json}\n" result = provider._extract_streaming_token_usage(chunk) assert result == {} def test_litellm_extract_streaming_token_usage_responses_api(): provider = LiteLLMProvider(EndpointConfig(**chat_config())) # Responses API returns usage in data.response.usage with input_tokens/output_tokens chunk = ( b'data: {"type":"response.completed","response":{"id":"resp_123",' b'"usage":{"input_tokens":9,"output_tokens":65,"total_tokens":74}}}\n' ) result = provider._extract_streaming_token_usage(chunk) assert result == { "input_tokens": 9, "output_tokens": 65, "total_tokens": 74, } def test_litellm_extract_passthrough_token_usage_openai_with_cached_tokens(): provider = LiteLLMProvider(EndpointConfig(**chat_config())) result = { "id": "chatcmpl-123", "usage": { "prompt_tokens": 50, "completion_tokens": 20, "total_tokens": 70, "prompt_tokens_details": {"cached_tokens": 30}, }, } token_usage = provider._extract_passthrough_token_usage(PassthroughAction.OPENAI_CHAT, result) assert token_usage == { "input_tokens": 50, "output_tokens": 20, "total_tokens": 70, "cache_read_input_tokens": 30, } def test_litellm_extract_passthrough_token_usage_anthropic_with_cached_tokens(): provider = LiteLLMProvider(EndpointConfig(**chat_config())) result = { "id": "msg_123", "usage": { "input_tokens": 100, "output_tokens": 50, "cache_read_input_tokens": 25, "cache_creation_input_tokens": 15, }, } token_usage = provider._extract_passthrough_token_usage( PassthroughAction.ANTHROPIC_MESSAGES, result ) assert token_usage == { "input_tokens": 140, "output_tokens": 50, "total_tokens": 190, "cache_read_input_tokens": 25, "cache_creation_input_tokens": 15, } def test_litellm_extract_streaming_token_usage_openai_with_cached_tokens(): provider = LiteLLMProvider(EndpointConfig(**chat_config())) chunk = ( b'data: {"id":"chatcmpl-123","usage":' b'{"prompt_tokens":50,"completion_tokens":20,"total_tokens":70,' b'"prompt_tokens_details":{"cached_tokens":30}}}\n' ) result = provider._extract_streaming_token_usage(chunk) assert result == { "input_tokens": 50, "output_tokens": 20, "total_tokens": 70, "cache_read_input_tokens": 30, } def test_litellm_extract_streaming_token_usage_anthropic_message_start_with_cached_tokens(): provider = LiteLLMProvider(EndpointConfig(**chat_config())) chunk = ( b'data: {"type":"message_start","message":{"usage":' b'{"input_tokens":100,"cache_read_input_tokens":25}}}\n' ) result = provider._extract_streaming_token_usage(chunk) assert result == { "input_tokens": 100, "cache_read_input_tokens": 25, } def test_litellm_extract_streaming_token_usage_responses_api_with_cached_tokens(): provider = LiteLLMProvider(EndpointConfig(**chat_config())) chunk = ( b'data: {"type":"response.completed","response":{"id":"resp_123",' b'"usage":{"input_tokens":100,"output_tokens":50,"total_tokens":150,' b'"input_tokens_details":{"cached_tokens":40}}}}\n' ) result = provider._extract_streaming_token_usage(chunk) assert result == { "input_tokens": 100, "output_tokens": 50, "total_tokens": 150, "cache_read_input_tokens": 40, }