feature(gigachat): add gigachat passthrough endpoint

This commit is contained in:
KnyazSh 2026-04-16 15:28:27 +00:00
parent 718985144a
commit bf36953dcb
14 changed files with 859 additions and 0 deletions

View file

@ -0,0 +1,122 @@
# GigaChat Passthrough
Pass-through endpoints for direct GigaChat API access via LiteLLM Proxy.
## Overview
| Feature | Supported | Notes |
|-------|-------|-------|
| Cost Tracking | ✅ | Works with proxy cost metadata and router models |
| Logging | ✅ | Logs requests and responses across LiteLLM integrations |
| Streaming | ✅ | Supported for streaming GigaChat chat completions |
## When to use this
- Use the native LiteLLM GigaChat provider for standard chat and embedding calls when possible.
- Use `/gigachat` passthrough when you need provider-specific GigaChat endpoints or raw GigaChat request shapes.
- This is useful for newer or less common GigaChat API endpoints that LiteLLM does not yet expose natively.
## How it works
Any path under `/gigachat` is treated as a provider-specific route and routed through LiteLLM's GigaChat passthrough path.
The proxy accepts the same request body shape as GigaChat and forwards it to the GigaChat backend.
### Proxy base URL mapping
| Original GigaChat URL | Proxy URL |
|-----------------------|-----------|
| `https://gigachat.devices.sberbank.ru/api/v1` | `http://0.0.0.0:4000/gigachat/api/v1` |
## Request format
The proxy requires a `model` field in the request body. For GigaChat passthrough, use the LightLLM model prefix format such as `gigachat/GigaChat-2-Max`.
### Example: Chat completion
```bash
curl --request POST \
--url http://0.0.0.0:4000/gigachat/api/v1/chat/completions \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--header 'x-api-key: $LITELLM_API_KEY' \
--data '{
"model": "gigachat/GigaChat-2-Max",
"messages": [
{"role": "user", "content": "Hello, world"}
]
}'
```
### Python example
```python
import requests
import os
response = requests.post(
"http://0.0.0.0:4000/gigachat/api/v1/chat/completions",
headers={
"Content-Type": "application/json",
"x-api-key": os.environ["LITELLM_API_KEY"],
},
json={
"model": "gigachat/GigaChat-2-Max",
"messages": [
{"role": "user", "content": "Hello, world"}
],
},
)
print(response.json())
```
## Authentication
- Authenticate to the proxy with `x-api-key: $LITELLM_API_KEY` or `Authorization: Bearer $LITELLM_API_KEY`.
- The proxy then uses the configured GigaChat credentials to authenticate with the upstream GigaChat API.
## Notes
- GigaChat uses OAuth-style credentials. Configure your GigaChat credentials in LiteLLM using `GIGACHAT_CREDENTIALS` or `GIGACHAT_API_KEY` as described in the main GigaChat provider docs.
- The proxy automatically handles GigaChat's self-signed SSL setup when forwarding requests, so you do not need to disable SSL verification from the client side.
- The `model` field is required for passthrough requests.
## Advanced
### Use with router-backed GigaChat models
If you define router models in `config.yaml`, you can use the passthrough endpoint with a router-backed GigaChat model:
```bash
curl --request POST \
--url http://0.0.0.0:4000/gigachat/api/v1/chat/completions \
--header 'Content-Type: application/json' \
--header 'x-api-key: $LITELLM_API_KEY' \
--data '{
"model": "gigachat/GigaChat-2-Max",
"messages": [
{"role": "user", "content": "Hello, world"}
]
}'
```
### Sending metadata
You can attach LiteLLM metadata for cost tracking and tags using `litellm_metadata` in the request body:
```bash
curl --request POST \
--url http://0.0.0.0:4000/gigachat/api/v1/chat/completions \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--header 'x-api-key: $LITELLM_API_KEY' \
--data '{
"model": "gigachat/GigaChat-2-Max",
"messages": [
{"role": "user", "content": "Hello, world"}
],
"litellm_metadata": {
"tags": ["test-tag"],
"user": "test-user"
}
}'
```

View file

@ -30,6 +30,9 @@ _OPTIONAL_KWARGS_KEYS = frozenset(
"aws_sts_endpoint",
"aws_external_id",
"aws_bedrock_runtime_endpoint",
"gigachat_scope",
"gigachat_auth_url",
"gigachat_access_token",
"tpm",
"rpm",
}

View file

@ -324,6 +324,9 @@ def get_llm_provider( # noqa: PLR0915
elif endpoint == "https://api.inference.wandb.ai/v1":
custom_llm_provider = "wandb"
dynamic_api_key = get_secret_str("WANDB_API_KEY")
elif endpoint == "https://gigachat.devices.sberbank.ru/api/v1":
custom_llm_provider = "gigachat"
dynamic_api_key = get_secret_str("GIGACHAT_API_KEY")
if api_base is not None and not isinstance(api_base, str):
raise Exception(
@ -459,6 +462,8 @@ def get_llm_provider( # noqa: PLR0915
custom_llm_provider = "amazon_nova"
elif model.startswith("sap/"):
custom_llm_provider = "sap"
elif model in litellm.gigachat_models or model.startswith("gigachat/"):
custom_llm_provider = "gigachat"
if not custom_llm_provider:
if litellm.suppress_debug_info is False:
print() # noqa
@ -944,6 +949,13 @@ def _get_openai_compatible_provider_info( # noqa: PLR0915
api_base or get_secret_str("MANUS_API_BASE") or "https://api.manus.im"
)
dynamic_api_key = api_key or get_secret_str("MANUS_API_KEY")
elif custom_llm_provider == "gigachat":
api_base = (
api_base
or get_secret_str("GIGACHAT_API_BASE")
or "https://gigachat.devices.sberbank.ru/api/v1"
)
dynamic_api_key = api_key or get_secret_str("GIGACHAT_API_KEY")
if api_base is not None and not isinstance(api_base, str):
raise Exception("api base needs to be a string. api_base={}".format(api_base))

View file

@ -15,9 +15,11 @@ API Documentation: https://developers.sber.ru/docs/ru/gigachat/api/overview
from .chat.transformation import GigaChatConfig, GigaChatError
from .embedding.transformation import GigaChatEmbeddingConfig
from .passthrough.transformation import GigaChatPassthroughConfig
__all__ = [
"GigaChatConfig",
"GigaChatEmbeddingConfig",
"GigaChatError",
"GigaChatPassthroughConfig",
]

View file

@ -0,0 +1,7 @@
"""
GigaChat passthrough Module
"""
from .transformation import GigaChatPassthroughConfig
__all__ = ["GigaChatPassthroughConfig"]

View file

@ -0,0 +1,196 @@
import json
from typing import TYPE_CHECKING, List, Optional, Tuple, cast
import httpx
from litellm.llms.base_llm.passthrough.transformation import BasePassthroughConfig
from litellm.llms.gigachat.authenticator import get_access_token
from litellm.llms.gigachat.chat.streaming import GigaChatModelResponseIterator
from litellm.llms.gigachat.utils import get_api_base
from litellm.secret_managers.main import get_secret_str
from litellm.types.llms.openai import AllMessageValues
if TYPE_CHECKING:
from httpx import URL, Response
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
from litellm.types.utils import CostResponseTypes
class GigaChatPassthroughConfig(BasePassthroughConfig):
def is_streaming_request(self, endpoint: str, request_data: dict) -> bool:
return request_data.get("stream", False)
def get_complete_url(
self,
api_base: Optional[str],
api_key: Optional[str],
model: str,
endpoint: str,
request_query_params: Optional[dict],
litellm_params: dict,
) -> Tuple["URL", str]:
"""Get complete API URL for chat completions."""
base_target_url = self.get_api_base(api_base)
if base_target_url is None:
raise Exception("GigaChat api base not found")
litellm_metadata = litellm_params.get("litellm_metadata") or {}
model_group = litellm_metadata.get("model_group")
if model_group and model_group in endpoint:
endpoint = endpoint.replace(model_group, model)
complete_url = f"{base_target_url}/chat/completions"
return (
httpx.URL(complete_url),
base_target_url,
)
def validate_environment(
self,
headers: dict,
model: str,
messages: List[AllMessageValues],
optional_params: dict,
litellm_params: dict,
api_key: Optional[str] = None,
api_base: Optional[str] = None,
) -> dict:
"""
Set up headers with OAuth token.
"""
# Get access token
access_token = get_access_token(
credentials=api_key, litellm_params=litellm_params
)
headers["Authorization"] = f"Bearer {access_token}"
headers["Content-Type"] = "application/json"
headers["Accept"] = "application/json"
return headers
def logging_non_streaming_response(
self,
model: str,
custom_llm_provider: str,
httpx_response: "Response",
request_data: dict,
logging_obj: "LiteLLMLoggingObj",
endpoint: str,
) -> Optional["CostResponseTypes"]:
from litellm import encoding
from litellm.types.utils import LlmProviders, ModelResponse
from litellm.utils import ProviderConfigManager
provider_chat_config = ProviderConfigManager.get_provider_chat_config(
provider=LlmProviders(custom_llm_provider),
model=model,
)
if provider_chat_config is None:
raise ValueError(f"No provider config found for model: {model}")
litellm_model_response: ModelResponse = provider_chat_config.transform_response(
model=model,
messages=request_data.get("messages", []),
raw_response=httpx_response,
model_response=ModelResponse(),
logging_obj=logging_obj,
optional_params={},
litellm_params={},
api_key="",
request_data=request_data,
encoding=encoding,
)
return litellm_model_response
def handle_logging_collected_chunks(
self,
all_chunks: List[str],
litellm_logging_obj: "LiteLLMLoggingObj",
model: str,
custom_llm_provider: str,
endpoint: str,
) -> Optional["CostResponseTypes"]:
"""
1. Convert all_chunks to a ModelResponseStream
2. combine model_response_stream to model_response
3. Return the model_response
"""
from litellm.litellm_core_utils.streaming_handler import (
convert_generic_chunk_to_model_response_stream,
generic_chunk_has_all_required_fields,
)
from litellm.main import stream_chunk_builder
from litellm.types.utils import GenericStreamingChunk, ModelResponseStream
all_translated_chunks = []
for chunk in all_chunks:
if isinstance(chunk, bytes):
chunk = chunk.decode("utf-8", errors="ignore")
if isinstance(chunk, str):
chunk = chunk.strip()
if not chunk or chunk == "[DONE]":
continue
if chunk.startswith("data: "):
chunk = chunk[6:]
try:
message = json.loads(chunk)
except json.JSONDecodeError:
continue
elif isinstance(chunk, dict):
message = chunk
else:
continue
gigachat_iterator = GigaChatModelResponseIterator(
streaming_response=None,
sync_stream=False,
)
translated_chunk = gigachat_iterator.chunk_parser(chunk=message)
if isinstance(
translated_chunk, dict
) and generic_chunk_has_all_required_fields(cast(dict, translated_chunk)):
chunk_obj = convert_generic_chunk_to_model_response_stream(
cast(GenericStreamingChunk, translated_chunk)
)
elif isinstance(translated_chunk, ModelResponseStream):
chunk_obj = translated_chunk
else:
continue
all_translated_chunks.append(chunk_obj)
if len(all_translated_chunks) > 0:
model_response = stream_chunk_builder(
chunks=all_translated_chunks,
logging_obj=litellm_logging_obj,
)
return model_response
return None
@staticmethod
def get_api_base(api_base: Optional[str] = None) -> Optional[str]:
return get_api_base(api_base)
@staticmethod
def get_api_key(
api_key: Optional[str] = None,
) -> Optional[str]:
return api_key or get_secret_str("GIGACHAT_API_KEY")
@staticmethod
def get_base_model(model: str) -> Optional[str]:
return model
def get_models(
self, api_key: Optional[str] = None, api_base: Optional[str] = None
) -> List[str]:
return super().get_models(api_key, api_base)

View file

@ -1590,6 +1590,9 @@ def completion( # type: ignore # noqa: PLR0915
litellm_request_debug=kwargs.get("litellm_request_debug", False),
tpm=kwargs.get("tpm"),
rpm=kwargs.get("rpm"),
gigachat_scope=kwargs.get("gigachat_scope"),
gigachat_auth_url=kwargs.get("gigachat_auth_url"),
gigachat_access_token=kwargs.get("gigachat_access_token"),
)
cast(LiteLLMLoggingObj, logging).update_environment_variables(
model=model,

View file

@ -405,6 +405,7 @@ class LiteLLMRoutes(enum.Enum):
"/vllm",
"/mistral",
"/milvus",
"/gigachat",
]
#########################################################

View file

@ -22,6 +22,7 @@ from litellm.constants import (
ALLOWED_VERTEX_AI_PASSTHROUGH_HEADERS,
BEDROCK_AGENT_RUNTIME_PASS_THROUGH_ROUTES,
)
from litellm.llms.custom_httpx.http_handler import get_async_httpx_client
from litellm.llms.anthropic.common_utils import AnthropicModelInfo
from litellm.llms.vertex_ai.vertex_llm_base import VertexBase
from litellm.proxy._types import *
@ -2369,3 +2370,244 @@ def create_generic_websocket_passthrough_endpoint(
_forward_headers=forward_headers,
cost_per_request=cost_per_request,
)
@router.api_route(
"/gigachat/{endpoint:path}",
methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
tags=["Gigachat Pass-through", "pass-through"],
)
async def gigachat_proxy_route(
endpoint: str,
request: Request,
fastapi_response: Response,
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
):
"""
[Docs](https://docs.litellm.ai/docs/pass_through/gigachat)
"""
from litellm.proxy.common_request_processing import ProxyBaseLLMRequestProcessing
from litellm.proxy.proxy_server import (
general_settings,
llm_router,
proxy_config,
proxy_logging_obj,
select_data_generator,
user_api_base,
user_max_tokens,
user_model,
user_request_timeout,
user_temperature,
version,
)
## check for streaming
request_body = await get_request_body(request)
is_router_model = is_passthrough_request_using_router_model(
request_body, llm_router
)
model = request_body.get("model")
if not model:
msg = "Model is required"
raise ValueError(msg)
# If router model, use dedicated router passthrough handler
# This uses the same common processing path as non-router models
if is_router_model and llm_router:
return await handle_gigachat_passthrough_router_model(
model=model,
endpoint=endpoint,
request=request,
request_body=request_body,
llm_router=llm_router,
user_api_key_dict=user_api_key_dict,
proxy_logging_obj=proxy_logging_obj,
general_settings=general_settings,
proxy_config=proxy_config,
select_data_generator=select_data_generator,
user_model=user_model,
user_temperature=user_temperature,
user_request_timeout=user_request_timeout,
user_max_tokens=user_max_tokens,
user_api_base=user_api_base,
version=version,
)
# Fall back to existing implementation for direct GigaChat models
verbose_proxy_logger.debug(
f"Gigachat passthrough: Using direct Gigachat model '{model}' for endpoint '{endpoint}'"
)
data: Dict[str, Any] = {}
data["method"] = request.method
data["endpoint"] = endpoint
data["json"] = request_body
data["custom_llm_provider"] = "gigachat"
client = get_async_httpx_client( # type: ignore
llm_provider=LlmProviders.GIGACHAT,
params={
"timeout": httpx.Timeout(timeout=600.0, connect=5.0),
"ssl_verify": False,
},
)
data["http_client"] = client
base_llm_response_processor = ProxyBaseLLMRequestProcessing(data=data)
try:
result = await base_llm_response_processor.base_passthrough_process_llm_request(
request=request,
fastapi_response=fastapi_response,
user_api_key_dict=user_api_key_dict,
proxy_logging_obj=proxy_logging_obj,
llm_router=llm_router,
general_settings=general_settings,
proxy_config=proxy_config,
select_data_generator=select_data_generator,
model=model,
user_model=user_model,
user_temperature=user_temperature,
user_request_timeout=user_request_timeout,
user_max_tokens=user_max_tokens,
user_api_base=user_api_base,
version=version,
)
return result
except Exception as e:
raise await base_llm_response_processor._handle_llm_api_exception(
e=e,
user_api_key_dict=user_api_key_dict,
proxy_logging_obj=proxy_logging_obj,
)
async def handle_gigachat_passthrough_router_model(
model: str,
endpoint: str,
request: Request,
request_body: dict,
llm_router: litellm.Router,
user_api_key_dict: UserAPIKeyAuth,
proxy_logging_obj,
general_settings: dict,
proxy_config,
select_data_generator,
user_model: Optional[str],
user_temperature: Optional[float],
user_request_timeout: Optional[float],
user_max_tokens: Optional[int],
user_api_base: Optional[str],
version: Optional[str],
) -> Union[Response, StreamingResponse]:
"""
Handle Gigachat passthrough for router models (models defined in config.yaml).
Uses the same common processing path as non-router models to ensure
metadata and hooks are properly initialized.
Args:
model: The router model name (e.g., "gigachat/gigachat-2")
endpoint: The Gigachat endpoint path (e.g., "/chat/completions")
request: The FastAPI request object
request_body: The parsed request body
llm_router: The LiteLLM router instance
user_api_key_dict: The user API key authentication dictionary
(additional args for common processing)
Returns:
Response or StreamingResponse depending on endpoint type
"""
from fastapi import Response as FastAPIResponse
from litellm.proxy.common_request_processing import ProxyBaseLLMRequestProcessing
# Detect streaming based on request body
is_streaming = request_body.get("stream", False)
data: Dict[str, Any] = await _read_request_body(request=request)
if user_api_key_dict is not None:
if data.get("metadata") is None:
data["metadata"] = {}
if (
hasattr(user_api_key_dict, "user_id")
and user_api_key_dict.user_id is not None
):
data["metadata"]["user_api_key_user_id"] = user_api_key_dict.user_id
if (
hasattr(user_api_key_dict, "team_id")
and user_api_key_dict.team_id is not None
):
data["metadata"]["user_api_key_team_id"] = user_api_key_dict.team_id
if (
hasattr(user_api_key_dict, "org_id")
and user_api_key_dict.org_id is not None
):
data["metadata"]["user_api_key_org_id"] = user_api_key_dict.org_id
if (
hasattr(user_api_key_dict, "agent_id")
and user_api_key_dict.agent_id is not None
):
data["metadata"]["agent_id"] = user_api_key_dict.agent_id
verbose_proxy_logger.debug(
f"Gigachat router passthrough: model='{model}', endpoint='{endpoint}', streaming={is_streaming}"
)
# Use the common processing path (same as non-router models)
# This ensures all metadata, hooks, and logging are properly initialized
data["model"] = model
data["method"] = request.method
data["endpoint"] = endpoint
data["json"] = request_body
data["custom_llm_provider"] = "gigachat"
client = get_async_httpx_client( # type: ignore
llm_provider=LlmProviders.GIGACHAT,
params={
"timeout": httpx.Timeout(timeout=600.0, connect=5.0),
"ssl_verify": False,
},
)
data["http_client"] = client
base_llm_response_processor = ProxyBaseLLMRequestProcessing(data=data)
# Use the common passthrough processing to handle metadata and hooks
# This also handles all response formatting (streaming/non-streaming) and exceptions
try:
result = await base_llm_response_processor.base_passthrough_process_llm_request(
request=request,
fastapi_response=FastAPIResponse(),
user_api_key_dict=user_api_key_dict,
proxy_logging_obj=proxy_logging_obj,
llm_router=llm_router,
general_settings=general_settings,
proxy_config=proxy_config,
select_data_generator=select_data_generator,
model=model,
user_model=user_model,
user_temperature=user_temperature,
user_request_timeout=user_request_timeout,
user_max_tokens=user_max_tokens,
user_api_base=user_api_base,
version=version,
)
if isinstance(result, StreamingResponse):
if result.headers.get("Content-Type") is None:
result.headers["Content-Type"] = "text/event-stream; charset=utf-8"
return result
return result
except Exception as e:
# Use common exception handling
raise await base_llm_response_processor._handle_llm_api_exception(
e=e,
user_api_key_dict=user_api_key_dict,
proxy_logging_obj=proxy_logging_obj,
)

View file

@ -1172,6 +1172,68 @@
],
"default_model_placeholder": "gpt-3.5-turbo"
},
{
"provider": "GIGACHAT",
"provider_display_name": "GigaChat",
"litellm_provider": "gigachat",
"credential_fields": [
{
"key": "api_base",
"label": "API Base",
"placeholder": null,
"tooltip": null,
"required": false,
"field_type": "text",
"options": null,
"default_value": null
},
{
"key": "api_key",
"label": "API Key",
"placeholder": null,
"tooltip": null,
"required": false,
"field_type": "password",
"options": null,
"default_value": null
},
{
"key": "gigachat_scope",
"label": "Scope",
"placeholder": null,
"tooltip": null,
"required": false,
"field_type": "select",
"options": [
"GIGACHAT_API_PERS",
"GIGACHAT_API_B2B",
"GIGACHAT_API_CORP"
],
"default_value": "GIGACHAT_API_PERS"
},
{
"key": "gigachat_auth_url",
"label": "Auth URL",
"placeholder": null,
"tooltip": null,
"required": false,
"field_type": "text",
"options": null,
"default_value": null
},
{
"key": "gigachat_access_token",
"label": "Access token",
"placeholder": null,
"tooltip": "Disable OAuth, provide value to authorization.",
"required": false,
"field_type": "password",
"options": null,
"default_value": null
}
],
"default_model_placeholder": "GigaChat-2"
},
{
"provider": "GITHUB",
"provider_display_name": "Github",

View file

@ -8675,6 +8675,12 @@ class ProviderConfigManager:
)
return AzurePassthroughConfig()
elif LlmProviders.GIGACHAT == provider:
from litellm.llms.gigachat.passthrough.transformation import (
GigaChatPassthroughConfig,
)
return GigaChatPassthroughConfig()
return None
@staticmethod

View file

@ -8,6 +8,7 @@ from unittest.mock import AsyncMock, MagicMock, Mock, patch
import httpx
import pytest
from fastapi import Request, Response
from fastapi.responses import StreamingResponse
from fastapi.testclient import TestClient
sys.path.insert(
@ -15,12 +16,14 @@ sys.path.insert(
) # Adds the parent directory to the system path
import litellm
from litellm.proxy.common_request_processing import ProxyBaseLLMRequestProcessing
from litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints import (
BaseOpenAIPassThroughHandler,
RouteChecks,
bedrock_llm_proxy_route,
create_pass_through_route,
cursor_proxy_route,
gigachat_proxy_route,
llm_passthrough_factory_proxy_route,
milvus_proxy_route,
openai_proxy_route,
@ -1466,6 +1469,176 @@ class TestVLLMProxyRoute:
mock_factory_route.assert_awaited_once()
class TestGigachatProxyRoute:
@pytest.mark.asyncio
@patch(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.get_request_body",
return_value={"model": "router-model", "stream": False},
)
@patch(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.is_passthrough_request_using_router_model",
return_value=True,
)
@patch("litellm.proxy.proxy_server.llm_router")
async def test_gigachat_proxy_route_with_router_model(
self, mock_llm_router, mock_is_router, mock_get_body
):
mock_request = MagicMock(spec=Request)
mock_request.method = "POST"
mock_request.headers = {"content-type": "application/json"}
mock_request.query_params = {}
mock_fastapi_response = MagicMock(spec=Response)
mock_user_api_key_dict = MagicMock()
mock_llm_router.allm_passthrough_route = AsyncMock(
return_value=httpx.Response(200, json={"response": "success"})
)
result = await gigachat_proxy_route(
endpoint="/chat/completions",
request=mock_request,
fastapi_response=mock_fastapi_response,
user_api_key_dict=mock_user_api_key_dict,
)
mock_is_router.assert_called_once()
mock_llm_router.allm_passthrough_route.assert_awaited_once()
assert isinstance(result, Response)
@pytest.mark.asyncio
@patch(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.get_request_body",
return_value={"model": "other-model"},
)
@patch(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.is_passthrough_request_using_router_model",
return_value=False,
)
@patch(
"litellm.proxy.common_request_processing.ProxyBaseLLMRequestProcessing.base_passthrough_process_llm_request",
new_callable=AsyncMock,
)
async def test_gigachat_proxy_route_fallback_to_http_pass_through(
self,
mock_base_passthrough,
mock_is_router,
mock_get_body,
):
mock_request = MagicMock(spec=Request)
mock_fastapi_response = MagicMock(spec=Response)
mock_user_api_key_dict = MagicMock()
expected_response = Response(
content=b'{"response": "success"}',
status_code=200,
media_type="application/json",
)
mock_base_passthrough.return_value = expected_response
result = await gigachat_proxy_route(
endpoint="/chat/completions",
request=mock_request,
fastapi_response=mock_fastapi_response,
user_api_key_dict=mock_user_api_key_dict,
)
assert isinstance(result, Response)
assert result.status_code == 200
assert result.body == b'{"response": "success"}'
mock_base_passthrough.assert_awaited_once()
@pytest.mark.asyncio
async def test_allm_passthrough_streaming_preserves_upstream_headers(self):
async def _stream() -> bytes:
yield b'data: {"id":"1"}\n\n'
class MockPassthroughStreamingResponse:
def __init__(self):
self.status_code = 201
self.headers = {
"content-type": "text/event-stream; charset=utf-8",
"x-request-id": "req-123",
"x-ratelimit-remaining-requests": "77",
"transfer-encoding": "chunked",
"content-encoding": "gzip",
}
self._iterator = _stream()
def __aiter__(self):
return self
async def __anext__(self):
return await self._iterator.__anext__()
processor = ProxyBaseLLMRequestProcessing(
data={
"model": "some-provider/model",
"stream": True,
"litellm_call_id": "call-123",
"litellm_logging_obj": MagicMock(litellm_call_id="call-123"),
}
)
mock_request = MagicMock(spec=Request)
mock_request.headers = {"content-type": "application/json"}
mock_fastapi_response = MagicMock(spec=Response)
mock_user_api_key_dict = MagicMock()
mock_user_api_key_dict.allowed_model_region = ""
mock_user_api_key_dict.spend = 0.0
mock_proxy_logging_obj = MagicMock()
mock_proxy_logging_obj.during_call_hook = AsyncMock(return_value=None)
mock_proxy_logging_obj.update_request_status = AsyncMock(return_value=None)
mock_proxy_logging_obj.post_call_response_headers_hook = AsyncMock(
return_value={"x-test-callback-header": "callback-value"}
)
streaming_response = MockPassthroughStreamingResponse()
async def _fake_route_request(*args, **kwargs):
async def _inner():
return streaming_response
return _inner()
with patch.object(
processor,
"common_processing_pre_call_logic",
new=AsyncMock(
return_value=(
processor.data,
processor.data["litellm_logging_obj"],
)
),
), patch(
"litellm.proxy.common_request_processing.route_request",
new=_fake_route_request,
), patch(
"litellm.proxy.common_request_processing.ProxyBaseLLMRequestProcessing.get_custom_headers",
return_value={"x-litellm-call-id": "call-123"},
):
result = await processor.base_passthrough_process_llm_request(
request=mock_request,
fastapi_response=mock_fastapi_response,
user_api_key_dict=mock_user_api_key_dict,
proxy_logging_obj=mock_proxy_logging_obj,
general_settings={},
proxy_config=MagicMock(),
select_data_generator=MagicMock(),
llm_router=None,
model="some-provider/model",
version="test-version",
)
assert isinstance(result, StreamingResponse)
assert result.status_code == 201
assert result.headers["content-type"] == "text/event-stream; charset=utf-8"
assert result.headers["x-request-id"] == "req-123"
assert result.headers["x-ratelimit-remaining-requests"] == "77"
assert result.headers["x-litellm-call-id"] == "call-123"
assert result.headers["x-test-callback-header"] == "callback-value"
assert "transfer-encoding" not in result.headers
assert "content-encoding" not in result.headers
class TestForwardHeaders:
"""
Test cases for _forward_headers parameter in passthrough endpoints

View file

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 28.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Слой_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 1000 1000" enable-background="new 0 0 1000 1000" xml:space="preserve">
<path fill-rule="evenodd" clip-rule="evenodd" fill="#000001" d="M500,0c276.1218262,0,500,223.8403168,500,500
c0,276.1593628-223.8781738,500-500,500C223.8771973,1000,0,776.1218262,0,500C0.0000314,223.8771973,223.8771973,0.0000314,500,0z
M911.703125,378.8250122c-12.6343384,87.6312561-57.1812744,159.9468384-94.3469238,207.0031128
c-57.359314,70.65625-134.203064,131.828125-222.3842773,177.03125
c-87.2250366,44.7218628-181.578186,71.8875122-272.8406677,78.5562744
c-15.4475098,0.890625-29.7947083,1.3311768-43.2412415,1.3311768c-16.0025024,0-30.7562714-0.640625-44.759079-1.8999023
c73.1806183,57.3218384,165.3878326,91.499939,265.5440674,91.499939l-0.0562744-0.0562744
c238.0687561,0,431.1000061-192.999939,431.1000061-431.0686951c0-41.3156433-5.828125-81.2562561-16.6843872-119.0875244
C913.2562256,381.0218811,912.4812622,379.8999939,911.703125,378.8250122z M561.3343506,45.6000023
c-147.2843323,0-281.1668701,60.73843-358.1281128,162.4078064l-0.4881287,0.5968781
c-33.2709351,40.5328217-55.3865662,84.9996796-63.910614,128.6078033
c-4.8540802,26.53125-3.9568939,50.8968811,2.7127991,72.1312561v-0.0562439
c15.8215637,47.5031128,54.3062439,85.5968628,97.9543762,96.7875061
c18.1562653,4.5218506,35.6100006,6.4124756,51.8393707,5.671875l4.2859497-0.3812256
C405.9468689,502.4718628,504.34375,432.9312439,586.046875,366.046875
c51.1312256-42.7937622,100.0405884-99.4146729,103.1906128-135.9850006
C617.9468384,264.6322021,544.3500366,309.6121826,459.5,370.4156189
c-9.4125061,6.7062378-22.2406311,4.828125-29.2437439-4.2875061
c-42.1281128-54.5405884-78.8125305-98.8321838-115.53125-139.2943726
c-3.9968872-4.4449921-5.9906311-10.4062347-5.3987427-16.3302917c0.555603-5.9284515,3.6299744-11.4168854,8.4081116-14.9740753
C430.2281189,111.1612473,542.125,63.3568764,650.6875,53.2768745
C621.5718994,48.1650009,591.7124634,45.6006279,561.3343506,45.6000023z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -40,6 +40,7 @@ export enum Providers {
FireworksAI = "Fireworks AI",
FRIENDLIAI = "Friendliai",
GALADRIEL = "Galadriel",
GIGACHAT = "GigaChat",
GITHUB_COPILOT = "Github Copilot",
Google_AI_Studio = "Google AI Studio",
GradientAI = "GradientAI",
@ -146,6 +147,7 @@ export const provider_map: Record<string, string> = {
FireworksAI: "fireworks_ai",
FRIENDLIAI: "friendliai",
GALADRIEL: "galadriel",
GIGACHAT: "gigachat",
GITHUB_COPILOT: "github_copilot",
Google_AI_Studio: "gemini",
GradientAI: "gradient_ai",
@ -247,6 +249,7 @@ export const providerLogoMap: Record<string, string> = {
[Providers.FEATHERLESS_AI]: `${asset_logos_folder}featherless.svg`,
[Providers.FireworksAI]: `${asset_logos_folder}fireworks.svg`,
[Providers.FRIENDLIAI]: `${asset_logos_folder}friendli.svg`,
[Providers.GIGACHAT]: `${asset_logos_folder}gigachat.svg`,
[Providers.GITHUB_COPILOT]: `${asset_logos_folder}github_copilot.svg`,
[Providers.Google_AI_Studio]: `${asset_logos_folder}google.svg`,
[Providers.GradientAI]: `${asset_logos_folder}gradientai.svg`,