9201ef759e
CI / lint (push) Waiting to run
CI / mypy (push) Waiting to run
CI / docs (push) Waiting to run
CI / test on 3.10 (standard) (push) Waiting to run
CI / test on 3.11 (standard) (push) Waiting to run
CI / test on 3.12 (standard) (push) Waiting to run
CI / test on 3.10 (all-extras) (push) Waiting to run
CI / test on 3.11 (all-extras) (push) Waiting to run
CI / test on 3.12 (all-extras) (push) Waiting to run
CI / test on 3.13 (all-extras) (push) Waiting to run
CI / test on 3.14 (pydantic-evals) (push) Waiting to run
CI / test on 3.13 (standard) (push) Waiting to run
CI / test on 3.14 (standard) (push) Waiting to run
CI / test on 3.14 (all-extras) (push) Waiting to run
CI / test on 3.10 (pydantic-ai-slim) (push) Waiting to run
CI / test on 3.11 (pydantic-ai-slim) (push) Waiting to run
CI / test on 3.12 (pydantic-ai-slim) (push) Waiting to run
CI / test on 3.13 (pydantic-ai-slim) (push) Waiting to run
CI / test on 3.14 (pydantic-ai-slim) (push) Waiting to run
CI / test on 3.10 (pydantic-evals) (push) Waiting to run
CI / test on 3.11 (pydantic-evals) (push) Waiting to run
CI / test on 3.12 (pydantic-evals) (push) Waiting to run
CI / test on 3.13 (pydantic-evals) (push) Waiting to run
CI / test on 3.10 (lowest-versions) (push) Waiting to run
CI / test on 3.11 (lowest-versions) (push) Waiting to run
CI / test on 3.12 (lowest-versions) (push) Waiting to run
CI / test on 3.13 (lowest-versions) (push) Waiting to run
CI / test on 3.14 (lowest-versions) (push) Waiting to run
CI / test examples on 3.11 (push) Waiting to run
CI / test examples on 3.12 (push) Waiting to run
CI / test examples on 3.13 (push) Waiting to run
CI / test examples on 3.14 (push) Waiting to run
CI / coverage (push) Blocked by required conditions
CI / check (push) Blocked by required conditions
CI / deploy-docs (push) Blocked by required conditions
CI / deploy-docs-preview (push) Blocked by required conditions
CI / build release artifacts (push) Blocked by required conditions
CI / publish to PyPI (push) Blocked by required conditions
CI / Send tweet (push) Blocked by required conditions
Harness Compat / harness compat (push) Failing after 0s
144 lines
6.4 KiB
Python
144 lines
6.4 KiB
Python
import httpx
|
|
import pytest
|
|
from pytest_mock import MockerFixture
|
|
|
|
from ..conftest import try_import
|
|
|
|
with try_import() as imports_successful:
|
|
from openai import AsyncOpenAI
|
|
|
|
from pydantic_ai.profiles.openai import OpenAIJsonSchemaTransformer
|
|
from pydantic_ai.providers.litellm import LiteLLMProvider
|
|
|
|
pytestmark = [
|
|
pytest.mark.skipif(not imports_successful(), reason='OpenAI client not installed'),
|
|
pytest.mark.anyio,
|
|
]
|
|
|
|
|
|
def test_init_with_api_config():
|
|
provider = LiteLLMProvider(api_key='test-key', api_base='https://custom.litellm.com/v1')
|
|
assert provider.base_url == 'https://custom.litellm.com/v1/'
|
|
assert provider.client.api_key == 'test-key'
|
|
|
|
|
|
def test_init_without_api_key():
|
|
provider = LiteLLMProvider()
|
|
assert provider.name == 'litellm'
|
|
assert provider.client.api_key == 'litellm-placeholder'
|
|
|
|
|
|
def test_init_with_openai_client():
|
|
openai_client = AsyncOpenAI(api_key='custom-key', base_url='https://custom.openai.com/v1')
|
|
provider = LiteLLMProvider(openai_client=openai_client)
|
|
assert provider.client == openai_client
|
|
assert provider.base_url == 'https://custom.openai.com/v1/'
|
|
|
|
|
|
def test_model_profile_returns_openai_compatible_profile(mocker: MockerFixture):
|
|
provider = LiteLLMProvider(api_key='test-key')
|
|
|
|
mock_profile: dict[str, object] = {'supports_tools': True}
|
|
mock_openai_profile = mocker.patch('pydantic_ai.providers.litellm.openai_model_profile', return_value=mock_profile)
|
|
|
|
profile = provider.model_profile('gpt-3.5-turbo')
|
|
|
|
# Verify openai_model_profile was called with the model name
|
|
mock_openai_profile.assert_called_once_with('gpt-3.5-turbo')
|
|
|
|
# Verify the returned profile is an OpenAIModelProfile with OpenAIJsonSchemaTransformer
|
|
assert isinstance(profile, dict)
|
|
assert profile.get('json_schema_transformer', None) == OpenAIJsonSchemaTransformer
|
|
|
|
|
|
def test_model_profile_with_different_models(mocker: MockerFixture):
|
|
provider = LiteLLMProvider(api_key='test-key')
|
|
|
|
mock_profile: dict[str, object] = {'supports_tools': True}
|
|
|
|
# Mock all profile functions
|
|
mock_profiles = {
|
|
'openai': mocker.patch('pydantic_ai.providers.litellm.openai_model_profile', return_value=mock_profile),
|
|
'anthropic': mocker.patch('pydantic_ai.providers.litellm.anthropic_model_profile', return_value=mock_profile),
|
|
'google': mocker.patch('pydantic_ai.providers.litellm.google_model_profile', return_value=mock_profile),
|
|
'meta': mocker.patch('pydantic_ai.providers.litellm.meta_model_profile', return_value=mock_profile),
|
|
'mistral': mocker.patch('pydantic_ai.providers.litellm.mistral_model_profile', return_value=mock_profile),
|
|
'cohere': mocker.patch('pydantic_ai.providers.litellm.cohere_model_profile', return_value=mock_profile),
|
|
'amazon': mocker.patch('pydantic_ai.providers.litellm.amazon_model_profile', return_value=mock_profile),
|
|
'deepseek': mocker.patch('pydantic_ai.providers.litellm.deepseek_model_profile', return_value=mock_profile),
|
|
'groq': mocker.patch('pydantic_ai.providers.litellm.groq_model_profile', return_value=mock_profile),
|
|
'grok': mocker.patch('pydantic_ai.providers.litellm.grok_model_profile', return_value=mock_profile),
|
|
'moonshotai': mocker.patch('pydantic_ai.providers.litellm.moonshotai_model_profile', return_value=mock_profile),
|
|
'qwen': mocker.patch('pydantic_ai.providers.litellm.qwen_model_profile', return_value=mock_profile),
|
|
}
|
|
|
|
# Test models without provider prefix (should use openai profile)
|
|
models_without_prefix = ['gpt-4', 'claude-sonnet-4-5', 'gemini-pro', 'llama2-70b']
|
|
|
|
for model in models_without_prefix:
|
|
profile = provider.model_profile(model)
|
|
assert isinstance(profile, dict)
|
|
assert profile.get('json_schema_transformer', None) == OpenAIJsonSchemaTransformer
|
|
|
|
# Verify openai_model_profile was called for each model without prefix
|
|
assert mock_profiles['openai'].call_count == len(models_without_prefix)
|
|
|
|
# Reset all call counts
|
|
for mock in mock_profiles.values():
|
|
mock.reset_mock()
|
|
|
|
# Test all provider prefixes
|
|
test_cases = [
|
|
('anthropic/claude-3-haiku', 'anthropic', 'claude-3-haiku'),
|
|
('openai/gpt-4-turbo', 'openai', 'gpt-4-turbo'),
|
|
('google/gemini-1.5-pro', 'google', 'gemini-1.5-pro'),
|
|
('mistralai/mistral-large', 'mistral', 'mistral-large'),
|
|
('mistral/mistral-7b', 'mistral', 'mistral-7b'),
|
|
('cohere/command-r', 'cohere', 'command-r'),
|
|
('amazon/titan-text', 'amazon', 'titan-text'),
|
|
('bedrock/claude-v2', 'amazon', 'claude-v2'),
|
|
('meta-llama/llama-3-8b', 'meta', 'llama-3-8b'),
|
|
('meta/llama-2-70b', 'meta', 'llama-2-70b'),
|
|
('groq/llama3-70b', 'groq', 'llama3-70b'),
|
|
('deepseek/deepseek-coder', 'deepseek', 'deepseek-coder'),
|
|
('moonshotai/moonshot-v1', 'moonshotai', 'moonshot-v1'),
|
|
('x-ai/grok-beta', 'grok', 'grok-beta'),
|
|
('qwen/qwen-72b', 'qwen', 'qwen-72b'),
|
|
]
|
|
|
|
for model_name, expected_profile, expected_suffix in test_cases:
|
|
profile = provider.model_profile(model_name)
|
|
assert isinstance(profile, dict)
|
|
assert profile.get('json_schema_transformer', None) == OpenAIJsonSchemaTransformer
|
|
# Verify the correct profile function was called with the correct suffix
|
|
mock_profiles[expected_profile].assert_called_with(expected_suffix)
|
|
mock_profiles[expected_profile].reset_mock()
|
|
|
|
# Test unknown provider prefix (should fall back to openai)
|
|
provider.model_profile('unknown-provider/some-model')
|
|
mock_profiles['openai'].assert_called_once_with('unknown-provider/some-model')
|
|
|
|
|
|
async def test_create_http_client_usage(mocker: MockerFixture):
|
|
# Create a real AsyncClient for the mock
|
|
async with httpx.AsyncClient() as mock_client:
|
|
mock_create_func = mocker.patch(
|
|
'pydantic_ai.providers.litellm.create_async_http_client', return_value=mock_client
|
|
)
|
|
|
|
provider = LiteLLMProvider(api_key='test-key')
|
|
|
|
mock_create_func.assert_called_once_with()
|
|
|
|
# Verify the client was created
|
|
assert isinstance(provider.client, AsyncOpenAI)
|
|
|
|
|
|
async def test_init_with_http_client_overrides_cached():
|
|
async with httpx.AsyncClient() as custom_client:
|
|
provider = LiteLLMProvider(api_key='test-key', http_client=custom_client)
|
|
|
|
# Verify the provider was created successfully with custom client
|
|
assert isinstance(provider.client, AsyncOpenAI)
|
|
assert provider.client.api_key == 'test-key'
|