litellm/tests/llm_translation/test_gigachat.py
0717376 12f02f6c54
feat: Add GigaChat provider support (#18564)
* feat: Add GigaChat provider support

Add native support for GigaChat API (Sber AI, Russia's leading LLM).

Supported features:
- Chat completions (sync/async)
- Streaming (sync/async)
- Function calling / Tools
- Structured output via JSON schema (emulated through function calls)
- Image input (base64 and URL)
- Embeddings

Closes #18515

* fix: resolve mypy type errors in GigaChat handler

- Fix _prepare_file_data return type (use 3-tuple for cleaner type flow)
- Add type annotations for lists in _process_content_parts methods
- Add type annotations in _collapse_user_messages
- Use ChatCompletionToolCallChunk for proper tool_use typing
- Add type: ignore[override] for astreaming async generator

* refactor(gigachat): migrate to BaseConfig pattern

* fix: remove unused imports

* fix: resolve mypy type errors

* fix: mypy type errors

* refactor: address review feedback for GigaChat provider

- Remove singleton pattern, reuse litellm HTTPHandler
- Move constants/errors to transformation files, delete common_utils.py
- Add models to model_prices_and_context_window.json
- Fix ssl_verify not passed to HTTP client for embeddings

* docs: update GigaChat documentation with ssl_verify requirement
2026-01-06 10:10:02 +05:30

349 lines
12 KiB
Python

"""
Tests for GigaChat LiteLLM Provider
Tests message transformation, parameter handling, and response transformation.
Run with: pytest tests/llm_translation/test_gigachat.py -v
"""
import json
import pytest
from unittest.mock import Mock, MagicMock
class TestGigaChatMessageTransformation:
"""Tests for message transformation (OpenAI -> GigaChat format)"""
@pytest.fixture
def config(self):
from litellm.llms.gigachat.chat.transformation import GigaChatConfig
return GigaChatConfig()
def test_simple_user_message(self, config):
"""Basic user message should pass through"""
messages = [{"role": "user", "content": "Hello"}]
result = config._transform_messages(messages)
assert len(result) == 1
assert result[0]["role"] == "user"
assert result[0]["content"] == "Hello"
def test_developer_role_to_system(self, config):
"""Developer role should be converted to system"""
messages = [{"role": "developer", "content": "You are helpful"}]
result = config._transform_messages(messages)
assert result[0]["role"] == "system"
def test_system_after_first_becomes_user(self, config):
"""System message after first position should become user"""
messages = [
{"role": "assistant", "content": "Response"},
{"role": "system", "content": "Additional instruction"},
]
result = config._transform_messages(messages)
assert result[0]["role"] == "assistant"
assert result[1]["role"] == "user" # system after first becomes user
def test_tool_role_to_function(self, config):
"""Tool role should be converted to function"""
messages = [{"role": "tool", "content": "result data"}]
result = config._transform_messages(messages)
assert result[0]["role"] == "function"
def test_tool_calls_to_function_call(self, config):
"""tool_calls should be converted to function_call"""
messages = [{
"role": "assistant",
"content": "",
"tool_calls": [{
"id": "call_123",
"type": "function",
"function": {
"name": "get_weather",
"arguments": '{"city": "Moscow"}'
}
}]
}]
result = config._transform_messages(messages)
assert "function_call" in result[0]
assert result[0]["function_call"]["name"] == "get_weather"
assert result[0]["function_call"]["arguments"] == {"city": "Moscow"}
assert "tool_calls" not in result[0]
def test_none_content_becomes_empty_string(self, config):
"""None content should become empty string"""
messages = [{"role": "assistant", "content": None}]
result = config._transform_messages(messages)
assert result[0]["content"] == ""
def test_name_field_removed(self, config):
"""name field should be removed (not supported by GigaChat)"""
messages = [{"role": "user", "content": "Hi", "name": "John"}]
result = config._transform_messages(messages)
assert "name" not in result[0]
class TestGigaChatCollapseUserMessages:
"""Tests for collapsing consecutive user messages"""
@pytest.fixture
def config(self):
from litellm.llms.gigachat.chat.transformation import GigaChatConfig
return GigaChatConfig()
def test_no_collapse_single_message(self, config):
"""Single message should not be changed"""
messages = [{"role": "user", "content": "Hello"}]
result = config._collapse_user_messages(messages)
assert len(result) == 1
assert result[0]["content"] == "Hello"
def test_collapse_consecutive_user_messages(self, config):
"""Consecutive user messages should be collapsed"""
messages = [
{"role": "user", "content": "First"},
{"role": "user", "content": "Second"},
{"role": "user", "content": "Third"},
]
result = config._collapse_user_messages(messages)
assert len(result) == 1
assert "First" in result[0]["content"]
assert "Second" in result[0]["content"]
assert "Third" in result[0]["content"]
def test_no_collapse_with_assistant_between(self, config):
"""Messages with assistant between should not be collapsed"""
messages = [
{"role": "user", "content": "First"},
{"role": "assistant", "content": "Response"},
{"role": "user", "content": "Second"},
]
result = config._collapse_user_messages(messages)
assert len(result) == 3
class TestGigaChatToolsTransformation:
"""Tests for tools -> functions conversion"""
@pytest.fixture
def config(self):
from litellm.llms.gigachat.chat.transformation import GigaChatConfig
return GigaChatConfig()
def test_single_tool_conversion(self, config):
"""Single tool should be converted correctly"""
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get weather for a city",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string"}
}
}
}
}]
result = config._convert_tools_to_functions(tools)
assert len(result) == 1
assert result[0]["name"] == "get_weather"
assert result[0]["description"] == "Get weather for a city"
def test_multiple_tools_conversion(self, config):
"""Multiple tools should all be converted"""
tools = [
{"type": "function", "function": {"name": "func1", "description": "First", "parameters": {"type": "object", "properties": {}}}},
{"type": "function", "function": {"name": "func2", "description": "Second", "parameters": {"type": "object", "properties": {}}}},
]
result = config._convert_tools_to_functions(tools)
assert len(result) == 2
assert result[0]["name"] == "func1"
assert result[1]["name"] == "func2"
class TestGigaChatParamsTransformation:
"""Tests for parameter transformation"""
@pytest.fixture
def config(self):
from litellm.llms.gigachat.chat.transformation import GigaChatConfig
return GigaChatConfig()
def test_temperature_zero_becomes_top_p_zero(self, config):
"""temperature=0 should become top_p=0"""
params = {"temperature": 0}
result = config.map_openai_params(
non_default_params=params,
optional_params={},
model="GigaChat",
drop_params=False,
)
assert "top_p" in result
assert result["top_p"] == 0
assert "temperature" not in result
def test_temperature_nonzero_preserved(self, config):
"""Non-zero temperature should be preserved"""
params = {"temperature": 0.7}
result = config.map_openai_params(
non_default_params=params,
optional_params={},
model="GigaChat",
drop_params=False,
)
assert result["temperature"] == 0.7
def test_max_completion_tokens_to_max_tokens(self, config):
"""max_completion_tokens should become max_tokens"""
params = {"max_completion_tokens": 100}
result = config.map_openai_params(
non_default_params=params,
optional_params={},
model="GigaChat",
drop_params=False,
)
assert result["max_tokens"] == 100
def test_structured_output_via_json_schema(self, config):
"""json_schema response_format should trigger structured output mode"""
params = {
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "person",
"schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"}
}
}
}
}
}
result = config.map_openai_params(
non_default_params=params,
optional_params={},
model="GigaChat",
drop_params=False,
)
assert "_structured_output" in result
assert result["_structured_output"] is True
assert "function_call" in result
assert result["function_call"]["name"] == "person"
class TestGigaChatProviderRegistration:
"""Tests for provider registration in LiteLLM"""
def test_gigachat_in_provider_list(self):
"""GigaChat should be in provider list"""
from litellm.types.utils import LlmProviders
assert hasattr(LlmProviders, "GIGACHAT")
assert LlmProviders.GIGACHAT.value == "gigachat"
def test_gigachat_in_chat_providers(self):
"""GigaChat should be in LITELLM_CHAT_PROVIDERS"""
from litellm.constants import LITELLM_CHAT_PROVIDERS
assert "gigachat" in LITELLM_CHAT_PROVIDERS
def test_gigachat_key_exists(self):
"""gigachat_key should be available"""
import litellm
assert hasattr(litellm, "gigachat_key")
def test_gigachat_config_exists(self):
"""GigaChatConfig should be available"""
import litellm
assert hasattr(litellm, "GigaChatConfig")
class TestGigaChatTransformRequest:
"""Tests for request transformation"""
@pytest.fixture
def config(self):
from litellm.llms.gigachat.chat.transformation import GigaChatConfig
return GigaChatConfig()
def test_basic_request(self, config):
"""Basic request should be transformed correctly"""
messages = [{"role": "user", "content": "Hello"}]
result = config.transform_request(
model="gigachat/GigaChat",
messages=messages,
optional_params={},
litellm_params={},
headers={},
)
assert result["model"] == "GigaChat"
assert len(result["messages"]) == 1
assert result["messages"][0]["role"] == "user"
def test_request_with_temperature(self, config):
"""Request with temperature should include it"""
messages = [{"role": "user", "content": "Hello"}]
result = config.transform_request(
model="gigachat/GigaChat",
messages=messages,
optional_params={"temperature": 0.7},
litellm_params={},
headers={},
)
assert result["temperature"] == 0.7
def test_request_with_functions(self, config):
"""Request with functions should include them"""
messages = [{"role": "user", "content": "Hello"}]
functions = [{"name": "test", "description": "Test", "parameters": {}}]
result = config.transform_request(
model="gigachat/GigaChat",
messages=messages,
optional_params={"functions": functions},
litellm_params={},
headers={},
)
assert "functions" in result
assert len(result["functions"]) == 1
class TestGigaChatSupportedParams:
"""Tests for supported parameters"""
@pytest.fixture
def config(self):
from litellm.llms.gigachat.chat.transformation import GigaChatConfig
return GigaChatConfig()
def test_supported_params(self, config):
"""Check supported parameters list"""
supported = config.get_supported_openai_params("GigaChat")
assert "temperature" in supported
assert "max_tokens" in supported
assert "max_completion_tokens" in supported
assert "tools" in supported
assert "response_format" in supported
assert "stream" in supported