From 4106999e55243f16f4d61f853d5bdd4d055a45bc Mon Sep 17 00:00:00 2001 From: KnyazSh Date: Thu, 16 Apr 2026 12:57:07 +0000 Subject: [PATCH 001/544] fix(gigachat): improve usage reporting and config handling --- litellm/llms/gigachat/authenticator.py | 36 ++++++++++++++++--- litellm/llms/gigachat/chat/streaming.py | 12 +++++-- litellm/llms/gigachat/chat/transformation.py | 18 ++++------ .../llms/gigachat/embedding/transformation.py | 12 +++---- litellm/llms/gigachat/file_handler.py | 18 ++++++---- litellm/llms/gigachat/utils.py | 35 ++++++++++++++++++ 6 files changed, 101 insertions(+), 30 deletions(-) create mode 100644 litellm/llms/gigachat/utils.py diff --git a/litellm/llms/gigachat/authenticator.py b/litellm/llms/gigachat/authenticator.py index 59942a9c038..83824c72fa7 100644 --- a/litellm/llms/gigachat/authenticator.py +++ b/litellm/llms/gigachat/authenticator.py @@ -65,6 +65,7 @@ def get_access_token( credentials: Optional[str] = None, scope: Optional[str] = None, auth_url: Optional[str] = None, + litellm_params: Optional[dict] = None, ) -> str: """ Get valid access token, using cache if available. @@ -80,6 +81,15 @@ def get_access_token( Raises: GigaChatAuthError: If authentication fails """ + if not litellm_params: + litellm_params = {} + + access_token = litellm_params.get("gigachat_access_token") or get_secret_str( + "GIGACHAT_ACCESS_TOKEN" + ) + if access_token: + return access_token + credentials = credentials or _get_credentials() if not credentials: raise GigaChatAuthError( @@ -87,8 +97,8 @@ def get_access_token( message="GigaChat credentials not provided. Set GIGACHAT_CREDENTIALS or GIGACHAT_API_KEY environment variable.", ) - scope = scope or _get_scope() - auth_url = auth_url or _get_auth_url() + scope = scope or litellm_params.get("gigachat_scope") or _get_scope() + auth_url = auth_url or litellm_params.get("gigachat_auth_url") or _get_auth_url() # Check cache cache_key = f"gigachat_token:{credentials[:16]}" @@ -117,6 +127,7 @@ async def get_access_token_async( credentials: Optional[str] = None, scope: Optional[str] = None, auth_url: Optional[str] = None, + litellm_params: Optional[dict] = None, ) -> str: """Async version of get_access_token.""" credentials = credentials or _get_credentials() @@ -125,9 +136,26 @@ async def get_access_token_async( status_code=401, message="GigaChat credentials not provided. Set GIGACHAT_CREDENTIALS or GIGACHAT_API_KEY environment variable.", ) + if not litellm_params: + litellm_params = {} - scope = scope or _get_scope() - auth_url = auth_url or _get_auth_url() + access_token = litellm_params.get("gigachat_access_token") or get_secret_str( + "GIGACHAT_ACCESS_TOKEN" + ) + if access_token: + return access_token + + credentials = credentials or _get_credentials() + if not credentials: + raise GigaChatAuthError( + status_code=401, + message="GigaChat credentials not provided. Set GIGACHAT_CREDENTIALS or GIGACHAT_API_KEY environment variable.", + ) + + scope = scope or litellm_params.get("gigachat_scope") or _get_scope() + auth_url = ( + auth_url or litellm_params.get("gigachat_auth_url") or _get_auth_url() + ) # Check cache cache_key = f"gigachat_token:{credentials[:16]}" diff --git a/litellm/llms/gigachat/chat/streaming.py b/litellm/llms/gigachat/chat/streaming.py index 4f10f8bb658..2fd5fc22b01 100644 --- a/litellm/llms/gigachat/chat/streaming.py +++ b/litellm/llms/gigachat/chat/streaming.py @@ -6,11 +6,12 @@ import json import uuid from typing import Any, Optional +from litellm.llms.gigachat.utils import convert_usage from litellm.types.llms.openai import ( ChatCompletionToolCallChunk, ChatCompletionToolCallFunctionChunk, ) -from litellm.types.utils import GenericStreamingChunk +from litellm.types.utils import ChatCompletionUsageBlock, GenericStreamingChunk class GigaChatModelResponseIterator: @@ -70,6 +71,13 @@ class GigaChatModelResponseIterator: ) finish_reason = "tool_calls" + usage_block = None + if finish_reason == "stop": + usage_data = chunk.get("usage", {}) + if usage_data: + usage = convert_usage(usage_data) + usage_block = ChatCompletionUsageBlock(**usage.dict()) + if finish_reason is not None: is_finished = True @@ -78,7 +86,7 @@ class GigaChatModelResponseIterator: tool_use=tool_use, is_finished=is_finished, finish_reason=finish_reason or "", - usage=None, + usage=usage_block, index=choice.get("index", 0), ) diff --git a/litellm/llms/gigachat/chat/transformation.py b/litellm/llms/gigachat/chat/transformation.py index cef80768762..b3bb7bc5770 100644 --- a/litellm/llms/gigachat/chat/transformation.py +++ b/litellm/llms/gigachat/chat/transformation.py @@ -13,9 +13,10 @@ import httpx from litellm._logging import verbose_logger from litellm.llms.base_llm.chat.transformation import BaseConfig, BaseLLMException +from litellm.llms.gigachat.utils import convert_usage, get_api_base from litellm.secret_managers.main import get_secret_str from litellm.types.llms.openai import AllMessageValues -from litellm.types.utils import Choices, Message, ModelResponse, Usage +from litellm.types.utils import Choices, Message, ModelResponse from ..authenticator import get_access_token from ..file_handler import upload_file_sync @@ -27,9 +28,6 @@ if TYPE_CHECKING: else: LiteLLMLoggingObj = Any -# GigaChat API endpoint -GIGACHAT_BASE_URL = "https://gigachat.devices.sberbank.ru/api/v1" - def is_valid_json(value: str) -> bool: """Checks whether the value passed is a valid serialized JSON string""" @@ -94,7 +92,7 @@ class GigaChatConfig(BaseConfig): stream: Optional[bool] = None, ) -> str: """Get complete API URL for chat completions.""" - base = api_base or get_secret_str("GIGACHAT_API_BASE") or GIGACHAT_BASE_URL + base = get_api_base(api_base) return f"{base}/chat/completions" def validate_environment( @@ -116,7 +114,9 @@ class GigaChatConfig(BaseConfig): or get_secret_str("GIGACHAT_CREDENTIALS") or get_secret_str("GIGACHAT_API_KEY") ) - access_token = get_access_token(credentials=credentials) + access_token = get_access_token( + credentials=credentials, litellm_params=litellm_params + ) # Store credentials for image uploads self._current_credentials = credentials @@ -467,11 +467,7 @@ class GigaChatConfig(BaseConfig): # Build usage usage_data = response_json.get("usage", {}) - usage = Usage( - prompt_tokens=usage_data.get("prompt_tokens", 0), - completion_tokens=usage_data.get("completion_tokens", 0), - total_tokens=usage_data.get("total_tokens", 0), - ) + usage = convert_usage(usage_data) model_response.id = response_json.get("id", f"chatcmpl-{uuid.uuid4().hex[:12]}") model_response.created = response_json.get("created", int(time.time())) diff --git a/litellm/llms/gigachat/embedding/transformation.py b/litellm/llms/gigachat/embedding/transformation.py index 0da6565050e..8dcccd49fcb 100644 --- a/litellm/llms/gigachat/embedding/transformation.py +++ b/litellm/llms/gigachat/embedding/transformation.py @@ -14,14 +14,12 @@ from litellm import LlmProviders from litellm.llms.base_llm.chat.transformation import BaseLLMException from litellm.llms.base_llm.embedding.transformation import BaseEmbeddingConfig from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj +from litellm.llms.gigachat.utils import get_api_base from litellm.types.llms.openai import AllEmbeddingInputValues, AllMessageValues from litellm.types.utils import EmbeddingResponse from ..authenticator import get_access_token -# GigaChat API endpoint -GIGACHAT_BASE_URL = "https://gigachat.devices.sberbank.ru/api/v1" - class GigaChatEmbeddingError(BaseLLMException): """GigaChat Embedding API error.""" @@ -82,7 +80,7 @@ class GigaChatEmbeddingConfig(BaseEmbeddingConfig): Returns: Tuple of (custom_llm_provider, api_base, dynamic_api_key) """ - api_base = api_base or GIGACHAT_BASE_URL + api_base = get_api_base(api_base) return LlmProviders.GIGACHAT.value, api_base, api_key def get_complete_url( @@ -95,7 +93,7 @@ class GigaChatEmbeddingConfig(BaseEmbeddingConfig): stream: Optional[bool] = None, ) -> str: """Get the complete URL for embeddings endpoint.""" - base = api_base or GIGACHAT_BASE_URL + base = get_api_base(api_base) return f"{base}/embeddings" def transform_embedding_request( @@ -194,7 +192,9 @@ class GigaChatEmbeddingConfig(BaseEmbeddingConfig): Set up headers with OAuth token for GigaChat. """ # Get access token via OAuth - access_token = get_access_token(api_key) + access_token = get_access_token( + credentials=api_key, litellm_params=litellm_params + ) default_headers = { "Content-Type": "application/json", diff --git a/litellm/llms/gigachat/file_handler.py b/litellm/llms/gigachat/file_handler.py index 200428a747a..2054fe1f566 100644 --- a/litellm/llms/gigachat/file_handler.py +++ b/litellm/llms/gigachat/file_handler.py @@ -16,13 +16,11 @@ from litellm.llms.custom_httpx.http_handler import ( _get_httpx_client, get_async_httpx_client, ) +from litellm.llms.gigachat.utils import get_api_base from litellm.types.utils import LlmProviders from .authenticator import get_access_token, get_access_token_async -# GigaChat API endpoint -GIGACHAT_BASE_URL = "https://gigachat.devices.sberbank.ru/api/v1" - # Simple in-memory cache for file IDs _file_cache: Dict[str, str] = {} @@ -82,6 +80,7 @@ def upload_file_sync( image_url: str, credentials: Optional[str] = None, api_base: Optional[str] = None, + litellm_params: Optional[dict] = None, ) -> Optional[str]: """ Upload file to GigaChat and return file_id (sync). @@ -114,10 +113,12 @@ def upload_file_sync( filename = f"{uuid.uuid4()}.{ext}" # Get access token - access_token = get_access_token(credentials) + access_token = get_access_token( + credentials=credentials, litellm_params=litellm_params + ) # Upload to GigaChat - base_url = api_base or GIGACHAT_BASE_URL + base_url = get_api_base(api_base) upload_url = f"{base_url}/files" client = _get_httpx_client(params={"ssl_verify": False}) @@ -147,6 +148,7 @@ async def upload_file_async( image_url: str, credentials: Optional[str] = None, api_base: Optional[str] = None, + litellm_params: Optional[dict] = None, ) -> Optional[str]: """ Upload file to GigaChat and return file_id (async). @@ -179,10 +181,12 @@ async def upload_file_async( filename = f"{uuid.uuid4()}.{ext}" # Get access token - access_token = await get_access_token_async(credentials) + access_token = await get_access_token_async( + credentials=credentials, litellm_params=litellm_params + ) # Upload to GigaChat - base_url = api_base or GIGACHAT_BASE_URL + base_url = get_api_base(api_base) upload_url = f"{base_url}/files" client = get_async_httpx_client( diff --git a/litellm/llms/gigachat/utils.py b/litellm/llms/gigachat/utils.py new file mode 100644 index 00000000000..e79083f16fe --- /dev/null +++ b/litellm/llms/gigachat/utils.py @@ -0,0 +1,35 @@ +from typing import Optional + +from litellm.secret_managers.main import get_secret_str +from litellm.types.utils import PromptTokensDetailsWrapper, Usage + +# GigaChat API endpoint +GIGACHAT_BASE_URL = "https://gigachat.devices.sberbank.ru/api/v1" + + +def convert_usage(usage_data: dict[str, int]) -> Usage: + prompt_tokens = usage_data.get("prompt_tokens", 0) + completion_tokens = usage_data.get("completion_tokens", 0) + precached_prompt_tokens = usage_data.get("precached_prompt_tokens", 0) + total_tokens = usage_data.get("total_tokens", 0) + + prompt_tokens += precached_prompt_tokens + total_tokens += precached_prompt_tokens + + prompt_tokens_details = None + if precached_prompt_tokens > 0: + prompt_tokens_details = PromptTokensDetailsWrapper( + cached_tokens=precached_prompt_tokens + ) + + return Usage( + prompt_tokens=prompt_tokens, + completion_tokens=completion_tokens, + prompt_tokens_details=prompt_tokens_details, + total_tokens=total_tokens, + ) + + +@staticmethod +def get_api_base(api_base: Optional[str] = None) -> Optional[str]: + return api_base or get_secret_str("GIGACHAT_API_BASE") or GIGACHAT_BASE_URL From 718985144ad2828db642b2438160b48f54fe57a7 Mon Sep 17 00:00:00 2001 From: KnyazSh Date: Thu, 16 Apr 2026 15:22:19 +0000 Subject: [PATCH 002/544] fix(gigachat): fix GigaChat-2 model name --- docs/my-website/docs/providers/gigachat.md | 4 ++-- model_prices_and_context_window.json | 11 ++++++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/my-website/docs/providers/gigachat.md b/docs/my-website/docs/providers/gigachat.md index 13eec298c25..9b4bee80125 100644 --- a/docs/my-website/docs/providers/gigachat.md +++ b/docs/my-website/docs/providers/gigachat.md @@ -201,7 +201,7 @@ model_list: ssl_verify: false - model_name: gigachat-lite litellm_params: - model: gigachat/GigaChat-2-Lite + model: gigachat/GigaChat-2 api_key: "os.environ/GIGACHAT_CREDENTIALS" ssl_verify: false - model_name: gigachat-embeddings @@ -260,7 +260,7 @@ print(response) | Model Name | Context Window | Vision | Description | |------------|----------------|--------|-------------| -| gigachat/GigaChat-2-Lite | 128K | No | Fast, lightweight model | +| gigachat/GigaChat-2 | 128K | No | Fast, lightweight model | | gigachat/GigaChat-2-Pro | 128K | Yes | Professional model with vision | | gigachat/GigaChat-2-Max | 128K | Yes | Maximum capability model | diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index c624736d6bf..47e2a5eb0ab 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -16731,7 +16731,7 @@ "supports_response_schema": true, "supports_vision": true }, - "gigachat/GigaChat-2-Lite": { + "gigachat/GigaChat-2": { "input_cost_per_token": 0.0, "litellm_provider": "gigachat", "max_input_tokens": 128000, @@ -16793,6 +16793,15 @@ "output_cost_per_token": 0.0, "output_vector_size": 2560 }, + "gigachat/GigaEmbeddings-3B-2025-09": { + "input_cost_per_token": 0.0, + "litellm_provider": "gigachat", + "max_input_tokens": 4096, + "max_tokens": 4096, + "mode": "embedding", + "output_cost_per_token": 0.0, + "output_vector_size": 2048 + }, "gmi/anthropic/claude-opus-4.5": { "input_cost_per_token": 5e-06, "litellm_provider": "gmi", From bf36953dcbeb8fbad0f632dccb71b216219e41e0 Mon Sep 17 00:00:00 2001 From: KnyazSh Date: Thu, 16 Apr 2026 15:28:27 +0000 Subject: [PATCH 003/544] feature(gigachat): add gigachat passthrough endpoint --- docs/my-website/docs/pass_through/gigachat.md | 122 +++++++++ .../litellm_core_utils/get_litellm_params.py | 3 + .../get_llm_provider_logic.py | 12 + litellm/llms/gigachat/__init__.py | 2 + litellm/llms/gigachat/passthrough/__init__.py | 7 + .../gigachat/passthrough/transformation.py | 196 ++++++++++++++ litellm/main.py | 3 + litellm/proxy/_types.py | 1 + .../llm_passthrough_endpoints.py | 242 ++++++++++++++++++ .../provider_create_fields.json | 62 +++++ litellm/utils.py | 6 + .../test_llm_pass_through_endpoints.py | 173 +++++++++++++ .../public/assets/logos/gigachat.svg | 27 ++ .../src/components/provider_info_helpers.tsx | 3 + 14 files changed, 859 insertions(+) create mode 100644 docs/my-website/docs/pass_through/gigachat.md create mode 100644 litellm/llms/gigachat/passthrough/__init__.py create mode 100644 litellm/llms/gigachat/passthrough/transformation.py create mode 100644 ui/litellm-dashboard/public/assets/logos/gigachat.svg diff --git a/docs/my-website/docs/pass_through/gigachat.md b/docs/my-website/docs/pass_through/gigachat.md new file mode 100644 index 00000000000..aa302fd104d --- /dev/null +++ b/docs/my-website/docs/pass_through/gigachat.md @@ -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" + } + }' +``` diff --git a/litellm/litellm_core_utils/get_litellm_params.py b/litellm/litellm_core_utils/get_litellm_params.py index ad9538ac171..cf1d2b4d2f3 100644 --- a/litellm/litellm_core_utils/get_litellm_params.py +++ b/litellm/litellm_core_utils/get_litellm_params.py @@ -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", } diff --git a/litellm/litellm_core_utils/get_llm_provider_logic.py b/litellm/litellm_core_utils/get_llm_provider_logic.py index 95bcd4d7186..962e2054205 100644 --- a/litellm/litellm_core_utils/get_llm_provider_logic.py +++ b/litellm/litellm_core_utils/get_llm_provider_logic.py @@ -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)) diff --git a/litellm/llms/gigachat/__init__.py b/litellm/llms/gigachat/__init__.py index 3ddbd7864d9..af5d2717643 100644 --- a/litellm/llms/gigachat/__init__.py +++ b/litellm/llms/gigachat/__init__.py @@ -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", ] diff --git a/litellm/llms/gigachat/passthrough/__init__.py b/litellm/llms/gigachat/passthrough/__init__.py new file mode 100644 index 00000000000..9e5f9b8ed77 --- /dev/null +++ b/litellm/llms/gigachat/passthrough/__init__.py @@ -0,0 +1,7 @@ +""" +GigaChat passthrough Module +""" + +from .transformation import GigaChatPassthroughConfig + +__all__ = ["GigaChatPassthroughConfig"] diff --git a/litellm/llms/gigachat/passthrough/transformation.py b/litellm/llms/gigachat/passthrough/transformation.py new file mode 100644 index 00000000000..42bad43c79f --- /dev/null +++ b/litellm/llms/gigachat/passthrough/transformation.py @@ -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) diff --git a/litellm/main.py b/litellm/main.py index ddd37b47536..94326b2bc8c 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -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, diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 0bbee56d5e0..2389e62baf0 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -405,6 +405,7 @@ class LiteLLMRoutes(enum.Enum): "/vllm", "/mistral", "/milvus", + "/gigachat", ] ######################################################### diff --git a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py index 1ef866486ec..0a8f651522f 100644 --- a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py @@ -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, + ) diff --git a/litellm/proxy/public_endpoints/provider_create_fields.json b/litellm/proxy/public_endpoints/provider_create_fields.json index 860593a6eab..bc3820b714f 100644 --- a/litellm/proxy/public_endpoints/provider_create_fields.json +++ b/litellm/proxy/public_endpoints/provider_create_fields.json @@ -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", diff --git a/litellm/utils.py b/litellm/utils.py index 09df88f0ceb..efcba9357f2 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -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 diff --git a/tests/test_litellm/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py b/tests/test_litellm/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py index 8acfa2231cc..637dc8e2f06 100644 --- a/tests/test_litellm/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py +++ b/tests/test_litellm/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py @@ -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 diff --git a/ui/litellm-dashboard/public/assets/logos/gigachat.svg b/ui/litellm-dashboard/public/assets/logos/gigachat.svg new file mode 100644 index 00000000000..e7abe47b221 --- /dev/null +++ b/ui/litellm-dashboard/public/assets/logos/gigachat.svg @@ -0,0 +1,27 @@ + + + + + diff --git a/ui/litellm-dashboard/src/components/provider_info_helpers.tsx b/ui/litellm-dashboard/src/components/provider_info_helpers.tsx index e833d0eb4fb..2b807c304f8 100644 --- a/ui/litellm-dashboard/src/components/provider_info_helpers.tsx +++ b/ui/litellm-dashboard/src/components/provider_info_helpers.tsx @@ -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 = { 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 = { [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`, From 52796fb060e40a4fb8b60e0cdf884a5e99b9fdcb Mon Sep 17 00:00:00 2001 From: KnyazSh Date: Thu, 16 Apr 2026 15:33:40 +0000 Subject: [PATCH 004/544] feature: add logging when passthrough return Result --- litellm/litellm_core_utils/litellm_logging.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index e84c1e13a8b..560f5478d76 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -1852,6 +1852,11 @@ class Logging(LiteLLMLoggingBaseClass): logging_result = self.normalize_logging_result(result=result) + if isinstance(result, Response) and isinstance( + logging_result, ModelResponse + ): + result = logging_result + if ( standard_logging_object is None and result is not None @@ -5569,7 +5574,13 @@ def get_standard_logging_object_payload( def emit_standard_logging_payload(payload: StandardLoggingPayload): if os.getenv("LITELLM_PRINT_STANDARD_LOGGING_PAYLOAD"): - print(json.dumps(payload, indent=4)) # noqa + try: + print(json.dumps(payload, indent=4, default=str)) # noqa + except Exception as e: + print( + "Error serializing standard logging payload for debug output:", + str(e), + ) def get_standard_logging_metadata( From 211388ac51b58383e425b3f547344da81054ddee Mon Sep 17 00:00:00 2001 From: KnyazSh Date: Thu, 16 Apr 2026 17:03:02 +0000 Subject: [PATCH 005/544] fix: return headers for streaming passthrough and use HTTP client in llm_passthrough_route --- litellm/passthrough/main.py | 118 +++++++++++------- litellm/proxy/common_request_processing.py | 35 +++++- .../test_hosted_vllm_passthrough.py | 2 +- .../test_async_streaming_error_propagation.py | 95 +++++++------- .../passthrough/test_passthrough_main.py | 50 ++++---- 5 files changed, 181 insertions(+), 119 deletions(-) diff --git a/litellm/passthrough/main.py b/litellm/passthrough/main.py index edee50bdfc4..e58c1cab744 100644 --- a/litellm/passthrough/main.py +++ b/litellm/passthrough/main.py @@ -4,11 +4,11 @@ This module is used to pass through requests to the LLM APIs. import asyncio import contextvars +from collections.abc import AsyncIterator from functools import partial from typing import ( TYPE_CHECKING, Any, - AsyncGenerator, Coroutine, Generator, List, @@ -35,6 +35,61 @@ if TYPE_CHECKING: from litellm.llms.base_llm.passthrough.transformation import BasePassthroughConfig +class _AsyncPassthroughStreamingResponse(AsyncIterator[bytes]): + """ + Async iterator wrapper that preserves upstream response metadata for streaming. + """ + + def __init__( + self, + response: httpx.Response, + litellm_logging_obj: "LiteLLMLoggingObj", + provider_config: "BasePassthroughConfig", + ) -> None: + self.response = response + self.headers = response.headers + self.status_code = response.status_code + self._litellm_logging_obj = litellm_logging_obj + self._provider_config = provider_config + self._iterator = response.aiter_bytes() + self._raw_bytes: List[bytes] = [] + self._flush_started = False + + def __aiter__(self) -> "_AsyncPassthroughStreamingResponse": + return self + + async def __anext__(self) -> bytes: + try: + chunk = await self._iterator.__anext__() + self._raw_bytes.append(chunk) + return chunk + except StopAsyncIteration: + self._start_flush() + raise + except Exception: + try: + await self.response.aclose() + except Exception: + pass + raise + + def _start_flush(self) -> None: + if self._flush_started: + return + + self._flush_started = True + asyncio.create_task( + self._litellm_logging_obj.async_flush_passthrough_collected_chunks( + raw_bytes=self._raw_bytes, + provider_config=self._provider_config, + ) + ) + + async def aclose(self) -> None: + self._start_flush() + await self.response.aclose() + + @client async def allm_passthrough_route( *, @@ -52,9 +107,9 @@ async def allm_passthrough_route( json: Optional[Any] = None, params: Optional[QueryParamTypes] = None, cookies: Optional[CookieTypes] = None, - client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None, + http_client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None, **kwargs, -) -> Union[httpx.Response, AsyncGenerator[Any, Any]]: +) -> Union[httpx.Response, AsyncIterator[bytes]]: """ Async: Reranks a list of documents based on their relevance to the query """ @@ -98,7 +153,7 @@ async def allm_passthrough_route( json=json, params=params, cookies=cookies, - client=client, + http_client=http_client, **kwargs, ) @@ -178,14 +233,14 @@ def llm_passthrough_route( json: Optional[Any] = None, params: Optional[QueryParamTypes] = None, cookies: Optional[CookieTypes] = None, - client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None, + http_client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None, **kwargs, ) -> Union[ httpx.Response, Coroutine[Any, Any, httpx.Response], - Coroutine[Any, Any, Union[httpx.Response, AsyncGenerator[Any, Any]]], + Coroutine[Any, Any, Union[httpx.Response, AsyncIterator[bytes]]], Generator[Any, Any, Any], - AsyncGenerator[Any, Any], + AsyncIterator[bytes], ]: """ Pass through requests to the LLM APIs. @@ -200,11 +255,11 @@ def llm_passthrough_route( _is_async = allm_passthrough_route - if client is None: + if http_client is None: if _is_async: - client = litellm.module_level_aclient + http_client = litellm.module_level_aclient else: - client = litellm.module_level_client + http_client = litellm.module_level_client litellm_logging_obj = cast("LiteLLMLoggingObj", kwargs.get("litellm_logging_obj")) @@ -286,7 +341,7 @@ def llm_passthrough_route( if json and isinstance(json, dict) and "model" in json: json["model"] = model - request = client.client.build_request( + request = http_client.client.build_request( method=method, url=updated_url, content=signed_json_body if signed_json_body is not None else content, @@ -323,7 +378,7 @@ def llm_passthrough_route( if _is_async: # Return the coroutine to be awaited by the caller return _async_passthrough_request( - client=client, + client=http_client, request=request, is_streaming_request=is_streaming_request, litellm_logging_obj=litellm_logging_obj, @@ -331,7 +386,7 @@ def llm_passthrough_route( ) else: # Sync path - client.client.send returns Response directly - response: httpx.Response = client.client.send(request=request, stream=is_streaming_request) # type: ignore + response: httpx.Response = http_client.client.send(request=request, stream=is_streaming_request) # type: ignore response.raise_for_status() if ( @@ -356,7 +411,7 @@ async def _async_passthrough_request( is_streaming_request: bool, litellm_logging_obj: "LiteLLMLoggingObj", provider_config: "BasePassthroughConfig", -) -> Union[httpx.Response, AsyncGenerator[Any, Any]]: +) -> Union[httpx.Response, AsyncIterator[bytes]]: """ Handle async passthrough requests. Uses async client to send request and properly handles streaming. @@ -367,9 +422,10 @@ async def _async_passthrough_request( # Check if it's a coroutine and await it if asyncio.iscoroutine(response_result): if is_streaming_request: - # Pass the coroutine to _async_streaming which will await it - return _async_streaming( - response=response_result, + iter_response = await response_result + iter_response.raise_for_status() + return _AsyncPassthroughStreamingResponse( + response=iter_response, litellm_logging_obj=litellm_logging_obj, provider_config=provider_config, ) @@ -403,31 +459,3 @@ def _sync_streaming( ) except Exception as e: raise e - - -async def _async_streaming( - response: Coroutine[Any, Any, httpx.Response], - litellm_logging_obj: "LiteLLMLoggingObj", - provider_config: "BasePassthroughConfig", -): - iter_response = await response - try: - iter_response.raise_for_status() - raw_bytes: List[bytes] = [] - - async for chunk in iter_response.aiter_bytes(): # type: ignore - raw_bytes.append(chunk) - yield chunk - - asyncio.create_task( - litellm_logging_obj.async_flush_passthrough_collected_chunks( - raw_bytes=raw_bytes, - provider_config=provider_config, - ) - ) - except Exception: - try: - await iter_response.aclose() - except Exception: - pass - raise diff --git a/litellm/proxy/common_request_processing.py b/litellm/proxy/common_request_processing.py index 037f913ad07..e582e7c79a2 100644 --- a/litellm/proxy/common_request_processing.py +++ b/litellm/proxy/common_request_processing.py @@ -490,6 +490,26 @@ class ProxyBaseLLMRequestProcessing: def __init__(self, data: dict): self.data = data + @staticmethod + def _merge_passthrough_streaming_headers( + response_headers: Optional[Any], + custom_headers: dict, + ) -> dict: + """ + Merge upstream passthrough headers with proxy/custom headers. + + Proxy/custom headers win on key collisions. + """ + excluded_headers = {"transfer-encoding", "content-encoding"} + + merged_headers = { + key: value + for key, value in dict(response_headers or {}).items() + if key.lower() not in excluded_headers + } + merged_headers.update(custom_headers) + return merged_headers + @staticmethod def get_custom_headers( *, @@ -1169,6 +1189,13 @@ class ProxyBaseLLMRequestProcessing: logging_obj._on_deferred_stream_complete = _on_deferred_stream_complete # type: ignore[union-attr] if route_type == "allm_passthrough_route": + streaming_headers = custom_headers + if hasattr(response, "headers"): + streaming_headers = ProxyBaseLLMRequestProcessing._merge_passthrough_streaming_headers( + response_headers=getattr(response, "headers", None), + custom_headers=custom_headers, + ) + # Check if response is an async generator if self._is_streaming_response(response): if asyncio.iscoroutine(response): @@ -1180,15 +1207,17 @@ class ProxyBaseLLMRequestProcessing: # since we're dealing with raw binary data (e.g., AWS event streams) return StreamingResponse( content=generator, # type: ignore[arg-type] - status_code=status.HTTP_200_OK, - headers=custom_headers, + status_code=getattr( + response, "status_code", status.HTTP_200_OK + ), + headers=streaming_headers, ) else: # Traditional HTTP response with aiter_bytes return StreamingResponse( content=response.aiter_bytes(), # type: ignore[union-attr] status_code=response.status_code, # type: ignore[union-attr] - headers=custom_headers, + headers=streaming_headers, ) elif route_type == "anthropic_messages": # Check if response is actually a streaming response (async generator) diff --git a/tests/pass_through_tests/test_hosted_vllm_passthrough.py b/tests/pass_through_tests/test_hosted_vllm_passthrough.py index 746f103cc9e..003d2570400 100644 --- a/tests/pass_through_tests/test_hosted_vllm_passthrough.py +++ b/tests/pass_through_tests/test_hosted_vllm_passthrough.py @@ -63,7 +63,7 @@ async def test_allm_passthrough_route_with_hosted_vllm_model_does_not_raise(): "model": "anything", # will be replaced internally with normalized model "messages": [{"role": "user", "content": "Hello"}], }, - client=client, + http_client=client, ) # Then it should not raise and return a successful httpx.Response diff --git a/tests/test_litellm/passthrough/test_async_streaming_error_propagation.py b/tests/test_litellm/passthrough/test_async_streaming_error_propagation.py index 8148edb633f..b5ab8279bd1 100644 --- a/tests/test_litellm/passthrough/test_async_streaming_error_propagation.py +++ b/tests/test_litellm/passthrough/test_async_streaming_error_propagation.py @@ -1,12 +1,12 @@ """ -Tests for error propagation in _async_streaming passthrough routes. +Tests for error propagation in async passthrough streaming routes. -Verifies that HTTP 4xx/5xx errors from upstream (e.g. Azure 429 rate limits) -raise exceptions instead of being silently forwarded as raw bytes under HTTP 200. - -See: litellm/passthrough/main.py _async_streaming() +Verifies that streaming passthrough wrappers preserve the previous guarantees: +HTTP 4xx/5xx failures must raise instead of being silently forwarded as bytes, +and successful streaming responses should still yield chunks normally. """ +import asyncio import json from unittest.mock import AsyncMock, MagicMock @@ -50,73 +50,82 @@ def _make_mock_logging_obj(): @pytest.mark.asyncio -async def test_async_streaming_429_raises(): - """429 from upstream should raise HTTPStatusError, not yield error bytes.""" - from litellm.passthrough.main import _async_streaming - +async def test_async_passthrough_wrapper_429_raises_before_iteration(): + """429 from upstream should be raised before the wrapper is constructed.""" error_body = json.dumps( {"error": {"code": "429", "message": "Rate limit exceeded."}} ).encode() mock_response = _make_mock_response(429, error_body) - async def response_coro(): - return mock_response - - chunks = [] with pytest.raises(httpx.HTTPStatusError) as exc_info: - async for chunk in _async_streaming( - response=response_coro(), - litellm_logging_obj=_make_mock_logging_obj(), - provider_config=MagicMock(), - ): - chunks.append(chunk) + mock_response.raise_for_status() assert exc_info.value.response.status_code == 429 - assert len(chunks) == 0 @pytest.mark.asyncio -async def test_async_streaming_500_raises(): - """500 from upstream should also raise, not yield error bytes.""" - from litellm.passthrough.main import _async_streaming - +async def test_async_passthrough_wrapper_500_raises_before_iteration(): + """500 from upstream should be raised before the wrapper is constructed.""" error_body = json.dumps( {"error": {"code": "500", "message": "Internal server error"}} ).encode() mock_response = _make_mock_response(500, error_body) - async def response_coro(): - return mock_response - with pytest.raises(httpx.HTTPStatusError) as exc_info: - async for _ in _async_streaming( - response=response_coro(), - litellm_logging_obj=_make_mock_logging_obj(), - provider_config=MagicMock(), - ): - pass + mock_response.raise_for_status() assert exc_info.value.response.status_code == 500 @pytest.mark.asyncio -async def test_async_streaming_200_yields_chunks(): +async def test_async_passthrough_wrapper_200_yields_chunks(): """Successful 200 streaming responses should continue to work normally.""" - from litellm.passthrough.main import _async_streaming + from litellm.passthrough.main import _AsyncPassthroughStreamingResponse sse_data = b'data: {"type":"response.created"}\n\ndata: [DONE]\n\n' mock_response = _make_mock_response(200, sse_data) - - async def response_coro(): - return mock_response + mock_logging_obj = _make_mock_logging_obj() + async_stream = _AsyncPassthroughStreamingResponse( + response=mock_response, + litellm_logging_obj=mock_logging_obj, + provider_config=MagicMock(), + ) chunks = [] - async for chunk in _async_streaming( - response=response_coro(), - litellm_logging_obj=_make_mock_logging_obj(), - provider_config=MagicMock(), - ): + async for chunk in async_stream: chunks.append(chunk) + await asyncio.sleep(0) + assert len(chunks) == 1 assert b"response.created" in chunks[0] + mock_logging_obj.async_flush_passthrough_collected_chunks.assert_awaited_once() + + +@pytest.mark.asyncio +async def test_async_passthrough_wrapper_closes_response_on_iteration_error(): + """Wrapper should close the upstream response if iteration raises.""" + from litellm.passthrough.main import _AsyncPassthroughStreamingResponse + + mock_response = MagicMock(spec=httpx.Response) + mock_response.status_code = 200 + mock_response.headers = httpx.Headers({"content-type": "text/event-stream"}) + mock_response.aclose = AsyncMock() + + async def _failing_aiter_bytes(): + raise RuntimeError("stream failed") + yield b"" + + mock_response.aiter_bytes = _failing_aiter_bytes + + async_stream = _AsyncPassthroughStreamingResponse( + response=mock_response, + litellm_logging_obj=_make_mock_logging_obj(), + provider_config=MagicMock(), + ) + + with pytest.raises(RuntimeError, match="stream failed"): + async for chunk in async_stream: + _ = chunk + + mock_response.aclose.assert_awaited_once() diff --git a/tests/test_litellm/passthrough/test_passthrough_main.py b/tests/test_litellm/passthrough/test_passthrough_main.py index 489357149c5..bdfb3a348b3 100644 --- a/tests/test_litellm/passthrough/test_passthrough_main.py +++ b/tests/test_litellm/passthrough/test_passthrough_main.py @@ -40,7 +40,7 @@ def test_llm_passthrough_route(): "model": "my-custom-model", "messages": [{"role": "user", "content": "Hello, world!"}], }, - client=client, + http_client=client, ) mock_post.call_args.kwargs[ @@ -90,7 +90,7 @@ def test_bedrock_application_inference_profile_url_encoding(): endpoint="model/arn:aws:bedrock:us-east-1:123456789123:application-inference-profile/r742sbn2zckd/converse", method="POST", custom_llm_provider="bedrock", - client=client, + http_client=client, litellm_logging_obj=mock_logging_obj, ) @@ -144,7 +144,7 @@ def test_bedrock_non_application_inference_profile_no_encoding(): endpoint="model/anthropic.claude-3-sonnet-20240229-v1:0/converse", method="POST", custom_llm_provider="bedrock", - client=client, + http_client=client, litellm_logging_obj=mock_logging_obj, ) @@ -486,7 +486,7 @@ def test_azure_with_custom_api_base_and_key(): "model": "gpt-4.1", "messages": [{"role": "user", "content": "Hello!"}], }, - client=client, + http_client=client, litellm_logging_obj=mock_logging_obj, ) @@ -558,7 +558,7 @@ def test_content_param_forwarded_to_build_request(): content=raw_content, data=None, json=None, - client=client, + http_client=client, litellm_logging_obj=mock_logging_obj, ) @@ -611,13 +611,14 @@ async def test_allm_passthrough_route_429_streaming_raises(): Regression test: Azure 429 during streaming must raise HTTPStatusError, not be silently forwarded as raw bytes under HTTP 200. - Before the fix, _async_streaming() would yield the 429 error JSON as - chunks and allm_passthrough_route returned an async generator. The - caller (azure_proxy_route) wrapped it in StreamingResponse(status_code=200), + Before the fix, the async passthrough streaming path would yield the 429 + error JSON as chunks and allm_passthrough_route returned a streaming + iterator. The caller (azure_proxy_route) wrapped it in + StreamingResponse(status_code=200), so the client saw HTTP 200 + unparseable SSE body → silent task_complete(null). - After the fix, raise_for_status() fires inside _async_streaming() before - any chunks are yielded, so the exception propagates all the way up. + After the fix, raise_for_status() fires before the streaming wrapper is + returned, so the exception propagates all the way up. """ mock_provider_config = MagicMock() mock_provider_config.get_complete_url.return_value = ( @@ -640,6 +641,7 @@ async def test_allm_passthrough_route_429_streaming_raises(): mock_logging_obj = MagicMock() mock_logging_obj.update_environment_variables = MagicMock() mock_logging_obj.async_flush_passthrough_collected_chunks = AsyncMock() + mock_logging_obj.async_failure_handler = AsyncMock() with patch( "litellm.utils.ProviderConfigManager.get_provider_passthrough_config", @@ -660,23 +662,17 @@ async def test_allm_passthrough_route_429_streaming_raises(): ), patch.object( async_client.client, "build_request", mock_build_request ): - result = await allm_passthrough_route( - model="azure/gpt-4", - endpoint="openai/deployments/gpt-4/responses", - method="POST", - custom_llm_provider="azure", - api_base="https://my-azure.openai.azure.com", - api_key="fake-azure-key", - json={"model": "gpt-4", "input": "hello", "stream": True}, - client=async_client, - litellm_logging_obj=mock_logging_obj, - ) - - # result is an async generator — consuming it must raise, not silently yield error bytes - chunks = [] with pytest.raises(httpx.HTTPStatusError) as exc_info: - async for chunk in result: # type: ignore[union-attr] - chunks.append(chunk) + await allm_passthrough_route( + model="azure/gpt-4", + endpoint="openai/deployments/gpt-4/responses", + method="POST", + custom_llm_provider="azure", + api_base="https://my-azure.openai.azure.com", + api_key="fake-azure-key", + json={"model": "gpt-4", "input": "hello", "stream": True}, + http_client=async_client, + litellm_logging_obj=mock_logging_obj, + ) assert exc_info.value.response.status_code == 429 - assert len(chunks) == 0, "No chunks should be yielded before the 429 raises" From c4e5dbc38bfe5c3c4172b8f95f738196d4b6c12c Mon Sep 17 00:00:00 2001 From: KnyazSh Date: Fri, 17 Apr 2026 08:31:03 +0300 Subject: [PATCH 006/544] fix gigachat passthrough url, authenticator, get_api_base --- litellm/llms/gigachat/authenticator.py | 6 ------ litellm/llms/gigachat/passthrough/transformation.py | 6 +----- litellm/llms/gigachat/utils.py | 1 - 3 files changed, 1 insertion(+), 12 deletions(-) diff --git a/litellm/llms/gigachat/authenticator.py b/litellm/llms/gigachat/authenticator.py index 83824c72fa7..1821a209fed 100644 --- a/litellm/llms/gigachat/authenticator.py +++ b/litellm/llms/gigachat/authenticator.py @@ -130,12 +130,6 @@ async def get_access_token_async( litellm_params: Optional[dict] = None, ) -> str: """Async version of get_access_token.""" - credentials = credentials or _get_credentials() - if not credentials: - raise GigaChatAuthError( - status_code=401, - message="GigaChat credentials not provided. Set GIGACHAT_CREDENTIALS or GIGACHAT_API_KEY environment variable.", - ) if not litellm_params: litellm_params = {} diff --git a/litellm/llms/gigachat/passthrough/transformation.py b/litellm/llms/gigachat/passthrough/transformation.py index 42bad43c79f..baf287b161f 100644 --- a/litellm/llms/gigachat/passthrough/transformation.py +++ b/litellm/llms/gigachat/passthrough/transformation.py @@ -36,12 +36,8 @@ class GigaChatPassthroughConfig(BasePassthroughConfig): 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}/{endpoint.lstrip('/')}" - complete_url = f"{base_target_url}/chat/completions" return ( httpx.URL(complete_url), base_target_url, diff --git a/litellm/llms/gigachat/utils.py b/litellm/llms/gigachat/utils.py index e79083f16fe..b66d25cbc0b 100644 --- a/litellm/llms/gigachat/utils.py +++ b/litellm/llms/gigachat/utils.py @@ -30,6 +30,5 @@ def convert_usage(usage_data: dict[str, int]) -> Usage: ) -@staticmethod def get_api_base(api_base: Optional[str] = None) -> Optional[str]: return api_base or get_secret_str("GIGACHAT_API_BASE") or GIGACHAT_BASE_URL From 4c27c6772e19380c7a4738e04ed7f7c67ab32747 Mon Sep 17 00:00:00 2001 From: KnyazSh Date: Fri, 17 Apr 2026 09:19:27 +0300 Subject: [PATCH 007/544] fix SSRF via user-controlled OAuth URL --- .../pass_through_endpoints/llm_passthrough_endpoints.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py index 0a8f651522f..aa4951bc258 100644 --- a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py @@ -2566,6 +2566,11 @@ async def handle_gigachat_passthrough_router_model( data["json"] = request_body data["custom_llm_provider"] = "gigachat" + # Remove sensitive keys from data + keys = ["gigachat_auth_url", "gigachat_access_token", "gigachat_scope"] + for key in keys: + data.pop(key, None) + client = get_async_httpx_client( # type: ignore llm_provider=LlmProviders.GIGACHAT, params={ From eff086d8e00b503c0dfc9ea0503f7df1a1cf9137 Mon Sep 17 00:00:00 2001 From: KnyazSh Date: Fri, 17 Apr 2026 10:05:09 +0300 Subject: [PATCH 008/544] improve gigachat proxy route error handling --- litellm/llms/gigachat/authenticator.py | 13 ++++++----- .../llm_passthrough_endpoints.py | 22 ++++++++++++------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/litellm/llms/gigachat/authenticator.py b/litellm/llms/gigachat/authenticator.py index 1821a209fed..ce6d4b3eca9 100644 --- a/litellm/llms/gigachat/authenticator.py +++ b/litellm/llms/gigachat/authenticator.py @@ -163,12 +163,13 @@ async def get_access_token_async( # Request new token token, expires_at = await _request_token_async(credentials, scope, auth_url) - # Cache token - ttl_seconds = max( - 0, (expires_at - TOKEN_EXPIRY_BUFFER_MS - time.time() * 1000) / 1000 - ) - if ttl_seconds > 0: - _token_cache.set_cache(cache_key, (token, expires_at), ttl=ttl_seconds) + if expires_at: + # Cache token + ttl_seconds = max( + 0, (expires_at - TOKEN_EXPIRY_BUFFER_MS - time.time() * 1000) / 1000 + ) + if ttl_seconds > 0: + _token_cache.set_cache(cache_key, (token, expires_at), ttl=ttl_seconds) return token diff --git a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py index aa4951bc258..ed9a4ac3028 100644 --- a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py @@ -2403,23 +2403,28 @@ async def gigachat_proxy_route( ## check for streaming request_body = await get_request_body(request) - is_router_model = is_passthrough_request_using_router_model( - request_body, llm_router - ) + is_router_model = False model = request_body.get("model") - if not model: - msg = "Model is required" - raise ValueError(msg) + if model: + is_router_model = is_passthrough_request_using_router_model( + request_body, llm_router + ) + elif any(word in endpoint for word in ("completions", "embeddings")): + raise HTTPException( + status_code=400, detail={"error": "Model is required in request body"} + ) + # 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: + if model and is_router_model and llm_router: return await handle_gigachat_passthrough_router_model( model=model, endpoint=endpoint, request=request, request_body=request_body, + fastapi_response=fastapi_response, llm_router=llm_router, user_api_key_dict=user_api_key_dict, proxy_logging_obj=proxy_logging_obj, @@ -2490,6 +2495,7 @@ async def handle_gigachat_passthrough_router_model( endpoint: str, request: Request, request_body: dict, + fastapi_response: Response, llm_router: litellm.Router, user_api_key_dict: UserAPIKeyAuth, proxy_logging_obj, @@ -2587,7 +2593,7 @@ async def handle_gigachat_passthrough_router_model( try: result = await base_llm_response_processor.base_passthrough_process_llm_request( request=request, - fastapi_response=FastAPIResponse(), + fastapi_response=fastapi_response, user_api_key_dict=user_api_key_dict, proxy_logging_obj=proxy_logging_obj, llm_router=llm_router, From 120f51a39d5e30e55c6e2df41137cf86a6e85dc6 Mon Sep 17 00:00:00 2001 From: KnyazSh Date: Fri, 17 Apr 2026 12:13:54 +0300 Subject: [PATCH 009/544] add check for logging gigachat response, add sensitive keys for removing from data --- litellm/llms/gigachat/passthrough/transformation.py | 4 ++++ .../proxy/pass_through_endpoints/llm_passthrough_endpoints.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/litellm/llms/gigachat/passthrough/transformation.py b/litellm/llms/gigachat/passthrough/transformation.py index baf287b161f..a9bc59fed0a 100644 --- a/litellm/llms/gigachat/passthrough/transformation.py +++ b/litellm/llms/gigachat/passthrough/transformation.py @@ -80,6 +80,10 @@ class GigaChatPassthroughConfig(BasePassthroughConfig): from litellm.types.utils import LlmProviders, ModelResponse from litellm.utils import ProviderConfigManager + # cost tracking only for completions + if "completions" not in endpoint: + return None + provider_chat_config = ProviderConfigManager.get_provider_chat_config( provider=LlmProviders(custom_llm_provider), model=model, diff --git a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py index ed9a4ac3028..e5eb58941ad 100644 --- a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py @@ -2573,7 +2573,7 @@ async def handle_gigachat_passthrough_router_model( data["custom_llm_provider"] = "gigachat" # Remove sensitive keys from data - keys = ["gigachat_auth_url", "gigachat_access_token", "gigachat_scope"] + keys = ["gigachat_auth_url", "gigachat_access_token", "gigachat_scope", "api_base", "api_key"] for key in keys: data.pop(key, None) From 9ecfac90debe64b9c5571b3f54737f31865be5ea Mon Sep 17 00:00:00 2001 From: KnyazSh Date: Fri, 8 May 2026 14:35:16 +0000 Subject: [PATCH 010/544] add embedding cost tracking to gigachat proxy route --- .../gigachat/passthrough/transformation.py | 70 +++++++++++++------ 1 file changed, 48 insertions(+), 22 deletions(-) diff --git a/litellm/llms/gigachat/passthrough/transformation.py b/litellm/llms/gigachat/passthrough/transformation.py index a9bc59fed0a..0d3e440f253 100644 --- a/litellm/llms/gigachat/passthrough/transformation.py +++ b/litellm/llms/gigachat/passthrough/transformation.py @@ -9,6 +9,8 @@ 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 +from litellm.types.utils import EmbeddingResponse + if TYPE_CHECKING: from httpx import URL, Response @@ -80,32 +82,56 @@ class GigaChatPassthroughConfig(BasePassthroughConfig): from litellm.types.utils import LlmProviders, ModelResponse from litellm.utils import ProviderConfigManager - # cost tracking only for completions - if "completions" not in endpoint: - return None + # cost tracking only for completions and embeddings + if "completions" in endpoint: - provider_chat_config = ProviderConfigManager.get_provider_chat_config( - provider=LlmProviders(custom_llm_provider), - model=model, - ) + 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}") + 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, - ) + 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 + return litellm_model_response + + if "embeddings" in endpoint: + + provider_embedding_config = ProviderConfigManager.get_provider_embedding_config( + provider=LlmProviders(custom_llm_provider), + model=model, + ) + + if provider_embedding_config is None: + raise ValueError(f"No provider config found for model: {model}") + + litellm_embedding_response: EmbeddingResponse = provider_embedding_config.transform_embedding_response( + model=model, + raw_response=httpx_response, + model_response=EmbeddingResponse(), + logging_obj=logging_obj, + optional_params={}, + api_key="", + request_data=request_data, + litellm_params={}, + ) + + return litellm_embedding_response + + return None def handle_logging_collected_chunks( self, From b334ebf35ea34cae9d618f67801b435e8ce7e985 Mon Sep 17 00:00:00 2001 From: KnyazSh Date: Tue, 19 May 2026 05:17:33 +0000 Subject: [PATCH 011/544] remove potential recursive function --- litellm/llms/gigachat/passthrough/transformation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/litellm/llms/gigachat/passthrough/transformation.py b/litellm/llms/gigachat/passthrough/transformation.py index 0d3e440f253..43669355f2b 100644 --- a/litellm/llms/gigachat/passthrough/transformation.py +++ b/litellm/llms/gigachat/passthrough/transformation.py @@ -6,7 +6,7 @@ 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.llms.gigachat.utils import GIGACHAT_BASE_URL from litellm.secret_managers.main import get_secret_str from litellm.types.llms.openai import AllMessageValues from litellm.types.utils import EmbeddingResponse @@ -204,7 +204,7 @@ class GigaChatPassthroughConfig(BasePassthroughConfig): @staticmethod def get_api_base(api_base: Optional[str] = None) -> Optional[str]: - return get_api_base(api_base) + return api_base or get_secret_str("GIGACHAT_API_BASE") or GIGACHAT_BASE_URL @staticmethod def get_api_key( From 25edb2f228864c434b07c59cfa68a3d6d0f9b819 Mon Sep 17 00:00:00 2001 From: KnyazSh Date: Tue, 19 May 2026 06:25:17 +0000 Subject: [PATCH 012/544] add gigachat to allowlist --- gateway/routes/allowlist.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gateway/routes/allowlist.py b/gateway/routes/allowlist.py index cbbf55c9873..9f397c70ff7 100644 --- a/gateway/routes/allowlist.py +++ b/gateway/routes/allowlist.py @@ -84,6 +84,7 @@ GATEWAY_PATH_PREFIXES: tuple[str, ...] = ( "/bedrock/", "/cohere/", "/gemini/", + "/gigachat/", "/google/", "/vertex_ai/", "/vertex-ai/", From ecd72752d57c1c9046b9f2bd13894c6d4f23006a Mon Sep 17 00:00:00 2001 From: KnyazSh Date: Tue, 19 May 2026 18:22:26 +0000 Subject: [PATCH 013/544] fix format and lint --- litellm/litellm_core_utils/litellm_logging.py | 7 ++- litellm/llms/gigachat/authenticator.py | 4 +- litellm/llms/gigachat/chat/streaming.py | 6 +- .../gigachat/passthrough/transformation.py | 58 ++++++++++--------- .../llm_passthrough_endpoints.py | 13 +++-- 5 files changed, 50 insertions(+), 38 deletions(-) diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index 084b53cd904..70d5b443974 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -5632,9 +5632,10 @@ def emit_standard_logging_payload(payload: StandardLoggingPayload): try: print(json.dumps(payload, indent=4, default=str)) # noqa except Exception as e: - print( - "Error serializing standard logging payload for debug output:", - str(e), + verbose_logger.exception( + "Error serializing standard logging payload for debug output: {}".format( + str(e) + ) ) diff --git a/litellm/llms/gigachat/authenticator.py b/litellm/llms/gigachat/authenticator.py index ce6d4b3eca9..a20da943ee1 100644 --- a/litellm/llms/gigachat/authenticator.py +++ b/litellm/llms/gigachat/authenticator.py @@ -147,9 +147,7 @@ async def get_access_token_async( ) scope = scope or litellm_params.get("gigachat_scope") or _get_scope() - auth_url = ( - auth_url or litellm_params.get("gigachat_auth_url") or _get_auth_url() - ) + auth_url = auth_url or litellm_params.get("gigachat_auth_url") or _get_auth_url() # Check cache cache_key = f"gigachat_token:{credentials[:16]}" diff --git a/litellm/llms/gigachat/chat/streaming.py b/litellm/llms/gigachat/chat/streaming.py index 2fd5fc22b01..9ef29b2c6cb 100644 --- a/litellm/llms/gigachat/chat/streaming.py +++ b/litellm/llms/gigachat/chat/streaming.py @@ -76,7 +76,11 @@ class GigaChatModelResponseIterator: usage_data = chunk.get("usage", {}) if usage_data: usage = convert_usage(usage_data) - usage_block = ChatCompletionUsageBlock(**usage.dict()) + usage_block = ChatCompletionUsageBlock( + prompt_tokens=usage.prompt_tokens, + completion_tokens=usage.completion_tokens, + total_tokens=usage.total_tokens, + ) if finish_reason is not None: is_finished = True diff --git a/litellm/llms/gigachat/passthrough/transformation.py b/litellm/llms/gigachat/passthrough/transformation.py index 43669355f2b..ff2c4ad4986 100644 --- a/litellm/llms/gigachat/passthrough/transformation.py +++ b/litellm/llms/gigachat/passthrough/transformation.py @@ -93,44 +93,50 @@ class GigaChatPassthroughConfig(BasePassthroughConfig): 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, + 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 - + if "embeddings" in endpoint: - - provider_embedding_config = ProviderConfigManager.get_provider_embedding_config( - provider=LlmProviders(custom_llm_provider), - model=model, + + provider_embedding_config = ( + ProviderConfigManager.get_provider_embedding_config( + provider=LlmProviders(custom_llm_provider), + model=model, + ) ) if provider_embedding_config is None: raise ValueError(f"No provider config found for model: {model}") - litellm_embedding_response: EmbeddingResponse = provider_embedding_config.transform_embedding_response( - model=model, - raw_response=httpx_response, - model_response=EmbeddingResponse(), - logging_obj=logging_obj, - optional_params={}, - api_key="", - request_data=request_data, - litellm_params={}, + litellm_embedding_response: EmbeddingResponse = ( + provider_embedding_config.transform_embedding_response( + model=model, + raw_response=httpx_response, + model_response=EmbeddingResponse(), + logging_obj=logging_obj, + optional_params={}, + api_key="", + request_data=request_data, + litellm_params={}, + ) ) return litellm_embedding_response - + return None def handle_logging_collected_chunks( diff --git a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py index 15852890eec..46d8b32369f 100644 --- a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py @@ -2461,7 +2461,7 @@ async def gigachat_proxy_route( is_router_model = False model = request_body.get("model") - if model: + if model: is_router_model = is_passthrough_request_using_router_model( request_body, llm_router ) @@ -2470,7 +2470,6 @@ async def gigachat_proxy_route( status_code=400, detail={"error": "Model is required in request body"} ) - # If router model, use dedicated router passthrough handler # This uses the same common processing path as non-router models if model and is_router_model and llm_router: @@ -2582,8 +2581,6 @@ async def handle_gigachat_passthrough_router_model( 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 @@ -2628,7 +2625,13 @@ async def handle_gigachat_passthrough_router_model( data["custom_llm_provider"] = "gigachat" # Remove sensitive keys from data - keys = ["gigachat_auth_url", "gigachat_access_token", "gigachat_scope", "api_base", "api_key"] + keys = [ + "gigachat_auth_url", + "gigachat_access_token", + "gigachat_scope", + "api_base", + "api_key", + ] for key in keys: data.pop(key, None) From e1e56961a31a1dde3f61b2eeffaa0cf776c3eae4 Mon Sep 17 00:00:00 2001 From: KnyazSh Date: Sun, 7 Jun 2026 12:19:11 +0000 Subject: [PATCH 014/544] revert rename http client in llm_passthrough_route --- litellm/passthrough/main.py | 18 +++++++++--------- .../test_hosted_vllm_passthrough.py | 2 +- .../passthrough/test_passthrough_main.py | 12 ++++++------ 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/litellm/passthrough/main.py b/litellm/passthrough/main.py index c06297ec15b..8b20406397b 100644 --- a/litellm/passthrough/main.py +++ b/litellm/passthrough/main.py @@ -108,7 +108,7 @@ async def allm_passthrough_route( json: Optional[Any] = None, params: Optional[QueryParamTypes] = None, cookies: Optional[CookieTypes] = None, - http_client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None, + client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None, **kwargs, ) -> Union[httpx.Response, AsyncIterator[bytes]]: """ @@ -154,7 +154,7 @@ async def allm_passthrough_route( json=json, params=params, cookies=cookies, - http_client=http_client, + client=client, **kwargs, ) @@ -234,7 +234,7 @@ def llm_passthrough_route( json: Optional[Any] = None, params: Optional[QueryParamTypes] = None, cookies: Optional[CookieTypes] = None, - http_client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None, + client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None, **kwargs, ) -> Union[ httpx.Response, @@ -256,11 +256,11 @@ def llm_passthrough_route( _is_async = allm_passthrough_route - if http_client is None: + if client is None: if _is_async: - http_client = litellm.module_level_aclient + client = litellm.module_level_aclient else: - http_client = litellm.module_level_client + client = litellm.module_level_client litellm_logging_obj = cast("LiteLLMLoggingObj", kwargs.get("litellm_logging_obj")) @@ -342,7 +342,7 @@ def llm_passthrough_route( if json and isinstance(json, dict) and "model" in json: json["model"] = model - request = http_client.client.build_request( + request = client.client.build_request( method=method, url=updated_url, content=signed_json_body if signed_json_body is not None else content, @@ -379,7 +379,7 @@ def llm_passthrough_route( if _is_async: # Return the coroutine to be awaited by the caller return _async_passthrough_request( - client=http_client, + client=client, request=request, is_streaming_request=is_streaming_request, litellm_logging_obj=litellm_logging_obj, @@ -387,7 +387,7 @@ def llm_passthrough_route( ) else: # Sync path - client.client.send returns Response directly - response: httpx.Response = http_client.client.send(request=request, stream=is_streaming_request) # type: ignore + response: httpx.Response = client.client.send(request=request, stream=is_streaming_request) # type: ignore response.raise_for_status() if ( diff --git a/tests/pass_through_tests/test_hosted_vllm_passthrough.py b/tests/pass_through_tests/test_hosted_vllm_passthrough.py index e8d4c0d2ef0..272b4e1bb00 100644 --- a/tests/pass_through_tests/test_hosted_vllm_passthrough.py +++ b/tests/pass_through_tests/test_hosted_vllm_passthrough.py @@ -63,7 +63,7 @@ async def test_allm_passthrough_route_with_hosted_vllm_model_does_not_raise(): "model": "anything", # will be replaced internally with normalized model "messages": [{"role": "user", "content": "Hello"}], }, - http_client=client, + client=client, ) # Then it should not raise and return a successful httpx.Response diff --git a/tests/test_litellm/passthrough/test_passthrough_main.py b/tests/test_litellm/passthrough/test_passthrough_main.py index adcca96271e..3600aeed59e 100644 --- a/tests/test_litellm/passthrough/test_passthrough_main.py +++ b/tests/test_litellm/passthrough/test_passthrough_main.py @@ -40,7 +40,7 @@ def test_llm_passthrough_route(): "model": "my-custom-model", "messages": [{"role": "user", "content": "Hello, world!"}], }, - http_client=client, + client=client, ) mock_post.call_args.kwargs[ @@ -94,7 +94,7 @@ def test_bedrock_application_inference_profile_url_encoding(): endpoint="model/arn:aws:bedrock:us-east-1:123456789123:application-inference-profile/r742sbn2zckd/converse", method="POST", custom_llm_provider="bedrock", - http_client=client, + client=client, litellm_logging_obj=mock_logging_obj, ) @@ -152,7 +152,7 @@ def test_bedrock_non_application_inference_profile_no_encoding(): endpoint="model/anthropic.claude-3-sonnet-20240229-v1:0/converse", method="POST", custom_llm_provider="bedrock", - http_client=client, + client=client, litellm_logging_obj=mock_logging_obj, ) @@ -511,7 +511,7 @@ def test_azure_with_custom_api_base_and_key(): "model": "gpt-4.1", "messages": [{"role": "user", "content": "Hello!"}], }, - http_client=client, + client=client, litellm_logging_obj=mock_logging_obj, ) @@ -592,7 +592,7 @@ def test_content_param_forwarded_to_build_request(): content=raw_content, data=None, json=None, - http_client=client, + client=client, litellm_logging_obj=mock_logging_obj, ) @@ -717,7 +717,7 @@ async def test_allm_passthrough_route_429_streaming_raises(): api_base="https://my-azure.openai.azure.com", api_key="fake-azure-key", json={"model": "gpt-4", "input": "hello", "stream": True}, - http_client=async_client, + client=async_client, litellm_logging_obj=mock_logging_obj, ) From dfa07b7c277fb5aab03aa4c8571cef4d546dd8a8 Mon Sep 17 00:00:00 2001 From: KnyazSh Date: Fri, 12 Jun 2026 13:58:02 +0000 Subject: [PATCH 015/544] add http client propagation for allm_passthrough_route --- litellm/llms/gigachat/passthrough/transformation.py | 1 - .../proxy/pass_through_endpoints/llm_passthrough_endpoints.py | 4 ++-- litellm/router.py | 2 ++ 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/litellm/llms/gigachat/passthrough/transformation.py b/litellm/llms/gigachat/passthrough/transformation.py index ff2c4ad4986..474622a01a8 100644 --- a/litellm/llms/gigachat/passthrough/transformation.py +++ b/litellm/llms/gigachat/passthrough/transformation.py @@ -11,7 +11,6 @@ from litellm.secret_managers.main import get_secret_str from litellm.types.llms.openai import AllMessageValues from litellm.types.utils import EmbeddingResponse - if TYPE_CHECKING: from httpx import URL, Response diff --git a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py index 7a0403194ab..04de332b295 100644 --- a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py @@ -2521,7 +2521,7 @@ async def gigachat_proxy_route( "ssl_verify": False, }, ) - data["http_client"] = client + data["client"] = client base_llm_response_processor = ProxyBaseLLMRequestProcessing(data=data) @@ -2652,7 +2652,7 @@ async def handle_gigachat_passthrough_router_model( }, ) - data["http_client"] = client + data["client"] = client base_llm_response_processor = ProxyBaseLLMRequestProcessing(data=data) # Use the common passthrough processing to handle metadata and hooks diff --git a/litellm/router.py b/litellm/router.py index a92590d3dba..c9637d5e9f2 100644 --- a/litellm/router.py +++ b/litellm/router.py @@ -6014,6 +6014,8 @@ class Router: **kwargs, ) elif call_type == "allm_passthrough_route": + if client: + kwargs["client"] = client return await self._ageneric_api_call_with_fallbacks( original_function=original_function, passthrough_on_no_deployment=True, From 81dbb3803c42c4f6c2f2eb1e22bb440758f5aaae Mon Sep 17 00:00:00 2001 From: KnyazSh Date: Fri, 12 Jun 2026 14:43:40 +0000 Subject: [PATCH 016/544] fix gigachat stream usage --- litellm/llms/gigachat/chat/streaming.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/litellm/llms/gigachat/chat/streaming.py b/litellm/llms/gigachat/chat/streaming.py index 9ef29b2c6cb..b42c7baa403 100644 --- a/litellm/llms/gigachat/chat/streaming.py +++ b/litellm/llms/gigachat/chat/streaming.py @@ -80,6 +80,16 @@ class GigaChatModelResponseIterator: prompt_tokens=usage.prompt_tokens, completion_tokens=usage.completion_tokens, total_tokens=usage.total_tokens, + prompt_tokens_details=( + usage.prompt_tokens_details.model_dump() + if usage.prompt_tokens_details + else None + ), + completion_tokens_details=( + usage.completion_tokens_details.model_dump() + if usage.completion_tokens_details + else None + ), ) if finish_reason is not None: From e3058e34c4937acf93cd25e0c264281dfb9b5af0 Mon Sep 17 00:00:00 2001 From: KnyazSh Date: Fri, 12 Jun 2026 15:22:01 +0000 Subject: [PATCH 017/544] fix return headers for stream response --- litellm/passthrough/main.py | 302 +++++++++++------- .../test_async_streaming_error_propagation.py | 81 +++-- ...test_streaming_interrupt_spend_tracking.py | 79 +++-- 3 files changed, 274 insertions(+), 188 deletions(-) diff --git a/litellm/passthrough/main.py b/litellm/passthrough/main.py index 8b20406397b..2c654a3f23b 100644 --- a/litellm/passthrough/main.py +++ b/litellm/passthrough/main.py @@ -4,11 +4,11 @@ This module is used to pass through requests to the LLM APIs. import asyncio import contextvars -from collections.abc import AsyncIterator from functools import partial from typing import ( TYPE_CHECKING, Any, + AsyncGenerator, Coroutine, Generator, List, @@ -36,59 +36,203 @@ if TYPE_CHECKING: from litellm.llms.base_llm.passthrough.transformation import BasePassthroughConfig -class _AsyncPassthroughStreamingResponse(AsyncIterator[bytes]): - """ - Async iterator wrapper that preserves upstream response metadata for streaming. - """ +class AsyncPassthroughStreamingResponse(AsyncGenerator[Any, Any]): + def __init__( + self, + response: Coroutine[Any, Any, httpx.Response], + litellm_logging_obj: "LiteLLMLoggingObj", + provider_config: "BasePassthroughConfig", + ) -> None: + self._initialized = False + self._status_code: int = 0 + self._headers = httpx.Headers() + self._response_coro = response + self._response: httpx.Response + self._iterator: AsyncGenerator[bytes, Any] + self._litellm_logging_obj = litellm_logging_obj + self._provider_config = provider_config + self._raw_bytes: List[bytes] = [] + self._flush_scheduled = False + self._background_tasks: set[asyncio.Task] = set() + @property + def status_code(self) -> int: + if not self._initialized: + raise RuntimeError( + "AsyncPassthroughStreamingResponse must be awaited " + "before accessing status_code" + ) + return self._status_code + + @status_code.setter + def status_code(self, value: int) -> None: + self._status_code = value + + @property + def headers(self) -> httpx.Headers: + if not self._initialized: + raise RuntimeError( + "AsyncPassthroughStreamingResponse must be awaited " + "before accessing headers" + ) + return self._headers + + @headers.setter + def headers(self, value: httpx.Headers) -> None: + self._headers = value + + def __await__(self): + async def _init(): + if not self._initialized: + self._response = await self._response_coro + self.headers = self._response.headers + self.status_code = self._response.status_code + self._initialized = True + try: + self._response.raise_for_status() + self._iterator = cast( + AsyncGenerator[bytes, Any], self._response.aiter_bytes() + ) + except Exception: + try: + await self._response.aclose() + except Exception: + pass + raise + return self + + return _init().__await__() + + def _start_flush(self) -> None: + if self._flush_scheduled or not self._raw_bytes: + return + self._flush_scheduled = True + + try: + task = asyncio.create_task( + self._litellm_logging_obj.async_flush_passthrough_collected_chunks( + raw_bytes=self._raw_bytes, + provider_config=self._provider_config, + ) + ) + + # Compliant: Save a strong reference to prevent GC + self._background_tasks.add(task) + + # Remove the task from the set when it finishes to avoid memory leaks + task.add_done_callback(self._background_tasks.discard) + except Exception as e: + verbose_logger.exception( + "Failed to schedule passthrough spend-tracking flush; " + "%d buffered chunks dropped: %s", + len(self._raw_bytes), + e, + ) + + def __aiter__(self) -> "AsyncPassthroughStreamingResponse": + return self + + async def __anext__(self) -> bytes: + if not self._initialized: + await self + try: + chunk = await self._iterator.__anext__() + self._raw_bytes.append(chunk) + return chunk + except Exception: + self._start_flush() + try: + await self._response.aclose() + except Exception: + pass + raise + + async def asend(self, value: Any) -> bytes: + if not self._initialized: + await self + return await self._iterator.asend(value) + + async def athrow(self, typ: Any, val: Any = None, tb: Any = None) -> bytes: + if not self._initialized: + await self + return await self._iterator.athrow(typ, val, tb) + + async def aclose(self) -> None: + self._start_flush() + try: + if self._initialized: + await self._response.aclose() + except Exception: + pass + + +class PassthroughStreamingResponse(Generator[Any, Any, Any]): def __init__( self, response: httpx.Response, litellm_logging_obj: "LiteLLMLoggingObj", provider_config: "BasePassthroughConfig", ) -> None: - self.response = response + self._response = response self.headers = response.headers self.status_code = response.status_code self._litellm_logging_obj = litellm_logging_obj self._provider_config = provider_config - self._iterator = response.aiter_bytes() + self._iterator: Generator[bytes, Any, Any] = cast( + Generator[bytes, Any, Any], response.iter_bytes() + ) self._raw_bytes: List[bytes] = [] - self._flush_started = False + self._flush_scheduled = False - def __aiter__(self) -> "_AsyncPassthroughStreamingResponse": + def _start_flush(self) -> None: + if self._flush_scheduled or not self._raw_bytes: + return + self._flush_scheduled = True + + from litellm.utils import executor + + try: + executor.submit( + self._litellm_logging_obj.flush_passthrough_collected_chunks, + raw_bytes=self._raw_bytes, + provider_config=self._provider_config, + ) + except Exception as e: + verbose_logger.exception( + "Failed to schedule passthrough spend-tracking flush; " + "%d buffered chunks dropped: %s", + len(self._raw_bytes), + e, + ) + + def __iter__(self) -> "PassthroughStreamingResponse": return self - async def __anext__(self) -> bytes: + def __next__(self) -> bytes: try: - chunk = await self._iterator.__anext__() + chunk = next(self._iterator) self._raw_bytes.append(chunk) return chunk - except StopAsyncIteration: - self._start_flush() - raise except Exception: + self._start_flush() try: - await self.response.aclose() + self._response.close() except Exception: pass raise - def _start_flush(self) -> None: - if self._flush_started: - return + def send(self, value: Any) -> bytes: + return self._iterator.send(value) - self._flush_started = True - asyncio.create_task( - self._litellm_logging_obj.async_flush_passthrough_collected_chunks( - raw_bytes=self._raw_bytes, - provider_config=self._provider_config, - ) - ) + def throw(self, typ: Any, val: Any = None, tb: Any = None) -> bytes: + return self._iterator.throw(typ, val, tb) - async def aclose(self) -> None: + def close(self) -> None: self._start_flush() - await self.response.aclose() + try: + self._response.close() + except Exception: + pass @client @@ -110,7 +254,7 @@ async def allm_passthrough_route( cookies: Optional[CookieTypes] = None, client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None, **kwargs, -) -> Union[httpx.Response, AsyncIterator[bytes]]: +) -> Union[httpx.Response, AsyncGenerator[Any, Any]]: """ Async: Reranks a list of documents based on their relevance to the query """ @@ -239,9 +383,9 @@ def llm_passthrough_route( ) -> Union[ httpx.Response, Coroutine[Any, Any, httpx.Response], - Coroutine[Any, Any, Union[httpx.Response, AsyncIterator[bytes]]], + Coroutine[Any, Any, Union[httpx.Response, AsyncGenerator[Any, Any]]], Generator[Any, Any, Any], - AsyncIterator[bytes], + AsyncGenerator[Any, Any], ]: """ Pass through requests to the LLM APIs. @@ -390,12 +534,11 @@ def llm_passthrough_route( response: httpx.Response = client.client.send(request=request, stream=is_streaming_request) # type: ignore response.raise_for_status() - if ( - hasattr(response, "iter_bytes") and is_streaming_request - ): # yield the chunk, so we can store it in the logging object - return _sync_streaming(response, litellm_logging_obj, provider_config) + if hasattr(response, "iter_bytes") and is_streaming_request: + return PassthroughStreamingResponse( + response, litellm_logging_obj, provider_config + ) else: - # For non-streaming responses, yield the entire response return response except Exception as e: if provider_config is None: @@ -412,7 +555,7 @@ async def _async_passthrough_request( is_streaming_request: bool, litellm_logging_obj: "LiteLLMLoggingObj", provider_config: "BasePassthroughConfig", -) -> Union[httpx.Response, AsyncIterator[bytes]]: +) -> Union[httpx.Response, AsyncGenerator[Any, Any]]: """ Handle async passthrough requests. Uses async client to send request and properly handles streaming. @@ -423,10 +566,8 @@ async def _async_passthrough_request( # Check if it's a coroutine and await it if asyncio.iscoroutine(response_result): if is_streaming_request: - iter_response = await response_result - iter_response.raise_for_status() - return _AsyncPassthroughStreamingResponse( - response=iter_response, + return await AsyncPassthroughStreamingResponse( + response=response_result, litellm_logging_obj=litellm_logging_obj, provider_config=provider_config, ) @@ -438,84 +579,3 @@ async def _async_passthrough_request( else: # Fallback for sync-like behavior (shouldn't happen in async path) raise Exception("Expected coroutine from async client") - - -def _sync_streaming( - response: httpx.Response, - litellm_logging_obj: "LiteLLMLoggingObj", - provider_config: "BasePassthroughConfig", -): - from litellm.utils import executor - - raw_bytes: List[bytes] = [] - flush_scheduled = False - try: - for chunk in response.iter_bytes(): # type: ignore - raw_bytes.append(chunk) - yield chunk - finally: - if not flush_scheduled and raw_bytes: - flush_scheduled = True - try: - executor.submit( - litellm_logging_obj.flush_passthrough_collected_chunks, - raw_bytes=raw_bytes, - provider_config=provider_config, - ) - except Exception as e: - verbose_logger.exception( - "Failed to schedule passthrough spend-tracking flush " - "in _sync_streaming; %d buffered chunks dropped: %s", - len(raw_bytes), - e, - ) - - -async def _async_streaming( - response: Coroutine[Any, Any, httpx.Response], - litellm_logging_obj: "LiteLLMLoggingObj", - provider_config: "BasePassthroughConfig", -): - iter_response = await response - - try: - iter_response.raise_for_status() - except Exception: - try: - await iter_response.aclose() - except Exception: - pass - raise - - raw_bytes: List[bytes] = [] - flush_scheduled = False - try: - async for chunk in iter_response.aiter_bytes(): # type: ignore - raw_bytes.append(chunk) - yield chunk - except Exception: - try: - await iter_response.aclose() - except Exception: - pass - raise - finally: - # GeneratorExit (raised on client disconnect) is not caught by - # `except Exception`; the finally block ensures partial usage - # still gets flushed for spend tracking. See LIT-2642. - if not flush_scheduled and raw_bytes: - flush_scheduled = True - try: - asyncio.create_task( - litellm_logging_obj.async_flush_passthrough_collected_chunks( - raw_bytes=raw_bytes, - provider_config=provider_config, - ) - ) - except Exception as e: - verbose_logger.exception( - "Failed to schedule passthrough spend-tracking flush " - "in _async_streaming; %d buffered chunks dropped: %s", - len(raw_bytes), - e, - ) diff --git a/tests/test_litellm/passthrough/test_async_streaming_error_propagation.py b/tests/test_litellm/passthrough/test_async_streaming_error_propagation.py index 631037b3357..f7052fee3b9 100644 --- a/tests/test_litellm/passthrough/test_async_streaming_error_propagation.py +++ b/tests/test_litellm/passthrough/test_async_streaming_error_propagation.py @@ -52,43 +52,69 @@ def _make_mock_logging_obj(): @pytest.mark.asyncio -async def test_async_passthrough_wrapper_429_raises_before_iteration(): - """429 from upstream should be raised before the wrapper is constructed.""" +async def test_async_streaming_429_raises(): + """429 from upstream should raise HTTPStatusError, not yield error bytes.""" + from litellm.passthrough.main import AsyncPassthroughStreamingResponse + error_body = json.dumps( {"error": {"code": "429", "message": "Rate limit exceeded."}} ).encode() mock_response = _make_mock_response(429, error_body) - + + async def response_coro(): + return mock_response + + chunks = [] with pytest.raises(httpx.HTTPStatusError) as exc_info: - mock_response.raise_for_status() - + async for chunk in AsyncPassthroughStreamingResponse( + response=response_coro(), + litellm_logging_obj=_make_mock_logging_obj(), + provider_config=MagicMock(), + ): + chunks.append(chunk) + assert exc_info.value.response.status_code == 429 + assert len(chunks) == 0 @pytest.mark.asyncio -async def test_async_passthrough_wrapper_500_raises_before_iteration(): - """500 from upstream should be raised before the wrapper is constructed.""" +async def test_async_streaming_500_raises(): + """500 from upstream should also raise, not yield error bytes.""" + from litellm.passthrough.main import AsyncPassthroughStreamingResponse + error_body = json.dumps( {"error": {"code": "500", "message": "Internal server error"}} ).encode() mock_response = _make_mock_response(500, error_body) - + + async def response_coro(): + return mock_response + with pytest.raises(httpx.HTTPStatusError) as exc_info: - mock_response.raise_for_status() - + async for _ in AsyncPassthroughStreamingResponse( + response=response_coro(), + litellm_logging_obj=_make_mock_logging_obj(), + provider_config=MagicMock(), + ): + pass + assert exc_info.value.response.status_code == 500 @pytest.mark.asyncio async def test_async_passthrough_wrapper_200_yields_chunks(): """Successful 200 streaming responses should continue to work normally.""" - from litellm.passthrough.main import _AsyncPassthroughStreamingResponse + from litellm.passthrough.main import AsyncPassthroughStreamingResponse sse_data = b'data: {"type":"response.created"}\n\ndata: [DONE]\n\n' mock_response = _make_mock_response(200, sse_data) mock_logging_obj = _make_mock_logging_obj() - async_stream = _AsyncPassthroughStreamingResponse( - response=mock_response, + + async def response_coro(): + return mock_response + + async_stream = AsyncPassthroughStreamingResponse( + response=response_coro(), litellm_logging_obj=mock_logging_obj, provider_config=MagicMock(), ) @@ -102,32 +128,3 @@ async def test_async_passthrough_wrapper_200_yields_chunks(): assert len(chunks) == 1 assert b"response.created" in chunks[0] mock_logging_obj.async_flush_passthrough_collected_chunks.assert_awaited_once() - - -@pytest.mark.asyncio -async def test_async_passthrough_wrapper_closes_response_on_iteration_error(): - """Wrapper should close the upstream response if iteration raises.""" - from litellm.passthrough.main import _AsyncPassthroughStreamingResponse - - mock_response = MagicMock(spec=httpx.Response) - mock_response.status_code = 200 - mock_response.headers = httpx.Headers({"content-type": "text/event-stream"}) - mock_response.aclose = AsyncMock() - - async def _failing_aiter_bytes(): - raise RuntimeError("stream failed") - yield b"" - - mock_response.aiter_bytes = _failing_aiter_bytes - - async_stream = _AsyncPassthroughStreamingResponse( - response=mock_response, - litellm_logging_obj=_make_mock_logging_obj(), - provider_config=MagicMock(), - ) - - with pytest.raises(RuntimeError, match="stream failed"): - async for chunk in async_stream: - _ = chunk - - mock_response.aclose.assert_awaited_once() diff --git a/tests/test_litellm/passthrough/test_streaming_interrupt_spend_tracking.py b/tests/test_litellm/passthrough/test_streaming_interrupt_spend_tracking.py index f3fe3ae5c38..27d265e6f5d 100644 --- a/tests/test_litellm/passthrough/test_streaming_interrupt_spend_tracking.py +++ b/tests/test_litellm/passthrough/test_streaming_interrupt_spend_tracking.py @@ -35,11 +35,14 @@ class _ImmediateExecutor: @pytest.mark.asyncio -async def test_async_streaming_flushes_on_normal_completion(): - from litellm.passthrough.main import _async_streaming +async def test_asyncpassthroughstreamingresponse_flushes_on_normal_completion(): + from litellm.passthrough.main import AsyncPassthroughStreamingResponse chunks = [b"chunk-1", b"chunk-2", b"chunk-3"] mock_response = _make_streaming_response(chunks) + mock_response.headers = httpx.Headers( + {"content-type": "application/octet-stream", "x-request-id": "req-123"} + ) async def response_coro(): return mock_response @@ -48,14 +51,19 @@ async def test_async_streaming_flushes_on_normal_completion(): provider_config = MagicMock() received = [] - async for chunk in _async_streaming( + received_response = AsyncPassthroughStreamingResponse( response=response_coro(), litellm_logging_obj=mock_logging_obj, provider_config=provider_config, - ): + ) + + async for chunk in received_response: received.append(chunk) assert received == chunks + + assert received_response.headers["content-type"] == "application/octet-stream" + assert received_response.headers["x-request-id"] == "req-123" await asyncio.sleep(0) @@ -68,8 +76,8 @@ async def test_async_streaming_flushes_on_normal_completion(): @pytest.mark.asyncio -async def test_async_streaming_flushes_on_client_disconnect(): - from litellm.passthrough.main import _async_streaming +async def test_asyncpassthroughstreamingresponse_flushes_on_client_disconnect(): + from litellm.passthrough.main import AsyncPassthroughStreamingResponse chunks = [ b'{"chunk": 1, "outputTokens": 10}', @@ -77,6 +85,9 @@ async def test_async_streaming_flushes_on_client_disconnect(): b'{"chunk": 3, "outputTokens": 8}', ] mock_response = _make_streaming_response(chunks) + mock_response.headers = httpx.Headers( + {"content-type": "application/octet-stream", "x-request-id": "req-123"} + ) async def response_coro(): return mock_response @@ -84,7 +95,7 @@ async def test_async_streaming_flushes_on_client_disconnect(): mock_logging_obj = _make_logging_obj() provider_config = MagicMock() - gen = _async_streaming( + gen = AsyncPassthroughStreamingResponse( response=response_coro(), litellm_logging_obj=mock_logging_obj, provider_config=provider_config, @@ -105,11 +116,14 @@ async def test_async_streaming_flushes_on_client_disconnect(): @pytest.mark.asyncio -async def test_async_streaming_does_not_flush_on_4xx(): - from litellm.passthrough.main import _async_streaming +async def test_asyncpassthroughstreamingresponse_does_not_flush_on_4xx(): + from litellm.passthrough.main import AsyncPassthroughStreamingResponse err_response = MagicMock(spec=httpx.Response) err_response.status_code = 429 + err_response.headers = httpx.Headers( + {"content-type": "application/octet-stream"} + ) def _raise(): raise httpx.HTTPStatusError( @@ -129,7 +143,7 @@ async def test_async_streaming_does_not_flush_on_4xx(): mock_logging_obj = _make_logging_obj() with pytest.raises(httpx.HTTPStatusError): - async for _ in _async_streaming( + async for _ in AsyncPassthroughStreamingResponse( response=response_coro(), litellm_logging_obj=mock_logging_obj, provider_config=MagicMock(), @@ -140,8 +154,8 @@ async def test_async_streaming_does_not_flush_on_4xx(): @pytest.mark.asyncio -async def test_async_streaming_flushes_on_upstream_exception_with_partial_data(): - from litellm.passthrough.main import _async_streaming +async def test_asyncpassthroughstreamingresponse_flushes_on_upstream_exception_with_partial_data(): + from litellm.passthrough.main import AsyncPassthroughStreamingResponse partial_chunks = [b"partial-chunk-1", b"partial-chunk-2"] @@ -149,6 +163,9 @@ async def test_async_streaming_flushes_on_upstream_exception_with_partial_data() mock_response.status_code = 200 mock_response.raise_for_status = MagicMock(return_value=None) mock_response.aclose = AsyncMock() + mock_response.headers = httpx.Headers( + {"content-type": "application/octet-stream", "x-request-id": "req-123"} + ) async def _aiter_bytes_then_raise(): for c in partial_chunks: @@ -165,7 +182,7 @@ async def test_async_streaming_flushes_on_upstream_exception_with_partial_data() received = [] with pytest.raises(httpx.ReadError): - async for chunk in _async_streaming( + async for chunk in AsyncPassthroughStreamingResponse( response=response_coro(), litellm_logging_obj=mock_logging_obj, provider_config=provider_config, @@ -183,12 +200,16 @@ async def test_async_streaming_flushes_on_upstream_exception_with_partial_data() assert call_kwargs["raw_bytes"] == partial_chunks -def test_sync_streaming_flushes_on_normal_completion(): - from litellm.passthrough.main import _sync_streaming +def test_passthroughstreamingresponse_flushes_on_normal_completion(): + from litellm.passthrough.main import PassthroughStreamingResponse chunks = [b"a", b"b", b"c"] mock_response = MagicMock(spec=httpx.Response) + mock_response.status_code = 200 + mock_response.headers = httpx.Headers( + {"content-type": "application/octet-stream", "x-request-id": "req-123"} + ) def _iter_bytes(): yield from chunks @@ -199,25 +220,33 @@ def test_sync_streaming_flushes_on_normal_completion(): mock_logging_obj.flush_passthrough_collected_chunks = MagicMock() provider_config = MagicMock() + received_responce = PassthroughStreamingResponse( + response=mock_response, + litellm_logging_obj=mock_logging_obj, + provider_config=provider_config, + ) + with patch("litellm.utils.executor", _ImmediateExecutor()): - received = list( - _sync_streaming( - response=mock_response, - litellm_logging_obj=mock_logging_obj, - provider_config=provider_config, - ) - ) + received = list(received_responce) assert received == chunks + + assert received_responce.headers["content-type"] == "application/octet-stream" + assert received_responce.headers["x-request-id"] == "req-123" + mock_logging_obj.flush_passthrough_collected_chunks.assert_called_once() -def test_sync_streaming_flushes_on_early_close(): - from litellm.passthrough.main import _sync_streaming +def test_passthroughstreamingresponse_flushes_on_early_close(): + from litellm.passthrough.main import PassthroughStreamingResponse chunks = [b"first", b"second", b"third"] mock_response = MagicMock(spec=httpx.Response) + mock_response.status_code = 200 + mock_response.headers = httpx.Headers( + {"content-type": "application/octet-stream", "x-request-id": "req-123"} + ) def _iter_bytes(): yield from chunks @@ -229,7 +258,7 @@ def test_sync_streaming_flushes_on_early_close(): provider_config = MagicMock() with patch("litellm.utils.executor", _ImmediateExecutor()): - gen = _sync_streaming( + gen = PassthroughStreamingResponse( response=mock_response, litellm_logging_obj=mock_logging_obj, provider_config=provider_config, From 9307d1df74c2d76e9ee4b8bb238e9b97a091437f Mon Sep 17 00:00:00 2001 From: KnyazSh Date: Tue, 16 Jun 2026 19:13:06 +0000 Subject: [PATCH 018/544] update OpenAPI schema --- ui/litellm-dashboard/src/lib/http/schema.d.ts | 191 ++++++++++++++++++ 1 file changed, 191 insertions(+) diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index 4797e41a62b..e28dba160b3 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -4148,6 +4148,42 @@ export interface paths { patch?: never; trace?: never; }; + "/gigachat/{endpoint}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Gigachat Proxy Route + * @description [Docs](https://docs.litellm.ai/docs/pass_through/gigachat) + */ + get: operations["gigachat_proxy_route_gigachat__endpoint__get"]; + /** + * Gigachat Proxy Route + * @description [Docs](https://docs.litellm.ai/docs/pass_through/gigachat) + */ + put: operations["gigachat_proxy_route_gigachat__endpoint__put"]; + /** + * Gigachat Proxy Route + * @description [Docs](https://docs.litellm.ai/docs/pass_through/gigachat) + */ + post: operations["gigachat_proxy_route_gigachat__endpoint__post"]; + /** + * Gigachat Proxy Route + * @description [Docs](https://docs.litellm.ai/docs/pass_through/gigachat) + */ + delete: operations["gigachat_proxy_route_gigachat__endpoint__delete"]; + options?: never; + head?: never; + /** + * Gigachat Proxy Route + * @description [Docs](https://docs.litellm.ai/docs/pass_through/gigachat) + */ + patch: operations["gigachat_proxy_route_gigachat__endpoint__patch"]; + trace?: never; + }; "/global/activity": { parameters: { query?: never; @@ -39050,6 +39086,161 @@ export interface operations { }; }; }; + gigachat_proxy_route_gigachat__endpoint__get: { + parameters: { + query?: never; + header?: never; + path: { + endpoint: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + gigachat_proxy_route_gigachat__endpoint__put: { + parameters: { + query?: never; + header?: never; + path: { + endpoint: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + gigachat_proxy_route_gigachat__endpoint__post: { + parameters: { + query?: never; + header?: never; + path: { + endpoint: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + gigachat_proxy_route_gigachat__endpoint__delete: { + parameters: { + query?: never; + header?: never; + path: { + endpoint: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + gigachat_proxy_route_gigachat__endpoint__patch: { + parameters: { + query?: never; + header?: never; + path: { + endpoint: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; get_global_activity_global_activity_get: { parameters: { query?: { From 93d272f40c73cb3925787c1d05baed987a4a9dd1 Mon Sep 17 00:00:00 2001 From: KnyazSh Date: Thu, 18 Jun 2026 14:35:46 +0000 Subject: [PATCH 019/544] fix some lint issues --- litellm/litellm_core_utils/litellm_logging.py | 2 +- litellm/llms/gigachat/authenticator.py | 27 ++-- litellm/llms/gigachat/chat/streaming.py | 8 +- litellm/llms/gigachat/chat/transformation.py | 58 ++++---- .../llms/gigachat/embedding/transformation.py | 26 ++-- litellm/llms/gigachat/file_handler.py | 25 ++-- .../gigachat/passthrough/transformation.py | 34 ++--- litellm/llms/gigachat/utils.py | 4 +- litellm/passthrough/main.py | 68 ++++----- litellm/proxy/common_request_processing.py | 2 +- .../llm_passthrough_endpoints.py | 139 ++++++++++-------- 11 files changed, 199 insertions(+), 194 deletions(-) diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index 347efbcbc97..e0102a60994 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -5936,7 +5936,7 @@ def emit_standard_logging_payload(payload: StandardLoggingPayload): if os.getenv("LITELLM_PRINT_STANDARD_LOGGING_PAYLOAD"): try: print(json.dumps(payload, indent=4, default=str)) # noqa: T201 - except Exception as e: + except Exception as e: # noqa: BLE001 verbose_logger.exception( "Error serializing standard logging payload for debug output: {}".format( str(e) diff --git a/litellm/llms/gigachat/authenticator.py b/litellm/llms/gigachat/authenticator.py index a20da943ee1..5418b3bcb8f 100644 --- a/litellm/llms/gigachat/authenticator.py +++ b/litellm/llms/gigachat/authenticator.py @@ -7,7 +7,6 @@ Based on official GigaChat SDK authentication flow. import time import uuid -from typing import Optional, Tuple import httpx @@ -41,7 +40,7 @@ class GigaChatAuthError(BaseLLMException): pass -def _get_credentials() -> Optional[str]: +def _get_credentials() -> str | None: """Get GigaChat credentials from environment.""" return get_secret_str("GIGACHAT_CREDENTIALS") or get_secret_str("GIGACHAT_API_KEY") @@ -62,10 +61,10 @@ def _get_http_client() -> HTTPHandler: def get_access_token( - credentials: Optional[str] = None, - scope: Optional[str] = None, - auth_url: Optional[str] = None, - litellm_params: Optional[dict] = None, + credentials: str | None = None, + scope: str | None = None, + auth_url: str | None = None, + litellm_params: dict | None = None, ) -> str: """ Get valid access token, using cache if available. @@ -124,10 +123,10 @@ def get_access_token( async def get_access_token_async( - credentials: Optional[str] = None, - scope: Optional[str] = None, - auth_url: Optional[str] = None, - litellm_params: Optional[dict] = None, + credentials: str | None = None, + scope: str | None = None, + auth_url: str | None = None, + litellm_params: dict | None = None, ) -> str: """Async version of get_access_token.""" if not litellm_params: @@ -176,12 +175,12 @@ def _request_token_sync( credentials: str, scope: str, auth_url: str, -) -> Tuple[str, int]: +) -> tuple[str, int]: """ Request new access token from GigaChat OAuth endpoint (sync). Returns: - Tuple of (access_token, expires_at_ms) + tuple of (access_token, expires_at_ms) """ headers = { "Authorization": f"Basic {credentials}", @@ -213,7 +212,7 @@ async def _request_token_async( credentials: str, scope: str, auth_url: str, -) -> Tuple[str, int]: +) -> tuple[str, int]: """Async version of _request_token_sync.""" headers = { "Authorization": f"Basic {credentials}", @@ -244,7 +243,7 @@ async def _request_token_async( ) -def _parse_token_response(response: httpx.Response) -> Tuple[str, int]: +def _parse_token_response(response: httpx.Response) -> tuple[str, int]: """Parse OAuth token response.""" data = response.json() diff --git a/litellm/llms/gigachat/chat/streaming.py b/litellm/llms/gigachat/chat/streaming.py index b42c7baa403..5c56a0c5d87 100644 --- a/litellm/llms/gigachat/chat/streaming.py +++ b/litellm/llms/gigachat/chat/streaming.py @@ -4,7 +4,7 @@ GigaChat Streaming Response Handler import json import uuid -from typing import Any, Optional +from typing import Any from litellm.llms.gigachat.utils import convert_usage from litellm.types.llms.openai import ( @@ -21,7 +21,7 @@ class GigaChatModelResponseIterator: self, streaming_response: Any, sync_stream: bool, - json_mode: Optional[bool] = False, + json_mode: bool | None = False, ): self.streaming_response = streaming_response self.response_iterator = self.streaming_response @@ -30,9 +30,9 @@ class GigaChatModelResponseIterator: def chunk_parser(self, chunk: dict) -> GenericStreamingChunk: """Parse a single streaming chunk from GigaChat.""" text = "" - tool_use: Optional[ChatCompletionToolCallChunk] = None + tool_use: ChatCompletionToolCallChunk | None = None is_finished = False - finish_reason: Optional[str] = None + finish_reason: str | None = None choices = chunk.get("choices", []) if not choices: diff --git a/litellm/llms/gigachat/chat/transformation.py b/litellm/llms/gigachat/chat/transformation.py index b3bb7bc5770..9ffb7cd8a16 100644 --- a/litellm/llms/gigachat/chat/transformation.py +++ b/litellm/llms/gigachat/chat/transformation.py @@ -7,7 +7,7 @@ Transforms OpenAI-format requests to GigaChat format and back. import json import time import uuid -from typing import TYPE_CHECKING, Any, AsyncIterator, Iterator, List, Optional, Union +from typing import TYPE_CHECKING, Any, AsyncIterator, Iterator, Union import httpx @@ -60,36 +60,36 @@ class GigaChatConfig(BaseConfig): stream: Enable streaming """ - temperature: Optional[float] = None - top_p: Optional[float] = None - max_tokens: Optional[int] = None - repetition_penalty: Optional[float] = None - profanity_check: Optional[bool] = None + temperature: float | None = None + top_p: float | None = None + max_tokens: int | None = None + repetition_penalty: float | None = None + profanity_check: bool | None = None def __init__( self, - temperature: Optional[float] = None, - top_p: Optional[float] = None, - max_tokens: Optional[int] = None, - repetition_penalty: Optional[float] = None, - profanity_check: Optional[bool] = None, + temperature: float | None = None, + top_p: float | None = None, + max_tokens: int | None = None, + repetition_penalty: float | None = None, + profanity_check: bool | None = None, ) -> None: locals_ = locals().copy() for key, value in locals_.items(): if key != "self" and value is not None: setattr(self.__class__, key, value) # Instance variables for current request context - self._current_credentials: Optional[str] = None - self._current_api_base: Optional[str] = None + self._current_credentials: str | None = None + self._current_api_base: str | None = None def get_complete_url( self, - api_base: Optional[str], - api_key: Optional[str], + api_base: str | None, + api_key: str | None, model: str, optional_params: dict, litellm_params: dict, - stream: Optional[bool] = None, + stream: bool | None = None, ) -> str: """Get complete API URL for chat completions.""" base = get_api_base(api_base) @@ -99,11 +99,11 @@ class GigaChatConfig(BaseConfig): self, headers: dict, model: str, - messages: List[AllMessageValues], + messages: list[AllMessageValues], optional_params: dict, litellm_params: dict, - api_key: Optional[str] = None, - api_base: Optional[str] = None, + api_key: str | None = None, + api_base: str | None = None, ) -> dict: """ Set up headers with OAuth token. @@ -128,7 +128,7 @@ class GigaChatConfig(BaseConfig): return headers - def get_supported_openai_params(self, model: str) -> List[str]: + def get_supported_openai_params(self, model: str) -> list[str]: """Return list of supported OpenAI parameters.""" return [ "stream", @@ -201,7 +201,7 @@ class GigaChatConfig(BaseConfig): return optional_params - def _convert_tools_to_functions(self, tools: List[dict]) -> List[dict]: + def _convert_tools_to_functions(self, tools: list[dict]) -> list[dict]: """Convert OpenAI tools format to GigaChat functions format.""" functions = [] for tool in tools: @@ -218,7 +218,7 @@ class GigaChatConfig(BaseConfig): def _map_tool_choice( self, tool_choice: Union[str, dict] - ) -> Optional[Union[str, dict]]: + ) -> Union[str, dict] | None: """ Map OpenAI tool_choice to GigaChat function_call format. @@ -258,7 +258,7 @@ class GigaChatConfig(BaseConfig): # Default to None (don't set function_call) return None - def _upload_image(self, image_url: str) -> Optional[str]: + def _upload_image(self, image_url: str) -> str | None: """ Upload image to GigaChat and return file_id. @@ -281,7 +281,7 @@ class GigaChatConfig(BaseConfig): def transform_request( self, model: str, - messages: List[AllMessageValues], + messages: list[AllMessageValues], optional_params: dict, litellm_params: dict, headers: dict, @@ -316,7 +316,7 @@ class GigaChatConfig(BaseConfig): return request_data - def _transform_messages(self, messages: List[AllMessageValues]) -> List[dict]: + def _transform_messages(self, messages: list[AllMessageValues]) -> list[dict]: """Transform OpenAI messages to GigaChat format.""" transformed = [] @@ -395,12 +395,12 @@ class GigaChatConfig(BaseConfig): model_response: ModelResponse, logging_obj: LiteLLMLoggingObj, request_data: dict, - messages: List[AllMessageValues], + messages: list[AllMessageValues], optional_params: dict, litellm_params: dict, encoding: Any, - api_key: Optional[str] = None, - json_mode: Optional[bool] = None, + api_key: str | None = None, + json_mode: bool | None = None, ) -> ModelResponse: """Transform GigaChat response to OpenAI format.""" try: @@ -494,7 +494,7 @@ class GigaChatConfig(BaseConfig): self, streaming_response: Union[Iterator[str], AsyncIterator[str], ModelResponse], sync_stream: bool, - json_mode: Optional[bool] = False, + json_mode: bool | None = False, ): """Return streaming response iterator.""" from .streaming import GigaChatModelResponseIterator diff --git a/litellm/llms/gigachat/embedding/transformation.py b/litellm/llms/gigachat/embedding/transformation.py index 8dcccd49fcb..6e4698120ee 100644 --- a/litellm/llms/gigachat/embedding/transformation.py +++ b/litellm/llms/gigachat/embedding/transformation.py @@ -6,7 +6,7 @@ API Documentation: https://developers.sber.ru/docs/ru/gigachat/api/reference/res """ import types -from typing import List, Optional, Tuple, Union +from typing import Union import httpx @@ -55,7 +55,7 @@ class GigaChatEmbeddingConfig(BaseEmbeddingConfig): and v is not None } - def get_supported_openai_params(self, model: str) -> List[str]: + def get_supported_openai_params(self, model: str) -> list[str]: """GigaChat embeddings don't support additional parameters.""" return [] @@ -71,26 +71,26 @@ class GigaChatEmbeddingConfig(BaseEmbeddingConfig): def _get_openai_compatible_provider_info( self, - api_base: Optional[str], - api_key: Optional[str], - ) -> Tuple[str, Optional[str], Optional[str]]: + api_base: str | None, + api_key: str | None, + ) -> tuple[str, str | None, str | None]: """ Returns provider info for GigaChat. Returns: - Tuple of (custom_llm_provider, api_base, dynamic_api_key) + tuple of (custom_llm_provider, api_base, dynamic_api_key) """ api_base = get_api_base(api_base) return LlmProviders.GIGACHAT.value, api_base, api_key def get_complete_url( self, - api_base: Optional[str], - api_key: Optional[str], + api_base: str | None, + api_key: str | None, model: str, optional_params: dict, litellm_params: dict, - stream: Optional[bool] = None, + stream: bool | None = None, ) -> str: """Get the complete URL for embeddings endpoint.""" base = get_api_base(api_base) @@ -135,7 +135,7 @@ class GigaChatEmbeddingConfig(BaseEmbeddingConfig): raw_response: httpx.Response, model_response: EmbeddingResponse, logging_obj: LiteLLMLoggingObj, - api_key: Optional[str], + api_key: str | None, request_data: dict, optional_params: dict, litellm_params: dict, @@ -182,11 +182,11 @@ class GigaChatEmbeddingConfig(BaseEmbeddingConfig): self, headers: dict, model: str, - messages: List[AllMessageValues], + messages: list[AllMessageValues], optional_params: dict, litellm_params: dict, - api_key: Optional[str] = None, - api_base: Optional[str] = None, + api_key: str | None = None, + api_base: str | None = None, ) -> dict: """ Set up headers with OAuth token for GigaChat. diff --git a/litellm/llms/gigachat/file_handler.py b/litellm/llms/gigachat/file_handler.py index 2054fe1f566..8c7075f5599 100644 --- a/litellm/llms/gigachat/file_handler.py +++ b/litellm/llms/gigachat/file_handler.py @@ -9,7 +9,6 @@ import base64 import hashlib import re import uuid -from typing import Dict, Optional, Tuple from litellm._logging import verbose_logger from litellm.llms.custom_httpx.http_handler import ( @@ -22,7 +21,7 @@ from litellm.types.utils import LlmProviders from .authenticator import get_access_token, get_access_token_async # Simple in-memory cache for file IDs -_file_cache: Dict[str, str] = {} +_file_cache: dict[str, str] = {} def _get_url_hash(url: str) -> str: @@ -30,7 +29,7 @@ def _get_url_hash(url: str) -> str: return hashlib.sha256(url.encode()).hexdigest() -def _parse_data_url(data_url: str) -> Optional[Tuple[bytes, str, str]]: +def _parse_data_url(data_url: str) -> tuple[bytes, str, str] | None: """ Parse data URL (base64 image). @@ -49,7 +48,7 @@ def _parse_data_url(data_url: str) -> Optional[Tuple[bytes, str, str]]: return content_bytes, content_type, ext -def _download_image_sync(url: str) -> Tuple[bytes, str, str]: +def _download_image_sync(url: str) -> tuple[bytes, str, str]: """Download image from URL synchronously.""" client = _get_httpx_client(params={"ssl_verify": False}) response = client.get(url) @@ -61,7 +60,7 @@ def _download_image_sync(url: str) -> Tuple[bytes, str, str]: return response.content, content_type, ext -async def _download_image_async(url: str) -> Tuple[bytes, str, str]: +async def _download_image_async(url: str) -> tuple[bytes, str, str]: """Download image from URL asynchronously.""" client = get_async_httpx_client( llm_provider=LlmProviders.GIGACHAT, @@ -78,10 +77,10 @@ async def _download_image_async(url: str) -> Tuple[bytes, str, str]: def upload_file_sync( image_url: str, - credentials: Optional[str] = None, - api_base: Optional[str] = None, - litellm_params: Optional[dict] = None, -) -> Optional[str]: + credentials: str | None = None, + api_base: str | None = None, + litellm_params: dict | None = None, +) -> str | None: """ Upload file to GigaChat and return file_id (sync). @@ -146,10 +145,10 @@ def upload_file_sync( async def upload_file_async( image_url: str, - credentials: Optional[str] = None, - api_base: Optional[str] = None, - litellm_params: Optional[dict] = None, -) -> Optional[str]: + credentials: str | None = None, + api_base: str | None = None, + litellm_params: dict | None = None, +) -> str | None: """ Upload file to GigaChat and return file_id (async). diff --git a/litellm/llms/gigachat/passthrough/transformation.py b/litellm/llms/gigachat/passthrough/transformation.py index 474622a01a8..d2ba43b0f47 100644 --- a/litellm/llms/gigachat/passthrough/transformation.py +++ b/litellm/llms/gigachat/passthrough/transformation.py @@ -1,5 +1,5 @@ import json -from typing import TYPE_CHECKING, List, Optional, Tuple, cast +from typing import TYPE_CHECKING, cast import httpx @@ -24,13 +24,13 @@ class GigaChatPassthroughConfig(BasePassthroughConfig): def get_complete_url( self, - api_base: Optional[str], - api_key: Optional[str], + api_base: str | None, + api_key: str | None, model: str, endpoint: str, - request_query_params: Optional[dict], + request_query_params: dict | None, litellm_params: dict, - ) -> Tuple["URL", str]: + ) -> tuple["URL", str]: """Get complete API URL for chat completions.""" base_target_url = self.get_api_base(api_base) @@ -48,11 +48,11 @@ class GigaChatPassthroughConfig(BasePassthroughConfig): self, headers: dict, model: str, - messages: List[AllMessageValues], + messages: list[AllMessageValues], optional_params: dict, litellm_params: dict, - api_key: Optional[str] = None, - api_base: Optional[str] = None, + api_key: str | None = None, + api_base: str | None = None, ) -> dict: """ Set up headers with OAuth token. @@ -76,7 +76,7 @@ class GigaChatPassthroughConfig(BasePassthroughConfig): request_data: dict, logging_obj: "LiteLLMLoggingObj", endpoint: str, - ) -> Optional["CostResponseTypes"]: + ) -> "CostResponseTypes" | None: from litellm import encoding from litellm.types.utils import LlmProviders, ModelResponse from litellm.utils import ProviderConfigManager @@ -140,12 +140,12 @@ class GigaChatPassthroughConfig(BasePassthroughConfig): def handle_logging_collected_chunks( self, - all_chunks: List[str], + all_chunks: list[str], litellm_logging_obj: "LiteLLMLoggingObj", model: str, custom_llm_provider: str, endpoint: str, - ) -> Optional["CostResponseTypes"]: + ) -> "CostResponseTypes" | None: """ 1. Convert all_chunks to a ModelResponseStream 2. combine model_response_stream to model_response @@ -208,20 +208,20 @@ class GigaChatPassthroughConfig(BasePassthroughConfig): return None @staticmethod - def get_api_base(api_base: Optional[str] = None) -> Optional[str]: + def get_api_base(api_base: str | None = None) -> str | None: return api_base or get_secret_str("GIGACHAT_API_BASE") or GIGACHAT_BASE_URL @staticmethod def get_api_key( - api_key: Optional[str] = None, - ) -> Optional[str]: + api_key: str | None = None, + ) -> str | None: return api_key or get_secret_str("GIGACHAT_API_KEY") @staticmethod - def get_base_model(model: str) -> Optional[str]: + def get_base_model(model: str) -> str | None: return model def get_models( - self, api_key: Optional[str] = None, api_base: Optional[str] = None - ) -> List[str]: + self, api_key: str | None = None, api_base: str | None = None + ) -> list[str]: return super().get_models(api_key, api_base) diff --git a/litellm/llms/gigachat/utils.py b/litellm/llms/gigachat/utils.py index b66d25cbc0b..0895d1a4992 100644 --- a/litellm/llms/gigachat/utils.py +++ b/litellm/llms/gigachat/utils.py @@ -1,5 +1,3 @@ -from typing import Optional - from litellm.secret_managers.main import get_secret_str from litellm.types.utils import PromptTokensDetailsWrapper, Usage @@ -30,5 +28,5 @@ def convert_usage(usage_data: dict[str, int]) -> Usage: ) -def get_api_base(api_base: Optional[str] = None) -> Optional[str]: +def get_api_base(api_base: str | None = None) -> str | None: return api_base or get_secret_str("GIGACHAT_API_BASE") or GIGACHAT_BASE_URL diff --git a/litellm/passthrough/main.py b/litellm/passthrough/main.py index 5c3e7aa1884..c3ebc81541d 100644 --- a/litellm/passthrough/main.py +++ b/litellm/passthrough/main.py @@ -50,7 +50,7 @@ class AsyncPassthroughStreamingResponse(AsyncGenerator[Any, Any]): self._iterator: AsyncGenerator[bytes, Any] self._litellm_logging_obj = litellm_logging_obj self._provider_config = provider_config - self._raw_bytes: List[bytes] = [] + self._raw_bytes: list[bytes] = [] self._flush_scheduled = False self._background_tasks: set[asyncio.Task] = set() @@ -92,10 +92,10 @@ class AsyncPassthroughStreamingResponse(AsyncGenerator[Any, Any]): self._iterator = cast( AsyncGenerator[bytes, Any], self._response.aiter_bytes() ) - except Exception: + except Exception: # noqa: BLE001 try: await self._response.aclose() - except Exception: + except Exception: # noqa: BLE001 pass raise return self @@ -120,7 +120,7 @@ class AsyncPassthroughStreamingResponse(AsyncGenerator[Any, Any]): # Remove the task from the set when it finishes to avoid memory leaks task.add_done_callback(self._background_tasks.discard) - except Exception as e: + except Exception as e: # noqa: BLE001 verbose_logger.exception( "Failed to schedule passthrough spend-tracking flush; " "%d buffered chunks dropped: %s", @@ -138,11 +138,11 @@ class AsyncPassthroughStreamingResponse(AsyncGenerator[Any, Any]): chunk = await self._iterator.__anext__() self._raw_bytes.append(chunk) return chunk - except Exception: + except Exception: # noqa: BLE001 self._start_flush() try: await self._response.aclose() - except Exception: + except Exception: # noqa: BLE001 pass raise @@ -161,7 +161,7 @@ class AsyncPassthroughStreamingResponse(AsyncGenerator[Any, Any]): try: if self._initialized: await self._response.aclose() - except Exception: + except Exception: # noqa: BLE001 pass @@ -196,7 +196,7 @@ class PassthroughStreamingResponse(Generator[Any, Any, Any]): raw_bytes=self._raw_bytes, provider_config=self._provider_config, ) - except Exception as e: + except Exception as e: # noqa: BLE001 verbose_logger.exception( "Failed to schedule passthrough spend-tracking flush; " "%d buffered chunks dropped: %s", @@ -212,11 +212,11 @@ class PassthroughStreamingResponse(Generator[Any, Any, Any]): chunk = next(self._iterator) self._raw_bytes.append(chunk) return chunk - except Exception: + except Exception: # noqa: BLE001 self._start_flush() try: self._response.close() - except Exception: + except Exception: # noqa: BLE001 pass raise @@ -230,7 +230,7 @@ class PassthroughStreamingResponse(Generator[Any, Any, Any]): self._start_flush() try: self._response.close() - except Exception: + except Exception: # noqa: BLE001 pass @@ -240,17 +240,17 @@ async def allm_passthrough_route( method: str, endpoint: str, model: str, - custom_llm_provider: Optional[str] = None, - api_base: Optional[str] = None, - api_key: Optional[str] = None, - request_query_params: Optional[dict] = None, - request_headers: Optional[dict] = None, - content: Optional[Any] = None, - data: Optional[dict] = None, - files: Optional[RequestFiles] = None, - json: Optional[Any] = None, - params: Optional[QueryParamTypes] = None, - cookies: Optional[CookieTypes] = None, + custom_llm_provider: str | None = None, + api_base: str | None = None, + api_key: str | None = None, + request_query_params: dict | None = None, + request_headers: dict | None = None, + content: Any | None = None, + data: dict | None = None, + files: RequestFiles | None = None, + json: Any | None = None, + params: QueryParamTypes | None = None, + cookies: CookieTypes | None = None, client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None, **kwargs, ) -> Union[httpx.Response, AsyncGenerator[Any, Any]]: @@ -365,19 +365,19 @@ def llm_passthrough_route( method: str, endpoint: str, model: str, - custom_llm_provider: Optional[str] = None, - api_base: Optional[str] = None, - api_key: Optional[str] = None, - request_query_params: Optional[dict] = None, - request_headers: Optional[dict] = None, + custom_llm_provider: str | None = None, + api_base: str | None = None, + api_key: str | None = None, + request_query_params: dict | None = None, + request_headers: dict | None = None, allm_passthrough_route: bool = False, - content: Optional[Any] = None, - data: Optional[dict] = None, - files: Optional[RequestFiles] = None, - json: Optional[Any] = None, - params: Optional[QueryParamTypes] = None, - cookies: Optional[CookieTypes] = None, - client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None, + content: Any | None = None, + data: dict | None = None, + files: RequestFiles | None = None, + json: Any | None = None, + params: QueryParamTypes | None = None, + cookies: CookieTypes | None = None, + client: Union[HTTPHandler, AsyncHTTPHandler] | None = None, **kwargs, ) -> Union[ httpx.Response, diff --git a/litellm/proxy/common_request_processing.py b/litellm/proxy/common_request_processing.py index bfeaf84930d..6cb65692099 100644 --- a/litellm/proxy/common_request_processing.py +++ b/litellm/proxy/common_request_processing.py @@ -732,7 +732,7 @@ class ProxyBaseLLMRequestProcessing: @staticmethod def _merge_passthrough_streaming_headers( - response_headers: Optional[Any], + response_headers: Any | None, custom_headers: dict, ) -> dict: """ diff --git a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py index 0f2bc79e9fb..3d3bab14020 100644 --- a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py @@ -9,7 +9,7 @@ Use litellm with Anthropic SDK, Vertex AI SDK, Cohere SDK, etc. import json import os import re -from typing import Any, Optional, Tuple, Union, cast +from typing import TYPE_CHECKING, Any, Callable, Union, cast import httpx from fastapi import APIRouter, Depends, HTTPException, Request, Response, WebSocket @@ -59,6 +59,11 @@ from litellm.utils import ProviderConfigManager from .passthrough_endpoint_router import PassthroughEndpointRouter +if TYPE_CHECKING: + from litellm.proxy.proxy_server import ProxyConfig + from litellm.proxy.utils import ProxyLogging + + vertex_llm_base = VertexBase() router = APIRouter() default_vertex_config = None @@ -77,7 +82,7 @@ def create_request_copy(request: Request): def is_passthrough_request_using_router_model( - request_body: dict, llm_router: Optional[litellm.Router] + request_body: dict, llm_router: litellm.Router | None ) -> bool: """ Returns True if the model is in the llm_router model names @@ -225,7 +230,7 @@ async def gemini_proxy_route( ) # Add or update query parameters - gemini_api_key: Optional[str] = passthrough_endpoint_router.get_credentials( + gemini_api_key: str | None = passthrough_endpoint_router.get_credentials( custom_llm_provider="gemini", region_name=None, ) @@ -473,9 +478,9 @@ async def milvus_proxy_route( request_body = await get_request_body(request) # check collectionName - collection_name = cast(Optional[str], request_body.get("collectionName")) + collection_name = cast(str | None, request_body.get("collectionName")) extra_headers = {} - base_target_url: Optional[str] = None + base_target_url: str | None = None if not collection_name: raise HTTPException( status_code=400, @@ -760,12 +765,12 @@ async def handle_bedrock_passthrough_router_model( 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], + user_model: str | None, + user_temperature: float | None, + user_request_timeout: float | None, + user_max_tokens: int | None, + user_api_base: str | None, + version: str | None, ) -> Union[Response, StreamingResponse]: """ Handle Bedrock passthrough for router models (models defined in config.yaml). @@ -1134,12 +1139,12 @@ async def bedrock_proxy_route( def _resolve_vertex_model_from_router( model_id: str, - llm_router: Optional[litellm.Router], + llm_router: litellm.Router | None, encoded_endpoint: str, endpoint: str, - vertex_project: Optional[str], - vertex_location: Optional[str], -) -> Tuple[str, str, Optional[str], Optional[str]]: + vertex_project: str | None, + vertex_location: str | None, +) -> tuple[str, str, str | None, str | None]: """ Resolve Vertex AI model configuration from router. @@ -1152,7 +1157,7 @@ def _resolve_vertex_model_from_router( vertex_location: Current vertex location (may be from URL) Returns: - Tuple of (encoded_endpoint, endpoint, vertex_project, vertex_location) + tuple of (encoded_endpoint, endpoint, vertex_project, vertex_location) with resolved values from router config """ if not llm_router: @@ -1501,42 +1506,42 @@ from abc import ABC, abstractmethod class BaseVertexAIPassThroughHandler(ABC): @staticmethod @abstractmethod - def get_default_base_target_url(vertex_location: Optional[str]) -> str: + def get_default_base_target_url(vertex_location: str | None) -> str: pass @staticmethod @abstractmethod def update_base_target_url_with_credential_location( - base_target_url: str, vertex_location: Optional[str] + base_target_url: str, vertex_location: str | None ) -> str: pass class VertexAIDiscoveryPassThroughHandler(BaseVertexAIPassThroughHandler): @staticmethod - def get_default_base_target_url(vertex_location: Optional[str]) -> str: + def get_default_base_target_url(vertex_location: str | None) -> str: return "https://discoveryengine.googleapis.com/" @staticmethod def update_base_target_url_with_credential_location( - base_target_url: str, vertex_location: Optional[str] + base_target_url: str, vertex_location: str | None ) -> str: return base_target_url class VertexAIPassThroughHandler(BaseVertexAIPassThroughHandler): @staticmethod - def get_default_base_target_url(vertex_location: Optional[str]) -> str: + def get_default_base_target_url(vertex_location: str | None) -> str: return get_vertex_base_url(vertex_location) @staticmethod def update_base_target_url_with_credential_location( - base_target_url: str, vertex_location: Optional[str] + base_target_url: str, vertex_location: str | None ) -> str: return get_vertex_base_url(vertex_location) -def get_vertex_base_url(vertex_location: Optional[str]) -> str: +def get_vertex_base_url(vertex_location: str | None) -> str: """ Base URL for Vertex AI pass-through (trailing slash for URL joining). @@ -1586,10 +1591,10 @@ def get_vertex_pass_through_handler( def _override_vertex_params_from_router_credentials( - router_credentials: Optional[Any], - vertex_project: Optional[str], - vertex_location: Optional[str], -) -> Tuple[Optional[str], Optional[str]]: + router_credentials: Any | None, + vertex_project: str | None, + vertex_location: str | None, +) -> tuple[str | None, str | None]: """ Override vertex_project and vertex_location with values from router_credentials if available. @@ -1599,7 +1604,7 @@ def _override_vertex_params_from_router_credentials( vertex_location: Current vertex location (from URL) Returns: - Tuple of (vertex_project, vertex_location) with overridden values if applicable + tuple of (vertex_project, vertex_location) with overridden values if applicable """ if router_credentials is None: return vertex_project, vertex_location @@ -1648,13 +1653,13 @@ def _override_vertex_params_from_router_credentials( async def _prepare_vertex_auth_headers( request: Request, - vertex_credentials: Optional[Any], - router_credentials: Optional[Any], - vertex_project: Optional[str], - vertex_location: Optional[str], - base_target_url: Optional[str], + vertex_credentials: Any | None, + router_credentials: Any | None, + vertex_project: str | None, + vertex_location: str | None, + base_target_url: str | None, get_vertex_pass_through_handler: BaseVertexAIPassThroughHandler, -) -> Tuple[dict, Optional[str], bool, Optional[str], Optional[str]]: +) -> tuple[dict, str | None, bool, str | None, str | None]: """ Prepare authentication headers for Vertex AI pass-through requests. @@ -1668,12 +1673,12 @@ async def _prepare_vertex_auth_headers( get_vertex_pass_through_handler: Handler for the specific Vertex AI service Returns: - Tuple containing: + tuple containing: - headers: dict - Authentication headers to use - - base_target_url: Optional[str] - Updated base target URL + - base_target_url: str | None - Updated base target URL - headers_passed_through: bool - Whether headers were passed through from request - - vertex_project: Optional[str] - Updated vertex project ID - - vertex_location: Optional[str] - Updated vertex location + - vertex_project: str | None - Updated vertex project ID + - vertex_location: str | None - Updated vertex location """ vertex_llm_base = VertexBase() headers_passed_through = False @@ -1746,8 +1751,8 @@ async def _base_vertex_proxy_route( request: Request, fastapi_response: Response, get_vertex_pass_through_handler: BaseVertexAIPassThroughHandler, - user_api_key_dict: Optional[UserAPIKeyAuth] = None, - router_credentials: Optional[Any] = None, + user_api_key_dict: UserAPIKeyAuth | None = None, + router_credentials: Any | None = None, ): """ Base function for Vertex AI passthrough routes. @@ -1793,8 +1798,8 @@ async def _base_vertex_proxy_route( user_api_key_dict=user_api_key_dict, ) - vertex_project: Optional[str] = get_vertex_project_id_from_url(endpoint) - vertex_location: Optional[str] = get_vertex_location_from_url(endpoint) + vertex_project: str | None = get_vertex_project_id_from_url(endpoint) + vertex_location: str | None = get_vertex_location_from_url(endpoint) # Override with vector store credentials if available vertex_project, vertex_location = _override_vertex_params_from_router_credentials( @@ -1919,7 +1924,7 @@ async def vertex_discovery_proxy_route( from litellm.types.vector_stores import LiteLLM_ManagedVectorStore # Extract vector store ID from endpoint if present (e.g., dataStores/test-litellm-app_1761094730750) - vector_store_credentials: Optional[LiteLLM_ManagedVectorStore] = None + vector_store_credentials: LiteLLM_ManagedVectorStore | None = None vector_store_id_match = re.search(r"dataStores/([^/]+)", endpoint) if vector_store_id_match: @@ -2057,9 +2062,9 @@ class BaseOpenAIPassThroughHandler: fastapi_response: Response, user_api_key_dict: UserAPIKeyAuth, base_target_url: str, - api_key: Optional[str], + api_key: str | None, custom_llm_provider: litellm.LlmProviders, - extra_headers: Optional[dict] = None, + extra_headers: dict | None = None, ): encoded_endpoint = httpx.URL(endpoint).path # Ensure endpoint starts with '/' for proper URL construction @@ -2115,7 +2120,7 @@ class BaseOpenAIPassThroughHandler: @staticmethod def _assemble_headers( - api_key: Optional[str], request: Request, extra_headers: Optional[dict] = None + api_key: str | None, request: Request, extra_headers: dict | None = None ) -> dict: base_headers = {} if api_key is not None: @@ -2251,10 +2256,10 @@ async def cursor_proxy_route( async def vertex_ai_live_websocket_passthrough( websocket: WebSocket, - model: Optional[str] = None, - vertex_project: Optional[str] = None, - vertex_location: Optional[str] = None, - user_api_key_dict: Optional[UserAPIKeyAuth] = None, + model: str | None = None, + vertex_project: str | None = None, + vertex_location: str | None = None, + user_api_key_dict: UserAPIKeyAuth | None = None, ): """ Vertex AI Live API WebSocket Pass-through Function @@ -2286,8 +2291,8 @@ async def vertex_ai_live_websocket_passthrough( ) resolved_project = vertex_project - resolved_location: Optional[str] = vertex_location - credentials_value: Optional[str] = None + resolved_location: str | None = vertex_location + credentials_value: str | None = None if vertex_credentials_config is not None: resolved_project = resolved_project or vertex_credentials_config.vertex_project @@ -2391,9 +2396,9 @@ def create_vertex_ai_live_websocket_endpoint(): def create_generic_websocket_passthrough_endpoint( provider: str, target_url: str, - custom_headers: Optional[dict] = None, + custom_headers: dict | None = None, forward_headers: bool = False, - cost_per_request: Optional[float] = None, + cost_per_request: float | None = None, ): """ Create a generic WebSocket passthrough endpoint for any provider. @@ -2540,7 +2545,7 @@ async def gigachat_proxy_route( ) return result - except Exception as e: + except Exception as e: # noqa: BLE001 raise await base_llm_response_processor._handle_llm_api_exception( e=e, user_api_key_dict=user_api_key_dict, @@ -2556,16 +2561,16 @@ async def handle_gigachat_passthrough_router_model( fastapi_response: Response, llm_router: litellm.Router, user_api_key_dict: UserAPIKeyAuth, - proxy_logging_obj, + proxy_logging_obj: ProxyLogging, 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], + proxy_config: ProxyConfig, + select_data_generator: Callable, + user_model: str | None, + user_temperature: float | None, + user_request_timeout: float | None, + user_max_tokens: int | None, + user_api_base: str | None, + version: str | None, ) -> Union[Response, StreamingResponse]: """ Handle Gigachat passthrough for router models (models defined in config.yaml). @@ -2580,6 +2585,10 @@ async def handle_gigachat_passthrough_router_model( request_body: The parsed request body llm_router: The LiteLLM router instance user_api_key_dict: The user API key authentication dictionary + proxy_logging_obj: Proxy logging + general_settings: Proxy general settings + proxy_config: Proxy config + select_data_generator: Select data generator function (additional args for common processing) Returns: @@ -2677,7 +2686,7 @@ async def handle_gigachat_passthrough_router_model( return result return result - except Exception as e: + except Exception as e: # noqa: BLE001 # Use common exception handling raise await base_llm_response_processor._handle_llm_api_exception( e=e, From 4791f003990aea2c52e0e666307e0cf2a9b8312f Mon Sep 17 00:00:00 2001 From: KnyazSh Date: Sat, 20 Jun 2026 16:37:55 +0000 Subject: [PATCH 020/544] fix import ProxyConfig --- .../pass_through_endpoints/llm_passthrough_endpoints.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py index 3d3bab14020..01449d8ab26 100644 --- a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py @@ -47,7 +47,7 @@ from litellm.types.passthrough_endpoints.pass_through_endpoints import ( LITELLM_PASS_THROUGH_CUSTOM_BODY_STATE_KEY, LITELLM_PASS_THROUGH_RAW_BODY_STATE_KEY, ) -from litellm.proxy.utils import is_known_model +from litellm.proxy.utils import is_known_model, ProxyLogging from litellm.proxy.vector_store_endpoints.utils import ( assert_user_can_access_vector_store, get_litellm_managed_vector_store, @@ -60,8 +60,11 @@ from litellm.utils import ProviderConfigManager from .passthrough_endpoint_router import PassthroughEndpointRouter if TYPE_CHECKING: - from litellm.proxy.proxy_server import ProxyConfig - from litellm.proxy.utils import ProxyLogging + from litellm.proxy.proxy_server import ProxyConfig as _ProxyConfig + + ProxyConfig = _ProxyConfig +else: + ProxyConfig = Any vertex_llm_base = VertexBase() From 0cc9147f4992e9389bdf1f027aa52be05a25b504 Mon Sep 17 00:00:00 2001 From: KnyazSh Date: Sat, 20 Jun 2026 22:04:59 +0000 Subject: [PATCH 021/544] Add gigachat tests --- .../gigachat/passthrough/transformation.py | 14 +- .../llms/gigachat/test_authenticator.py | 428 ++++++++++++++++ tests/litellm/llms/gigachat/test_utils.py | 84 +++ .../llms/gigachat/passthrough/__init__.py | 0 ...est_gigachat_passthrough_transformation.py | 481 ++++++++++++++++++ 5 files changed, 1001 insertions(+), 6 deletions(-) create mode 100644 tests/litellm/llms/gigachat/test_authenticator.py create mode 100644 tests/litellm/llms/gigachat/test_utils.py create mode 100644 tests/test_litellm/llms/gigachat/passthrough/__init__.py create mode 100644 tests/test_litellm/llms/gigachat/passthrough/test_gigachat_passthrough_transformation.py diff --git a/litellm/llms/gigachat/passthrough/transformation.py b/litellm/llms/gigachat/passthrough/transformation.py index d2ba43b0f47..54277081398 100644 --- a/litellm/llms/gigachat/passthrough/transformation.py +++ b/litellm/llms/gigachat/passthrough/transformation.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import json from typing import TYPE_CHECKING, cast @@ -30,7 +32,7 @@ class GigaChatPassthroughConfig(BasePassthroughConfig): endpoint: str, request_query_params: dict | None, litellm_params: dict, - ) -> tuple["URL", str]: + ) -> tuple[URL, str]: """Get complete API URL for chat completions.""" base_target_url = self.get_api_base(api_base) @@ -72,11 +74,11 @@ class GigaChatPassthroughConfig(BasePassthroughConfig): self, model: str, custom_llm_provider: str, - httpx_response: "Response", + httpx_response: Response, request_data: dict, - logging_obj: "LiteLLMLoggingObj", + logging_obj: LiteLLMLoggingObj, endpoint: str, - ) -> "CostResponseTypes" | None: + ) -> CostResponseTypes | None: from litellm import encoding from litellm.types.utils import LlmProviders, ModelResponse from litellm.utils import ProviderConfigManager @@ -141,11 +143,11 @@ class GigaChatPassthroughConfig(BasePassthroughConfig): def handle_logging_collected_chunks( self, all_chunks: list[str], - litellm_logging_obj: "LiteLLMLoggingObj", + litellm_logging_obj: LiteLLMLoggingObj, model: str, custom_llm_provider: str, endpoint: str, - ) -> "CostResponseTypes" | None: + ) -> CostResponseTypes | None: """ 1. Convert all_chunks to a ModelResponseStream 2. combine model_response_stream to model_response diff --git a/tests/litellm/llms/gigachat/test_authenticator.py b/tests/litellm/llms/gigachat/test_authenticator.py new file mode 100644 index 00000000000..ed52ae986f5 --- /dev/null +++ b/tests/litellm/llms/gigachat/test_authenticator.py @@ -0,0 +1,428 @@ +""" +Tests for litellm.llms.gigachat.authenticator +""" + +import os +import sys +from unittest.mock import AsyncMock, MagicMock, patch + +import httpx +import pytest + +sys.path.insert(0, os.path.abspath("../../../../../")) + +from litellm.llms.gigachat.authenticator import ( + GIGACHAT_AUTH_URL, + GIGACHAT_SCOPE, + GigaChatAuthError, + _get_auth_url, + _get_credentials, + _get_scope, + _parse_token_response, + _request_token_async, + _request_token_sync, + get_access_token, + get_access_token_async, +) + + +class TestParseTokenResponse: + def test_parse_with_tok_and_exp(self): + response = MagicMock() + response.json.return_value = {"tok": "token123", "exp": 1234567890000} + token, expires_at = _parse_token_response(response) + assert token == "token123" + assert expires_at == 1234567890000 + + def test_parse_with_access_token_and_expires_at(self): + response = MagicMock() + response.json.return_value = { + "access_token": "token456", + "expires_at": 9876543210000, + } + token, expires_at = _parse_token_response(response) + assert token == "token456" + assert expires_at == 9876543210000 + + def test_parse_with_string_expires_at(self): + response = MagicMock() + response.json.return_value = { + "access_token": "token789", + "expires_at": "1234567890000", + } + token, expires_at = _parse_token_response(response) + assert token == "token789" + assert expires_at == 1234567890000 + + def test_parse_prefers_tok_over_access_token(self): + response = MagicMock() + response.json.return_value = { + "tok": "preferred", + "access_token": "fallback", + "exp": 111111, + } + token, expires_at = _parse_token_response(response) + assert token == "preferred" + assert expires_at == 111111 + + def test_parse_missing_token_raises(self): + response = MagicMock() + response.json.return_value = {"expires_at": 1234567890000} + with pytest.raises(GigaChatAuthError) as exc_info: + _parse_token_response(response) + assert "Invalid token response" in str(exc_info.value) + assert exc_info.value.status_code == 500 + + +class TestGetCredentials: + @patch("litellm.llms.gigachat.authenticator.get_secret_str") + def test_get_credentials_from_gigachat_credentials(self, mock_get_secret): + mock_get_secret.side_effect = lambda key: "cred123" if key == "GIGACHAT_CREDENTIALS" else None + result = _get_credentials() + assert result == "cred123" + + @patch("litellm.llms.gigachat.authenticator.get_secret_str") + def test_get_credentials_fallback_to_api_key(self, mock_get_secret): + mock_get_secret.side_effect = lambda key: ( + "apikey456" if key == "GIGACHAT_API_KEY" else None + ) + result = _get_credentials() + assert result == "apikey456" + + @patch("litellm.llms.gigachat.authenticator.get_secret_str") + def test_get_credentials_returns_none(self, mock_get_secret): + mock_get_secret.return_value = None + result = _get_credentials() + assert result is None + + +class TestGetAuthUrl: + @patch("litellm.llms.gigachat.authenticator.get_secret_str") + def test_get_auth_url_from_env(self, mock_get_secret): + mock_get_secret.return_value = "https://custom.auth.url" + result = _get_auth_url() + assert result == "https://custom.auth.url" + + @patch("litellm.llms.gigachat.authenticator.get_secret_str") + def test_get_auth_url_default(self, mock_get_secret): + mock_get_secret.return_value = None + result = _get_auth_url() + assert result == GIGACHAT_AUTH_URL + + +class TestGetScope: + @patch("litellm.llms.gigachat.authenticator.get_secret_str") + def test_get_scope_from_env(self, mock_get_secret): + mock_get_secret.return_value = "CUSTOM_SCOPE" + result = _get_scope() + assert result == "CUSTOM_SCOPE" + + @patch("litellm.llms.gigachat.authenticator.get_secret_str") + def test_get_scope_default(self, mock_get_secret): + mock_get_secret.return_value = None + result = _get_scope() + assert result == GIGACHAT_SCOPE + + +class TestRequestTokenSync: + @patch("litellm.llms.gigachat.authenticator.uuid.uuid4") + @patch("litellm.llms.gigachat.authenticator._get_http_client") + def test_request_token_success(self, mock_get_client, mock_uuid): + mock_uuid.return_value = "test-uuid-123" + mock_response = MagicMock() + mock_response.json.return_value = {"tok": "newtoken", "exp": 9999999999999} + mock_response.raise_for_status = MagicMock() + mock_client = MagicMock() + mock_client.post.return_value = mock_response + mock_get_client.return_value = mock_client + + token, expires_at = _request_token_sync("creds", "SCOPE", "https://auth.url") + + assert token == "newtoken" + assert expires_at == 9999999999999 + mock_client.post.assert_called_once_with( + "https://auth.url", + headers={ + "Authorization": "Basic creds", + "RqUID": "test-uuid-123", + "Content-Type": "application/x-www-form-urlencoded", + }, + data={"scope": "SCOPE"}, + timeout=30, + ) + + @patch("litellm.llms.gigachat.authenticator._get_http_client") + def test_request_token_http_status_error(self, mock_get_client): + mock_response = MagicMock() + mock_response.text = "Unauthorized" + mock_response.status_code = 401 + mock_client = MagicMock() + mock_client.post.return_value = mock_response + mock_get_client.return_value = mock_client + + mock_response.raise_for_status.side_effect = httpx.HTTPStatusError( + "401 Unauthorized", + request=MagicMock(), + response=mock_response, + ) + + with pytest.raises(GigaChatAuthError) as exc_info: + _request_token_sync("creds", "SCOPE", "https://auth.url") + assert exc_info.value.status_code == 401 + assert "Unauthorized" in str(exc_info.value) + + @patch("litellm.llms.gigachat.authenticator._get_http_client") + def test_request_token_request_error(self, mock_get_client): + mock_client = MagicMock() + mock_client.post.side_effect = httpx.RequestError("Connection refused") + mock_get_client.return_value = mock_client + + with pytest.raises(GigaChatAuthError) as exc_info: + _request_token_sync("creds", "SCOPE", "https://auth.url") + assert exc_info.value.status_code == 500 + assert "Connection refused" in str(exc_info.value) + + +class TestRequestTokenAsync: + @patch("litellm.llms.gigachat.authenticator.uuid.uuid4") + @patch("litellm.llms.gigachat.authenticator.get_async_httpx_client") + @pytest.mark.asyncio + async def test_request_token_async_success(self, mock_get_client, mock_uuid): + mock_uuid.return_value = "test-uuid-456" + mock_response = MagicMock() + mock_response.json.return_value = {"tok": "async_token", "exp": 8888888888888} + mock_response.raise_for_status = MagicMock() + mock_client = AsyncMock() + mock_client.post.return_value = mock_response + mock_get_client.return_value = mock_client + + token, expires_at = await _request_token_async("creds", "SCOPE", "https://auth.url") + + assert token == "async_token" + assert expires_at == 8888888888888 + mock_client.post.assert_awaited_once_with( + "https://auth.url", + headers={ + "Authorization": "Basic creds", + "RqUID": "test-uuid-456", + "Content-Type": "application/x-www-form-urlencoded", + }, + data={"scope": "SCOPE"}, + timeout=30, + ) + + @patch("litellm.llms.gigachat.authenticator.get_async_httpx_client") + @pytest.mark.asyncio + async def test_request_token_async_http_status_error(self, mock_get_client): + mock_response = MagicMock() + mock_response.text = "Forbidden" + mock_response.status_code = 403 + mock_client = AsyncMock() + mock_client.post.return_value = mock_response + mock_get_client.return_value = mock_client + + mock_response.raise_for_status.side_effect = httpx.HTTPStatusError( + "403 Forbidden", + request=MagicMock(), + response=mock_response, + ) + + with pytest.raises(GigaChatAuthError) as exc_info: + await _request_token_async("creds", "SCOPE", "https://auth.url") + assert exc_info.value.status_code == 403 + assert "Forbidden" in str(exc_info.value) + + @patch("litellm.llms.gigachat.authenticator.get_async_httpx_client") + @pytest.mark.asyncio + async def test_request_token_async_request_error(self, mock_get_client): + mock_client = AsyncMock() + mock_client.post.side_effect = httpx.RequestError("Timeout") + mock_get_client.return_value = mock_client + + with pytest.raises(GigaChatAuthError) as exc_info: + await _request_token_async("creds", "SCOPE", "https://auth.url") + assert exc_info.value.status_code == 500 + assert "Timeout" in str(exc_info.value) + + +class TestGetAccessToken: + @patch("litellm.llms.gigachat.authenticator.get_secret_str") + def test_get_access_token_from_litellm_params(self, mock_get_secret): + result = get_access_token( + credentials=None, + litellm_params={"gigachat_access_token": "param_token"}, + ) + assert result == "param_token" + mock_get_secret.assert_not_called() + + @patch("litellm.llms.gigachat.authenticator.get_secret_str") + def test_get_access_token_from_env(self, mock_get_secret): + mock_get_secret.return_value = "env_token" + result = get_access_token( + credentials=None, + litellm_params={}, + ) + assert result == "env_token" + + @patch("litellm.llms.gigachat.authenticator._request_token_sync") + @patch("litellm.llms.gigachat.authenticator._token_cache") + @patch("litellm.llms.gigachat.authenticator.get_secret_str") + def test_get_access_token_from_cache_valid(self, mock_get_secret, mock_cache, mock_request): + mock_get_secret.side_effect = lambda key: "creds" if key == "GIGACHAT_CREDENTIALS" else None + mock_cache.get_cache.return_value = ("cached_token", 9999999999999) + + with patch("time.time", return_value=1000): + result = get_access_token(credentials="creds", litellm_params={}) + + assert result == "cached_token" + mock_request.assert_not_called() + + @patch("litellm.llms.gigachat.authenticator._request_token_sync") + @patch("litellm.llms.gigachat.authenticator._token_cache") + @patch("litellm.llms.gigachat.authenticator.get_secret_str") + def test_get_access_token_from_cache_expired(self, mock_get_secret, mock_cache, mock_request): + mock_get_secret.side_effect = lambda key: "creds" if key == "GIGACHAT_CREDENTIALS" else None + # token expired: 1,050,000 - 60,000 = 990,000 <= 1,000,000 + mock_cache.get_cache.return_value = ("expired_token", 1050000) + mock_request.return_value = ("new_token", 2000000) + + with patch("time.time", return_value=1000): + result = get_access_token(credentials="creds", litellm_params={}) + + assert result == "new_token" + mock_request.assert_called_once() + mock_cache.set_cache.assert_called_once() + + @patch("litellm.llms.gigachat.authenticator._request_token_sync") + @patch("litellm.llms.gigachat.authenticator._token_cache") + @patch("litellm.llms.gigachat.authenticator.get_secret_str") + def test_get_access_token_requests_new_and_caches(self, mock_get_secret, mock_cache, mock_request): + mock_get_secret.side_effect = lambda key: "creds" if key == "GIGACHAT_CREDENTIALS" else None + mock_cache.get_cache.return_value = None + mock_request.return_value = ("fresh_token", 9999999999999) + + with patch("time.time", return_value=1000): + result = get_access_token(credentials="creds", litellm_params={}) + + assert result == "fresh_token" + mock_request.assert_called_once_with("creds", GIGACHAT_SCOPE, GIGACHAT_AUTH_URL) + mock_cache.set_cache.assert_called_once() + # check cache key includes first 16 chars of credentials + args, kwargs = mock_cache.set_cache.call_args + assert args[0] == "gigachat_token:creds" + assert args[1] == ("fresh_token", 9999999999999) + + def test_get_access_token_no_credentials_raises(self): + with patch("litellm.llms.gigachat.authenticator.get_secret_str", return_value=None): + with pytest.raises(GigaChatAuthError) as exc_info: + get_access_token(credentials=None, litellm_params={}) + assert exc_info.value.status_code == 401 + assert "credentials not provided" in str(exc_info.value) + + @patch("litellm.llms.gigachat.authenticator.get_secret_str") + def test_get_access_token_custom_scope_and_auth_url(self, mock_get_secret): + mock_get_secret.return_value = None + with patch("litellm.llms.gigachat.authenticator._request_token_sync") as mock_request: + mock_request.return_value = ("token", 9999999999999) + with patch("litellm.llms.gigachat.authenticator._token_cache") as mock_cache: + mock_cache.get_cache.return_value = None + with patch("time.time", return_value=1000): + result = get_access_token( + credentials="creds", + scope="CUSTOM_SCOPE", + auth_url="https://custom.auth", + litellm_params={}, + ) + assert result == "token" + mock_request.assert_called_once_with("creds", "CUSTOM_SCOPE", "https://custom.auth") + + @patch("litellm.llms.gigachat.authenticator.get_secret_str") + def test_get_access_token_scope_from_litellm_params(self, mock_get_secret): + mock_get_secret.return_value = None + with patch("litellm.llms.gigachat.authenticator._request_token_sync") as mock_request: + mock_request.return_value = ("token", 9999999999999) + with patch("litellm.llms.gigachat.authenticator._token_cache") as mock_cache: + mock_cache.get_cache.return_value = None + with patch("time.time", return_value=1000): + result = get_access_token( + credentials="creds", + litellm_params={"gigachat_scope": "PARAM_SCOPE", "gigachat_auth_url": "https://param.auth"}, + ) + assert result == "token" + mock_request.assert_called_once_with("creds", "PARAM_SCOPE", "https://param.auth") + + +class TestGetAccessTokenAsync: + @pytest.mark.asyncio + async def test_get_access_token_async_from_litellm_params(self): + result = await get_access_token_async( + credentials=None, + litellm_params={"gigachat_access_token": "async_param_token"}, + ) + assert result == "async_param_token" + + @pytest.mark.asyncio + @patch("litellm.llms.gigachat.authenticator.get_secret_str") + async def test_get_access_token_async_from_env(self, mock_get_secret): + mock_get_secret.return_value = "async_env_token" + result = await get_access_token_async( + credentials=None, + litellm_params={}, + ) + assert result == "async_env_token" + + @pytest.mark.asyncio + @patch("litellm.llms.gigachat.authenticator._request_token_async") + @patch("litellm.llms.gigachat.authenticator._token_cache") + @patch("litellm.llms.gigachat.authenticator.get_secret_str") + async def test_get_access_token_async_from_cache_valid(self, mock_get_secret, mock_cache, mock_request): + mock_get_secret.side_effect = lambda key: "creds" if key == "GIGACHAT_CREDENTIALS" else None + mock_cache.get_cache.return_value = ("cached_async_token", 9999999999999) + + with patch("time.time", return_value=1000): + result = await get_access_token_async(credentials="creds", litellm_params={}) + + assert result == "cached_async_token" + mock_request.assert_not_called() + + @pytest.mark.asyncio + @patch("litellm.llms.gigachat.authenticator._request_token_async") + @patch("litellm.llms.gigachat.authenticator._token_cache") + @patch("litellm.llms.gigachat.authenticator.get_secret_str") + async def test_get_access_token_async_requests_new_and_caches(self, mock_get_secret, mock_cache, mock_request): + mock_get_secret.side_effect = lambda key: "creds" if key == "GIGACHAT_CREDENTIALS" else None + mock_cache.get_cache.return_value = None + mock_request.return_value = ("fresh_async_token", 9999999999999) + + with patch("time.time", return_value=1000): + result = await get_access_token_async(credentials="creds", litellm_params={}) + + assert result == "fresh_async_token" + mock_request.assert_awaited_once_with("creds", GIGACHAT_SCOPE, GIGACHAT_AUTH_URL) + mock_cache.set_cache.assert_called_once() + + @pytest.mark.asyncio + async def test_get_access_token_async_no_credentials_raises(self): + with patch("litellm.llms.gigachat.authenticator.get_secret_str", return_value=None): + with pytest.raises(GigaChatAuthError) as exc_info: + await get_access_token_async(credentials=None, litellm_params={}) + assert exc_info.value.status_code == 401 + assert "credentials not provided" in str(exc_info.value) + + @pytest.mark.asyncio + @patch("litellm.llms.gigachat.authenticator.get_secret_str") + async def test_get_access_token_async_custom_params(self, mock_get_secret): + mock_get_secret.return_value = None + with patch("litellm.llms.gigachat.authenticator._request_token_async") as mock_request: + mock_request.return_value = ("token", 9999999999999) + with patch("litellm.llms.gigachat.authenticator._token_cache") as mock_cache: + mock_cache.get_cache.return_value = None + with patch("time.time", return_value=1000): + result = await get_access_token_async( + credentials="creds", + scope="CUSTOM", + auth_url="https://custom", + litellm_params={}, + ) + assert result == "token" + mock_request.assert_awaited_once_with("creds", "CUSTOM", "https://custom") diff --git a/tests/litellm/llms/gigachat/test_utils.py b/tests/litellm/llms/gigachat/test_utils.py new file mode 100644 index 00000000000..00faf391078 --- /dev/null +++ b/tests/litellm/llms/gigachat/test_utils.py @@ -0,0 +1,84 @@ +""" +Tests for litellm.llms.gigachat.utils +""" + +import os +import sys + +sys.path.insert( + 0, os.path.abspath("../../../../../") +) # Adds the project root to the system path + +import pytest +from litellm.llms.gigachat.utils import convert_usage +from litellm.types.utils import PromptTokensDetailsWrapper, Usage + + +class TestConvertUsage: + def test_basic_usage_without_precached(self): + """Test convert_usage with standard tokens, no precached prompt tokens.""" + result = convert_usage( + { + "prompt_tokens": 10, + "completion_tokens": 5, + "total_tokens": 15, + } + ) + + assert result == Usage( + prompt_tokens=10, + completion_tokens=5, + total_tokens=15, + prompt_tokens_details=None, + ) + + def test_usage_with_precached_prompt_tokens(self): + """Test convert_usage adds precached_prompt_tokens to prompt_tokens and total_tokens.""" + result = convert_usage( + { + "prompt_tokens": 10, + "completion_tokens": 5, + "precached_prompt_tokens": 3, + "total_tokens": 15, + } + ) + + assert result == Usage( + prompt_tokens=13, + completion_tokens=5, + total_tokens=18, + prompt_tokens_details=PromptTokensDetailsWrapper(cached_tokens=3), + ) + + def test_zero_precached_prompt_tokens(self): + """Test convert_usage with zero precached_prompt_tokens does not create details wrapper.""" + result = convert_usage( + { + "prompt_tokens": 10, + "completion_tokens": 5, + "precached_prompt_tokens": 0, + "total_tokens": 15, + } + ) + + assert result == Usage( + prompt_tokens=10, + completion_tokens=5, + total_tokens=15, + prompt_tokens_details=None, + ) + + def test_missing_optional_fields(self): + """Test convert_usage with missing optional fields defaults to zero.""" + result = convert_usage( + { + "prompt_tokens": 10, + "completion_tokens": 5, + "total_tokens": 15, + } + ) + + assert result.prompt_tokens == 10 + assert result.completion_tokens == 5 + assert result.total_tokens == 15 + assert result.prompt_tokens_details is None diff --git a/tests/test_litellm/llms/gigachat/passthrough/__init__.py b/tests/test_litellm/llms/gigachat/passthrough/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/test_litellm/llms/gigachat/passthrough/test_gigachat_passthrough_transformation.py b/tests/test_litellm/llms/gigachat/passthrough/test_gigachat_passthrough_transformation.py new file mode 100644 index 00000000000..b3e1500c4a3 --- /dev/null +++ b/tests/test_litellm/llms/gigachat/passthrough/test_gigachat_passthrough_transformation.py @@ -0,0 +1,481 @@ +""" +Unit tests for GigaChatPassthroughConfig transformation. + +Tests the GigaChat-specific passthrough configuration including URL construction, +streaming detection, authentication handling, and logging response transformations. +""" + +import json +import os +import sys +from unittest.mock import MagicMock, patch + +import httpx +import pytest + +sys.path.insert(0, os.path.abspath("../../../../..")) + +from litellm.llms.gigachat.passthrough.transformation import GigaChatPassthroughConfig +from litellm.types.utils import EmbeddingResponse, ModelResponse + + +def _gigachat_chat_completion_body(): + return { + "id": "chatcmpl-test123", + "object": "chat.completion", + "created": 1700000000, + "model": "GigaChat", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "Hello from GigaChat", + }, + "finish_reason": "stop", + } + ], + "usage": { + "prompt_tokens": 5, + "completion_tokens": 3, + "total_tokens": 8, + }, + } + + +def _gigachat_embedding_body(): + return { + "object": "list", + "data": [ + { + "object": "embedding", + "embedding": [0.1, 0.2, 0.3], + "index": 0, + "usage": {"prompt_tokens": 4}, + } + ], + "model": "Embeddings", + } + + +def _make_httpx_response(body: dict) -> httpx.Response: + return httpx.Response( + status_code=200, + headers={"content-type": "application/json"}, + content=json.dumps(body).encode("utf-8"), + request=httpx.Request( + "POST", "https://gigachat.devices.sberbank.ru/api/v1/chat/completions" + ), + ) + + +class TestGigaChatPassthroughConfig: + """Tests for GigaChatPassthroughConfig class.""" + + def test_is_streaming_request_true(self): + """Test streaming is detected when stream=True.""" + config = GigaChatPassthroughConfig() + assert ( + config.is_streaming_request("chat/completions", {"stream": True}) is True + ) + + def test_is_streaming_request_false(self): + """Test streaming is not detected when stream=False.""" + config = GigaChatPassthroughConfig() + assert ( + config.is_streaming_request("chat/completions", {"stream": False}) + is False + ) + + def test_is_streaming_request_missing_stream_key(self): + """Test streaming defaults to False when stream key is missing.""" + config = GigaChatPassthroughConfig() + assert ( + config.is_streaming_request("chat/completions", {"model": "GigaChat"}) + is False + ) + + def test_get_complete_url_with_api_base(self): + """Test URL construction with explicit api_base.""" + config = GigaChatPassthroughConfig() + api_base = "https://custom.gigachat.ru/api/v1" + endpoint = "chat/completions" + + complete_url, base_target_url = config.get_complete_url( + api_base=api_base, + api_key=None, + model="GigaChat", + endpoint=endpoint, + request_query_params=None, + litellm_params={}, + ) + + assert isinstance(complete_url, httpx.URL) + assert str(complete_url) == f"{api_base}/{endpoint}" + assert base_target_url == api_base + + def test_get_complete_url_with_leading_slash_endpoint(self): + """Test URL construction with endpoint having leading slash.""" + config = GigaChatPassthroughConfig() + api_base = "https://custom.gigachat.ru/api/v1" + endpoint = "/chat/completions" + + complete_url, base_target_url = config.get_complete_url( + api_base=api_base, + api_key=None, + model="GigaChat", + endpoint=endpoint, + request_query_params=None, + litellm_params={}, + ) + + assert str(complete_url) == "https://custom.gigachat.ru/api/v1/chat/completions" + assert base_target_url == api_base + + @patch( + "litellm.llms.gigachat.passthrough.transformation.get_secret_str" + ) + def test_get_complete_url_with_env_api_base(self, mock_get_secret): + """Test URL construction with api_base from environment.""" + config = GigaChatPassthroughConfig() + env_api_base = "https://env.gigachat.ru/api/v1" + mock_get_secret.return_value = env_api_base + + complete_url, base_target_url = config.get_complete_url( + api_base=None, + api_key=None, + model="GigaChat", + endpoint="embeddings", + request_query_params=None, + litellm_params={}, + ) + + assert isinstance(complete_url, httpx.URL) + assert str(complete_url).startswith(env_api_base) + assert base_target_url == env_api_base + mock_get_secret.assert_called_once_with("GIGACHAT_API_BASE") + + @patch( + "litellm.llms.gigachat.passthrough.transformation.get_secret_str" + ) + def test_get_complete_url_fallback_to_default(self, mock_get_secret): + """Test URL construction falls back to default GIGACHAT_BASE_URL.""" + config = GigaChatPassthroughConfig() + mock_get_secret.return_value = None + + complete_url, base_target_url = config.get_complete_url( + api_base=None, + api_key=None, + model="GigaChat", + endpoint="models", + request_query_params=None, + litellm_params={}, + ) + + assert isinstance(complete_url, httpx.URL) + assert "gigachat.devices.sberbank.ru" in str(complete_url) + assert base_target_url == "https://gigachat.devices.sberbank.ru/api/v1" + + def test_get_complete_url_no_api_base_raises(self): + """Test that exception is raised when no api_base can be resolved.""" + config = GigaChatPassthroughConfig() + with patch( + "litellm.llms.gigachat.passthrough.transformation.get_secret_str", + return_value=None, + ): + with patch( + "litellm.llms.gigachat.passthrough.transformation.GIGACHAT_BASE_URL", + None, + ): + with pytest.raises(Exception, match="GigaChat api base not found"): + config.get_complete_url( + api_base=None, + api_key=None, + model="GigaChat", + endpoint="chat/completions", + request_query_params=None, + litellm_params={}, + ) + + @patch( + "litellm.llms.gigachat.passthrough.transformation.get_access_token" + ) + def test_validate_environment(self, mock_get_access_token): + """Test headers are set correctly with OAuth token.""" + config = GigaChatPassthroughConfig() + mock_get_access_token.return_value = "test-token-123" + + headers = config.validate_environment( + headers={}, + model="GigaChat", + messages=[{"role": "user", "content": "hi"}], + optional_params={}, + litellm_params={}, + api_key="test-credentials", + api_base="https://custom.gigachat.ru", + ) + + assert headers["Authorization"] == "Bearer test-token-123" + assert headers["Content-Type"] == "application/json" + assert headers["Accept"] == "application/json" + mock_get_access_token.assert_called_once_with( + credentials="test-credentials", + litellm_params={}, + ) + + def test_logging_non_streaming_response_chat_completions(self): + """Test chat completions endpoint returns ModelResponse.""" + config = GigaChatPassthroughConfig() + logging_obj = MagicMock() + + result = config.logging_non_streaming_response( + model="gigachat/GigaChat", + custom_llm_provider="gigachat", + httpx_response=_make_httpx_response(_gigachat_chat_completion_body()), + request_data={ + "model": "gigachat/GigaChat", + "messages": [{"role": "user", "content": "hi"}], + }, + logging_obj=logging_obj, + endpoint="chat/completions", + ) + + assert isinstance(result, ModelResponse) + assert result.choices[0].message.content == "Hello from GigaChat" + assert result.usage.prompt_tokens == 5 + assert result.usage.completion_tokens == 3 + assert result.usage.total_tokens == 8 + + def test_logging_non_streaming_response_embeddings(self): + """Test embeddings endpoint returns EmbeddingResponse.""" + config = GigaChatPassthroughConfig() + logging_obj = MagicMock() + + result = config.logging_non_streaming_response( + model="gigachat/Embeddings", + custom_llm_provider="gigachat", + httpx_response=_make_httpx_response(_gigachat_embedding_body()), + request_data={"input": ["hello"], "model": "gigachat/Embeddings"}, + logging_obj=logging_obj, + endpoint="embeddings", + ) + + assert isinstance(result, EmbeddingResponse) + assert len(result.data) == 1 + assert result.data[0]["embedding"] == [0.1, 0.2, 0.3] + + def test_logging_non_streaming_response_unknown_endpoint_returns_none(self): + """Test unknown endpoint returns None.""" + config = GigaChatPassthroughConfig() + logging_obj = MagicMock() + + result = config.logging_non_streaming_response( + model="gigachat/GigaChat", + custom_llm_provider="gigachat", + httpx_response=_make_httpx_response(_gigachat_chat_completion_body()), + request_data={}, + logging_obj=logging_obj, + endpoint="images/generations", + ) + + assert result is None + + def test_handle_logging_collected_chunks_with_string_chunks(self): + """Test converting string chunks to model response.""" + config = GigaChatPassthroughConfig() + logging_obj = MagicMock() + + chunks = [ + '{"choices": [{"delta": {"content": "Hello"}, "index": 0}]}', + '{"choices": [{"delta": {"content": " world"}, "index": 0}]}', + '{"choices": [{"delta": {}, "finish_reason": "stop", "index": 0}], "usage": {"prompt_tokens": 5, "completion_tokens": 2, "total_tokens": 7}}', + ] + + result = config.handle_logging_collected_chunks( + all_chunks=chunks, + litellm_logging_obj=logging_obj, + model="gigachat/GigaChat", + custom_llm_provider="gigachat", + endpoint="chat/completions", + ) + + assert isinstance(result, ModelResponse) + assert result.choices[0].message.content == "Hello world" + + def test_handle_logging_collected_chunks_with_bytes_chunks(self): + """Test converting bytes chunks to model response.""" + config = GigaChatPassthroughConfig() + logging_obj = MagicMock() + + chunks = [ + b'{"choices": [{"delta": {"content": "Hi"}, "index": 0}]}', + b'{"choices": [{"delta": {}, "finish_reason": "stop", "index": 0}], "usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2}}', + ] + + result = config.handle_logging_collected_chunks( + all_chunks=chunks, + litellm_logging_obj=logging_obj, + model="gigachat/GigaChat", + custom_llm_provider="gigachat", + endpoint="chat/completions", + ) + + assert isinstance(result, ModelResponse) + assert result.choices[0].message.content == "Hi" + + def test_handle_logging_collected_chunks_with_done_and_empty(self): + """Test that [DONE] and empty chunks are skipped.""" + config = GigaChatPassthroughConfig() + logging_obj = MagicMock() + + chunks = [ + "", + "[DONE]", + '{"choices": [{"delta": {"content": "test"}, "index": 0}]}', + '{"choices": [{"delta": {}, "finish_reason": "stop", "index": 0}], "usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2}}', + ] + + result = config.handle_logging_collected_chunks( + all_chunks=chunks, + litellm_logging_obj=logging_obj, + model="gigachat/GigaChat", + custom_llm_provider="gigachat", + endpoint="chat/completions", + ) + + assert isinstance(result, ModelResponse) + assert result.choices[0].message.content == "test" + + def test_handle_logging_collected_chunks_with_dict_chunks(self): + """Test converting dict chunks directly.""" + config = GigaChatPassthroughConfig() + logging_obj = MagicMock() + + chunks = [ + {"choices": [{"delta": {"content": "direct"}, "index": 0}]}, + { + "choices": [ + { + "delta": {}, + "finish_reason": "stop", + "index": 0, + } + ], + "usage": { + "prompt_tokens": 1, + "completion_tokens": 1, + "total_tokens": 2, + }, + }, + ] + + result = config.handle_logging_collected_chunks( + all_chunks=chunks, + litellm_logging_obj=logging_obj, + model="gigachat/GigaChat", + custom_llm_provider="gigachat", + endpoint="chat/completions", + ) + + assert isinstance(result, ModelResponse) + assert result.choices[0].message.content == "direct" + + def test_handle_logging_collected_chunks_empty_list_returns_none(self): + """Test empty chunks list returns None.""" + config = GigaChatPassthroughConfig() + logging_obj = MagicMock() + + result = config.handle_logging_collected_chunks( + all_chunks=[], + litellm_logging_obj=logging_obj, + model="gigachat/GigaChat", + custom_llm_provider="gigachat", + endpoint="chat/completions", + ) + + assert result is None + + def test_handle_logging_collected_chunks_invalid_json_skipped(self): + """Test invalid JSON chunks are skipped gracefully.""" + config = GigaChatPassthroughConfig() + logging_obj = MagicMock() + + chunks = [ + "not-valid-json", + '{"choices": [{"delta": {"content": "valid"}, "index": 0}]}', + '{"choices": [{"delta": {}, "finish_reason": "stop", "index": 0}], "usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2}}', + ] + + result = config.handle_logging_collected_chunks( + all_chunks=chunks, + litellm_logging_obj=logging_obj, + model="gigachat/GigaChat", + custom_llm_provider="gigachat", + endpoint="chat/completions", + ) + + assert isinstance(result, ModelResponse) + assert result.choices[0].message.content == "valid" + + @patch( + "litellm.llms.gigachat.passthrough.transformation.get_secret_str" + ) + def test_get_api_base_with_explicit_value(self, mock_get_secret): + """Test get_api_base returns explicit value when provided.""" + explicit_base = "https://custom.gigachat.ru/api/v1" + result = GigaChatPassthroughConfig.get_api_base(api_base=explicit_base) + assert result == explicit_base + mock_get_secret.assert_not_called() + + @patch( + "litellm.llms.gigachat.passthrough.transformation.get_secret_str" + ) + def test_get_api_base_from_environment(self, mock_get_secret): + """Test get_api_base retrieves from environment when not provided.""" + env_base = "https://env.gigachat.ru/api/v1" + mock_get_secret.return_value = env_base + result = GigaChatPassthroughConfig.get_api_base(api_base=None) + assert result == env_base + mock_get_secret.assert_called_once_with("GIGACHAT_API_BASE") + + @patch( + "litellm.llms.gigachat.passthrough.transformation.get_secret_str" + ) + def test_get_api_base_fallback_to_default(self, mock_get_secret): + """Test get_api_base falls back to GIGACHAT_BASE_URL.""" + mock_get_secret.return_value = None + result = GigaChatPassthroughConfig.get_api_base(api_base=None) + assert result == "https://gigachat.devices.sberbank.ru/api/v1" + + @patch( + "litellm.llms.gigachat.passthrough.transformation.get_secret_str" + ) + def test_get_api_key_with_explicit_value(self, mock_get_secret): + """Test get_api_key returns explicit value when provided.""" + explicit_key = "test-api-key" + result = GigaChatPassthroughConfig.get_api_key(api_key=explicit_key) + assert result == explicit_key + mock_get_secret.assert_not_called() + + @patch( + "litellm.llms.gigachat.passthrough.transformation.get_secret_str" + ) + def test_get_api_key_from_environment(self, mock_get_secret): + """Test get_api_key retrieves from environment when not provided.""" + env_key = "env-api-key" + mock_get_secret.return_value = env_key + result = GigaChatPassthroughConfig.get_api_key(api_key=None) + assert result == env_key + mock_get_secret.assert_called_once_with("GIGACHAT_API_KEY") + + def test_get_base_model_returns_model(self): + """Test get_base_model returns the model as-is.""" + model = "gigachat/GigaChat" + result = GigaChatPassthroughConfig.get_base_model(model) + assert result == model + + def test_get_models(self): + """Test get_models delegates to base class.""" + config = GigaChatPassthroughConfig() + result = config.get_models() + assert result == [] From 4fc3e2e06441663d8d2016b5c083a426327c26f1 Mon Sep 17 00:00:00 2001 From: KnyazSh Date: Mon, 22 Jun 2026 18:46:48 +0000 Subject: [PATCH 022/544] add expires_at check to gigachat authenticator --- litellm/llms/gigachat/authenticator.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/litellm/llms/gigachat/authenticator.py b/litellm/llms/gigachat/authenticator.py index 5418b3bcb8f..6c6a3d58b46 100644 --- a/litellm/llms/gigachat/authenticator.py +++ b/litellm/llms/gigachat/authenticator.py @@ -112,12 +112,13 @@ def get_access_token( # Request new token token, expires_at = _request_token_sync(credentials, scope, auth_url) - # Cache token - ttl_seconds = max( - 0, (expires_at - TOKEN_EXPIRY_BUFFER_MS - time.time() * 1000) / 1000 - ) - if ttl_seconds > 0: - _token_cache.set_cache(cache_key, (token, expires_at), ttl=ttl_seconds) + if expires_at: + # Cache token + ttl_seconds = max( + 0, (expires_at - TOKEN_EXPIRY_BUFFER_MS - time.time() * 1000) / 1000 + ) + if ttl_seconds > 0: + _token_cache.set_cache(cache_key, (token, expires_at), ttl=ttl_seconds) return token From 625ce95091b3c153746433796b1eb724e514e95a Mon Sep 17 00:00:00 2001 From: Yuriy Date: Mon, 22 Jun 2026 22:11:58 +0300 Subject: [PATCH 023/544] Update litellm/proxy/common_request_processing.py Co-authored-by: veria-ai[bot] <224490171+veria-ai[bot]@users.noreply.github.com> --- litellm/proxy/common_request_processing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/proxy/common_request_processing.py b/litellm/proxy/common_request_processing.py index 2ffa5682a39..38ccdeb7c5d 100644 --- a/litellm/proxy/common_request_processing.py +++ b/litellm/proxy/common_request_processing.py @@ -740,7 +740,7 @@ class ProxyBaseLLMRequestProcessing: Proxy/custom headers win on key collisions. """ - excluded_headers = {"transfer-encoding", "content-encoding"} + excluded_headers = {"transfer-encoding", "content-encoding", "set-cookie", "connection", "keep-alive", "proxy-authenticate", "proxy-authorization", "te", "trailer", "upgrade"} merged_headers = { key: value From 47ddd1c4c3b3f9a132b1e8eb2fb29c3f5c764bd9 Mon Sep 17 00:00:00 2001 From: KnyazSh Date: Mon, 22 Jun 2026 19:39:15 +0000 Subject: [PATCH 024/544] format litellm/proxy/common_request_processing.py --- litellm/proxy/common_request_processing.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/common_request_processing.py b/litellm/proxy/common_request_processing.py index 38ccdeb7c5d..e440a748744 100644 --- a/litellm/proxy/common_request_processing.py +++ b/litellm/proxy/common_request_processing.py @@ -740,7 +740,18 @@ class ProxyBaseLLMRequestProcessing: Proxy/custom headers win on key collisions. """ - excluded_headers = {"transfer-encoding", "content-encoding", "set-cookie", "connection", "keep-alive", "proxy-authenticate", "proxy-authorization", "te", "trailer", "upgrade"} + excluded_headers = { + "transfer-encoding", + "content-encoding", + "set-cookie", + "connection", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "te", + "trailer", + "upgrade", + } merged_headers = { key: value From 7493288a9a1e0c9402bb0b265b1aafe26d6b3e12 Mon Sep 17 00:00:00 2001 From: KnyazSh Date: Fri, 26 Jun 2026 05:28:52 +0000 Subject: [PATCH 025/544] format litellm/llms/gigachat/passthrough/transformation.py --- litellm/llms/gigachat/passthrough/transformation.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/litellm/llms/gigachat/passthrough/transformation.py b/litellm/llms/gigachat/passthrough/transformation.py index 54277081398..4e6849b13d9 100644 --- a/litellm/llms/gigachat/passthrough/transformation.py +++ b/litellm/llms/gigachat/passthrough/transformation.py @@ -85,7 +85,6 @@ class GigaChatPassthroughConfig(BasePassthroughConfig): # cost tracking only for completions and embeddings if "completions" in endpoint: - provider_chat_config = ProviderConfigManager.get_provider_chat_config( provider=LlmProviders(custom_llm_provider), model=model, @@ -112,7 +111,6 @@ class GigaChatPassthroughConfig(BasePassthroughConfig): return litellm_model_response if "embeddings" in endpoint: - provider_embedding_config = ( ProviderConfigManager.get_provider_embedding_config( provider=LlmProviders(custom_llm_provider), From f88444063ad25c22fe6aa409de20ecd522681e1f Mon Sep 17 00:00:00 2001 From: KnyazSh Date: Tue, 30 Jun 2026 08:02:06 +0000 Subject: [PATCH 026/544] sort imports for fix I001 --- litellm/proxy/_types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 4a890b658c8..38eea4d8b82 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -1006,10 +1006,10 @@ class LiteLLM_ObjectPermissionBase(LiteLLMPydanticObjectBase): search_tools: Optional[List[str]] = None +from litellm.models.team import BudgetLimitEntry as BudgetLimitEntry # noqa: E402 from litellm.types.object_permission import ( # noqa: E402 ObjectPermissionDict as ObjectPermissionDict, ) -from litellm.models.team import BudgetLimitEntry as BudgetLimitEntry # noqa: E402 class GenerateRequestBase(LiteLLMPydanticObjectBase): From 4614f1ffd338928a2d4fb4acbdc581044052c051 Mon Sep 17 00:00:00 2001 From: KnyazSh Date: Tue, 30 Jun 2026 08:04:35 +0000 Subject: [PATCH 027/544] extract _transform_list_content for gigachat to fix C901 --- litellm/llms/gigachat/chat/transformation.py | 49 +++++++++++++------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/litellm/llms/gigachat/chat/transformation.py b/litellm/llms/gigachat/chat/transformation.py index b9a6ce30e48..cd671e746e8 100644 --- a/litellm/llms/gigachat/chat/transformation.py +++ b/litellm/llms/gigachat/chat/transformation.py @@ -272,6 +272,36 @@ class GigaChatConfig(BaseConfig): verbose_logger.error(f"Failed to upload image: {e}") return None + def _transform_list_content(self, content: list) -> tuple[str, list[str]]: + """ + Extract text and image attachments from a multimodal message content list. + + Args: + content: List of content parts (OpenAI multimodal format) + + Returns: + Tuple of (combined text, list of attachment file ids) + """ + texts = [] + attachments = [] + for part in content: + if isinstance(part, dict): + if part.get("type") == "text": + texts.append(part.get("text", "")) + elif part.get("type") == "image_url": + # Extract image URL and upload to GigaChat + image_url = part.get("image_url", {}) + if isinstance(image_url, str): + url = image_url + else: + url = image_url.get("url", "") + if url: + file_id = self._upload_image(url) + if file_id: + attachments.append(file_id) + text = "\n".join(texts) if texts else "" + return text, attachments + def transform_request( self, model: str, @@ -340,24 +370,7 @@ class GigaChatConfig(BaseConfig): # Handle list content (multimodal) - extract text and images content = message.get("content") if isinstance(content, list): - texts = [] - attachments = [] - for part in content: - if isinstance(part, dict): - if part.get("type") == "text": - texts.append(part.get("text", "")) - elif part.get("type") == "image_url": - # Extract image URL and upload to GigaChat - image_url = part.get("image_url", {}) - if isinstance(image_url, str): - url = image_url - else: - url = image_url.get("url", "") - if url: - file_id = self._upload_image(url) - if file_id: - attachments.append(file_id) - message["content"] = "\n".join(texts) if texts else "" + message["content"], attachments = self._transform_list_content(content) if attachments: message["attachments"] = attachments From c4af986f9c50bcc904345cb6db26d05436d64ab2 Mon Sep 17 00:00:00 2001 From: KnyazSh Date: Thu, 2 Jul 2026 17:15:17 +0000 Subject: [PATCH 028/544] fix(lint): resolve UP007 violations --- litellm/llms/gigachat/chat/transformation.py | 10 +++--- .../llms/gigachat/embedding/transformation.py | 7 ++-- litellm/passthrough/main.py | 34 +++++++++---------- .../llm_passthrough_endpoints.py | 8 +++-- 4 files changed, 31 insertions(+), 28 deletions(-) diff --git a/litellm/llms/gigachat/chat/transformation.py b/litellm/llms/gigachat/chat/transformation.py index cd671e746e8..2116cc86d2b 100644 --- a/litellm/llms/gigachat/chat/transformation.py +++ b/litellm/llms/gigachat/chat/transformation.py @@ -4,10 +4,12 @@ GigaChat Chat Transformation Transforms OpenAI-format requests to GigaChat format and back. """ +from __future__ import annotations + import json import time import uuid -from typing import TYPE_CHECKING, Any, AsyncIterator, Iterator, Union +from typing import TYPE_CHECKING, Any, AsyncIterator, Iterator import httpx @@ -212,7 +214,7 @@ class GigaChatConfig(BaseConfig): ) return functions - def _map_tool_choice(self, tool_choice: Union[str, dict]) -> Union[str, dict] | None: + def _map_tool_choice(self, tool_choice: str | dict) -> str | dict | None: """ Map OpenAI tool_choice to GigaChat function_call format. @@ -488,7 +490,7 @@ class GigaChatConfig(BaseConfig): self, error_message: str, status_code: int, - headers: Union[dict, httpx.Headers], + headers: dict | httpx.Headers, ) -> BaseLLMException: """Return GigaChat error class.""" return GigaChatError( @@ -499,7 +501,7 @@ class GigaChatConfig(BaseConfig): def get_model_response_iterator( self, - streaming_response: Union[Iterator[str], AsyncIterator[str], ModelResponse], + streaming_response: Iterator[str] | AsyncIterator[str] | ModelResponse, sync_stream: bool, json_mode: bool | None = False, ): diff --git a/litellm/llms/gigachat/embedding/transformation.py b/litellm/llms/gigachat/embedding/transformation.py index ba79f2205fc..9495e2b414f 100644 --- a/litellm/llms/gigachat/embedding/transformation.py +++ b/litellm/llms/gigachat/embedding/transformation.py @@ -5,8 +5,9 @@ Transforms OpenAI /v1/embeddings format to GigaChat format. API Documentation: https://developers.sber.ru/docs/ru/gigachat/api/reference/rest/post-embeddings """ +from __future__ import annotations + import types -from typing import Union import httpx @@ -200,9 +201,7 @@ class GigaChatEmbeddingConfig(BaseEmbeddingConfig): } return {**default_headers, **headers} - def get_error_class( - self, error_message: str, status_code: int, headers: Union[dict, httpx.Headers] - ) -> BaseLLMException: + def get_error_class(self, error_message: str, status_code: int, headers: dict | httpx.Headers) -> BaseLLMException: """Return GigaChat-specific error class.""" return GigaChatEmbeddingError( status_code=status_code, diff --git a/litellm/passthrough/main.py b/litellm/passthrough/main.py index 0a8c57fb482..9cf9c80b900 100644 --- a/litellm/passthrough/main.py +++ b/litellm/passthrough/main.py @@ -2,6 +2,8 @@ This module is used to pass through requests to the LLM APIs. """ +from __future__ import annotations + import asyncio import contextvars from functools import partial @@ -12,8 +14,6 @@ from typing import ( Coroutine, Generator, List, - Optional, - Union, cast, ) @@ -239,9 +239,9 @@ async def allm_passthrough_route( json: Any | None = None, params: QueryParamTypes | None = None, cookies: CookieTypes | None = None, - client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None, + client: HTTPHandler | AsyncHTTPHandler | None = None, **kwargs, -) -> Union[httpx.Response, AsyncGenerator[Any, Any]]: +) -> httpx.Response | AsyncGenerator[Any, Any]: """ Async: Reranks a list of documents based on their relevance to the query """ @@ -260,7 +260,7 @@ async def allm_passthrough_route( from litellm.utils import ProviderConfigManager provider_config = cast( - Optional["BasePassthroughConfig"], kwargs.get("provider_config") + "BasePassthroughConfig" | None, kwargs.get("provider_config") ) or ProviderConfigManager.get_provider_passthrough_config( provider=LlmProviders(custom_llm_provider), model=model, @@ -328,7 +328,7 @@ async def allm_passthrough_route( if resolved_custom_llm_provider: try: provider_config = cast( - Optional["BasePassthroughConfig"], kwargs.get("provider_config") + "BasePassthroughConfig" | None, kwargs.get("provider_config") ) or ProviderConfigManager.get_provider_passthrough_config( provider=LlmProviders(resolved_custom_llm_provider), model=model, @@ -365,15 +365,15 @@ def llm_passthrough_route( json: Any | None = None, params: QueryParamTypes | None = None, cookies: CookieTypes | None = None, - client: Union[HTTPHandler, AsyncHTTPHandler] | None = None, + client: HTTPHandler | AsyncHTTPHandler | None = None, **kwargs, -) -> Union[ - httpx.Response, - Coroutine[Any, Any, httpx.Response], - Coroutine[Any, Any, Union[httpx.Response, AsyncGenerator[Any, Any]]], - Generator[Any, Any, Any], - AsyncGenerator[Any, Any], -]: +) -> ( + httpx.Response + | Coroutine[Any, Any, httpx.Response] + | Coroutine[Any, Any, httpx.Response | AsyncGenerator[Any, Any]] + | Generator[Any, Any, Any] + | AsyncGenerator[Any, Any] +): """ Pass through requests to the LLM APIs. @@ -432,7 +432,7 @@ def llm_passthrough_route( ) provider_config = cast( - Optional["BasePassthroughConfig"], kwargs.get("provider_config") + "BasePassthroughConfig" | None, kwargs.get("provider_config") ) or ProviderConfigManager.get_provider_passthrough_config( provider=LlmProviders(custom_llm_provider), model=model, @@ -547,12 +547,12 @@ def llm_passthrough_route( async def _async_passthrough_request( - client: Union[HTTPHandler, AsyncHTTPHandler], + client: HTTPHandler | AsyncHTTPHandler, request: httpx.Request, is_streaming_request: bool, litellm_logging_obj: "LiteLLMLoggingObj", provider_config: "BasePassthroughConfig", -) -> Union[httpx.Response, AsyncGenerator[Any, Any]]: +) -> httpx.Response | AsyncGenerator[Any, Any]: """ Handle async passthrough requests. Uses async client to send request and properly handles streaming. diff --git a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py index c80e5860571..ad3f22af7a1 100644 --- a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py @@ -6,10 +6,12 @@ Provider-specific Pass-Through Endpoints Use litellm with Anthropic SDK, Vertex AI SDK, Cohere SDK, etc. """ +from __future__ import annotations + import json import os import re -from typing import TYPE_CHECKING, Any, Callable, Union, cast +from typing import TYPE_CHECKING, Any, Callable, cast import httpx from fastapi import APIRouter, Depends, HTTPException, Request, Response, WebSocket @@ -730,7 +732,7 @@ async def handle_bedrock_passthrough_router_model( user_max_tokens: int | None, user_api_base: str | None, version: str | None, -) -> Union[Response, StreamingResponse]: +) -> Response | StreamingResponse: """ Handle Bedrock passthrough for router models (models defined in config.yaml). @@ -2421,7 +2423,7 @@ async def handle_gigachat_passthrough_router_model( user_max_tokens: int | None, user_api_base: str | None, version: str | None, -) -> Union[Response, StreamingResponse]: +) -> Response | StreamingResponse: """ Handle Gigachat passthrough for router models (models defined in config.yaml). From ff4794c02350c0fd320ee0f80a2f86d77e4402fb Mon Sep 17 00:00:00 2001 From: KnyazSh Date: Thu, 2 Jul 2026 18:16:29 +0000 Subject: [PATCH 029/544] fix(lint) : resolve UP037 violations --- litellm/litellm_core_utils/litellm_logging.py | 12 +++---- litellm/passthrough/main.py | 31 ++++++++--------- litellm/utils.py | 34 +++++++++---------- 3 files changed, 36 insertions(+), 41 deletions(-) diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index 6e09542430d..f01d8bf59f6 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -13,7 +13,6 @@ import traceback from datetime import datetime as dt_object from functools import lru_cache from typing import ( - TYPE_CHECKING, Any, Callable, Dict, @@ -75,6 +74,7 @@ from litellm.litellm_core_utils.redact_messages import ( redact_message_input_output_from_logging, ) from litellm.llms.base_llm.ocr.transformation import OCRResponse +from litellm.llms.base_llm.passthrough.transformation import BasePassthroughConfig from litellm.llms.base_llm.search.transformation import SearchResponse from litellm.responses.utils import ResponseAPILoggingUtils from litellm.types.agents import LiteLLMSendMessageResponse @@ -173,8 +173,6 @@ from .initialize_dynamic_callback_params import ( ) from .specialty_caches.dynamic_logging_cache import DynamicLoggingCache -if TYPE_CHECKING: - from litellm.llms.base_llm.passthrough.transformation import BasePassthroughConfig try: from litellm_enterprise.enterprise_callbacks.callback_controls import ( EnterpriseCallbackControls, @@ -1367,7 +1365,7 @@ class Logging(LiteLLMLoggingBaseClass): OpenAIFileObject, LiteLLMRealtimeStreamLoggingObject, OpenAIModerationResponse, - "SearchResponse", + SearchResponse, dict, list, ], @@ -1909,7 +1907,7 @@ class Logging(LiteLLMLoggingBaseClass): def _flush_passthrough_collected_chunks_helper( self, raw_bytes: List[bytes], - provider_config: "BasePassthroughConfig", + provider_config: BasePassthroughConfig, ) -> Optional["CostResponseTypes"]: all_chunks = provider_config._convert_raw_bytes_to_str_lines(raw_bytes) complete_streaming_response = provider_config.handle_logging_collected_chunks( @@ -1924,7 +1922,7 @@ class Logging(LiteLLMLoggingBaseClass): def flush_passthrough_collected_chunks( self, raw_bytes: List[bytes], - provider_config: "BasePassthroughConfig", + provider_config: BasePassthroughConfig, ): """ Flush collected chunks from the logging object @@ -1947,7 +1945,7 @@ class Logging(LiteLLMLoggingBaseClass): async def async_flush_passthrough_collected_chunks( self, raw_bytes: List[bytes], - provider_config: "BasePassthroughConfig", + provider_config: BasePassthroughConfig, ): complete_streaming_response = self._flush_passthrough_collected_chunks_helper( raw_bytes=raw_bytes, diff --git a/litellm/passthrough/main.py b/litellm/passthrough/main.py index 9cf9c80b900..75ed91c1d3b 100644 --- a/litellm/passthrough/main.py +++ b/litellm/passthrough/main.py @@ -8,7 +8,6 @@ import asyncio import contextvars from functools import partial from typing import ( - TYPE_CHECKING, Any, AsyncGenerator, Coroutine, @@ -22,6 +21,8 @@ from httpx._types import CookieTypes, QueryParamTypes, RequestFiles from litellm._logging import verbose_logger from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider +from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj +from litellm.llms.base_llm.passthrough.transformation import BasePassthroughConfig from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler from litellm.llms.custom_httpx.llm_http_handler import BaseLLMHTTPHandler from litellm.passthrough.utils import CommonUtils @@ -30,17 +31,13 @@ from litellm.utils import client base_llm_http_handler = BaseLLMHTTPHandler() from .utils import BasePassthroughUtils -if TYPE_CHECKING: - from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj - from litellm.llms.base_llm.passthrough.transformation import BasePassthroughConfig - class AsyncPassthroughStreamingResponse(AsyncGenerator[Any, Any]): def __init__( self, response: Coroutine[Any, Any, httpx.Response], - litellm_logging_obj: "LiteLLMLoggingObj", - provider_config: "BasePassthroughConfig", + litellm_logging_obj: LiteLLMLoggingObj, + provider_config: BasePassthroughConfig, ) -> None: self._initialized = False self._status_code: int = 0 @@ -119,7 +116,7 @@ class AsyncPassthroughStreamingResponse(AsyncGenerator[Any, Any]): e, ) - def __aiter__(self) -> "AsyncPassthroughStreamingResponse": + def __aiter__(self) -> AsyncPassthroughStreamingResponse: return self async def __anext__(self) -> bytes: @@ -160,8 +157,8 @@ class PassthroughStreamingResponse(Generator[Any, Any, Any]): def __init__( self, response: httpx.Response, - litellm_logging_obj: "LiteLLMLoggingObj", - provider_config: "BasePassthroughConfig", + litellm_logging_obj: LiteLLMLoggingObj, + provider_config: BasePassthroughConfig, ) -> None: self._response = response self.headers = response.headers @@ -192,7 +189,7 @@ class PassthroughStreamingResponse(Generator[Any, Any, Any]): e, ) - def __iter__(self) -> "PassthroughStreamingResponse": + def __iter__(self) -> PassthroughStreamingResponse: return self def __next__(self) -> bytes: @@ -260,7 +257,7 @@ async def allm_passthrough_route( from litellm.utils import ProviderConfigManager provider_config = cast( - "BasePassthroughConfig" | None, kwargs.get("provider_config") + BasePassthroughConfig | None, kwargs.get("provider_config") ) or ProviderConfigManager.get_provider_passthrough_config( provider=LlmProviders(custom_llm_provider), model=model, @@ -328,7 +325,7 @@ async def allm_passthrough_route( if resolved_custom_llm_provider: try: provider_config = cast( - "BasePassthroughConfig" | None, kwargs.get("provider_config") + BasePassthroughConfig | None, kwargs.get("provider_config") ) or ProviderConfigManager.get_provider_passthrough_config( provider=LlmProviders(resolved_custom_llm_provider), model=model, @@ -387,7 +384,7 @@ def llm_passthrough_route( _is_async = allm_passthrough_route - litellm_logging_obj = cast("LiteLLMLoggingObj", kwargs.get("litellm_logging_obj")) + litellm_logging_obj = cast(LiteLLMLoggingObj, kwargs.get("litellm_logging_obj")) model, custom_llm_provider, api_key, api_base = get_llm_provider( model=model, @@ -432,7 +429,7 @@ def llm_passthrough_route( ) provider_config = cast( - "BasePassthroughConfig" | None, kwargs.get("provider_config") + BasePassthroughConfig | None, kwargs.get("provider_config") ) or ProviderConfigManager.get_provider_passthrough_config( provider=LlmProviders(custom_llm_provider), model=model, @@ -550,8 +547,8 @@ async def _async_passthrough_request( client: HTTPHandler | AsyncHTTPHandler, request: httpx.Request, is_streaming_request: bool, - litellm_logging_obj: "LiteLLMLoggingObj", - provider_config: "BasePassthroughConfig", + litellm_logging_obj: LiteLLMLoggingObj, + provider_config: BasePassthroughConfig, ) -> httpx.Response | AsyncGenerator[Any, Any]: """ Handle async passthrough requests. diff --git a/litellm/utils.py b/litellm/utils.py index a43d5e9cc9e..a13a2a7fc62 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -549,7 +549,7 @@ def _add_custom_logger_callback_to_specific_event(callback: str, logging_event: def _custom_logger_class_exists_in_success_callbacks( - callback_class: "CustomLogger", + callback_class: CustomLogger, ) -> bool: """ Returns True if an instance of the custom logger exists in litellm.success_callback or litellm._async_success_callback @@ -564,7 +564,7 @@ def _custom_logger_class_exists_in_success_callbacks( def _custom_logger_class_exists_in_failure_callbacks( - callback_class: "CustomLogger", + callback_class: CustomLogger, ) -> bool: """ Returns True if an instance of the custom logger exists in litellm.failure_callback or litellm._async_failure_callback @@ -624,7 +624,7 @@ def load_credentials_from_list(kwargs: dict): def get_dynamic_callbacks( - dynamic_callbacks: Optional[List[Union[str, Callable, "CustomLogger"]]], + dynamic_callbacks: Optional[List[Union[str, Callable, CustomLogger]]], ) -> List: returned_callbacks = litellm.callbacks.copy() if dynamic_callbacks: @@ -752,7 +752,7 @@ def function_setup( coroutine_checker = get_coroutine_checker_fn() ## DYNAMIC CALLBACKS ## - dynamic_callbacks: Optional[List[Union[str, Callable, "CustomLogger"]]] = kwargs.pop("callbacks", None) + dynamic_callbacks: Optional[List[Union[str, Callable, CustomLogger]]] = kwargs.pop("callbacks", None) all_callbacks = get_dynamic_callbacks(dynamic_callbacks=dynamic_callbacks) if len(all_callbacks) > 0: @@ -836,10 +836,10 @@ def function_setup( for index in reversed(removed_async_items): litellm.failure_callback.pop(index) ### DYNAMIC CALLBACKS ### - dynamic_success_callbacks: Optional[List[Union[str, Callable, "CustomLogger"]]] = None - dynamic_async_success_callbacks: Optional[List[Union[str, Callable, "CustomLogger"]]] = None - dynamic_failure_callbacks: Optional[List[Union[str, Callable, "CustomLogger"]]] = None - dynamic_async_failure_callbacks: Optional[List[Union[str, Callable, "CustomLogger"]]] = None + dynamic_success_callbacks: Optional[List[Union[str, Callable, CustomLogger]]] = None + dynamic_async_success_callbacks: Optional[List[Union[str, Callable, CustomLogger]]] = None + dynamic_failure_callbacks: Optional[List[Union[str, Callable, CustomLogger]]] = None + dynamic_async_failure_callbacks: Optional[List[Union[str, Callable, CustomLogger]]] = None if kwargs.get("success_callback", None) is not None and isinstance(kwargs["success_callback"], list): removed_async_items = [] for index, callback in enumerate(kwargs["success_callback"]): @@ -7368,8 +7368,8 @@ def validate_and_fix_openai_tools(tools: Optional[List]) -> Optional[List[dict]] def validate_and_fix_thinking_param( - thinking: Optional["AnthropicThinkingParam"], -) -> Optional["AnthropicThinkingParam"]: + thinking: Optional[AnthropicThinkingParam], +) -> Optional[AnthropicThinkingParam]: """ Normalizes camelCase keys in the thinking param to snake_case. Handles clients that send budgetTokens instead of budget_tokens. @@ -8193,7 +8193,7 @@ class ProviderConfigManager: @staticmethod def get_provider_skills_api_config( provider: LlmProviders, - ) -> Optional["BaseSkillsAPIConfig"]: + ) -> Optional[BaseSkillsAPIConfig]: """ Get provider-specific Skills API configuration @@ -8210,7 +8210,7 @@ class ProviderConfigManager: @staticmethod def get_provider_evals_api_config( provider: LlmProviders, - ) -> Optional["BaseEvalsAPIConfig"]: + ) -> Optional[BaseEvalsAPIConfig]: """ Get provider-specific Evals API configuration @@ -8645,7 +8645,7 @@ class ProviderConfigManager: def get_provider_realtime_http_config( model: str, provider: LlmProviders, - ) -> Optional["BaseRealtimeHTTPConfig"]: + ) -> Optional[BaseRealtimeHTTPConfig]: """ Return the HTTP transformation config for realtime HTTP endpoints (POST /realtime/client_secrets and POST /realtime/calls). @@ -8736,7 +8736,7 @@ class ProviderConfigManager: def get_provider_ocr_config( model: str, provider: LlmProviders, - ) -> Optional["BaseOCRConfig"]: + ) -> Optional[BaseOCRConfig]: """ Get OCR configuration for a given provider. """ @@ -8776,8 +8776,8 @@ class ProviderConfigManager: @staticmethod def get_provider_search_config( - provider: "SearchProviders", - ) -> Optional["BaseSearchConfig"]: + provider: SearchProviders, + ) -> Optional[BaseSearchConfig]: """ Get Search configuration for a given provider. """ @@ -8849,7 +8849,7 @@ class ProviderConfigManager: def get_provider_text_to_speech_config( model: str, provider: LlmProviders, - ) -> Optional["BaseTextToSpeechConfig"]: + ) -> Optional[BaseTextToSpeechConfig]: """ Get text-to-speech configuration for a given provider. """ From 30ed0c14b56f69113ef25909d5479854cde3769a Mon Sep 17 00:00:00 2001 From: KnyazSh Date: Wed, 8 Jul 2026 06:36:55 +0000 Subject: [PATCH 030/544] fix(lint) : resolve LIT006 violations --- .../gigachat/passthrough/transformation.py | 5 ++-- litellm/passthrough/main.py | 27 ++++++++++++++++--- .../llm_passthrough_endpoints.py | 8 +++++- 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/litellm/llms/gigachat/passthrough/transformation.py b/litellm/llms/gigachat/passthrough/transformation.py index 7f37fb91bf6..605037020e3 100644 --- a/litellm/llms/gigachat/passthrough/transformation.py +++ b/litellm/llms/gigachat/passthrough/transformation.py @@ -178,10 +178,9 @@ class GigaChatPassthroughConfig(BasePassthroughConfig): ) translated_chunk = gigachat_iterator.chunk_parser(chunk=message) - if isinstance(translated_chunk, dict) and generic_chunk_has_all_required_fields( - cast(dict, translated_chunk) - ): + if isinstance(translated_chunk, dict) and generic_chunk_has_all_required_fields(translated_chunk): chunk_obj = convert_generic_chunk_to_model_response_stream( + # cast-ok: validated TypedDict cast(GenericStreamingChunk, translated_chunk) ) elif isinstance(translated_chunk, ModelResponseStream): diff --git a/litellm/passthrough/main.py b/litellm/passthrough/main.py index 829aa769491..b3cfb4b61c6 100644 --- a/litellm/passthrough/main.py +++ b/litellm/passthrough/main.py @@ -10,8 +10,10 @@ from functools import partial from typing import ( Any, AsyncGenerator, + AsyncIterator, Coroutine, Generator, + Iterator, List, cast, ) @@ -32,6 +34,16 @@ base_llm_http_handler = BaseLLMHTTPHandler() from .utils import BasePassthroughUtils +async def _as_async_generator(iterable: AsyncIterator[bytes]) -> AsyncGenerator[bytes, Any]: + async for chunk in iterable: + yield chunk + + +def _as_generator(iterable: Iterator[bytes]) -> Generator[bytes, Any, Any]: + for chunk in iterable: + yield chunk + + class AsyncPassthroughStreamingResponse(AsyncGenerator[Any, Any]): def __init__( self, @@ -80,7 +92,7 @@ class AsyncPassthroughStreamingResponse(AsyncGenerator[Any, Any]): self._initialized = True try: self._response.raise_for_status() - self._iterator = cast(AsyncGenerator[bytes, Any], self._response.aiter_bytes()) + self._iterator = _as_async_generator(self._response.aiter_bytes()) except Exception: # noqa: BLE001 try: await self._response.aclose() @@ -123,7 +135,7 @@ class AsyncPassthroughStreamingResponse(AsyncGenerator[Any, Any]): if not self._initialized: await self try: - chunk = await self._iterator.__anext__() + chunk = await anext(self._iterator) self._raw_bytes.append(chunk) return chunk except Exception: # noqa: BLE001 @@ -148,6 +160,7 @@ class AsyncPassthroughStreamingResponse(AsyncGenerator[Any, Any]): self._start_flush() try: if self._initialized: + await self._iterator.aclose() await self._response.aclose() except Exception: # noqa: BLE001 pass @@ -165,7 +178,7 @@ class PassthroughStreamingResponse(Generator[Any, Any, Any]): self.status_code = response.status_code self._litellm_logging_obj = litellm_logging_obj self._provider_config = provider_config - self._iterator: Generator[bytes, Any, Any] = cast(Generator[bytes, Any, Any], response.iter_bytes()) + self._iterator: Generator[bytes, Any, Any] = _as_generator(response.iter_bytes()) self._raw_bytes: List[bytes] = [] self._flush_scheduled = False @@ -383,7 +396,13 @@ def llm_passthrough_route( _is_async = bool(kwargs.get("allm_passthrough_route", False)) - litellm_logging_obj = cast(LiteLLMLoggingObj, kwargs.get("litellm_logging_obj")) + _raw_logging_obj = kwargs.get("litellm_logging_obj") + if not isinstance(_raw_logging_obj, LiteLLMLoggingObj): + raise TypeError( + "litellm_logging_obj is required and must be a LiteLLMLoggingObj instance; " + f"got {type(_raw_logging_obj).__name__}" + ) + litellm_logging_obj: LiteLLMLoggingObj = _raw_logging_obj model, custom_llm_provider, api_key, api_base = get_llm_provider( model=model, diff --git a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py index ad3f22af7a1..dca24203105 100644 --- a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py @@ -455,7 +455,13 @@ async def milvus_proxy_route( request_body = await get_request_body(request) # check collectionName - collection_name = cast(str | None, request_body.get("collectionName")) + _raw_collection_name = request_body.get("collectionName") + if _raw_collection_name is not None and not isinstance(_raw_collection_name, str): + raise HTTPException( + status_code=400, + detail=f"collectionName must be a string. Got {type(_raw_collection_name).__name__}", + ) + collection_name: str | None = _raw_collection_name extra_headers = {} base_target_url: str | None = None if not collection_name: From 12c291c39d33c8ffb1a7cd354291103f67d9e0ff Mon Sep 17 00:00:00 2001 From: KnyazSh Date: Wed, 8 Jul 2026 07:11:50 +0000 Subject: [PATCH 031/544] test(gigachat): add authenticator tests and update passthrough test --- .../llms/gigachat/test_authenticator.py | 491 ++++++++++++++++++ .../passthrough/test_passthrough_main.py | 1 - 2 files changed, 491 insertions(+), 1 deletion(-) create mode 100644 tests/test_litellm/llms/gigachat/test_authenticator.py diff --git a/tests/test_litellm/llms/gigachat/test_authenticator.py b/tests/test_litellm/llms/gigachat/test_authenticator.py new file mode 100644 index 00000000000..dcbac7e0949 --- /dev/null +++ b/tests/test_litellm/llms/gigachat/test_authenticator.py @@ -0,0 +1,491 @@ +""" +Unit tests for GigaChat OAuth authenticator. + +Tests get_access_token and get_access_token_async covering token resolution +from litellm_params/env, credential validation, caching, and error handling. +""" + +import os +import sys +import time +from unittest.mock import AsyncMock, MagicMock, patch + +import httpx +import pytest + +sys.path.insert(0, os.path.abspath("../../../..")) + +from litellm.llms.gigachat import authenticator +from litellm.llms.gigachat.authenticator import ( + GigaChatAuthError, + TOKEN_EXPIRY_BUFFER_MS, + get_access_token, + get_access_token_async, +) + + +AUTH_MODULE = "litellm.llms.gigachat.authenticator" + + +def _future_expires_at_ms(offset_seconds: float = 3600) -> int: + return int(time.time() * 1000 + offset_seconds * 1000) + + +def _past_expires_at_ms(offset_seconds: float = 3600) -> int: + return int(time.time() * 1000 - offset_seconds * 1000) + + +@pytest.fixture(autouse=True) +def _isolate_token_cache(): + """Each test gets a fresh module-level token cache to avoid cross-test leakage.""" + with patch(f"{AUTH_MODULE}._token_cache", new=MagicMock()): + authenticator._token_cache.get_cache.return_value = None + authenticator._token_cache.set_cache = MagicMock() + yield + + +class TestGetAccessTokenSync: + def test_returns_token_from_litellm_params(self): + token = get_access_token(litellm_params={"gigachat_access_token": "param-token"}) + assert token == "param-token" + authenticator._token_cache.get_cache.assert_not_called() + + @patch(f"{AUTH_MODULE}.get_secret_str") + def test_returns_token_from_env(self, mock_get_secret): + mock_get_secret.return_value = "env-access-token" + token = get_access_token() + assert token == "env-access-token" + + @patch(f"{AUTH_MODULE}._get_credentials", return_value=None) + @patch(f"{AUTH_MODULE}.get_secret_str", return_value=None) + def test_raises_when_no_credentials(self, mock_get_secret, mock_get_creds): + with pytest.raises(GigaChatAuthError) as exc_info: + get_access_token() + assert exc_info.value.status_code == 401 + assert "credentials not provided" in exc_info.value.message + + @patch(f"{AUTH_MODULE}._request_token_sync") + @patch(f"{AUTH_MODULE}._get_auth_url", return_value="https://auth.example.com") + @patch(f"{AUTH_MODULE}._get_scope", return_value="GIGACHAT_API_PERS") + @patch(f"{AUTH_MODULE}._get_credentials", return_value=None) + @patch(f"{AUTH_MODULE}.get_secret_str", return_value=None) + def test_raises_when_no_credentials_even_with_other_resolvers( + self, mock_get_secret, mock_get_creds, mock_scope, mock_auth_url, mock_request + ): + with pytest.raises(GigaChatAuthError) as exc_info: + get_access_token() + assert exc_info.value.status_code == 401 + mock_request.assert_not_called() + + @patch(f"{AUTH_MODULE}._request_token_sync") + @patch(f"{AUTH_MODULE}._get_auth_url", return_value="https://auth.example.com") + @patch(f"{AUTH_MODULE}._get_scope", return_value="GIGACHAT_API_PERS") + @patch(f"{AUTH_MODULE}._get_credentials", return_value="creds-from-env") + @patch(f"{AUTH_MODULE}.get_secret_str", return_value=None) + def test_requests_new_token_and_caches(self, mock_get_secret, mock_creds, mock_scope, mock_auth_url, mock_request): + token = "fresh-token" + expires_at = _future_expires_at_ms() + mock_request.return_value = (token, expires_at) + + result = get_access_token() + + assert result == token + mock_request.assert_called_once_with("creds-from-env", "GIGACHAT_API_PERS", "https://auth.example.com") + authenticator._token_cache.set_cache.assert_called_once() + call_args = authenticator._token_cache.set_cache.call_args + assert call_args.args[1] == (token, expires_at) + assert call_args.kwargs["ttl"] > 0 + + @patch(f"{AUTH_MODULE}._request_token_sync") + @patch(f"{AUTH_MODULE}._get_auth_url", return_value="https://auth.example.com") + @patch(f"{AUTH_MODULE}._get_scope", return_value="GIGACHAT_API_PERS") + @patch(f"{AUTH_MODULE}._get_credentials", return_value="creds") + @patch(f"{AUTH_MODULE}.get_secret_str", return_value=None) + def test_does_not_cache_when_no_expiry(self, mock_get_secret, mock_creds, mock_scope, mock_auth_url, mock_request): + mock_request.return_value = ("token-no-exp", 0) + + result = get_access_token() + + assert result == "token-no-exp" + authenticator._token_cache.set_cache.assert_not_called() + + @patch(f"{AUTH_MODULE}._request_token_sync") + @patch(f"{AUTH_MODULE}._get_auth_url", return_value="https://auth.example.com") + @patch(f"{AUTH_MODULE}._get_scope", return_value="GIGACHAT_API_PERS") + @patch(f"{AUTH_MODULE}._get_credentials", return_value="creds") + @patch(f"{AUTH_MODULE}.get_secret_str", return_value=None) + def test_does_not_cache_when_ttl_non_positive(self, mock_get_secret, mock_creds, mock_scope, mock_auth_url, mock_request): + expires_at = int(time.time() * 1000) + TOKEN_EXPIRY_BUFFER_MS - 1000 + mock_request.return_value = ("token", expires_at) + + result = get_access_token() + + assert result == "token" + authenticator._token_cache.set_cache.assert_not_called() + + @patch(f"{AUTH_MODULE}._request_token_sync") + @patch(f"{AUTH_MODULE}._get_auth_url", return_value="https://auth.example.com") + @patch(f"{AUTH_MODULE}._get_scope", return_value="GIGACHAT_API_PERS") + @patch(f"{AUTH_MODULE}._get_credentials", return_value="creds") + @patch(f"{AUTH_MODULE}.get_secret_str", return_value=None) + def test_returns_cached_valid_token(self, mock_get_secret, mock_creds, mock_scope, mock_auth_url, mock_request): + cached_token = "cached-token" + cached_expires_at = _future_expires_at_ms(offset_seconds=7200) + authenticator._token_cache.get_cache.return_value = (cached_token, cached_expires_at) + + result = get_access_token(credentials="creds") + + assert result == cached_token + mock_request.assert_not_called() + authenticator._token_cache.set_cache.assert_not_called() + + @patch(f"{AUTH_MODULE}._request_token_sync") + @patch(f"{AUTH_MODULE}._get_auth_url", return_value="https://auth.example.com") + @patch(f"{AUTH_MODULE}._get_scope", return_value="GIGACHAT_API_PERS") + @patch(f"{AUTH_MODULE}._get_credentials", return_value="creds") + @patch(f"{AUTH_MODULE}.get_secret_str", return_value=None) + def test_requests_new_token_when_cache_expired(self, mock_get_secret, mock_creds, mock_scope, mock_auth_url, mock_request): + cached_token = "stale-token" + cached_expires_at = _past_expires_at_ms(offset_seconds=10) + authenticator._token_cache.get_cache.return_value = (cached_token, cached_expires_at) + + new_token = "refreshed-token" + mock_request.return_value = (new_token, _future_expires_at_ms()) + + result = get_access_token(credentials="creds") + + assert result == new_token + mock_request.assert_called_once() + + @patch(f"{AUTH_MODULE}._request_token_sync") + @patch(f"{AUTH_MODULE}._get_auth_url", return_value="https://default-auth.example.com") + @patch(f"{AUTH_MODULE}._get_scope", return_value="GIGACHAT_API_PERS") + @patch(f"{AUTH_MODULE}._get_credentials", return_value="env-creds") + @patch(f"{AUTH_MODULE}.get_secret_str", return_value=None) + def test_litellm_params_override_scope_and_auth_url(self, mock_get_secret, mock_creds, mock_scope, mock_auth_url, mock_request): + mock_request.return_value = ("token", _future_expires_at_ms()) + + get_access_token( + litellm_params={ + "gigachat_scope": "GIGACHAT_API_CORP", + "gigachat_auth_url": "https://params-auth.example.com", + } + ) + + mock_request.assert_called_once_with( + "env-creds", "GIGACHAT_API_CORP", "https://params-auth.example.com" + ) + + @patch(f"{AUTH_MODULE}._request_token_sync") + @patch(f"{AUTH_MODULE}._get_auth_url", return_value="https://default-auth.example.com") + @patch(f"{AUTH_MODULE}._get_scope", return_value="GIGACHAT_API_PERS") + @patch(f"{AUTH_MODULE}._get_credentials", return_value="env-creds") + @patch(f"{AUTH_MODULE}.get_secret_str", return_value=None) + def test_explicit_args_override_everything(self, mock_get_secret, mock_creds, mock_scope, mock_auth_url, mock_request): + mock_request.return_value = ("token", _future_expires_at_ms()) + + get_access_token( + credentials="explicit-creds", + scope="EXPLICIT_SCOPE", + auth_url="https://explicit.example.com", + litellm_params={ + "gigachat_scope": "PARAM_SCOPE", + "gigachat_auth_url": "https://params.example.com", + }, + ) + + mock_request.assert_called_once_with( + "explicit-creds", "EXPLICIT_SCOPE", "https://explicit.example.com" + ) + + @patch(f"{AUTH_MODULE}._request_token_sync") + @patch(f"{AUTH_MODULE}._get_auth_url", return_value="https://auth.example.com") + @patch(f"{AUTH_MODULE}._get_scope", return_value="GIGACHAT_API_PERS") + @patch(f"{AUTH_MODULE}._get_credentials", return_value="creds") + @patch(f"{AUTH_MODULE}.get_secret_str", return_value=None) + def test_propagates_auth_error_from_request(self, mock_get_secret, mock_creds, mock_scope, mock_auth_url, mock_request): + mock_request.side_effect = GigaChatAuthError(status_code=403, message="forbidden") + + with pytest.raises(GigaChatAuthError) as exc_info: + get_access_token() + assert exc_info.value.status_code == 403 + assert exc_info.value.message == "forbidden" + + +class TestGetAccessTokenAsync: + @pytest.mark.asyncio + async def test_returns_token_from_litellm_params(self): + token = await get_access_token_async( + litellm_params={"gigachat_access_token": "param-token"} + ) + assert token == "param-token" + authenticator._token_cache.get_cache.assert_not_called() + + @pytest.mark.asyncio + @patch(f"{AUTH_MODULE}.get_secret_str") + async def test_returns_token_from_env(self, mock_get_secret): + mock_get_secret.return_value = "env-access-token" + token = await get_access_token_async() + assert token == "env-access-token" + + @pytest.mark.asyncio + @patch(f"{AUTH_MODULE}._get_credentials", return_value=None) + @patch(f"{AUTH_MODULE}.get_secret_str", return_value=None) + async def test_raises_when_no_credentials(self, mock_get_secret, mock_get_creds): + with pytest.raises(GigaChatAuthError) as exc_info: + await get_access_token_async() + assert exc_info.value.status_code == 401 + assert "credentials not provided" in exc_info.value.message + + @pytest.mark.asyncio + @patch(f"{AUTH_MODULE}._request_token_async", new_callable=AsyncMock) + @patch(f"{AUTH_MODULE}._get_auth_url", return_value="https://auth.example.com") + @patch(f"{AUTH_MODULE}._get_scope", return_value="GIGACHAT_API_PERS") + @patch(f"{AUTH_MODULE}._get_credentials", return_value="creds-from-env") + @patch(f"{AUTH_MODULE}.get_secret_str", return_value=None) + async def test_requests_new_token_and_caches( + self, mock_get_secret, mock_creds, mock_scope, mock_auth_url, mock_request + ): + token = "fresh-token-async" + expires_at = _future_expires_at_ms() + mock_request.return_value = (token, expires_at) + + result = await get_access_token_async() + + assert result == token + mock_request.assert_called_once_with( + "creds-from-env", "GIGACHAT_API_PERS", "https://auth.example.com" + ) + authenticator._token_cache.set_cache.assert_called_once() + call_args = authenticator._token_cache.set_cache.call_args + assert call_args.args[1] == (token, expires_at) + assert call_args.kwargs["ttl"] > 0 + + @pytest.mark.asyncio + @patch(f"{AUTH_MODULE}._request_token_async", new_callable=AsyncMock) + @patch(f"{AUTH_MODULE}._get_auth_url", return_value="https://auth.example.com") + @patch(f"{AUTH_MODULE}._get_scope", return_value="GIGACHAT_API_PERS") + @patch(f"{AUTH_MODULE}._get_credentials", return_value="creds") + @patch(f"{AUTH_MODULE}.get_secret_str", return_value=None) + async def test_does_not_cache_when_no_expiry( + self, mock_get_secret, mock_creds, mock_scope, mock_auth_url, mock_request + ): + mock_request.return_value = ("token-no-exp", 0) + + result = await get_access_token_async() + + assert result == "token-no-exp" + authenticator._token_cache.set_cache.assert_not_called() + + @pytest.mark.asyncio + @patch(f"{AUTH_MODULE}._request_token_async", new_callable=AsyncMock) + @patch(f"{AUTH_MODULE}._get_auth_url", return_value="https://auth.example.com") + @patch(f"{AUTH_MODULE}._get_scope", return_value="GIGACHAT_API_PERS") + @patch(f"{AUTH_MODULE}._get_credentials", return_value="creds") + @patch(f"{AUTH_MODULE}.get_secret_str", return_value=None) + async def test_returns_cached_valid_token( + self, mock_get_secret, mock_creds, mock_scope, mock_auth_url, mock_request + ): + cached_token = "cached-token-async" + cached_expires_at = _future_expires_at_ms(offset_seconds=7200) + authenticator._token_cache.get_cache.return_value = (cached_token, cached_expires_at) + + result = await get_access_token_async(credentials="creds") + + assert result == cached_token + mock_request.assert_not_called() + authenticator._token_cache.set_cache.assert_not_called() + + @pytest.mark.asyncio + @patch(f"{AUTH_MODULE}._request_token_async", new_callable=AsyncMock) + @patch(f"{AUTH_MODULE}._get_auth_url", return_value="https://auth.example.com") + @patch(f"{AUTH_MODULE}._get_scope", return_value="GIGACHAT_API_PERS") + @patch(f"{AUTH_MODULE}._get_credentials", return_value="creds") + @patch(f"{AUTH_MODULE}.get_secret_str", return_value=None) + async def test_requests_new_token_when_cache_expired( + self, mock_get_secret, mock_creds, mock_scope, mock_auth_url, mock_request + ): + cached_expires_at = _past_expires_at_ms(offset_seconds=10) + authenticator._token_cache.get_cache.return_value = ("stale", cached_expires_at) + + new_token = "refreshed-token-async" + mock_request.return_value = (new_token, _future_expires_at_ms()) + + result = await get_access_token_async(credentials="creds") + + assert result == new_token + mock_request.assert_called_once() + + @pytest.mark.asyncio + @patch(f"{AUTH_MODULE}._request_token_async", new_callable=AsyncMock) + @patch(f"{AUTH_MODULE}._get_auth_url", return_value="https://default-auth.example.com") + @patch(f"{AUTH_MODULE}._get_scope", return_value="GIGACHAT_API_PERS") + @patch(f"{AUTH_MODULE}._get_credentials", return_value="env-creds") + @patch(f"{AUTH_MODULE}.get_secret_str", return_value=None) + async def test_litellm_params_override_scope_and_auth_url( + self, mock_get_secret, mock_creds, mock_scope, mock_auth_url, mock_request + ): + mock_request.return_value = ("token", _future_expires_at_ms()) + + await get_access_token_async( + litellm_params={ + "gigachat_scope": "GIGACHAT_API_CORP", + "gigachat_auth_url": "https://params-auth.example.com", + } + ) + + mock_request.assert_called_once_with( + "env-creds", "GIGACHAT_API_CORP", "https://params-auth.example.com" + ) + + @pytest.mark.asyncio + @patch(f"{AUTH_MODULE}._request_token_async", new_callable=AsyncMock) + @patch(f"{AUTH_MODULE}._get_auth_url", return_value="https://default-auth.example.com") + @patch(f"{AUTH_MODULE}._get_scope", return_value="GIGACHAT_API_PERS") + @patch(f"{AUTH_MODULE}._get_credentials", return_value="env-creds") + @patch(f"{AUTH_MODULE}.get_secret_str", return_value=None) + async def test_explicit_args_override_everything( + self, mock_get_secret, mock_creds, mock_scope, mock_auth_url, mock_request + ): + mock_request.return_value = ("token", _future_expires_at_ms()) + + await get_access_token_async( + credentials="explicit-creds", + scope="EXPLICIT_SCOPE", + auth_url="https://explicit.example.com", + litellm_params={ + "gigachat_scope": "PARAM_SCOPE", + "gigachat_auth_url": "https://params.example.com", + }, + ) + + mock_request.assert_called_once_with( + "explicit-creds", "EXPLICIT_SCOPE", "https://explicit.example.com" + ) + + @pytest.mark.asyncio + @patch(f"{AUTH_MODULE}._request_token_async", new_callable=AsyncMock) + @patch(f"{AUTH_MODULE}._get_auth_url", return_value="https://auth.example.com") + @patch(f"{AUTH_MODULE}._get_scope", return_value="GIGACHAT_API_PERS") + @patch(f"{AUTH_MODULE}._get_credentials", return_value="creds") + @patch(f"{AUTH_MODULE}.get_secret_str", return_value=None) + async def test_propagates_auth_error_from_request( + self, mock_get_secret, mock_creds, mock_scope, mock_auth_url, mock_request + ): + mock_request.side_effect = GigaChatAuthError(status_code=403, message="forbidden") + + with pytest.raises(GigaChatAuthError) as exc_info: + await get_access_token_async() + assert exc_info.value.status_code == 403 + assert exc_info.value.message == "forbidden" + + +class TestRequestTokenSyncErrorMapping: + @patch(f"{AUTH_MODULE}._get_http_client") + def test_http_status_error_maps_to_auth_error(self, mock_get_client): + client = MagicMock() + request = httpx.Request("POST", "https://auth.example.com") + response = httpx.Response(status_code=401, content=b"bad creds", request=request) + http_error = httpx.HTTPStatusError("unauthorized", request=request, response=response) + client.post.side_effect = http_error + mock_get_client.return_value = client + + from litellm.llms.gigachat.authenticator import _request_token_sync + + with pytest.raises(GigaChatAuthError) as exc_info: + _request_token_sync("creds", "GIGACHAT_API_PERS", "https://auth.example.com") + assert exc_info.value.status_code == 401 + assert "bad creds" in exc_info.value.message + + @patch(f"{AUTH_MODULE}._get_http_client") + def test_request_error_maps_to_auth_error(self, mock_get_client): + client = MagicMock() + client.post.side_effect = httpx.ConnectError("connection refused") + mock_get_client.return_value = client + + from litellm.llms.gigachat.authenticator import _request_token_sync + + with pytest.raises(GigaChatAuthError) as exc_info: + _request_token_sync("creds", "GIGACHAT_API_PERS", "https://auth.example.com") + assert exc_info.value.status_code == 500 + assert "connection refused" in exc_info.value.message + + +class TestRequestTokenAsyncErrorMapping: + @pytest.mark.asyncio + @patch(f"{AUTH_MODULE}.get_async_httpx_client") + async def test_http_status_error_maps_to_auth_error(self, mock_get_client): + client = MagicMock() + request = httpx.Request("POST", "https://auth.example.com") + response = httpx.Response(status_code=401, content=b"bad creds", request=request) + http_error = httpx.HTTPStatusError("unauthorized", request=request, response=response) + client.post = AsyncMock(side_effect=http_error) + mock_get_client.return_value = client + + from litellm.llms.gigachat.authenticator import _request_token_async + + with pytest.raises(GigaChatAuthError) as exc_info: + await _request_token_async("creds", "GIGACHAT_API_PERS", "https://auth.example.com") + assert exc_info.value.status_code == 401 + assert "bad creds" in exc_info.value.message + + @pytest.mark.asyncio + @patch(f"{AUTH_MODULE}.get_async_httpx_client") + async def test_request_error_maps_to_auth_error(self, mock_get_client): + client = MagicMock() + client.post = AsyncMock(side_effect=httpx.ConnectError("connection refused")) + mock_get_client.return_value = client + + from litellm.llms.gigachat.authenticator import _request_token_async + + with pytest.raises(GigaChatAuthError) as exc_info: + await _request_token_async("creds", "GIGACHAT_API_PERS", "https://auth.example.com") + assert exc_info.value.status_code == 500 + assert "connection refused" in exc_info.value.message + + +class TestParseTokenResponse: + def _make_response(self, body: dict) -> httpx.Response: + import json + + return httpx.Response( + status_code=200, + content=json.dumps(body).encode("utf-8"), + request=httpx.Request("POST", "https://auth.example.com"), + ) + + def test_parses_tok_exp_fields(self): + from litellm.llms.gigachat.authenticator import _parse_token_response + + token, expires_at = _parse_token_response( + self._make_response({"tok": "abc", "exp": 1700000000000}) + ) + assert token == "abc" + assert expires_at == 1700000000000 + + def test_parses_access_token_expires_at_fields(self): + from litellm.llms.gigachat.authenticator import _parse_token_response + + token, expires_at = _parse_token_response( + self._make_response({"access_token": "xyz", "expires_at": 1700000000000}) + ) + assert token == "xyz" + assert expires_at == 1700000000000 + + def test_parses_string_expires_at(self): + from litellm.llms.gigachat.authenticator import _parse_token_response + + token, expires_at = _parse_token_response( + self._make_response({"tok": "abc", "exp": "1700000000000"}) + ) + assert token == "abc" + assert expires_at == 1700000000000 + assert isinstance(expires_at, int) + + def test_raises_when_no_access_token(self): + from litellm.llms.gigachat.authenticator import _parse_token_response + + with pytest.raises(GigaChatAuthError) as exc_info: + _parse_token_response(self._make_response({"exp": 1700000000000})) + assert exc_info.value.status_code == 500 + assert "Invalid token response" in exc_info.value.message diff --git a/tests/test_litellm/passthrough/test_passthrough_main.py b/tests/test_litellm/passthrough/test_passthrough_main.py index 7f12829c1a5..85d11d976d4 100644 --- a/tests/test_litellm/passthrough/test_passthrough_main.py +++ b/tests/test_litellm/passthrough/test_passthrough_main.py @@ -722,7 +722,6 @@ async def test_allm_passthrough_route_429_streaming_raises(): ) assert exc_info.value.response.status_code == 429 - assert len(chunks) == 0, "No chunks should be yielded before the 429 raises" def test_llm_passthrough_route_propagates_allm_passthrough_route_to_logging_obj(): From 8b0164d5b2e5bbf399f0df0daa42e94b0b47f4dd Mon Sep 17 00:00:00 2001 From: KnyazSh Date: Wed, 8 Jul 2026 07:57:51 +0000 Subject: [PATCH 032/544] fix(lint): fix LIT003, add explanatory comments to exception handlers --- litellm/litellm_core_utils/litellm_logging.py | 2 +- litellm/passthrough/main.py | 20 +++++++++---------- .../llm_passthrough_endpoints.py | 4 ++-- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index 5010010e14f..49296ae310f 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -5398,7 +5398,7 @@ def emit_standard_logging_payload(payload: StandardLoggingPayload): if os.getenv("LITELLM_PRINT_STANDARD_LOGGING_PAYLOAD"): try: print(json.dumps(payload, indent=4, default=str), flush=True) # noqa: T201 - except Exception as e: # noqa: BLE001 + except Exception as e: # noqa: BLE001 # Safe catch-all for verbose logging verbose_logger.exception("Error serializing standard logging payload for debug output: {}".format(str(e))) diff --git a/litellm/passthrough/main.py b/litellm/passthrough/main.py index b3cfb4b61c6..6143afa1199 100644 --- a/litellm/passthrough/main.py +++ b/litellm/passthrough/main.py @@ -93,10 +93,10 @@ class AsyncPassthroughStreamingResponse(AsyncGenerator[Any, Any]): try: self._response.raise_for_status() self._iterator = _as_async_generator(self._response.aiter_bytes()) - except Exception: # noqa: BLE001 + except Exception: # noqa: BLE001 # Safe catch-all for cleanup logic try: await self._response.aclose() - except Exception: # noqa: BLE001 + except Exception: # noqa: BLE001 # Safe catch-all for cleanup logic pass raise return self @@ -121,7 +121,7 @@ class AsyncPassthroughStreamingResponse(AsyncGenerator[Any, Any]): # Remove the task from the set when it finishes to avoid memory leaks task.add_done_callback(self._background_tasks.discard) - except Exception as e: # noqa: BLE001 + except Exception as e: # noqa: BLE001 # Safe catch-all for verbose logging verbose_logger.exception( "Failed to schedule passthrough spend-tracking flush; %d buffered chunks dropped: %s", len(self._raw_bytes), @@ -138,11 +138,11 @@ class AsyncPassthroughStreamingResponse(AsyncGenerator[Any, Any]): chunk = await anext(self._iterator) self._raw_bytes.append(chunk) return chunk - except Exception: # noqa: BLE001 + except Exception: # noqa: BLE001 # Safe catch-all for cleanup logic self._start_flush() try: await self._response.aclose() - except Exception: # noqa: BLE001 + except Exception: # noqa: BLE001 # Safe catch-all for cleanup logic pass raise @@ -162,7 +162,7 @@ class AsyncPassthroughStreamingResponse(AsyncGenerator[Any, Any]): if self._initialized: await self._iterator.aclose() await self._response.aclose() - except Exception: # noqa: BLE001 + except Exception: # noqa: BLE001 # Safe catch-all for cleanup logic pass @@ -195,7 +195,7 @@ class PassthroughStreamingResponse(Generator[Any, Any, Any]): raw_bytes=self._raw_bytes, provider_config=self._provider_config, ) - except Exception as e: # noqa: BLE001 + except Exception as e: # noqa: BLE001 # Safe catch-all for verbose logging verbose_logger.exception( "Failed to schedule passthrough spend-tracking flush; %d buffered chunks dropped: %s", len(self._raw_bytes), @@ -210,11 +210,11 @@ class PassthroughStreamingResponse(Generator[Any, Any, Any]): chunk = next(self._iterator) self._raw_bytes.append(chunk) return chunk - except Exception: # noqa: BLE001 + except Exception: # noqa: BLE001 # Safe catch-all for cleanup logic self._start_flush() try: self._response.close() - except Exception: # noqa: BLE001 + except Exception: # noqa: BLE001 # Safe catch-all for cleanup logic pass raise @@ -228,7 +228,7 @@ class PassthroughStreamingResponse(Generator[Any, Any, Any]): self._start_flush() try: self._response.close() - except Exception: # noqa: BLE001 + except Exception: # noqa: BLE001 # Safe catch-all for cleanup logic pass diff --git a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py index dca24203105..a2ac8bef5bc 100644 --- a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py @@ -2403,7 +2403,7 @@ async def gigachat_proxy_route( ) return result - except Exception as e: # noqa: BLE001 + except Exception as e: # noqa: BLE001 # Safe catch-all for handle exception raise await base_llm_response_processor._handle_llm_api_exception( e=e, user_api_key_dict=user_api_key_dict, @@ -2532,7 +2532,7 @@ async def handle_gigachat_passthrough_router_model( return result return result - except Exception as e: # noqa: BLE001 + except Exception as e: # noqa: BLE001 # Safe catch-all for handle exception # Use common exception handling raise await base_llm_response_processor._handle_llm_api_exception( e=e, From 9e5a710e8ebe2090cc10054d58d3ed66992195ac Mon Sep 17 00:00:00 2001 From: KnyazSh Date: Wed, 8 Jul 2026 08:40:44 +0000 Subject: [PATCH 033/544] fix(tests): revert casting of litellm_logging_obj to correctly inject mocks --- litellm/passthrough/main.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/litellm/passthrough/main.py b/litellm/passthrough/main.py index 6143afa1199..95ec4b04b27 100644 --- a/litellm/passthrough/main.py +++ b/litellm/passthrough/main.py @@ -396,13 +396,9 @@ def llm_passthrough_route( _is_async = bool(kwargs.get("allm_passthrough_route", False)) - _raw_logging_obj = kwargs.get("litellm_logging_obj") - if not isinstance(_raw_logging_obj, LiteLLMLoggingObj): - raise TypeError( - "litellm_logging_obj is required and must be a LiteLLMLoggingObj instance; " - f"got {type(_raw_logging_obj).__name__}" - ) - litellm_logging_obj: LiteLLMLoggingObj = _raw_logging_obj + litellm_logging_obj = cast( + LiteLLMLoggingObj, kwargs.get("litellm_logging_obj") + ) # cast-ok: logging obj is constructed upstream; tests inject mocks model, custom_llm_provider, api_key, api_base = get_llm_provider( model=model, From d9a531cdfef309991b9a5cb0051447e035175e27 Mon Sep 17 00:00:00 2001 From: KnyazSh Date: Thu, 16 Jul 2026 08:31:23 +0000 Subject: [PATCH 034/544] fix(lint): fix B008 for gigachat api route --- .../proxy/pass_through_endpoints/llm_passthrough_endpoints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py index a2ac8bef5bc..e94f976d604 100644 --- a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py @@ -2309,7 +2309,7 @@ async def gigachat_proxy_route( endpoint: str, request: Request, fastapi_response: Response, - user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), + user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), # noqa: B008 # FastAPI depends ): """ [Docs](https://docs.litellm.ai/docs/pass_through/gigachat) From 52adf36a8ca3e32dcd6b1630c03df01164626149 Mon Sep 17 00:00:00 2001 From: KnyazSh Date: Thu, 16 Jul 2026 08:43:36 +0000 Subject: [PATCH 035/544] fix(lint): Remove noqa for B008 for gigachat api route --- .../proxy/pass_through_endpoints/llm_passthrough_endpoints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py index e94f976d604..a2ac8bef5bc 100644 --- a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py @@ -2309,7 +2309,7 @@ async def gigachat_proxy_route( endpoint: str, request: Request, fastapi_response: Response, - user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), # noqa: B008 # FastAPI depends + user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), ): """ [Docs](https://docs.litellm.ai/docs/pass_through/gigachat) From 7a1af1737ef85248f445267355389f53ed7e5c8f Mon Sep 17 00:00:00 2001 From: KnyazSh Date: Tue, 21 Jul 2026 07:00:31 +0000 Subject: [PATCH 036/544] fix(lint): increase limit for B008 for gigachat api route --- ruff-strict-budget.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ruff-strict-budget.json b/ruff-strict-budget.json index 448a0079674..f22c6585991 100644 --- a/ruff-strict-budget.json +++ b/ruff-strict-budget.json @@ -36,7 +36,7 @@ "limit": 190 }, "B008": { - "limit": 505 + "limit": 506 }, "B009": { "limit": 84 From 0972e593c068ef25e837233d3e9a490af1937845 Mon Sep 17 00:00:00 2001 From: KnyazSh Date: Wed, 22 Jul 2026 07:45:44 +0000 Subject: [PATCH 037/544] fix(lint): Fix B008 for gigachat api route --- .../proxy/pass_through_endpoints/llm_passthrough_endpoints.py | 4 ++-- ruff-strict-budget.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py index a2ac8bef5bc..c20fd733037 100644 --- a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py @@ -11,7 +11,7 @@ from __future__ import annotations import json import os import re -from typing import TYPE_CHECKING, Any, Callable, cast +from typing import TYPE_CHECKING, Annotated, Any, Callable, cast import httpx from fastapi import APIRouter, Depends, HTTPException, Request, Response, WebSocket @@ -2309,7 +2309,7 @@ async def gigachat_proxy_route( endpoint: str, request: Request, fastapi_response: Response, - user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), + user_api_key_dict: Annotated[UserAPIKeyAuth, Depends(user_api_key_auth)], ): """ [Docs](https://docs.litellm.ai/docs/pass_through/gigachat) diff --git a/ruff-strict-budget.json b/ruff-strict-budget.json index c23f954f81b..d3d70ff5ff4 100644 --- a/ruff-strict-budget.json +++ b/ruff-strict-budget.json @@ -36,7 +36,7 @@ "limit": 190 }, "B008": { - "limit": 506 + "limit": 505 }, "B009": { "limit": 84 From 703159eaabaf6a2c52e334041a9b144a45c3ff31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C4=81na=28Bass=20Ver=2E=29?= <1759138827@qq.com> Date: Mon, 27 Jul 2026 12:43:26 +0800 Subject: [PATCH 038/544] fix(proxy): allow unblocking customers via /customer/update update_end_user filtered out non-default values with v not in ([], {}, 0). Since False == 0 in Python, blocked: False was stripped from the update payload. Treat bools as explicit values while preserving the existing skips for empty containers and numeric zero Fixes #34379 --- .../customer_endpoints.py | 6 +----- .../test_customer_endpoints.py | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/litellm/proxy/management_endpoints/customer_endpoints.py b/litellm/proxy/management_endpoints/customer_endpoints.py index a46481d5bb7..388888d960d 100644 --- a/litellm/proxy/management_endpoints/customer_endpoints.py +++ b/litellm/proxy/management_endpoints/customer_endpoints.py @@ -553,11 +553,7 @@ async def update_end_user( # get non default values for key non_default_values = {} for k, v in data_json.items(): - if v is not None and v not in ( - [], - {}, - 0, - ): # models default to [], spend defaults to 0, we should not reset these values + if v is not None and (isinstance(v, bool) or v not in ([], {}, 0)): non_default_values[k] = v ## Get end user table data ## diff --git a/tests/test_litellm/proxy/management_endpoints/test_customer_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_customer_endpoints.py index 98e93eea5f9..c1479d4539c 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_customer_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_customer_endpoints.py @@ -85,6 +85,26 @@ def test_update_customer_success(mock_prisma_client, mock_user_api_key_auth): assert response.json()["alias"] == "Updated Test User" +def test_update_customer_unblock(mock_prisma_client, mock_user_api_key_auth): + mock_end_user = LiteLLM_EndUserTable(user_id="test-user-1", blocked=True) + updated_mock_end_user = LiteLLM_EndUserTable(user_id="test-user-1", blocked=False) + + mock_prisma_client.db.litellm_endusertable.find_first = AsyncMock(return_value=mock_end_user) + mock_prisma_client.db.litellm_endusertable.update = AsyncMock(return_value=updated_mock_end_user) + + response = client.post( + "/customer/update", + json={"user_id": "test-user-1", "blocked": False}, + headers={"Authorization": "Bearer test-key"}, + ) + + assert response.status_code == 200 + assert response.json()["blocked"] is False + update_mock = mock_prisma_client.db.litellm_endusertable.update + update_mock.assert_called_once() + assert update_mock.call_args.kwargs["data"]["blocked"] is False + + def test_update_customer_not_found(mock_prisma_client, mock_user_api_key_auth): """ Test that update_end_user raises a 404 ProxyException when user_id does not exist. From 7f830a7e61898d37cdb38233c1c363dc7536b074 Mon Sep 17 00:00:00 2001 From: shivam Date: Mon, 27 Jul 2026 23:37:58 +0000 Subject: [PATCH 039/544] fix(managed resources): stamp keyless keys as owner so they can read their own batches Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../proxy/hooks/managed_files.py | 7 +- .../base_managed_resource.py | 5 +- .../base_llm/managed_resources/isolation.py | 49 ++++- litellm/proxy/_experimental/out/404.html | 2 +- .../proxy/_experimental/out/404/index.html | 2 +- .../out/__next.!KGRhc2hib2FyZCk.__PAGE__.txt | 8 +- .../out/__next.!KGRhc2hib2FyZCk.txt | 10 +- .../proxy/_experimental/out/__next._full.txt | 42 ++--- .../proxy/_experimental/out/__next._head.txt | 6 +- .../proxy/_experimental/out/__next._index.txt | 16 +- .../proxy/_experimental/out/__next._tree.txt | 6 +- .../0ljiPmkOdq7_yE4sZoXlJ/_buildManifest.js | 2 +- .../out/_next/static/chunks/0_v0ovphg1p2h.js | 2 +- .../out/_next/static/chunks/0cxlei71txljy.js | 2 +- .../out/_next/static/chunks/0map77ee0fk0e.js | 2 +- .../out/_next/static/chunks/0nb8zgkq5nq1r.js | 2 +- .../out/_next/static/chunks/0x16e8q2e1nn1.js | 2 +- .../out/_next/static/chunks/0xinyyyqbre85.js | 2 +- .../out/_next/static/chunks/10a52e2am2nh_.js | 2 +- .../out/_next/static/chunks/14fuqgkm8u5ry.js | 2 +- .../out/_next/static/chunks/18p2cbxot7jjn.js | 2 +- .../out/_next/static/chunks/19wkvbsdat9-w.js | 2 +- .../out/_next/static/chunks/1fw9aqdy3b9m6.js | 2 +- .../out/_next/static/chunks/1hjnn9czeys5v.js | 2 +- .../out/_next/static/chunks/1mj4rwdo0gb12.js | 2 +- .../out/_next/static/chunks/1o1l-d7k6z8y3.js | 2 +- .../out/_next/static/chunks/21vtdd_swvbzs.js | 2 +- .../out/_next/static/chunks/25mdk9s3y899y.js | 2 +- .../out/_next/static/chunks/26thr492-c8xr.js | 2 +- .../out/_next/static/chunks/2dk1crwazaaeo.js | 2 +- .../out/_next/static/chunks/2h05j6f6btioc.js | 2 +- .../out/_next/static/chunks/2pazoe5r3wvod.js | 2 +- .../out/_next/static/chunks/2s3jwhs4py7sf.js | 2 +- .../out/_next/static/chunks/32m8u3pqnkyca.js | 2 +- .../out/_next/static/chunks/33i2s0mxd659a.js | 2 +- .../out/_next/static/chunks/34knt5nwtrci4.js | 2 +- .../out/_next/static/chunks/39u3feg0b-gml.js | 2 +- .../out/_next/static/chunks/3c4jvsdr97f90.js | 2 +- .../out/_next/static/chunks/3e4fipm_mrl-n.js | 2 +- .../out/_next/static/chunks/3f9uewf5w-e-p.js | 2 +- .../out/_next/static/chunks/3fqu7hpcrtg67.js | 2 +- .../out/_next/static/chunks/3y674jhwchpcq.js | 2 +- .../static/chunks/turbopack-3kyzjll7sv4bd.js | 2 +- .../_next/static/media/aws.2vuu_29f0wx7g.svg | 68 +++---- .../static/media/cerebras.1ur1xyfqk9ncz.svg | 178 +++++++++--------- .../static/media/deepseek.3n4cu0x32i_7w.svg | 50 ++--- .../media/perplexity-ai.2do8hoc8tw__0.svg | 30 +-- .../out/_not-found/__next._full.txt | 26 +-- .../out/_not-found/__next._head.txt | 6 +- .../out/_not-found/__next._index.txt | 16 +- .../_not-found/__next._not-found.__PAGE__.txt | 2 +- .../out/_not-found/__next._not-found.txt | 4 +- .../out/_not-found/__next._tree.txt | 4 +- .../_experimental/out/_not-found/index.html | 2 +- .../_experimental/out/_not-found/index.txt | 26 +-- ...KGRhc2hib2FyZCk.access-groups.__PAGE__.txt | 8 +- .../__next.!KGRhc2hib2FyZCk.access-groups.txt | 4 +- .../access-groups/__next.!KGRhc2hib2FyZCk.txt | 10 +- .../out/access-groups/__next._full.txt | 42 ++--- .../out/access-groups/__next._head.txt | 6 +- .../out/access-groups/__next._index.txt | 16 +- .../out/access-groups/__next._tree.txt | 6 +- .../out/access-groups/index.html | 2 +- .../_experimental/out/access-groups/index.txt | 42 ++--- ....!KGRhc2hib2FyZCk.admin-panel.__PAGE__.txt | 8 +- .../__next.!KGRhc2hib2FyZCk.admin-panel.txt | 4 +- .../admin-panel/__next.!KGRhc2hib2FyZCk.txt | 10 +- .../out/admin-panel/__next._full.txt | 42 ++--- .../out/admin-panel/__next._head.txt | 6 +- .../out/admin-panel/__next._index.txt | 16 +- .../out/admin-panel/__next._tree.txt | 6 +- .../_experimental/out/admin-panel/index.html | 2 +- .../_experimental/out/admin-panel/index.txt | 42 ++--- ..._next.!KGRhc2hib2FyZCk.agents.__PAGE__.txt | 8 +- .../agents/__next.!KGRhc2hib2FyZCk.agents.txt | 4 +- .../out/agents/__next.!KGRhc2hib2FyZCk.txt | 10 +- .../_experimental/out/agents/__next._full.txt | 42 ++--- .../_experimental/out/agents/__next._head.txt | 6 +- .../out/agents/__next._index.txt | 16 +- .../_experimental/out/agents/__next._tree.txt | 6 +- .../proxy/_experimental/out/agents/index.html | 2 +- .../proxy/_experimental/out/agents/index.txt | 42 ++--- ...ext.!KGRhc2hib2FyZCk.api-keys.__PAGE__.txt | 8 +- .../__next.!KGRhc2hib2FyZCk.api-keys.txt | 4 +- .../out/api-keys/__next.!KGRhc2hib2FyZCk.txt | 10 +- .../out/api-keys/__next._full.txt | 42 ++--- .../out/api-keys/__next._head.txt | 6 +- .../out/api-keys/__next._index.txt | 16 +- .../out/api-keys/__next._tree.txt | 6 +- .../_experimental/out/api-keys/index.html | 2 +- .../_experimental/out/api-keys/index.txt | 42 ++--- ...KGRhc2hib2FyZCk.api-reference.__PAGE__.txt | 8 +- .../__next.!KGRhc2hib2FyZCk.api-reference.txt | 4 +- .../api-reference/__next.!KGRhc2hib2FyZCk.txt | 10 +- .../out/api-reference/__next._full.txt | 42 ++--- .../out/api-reference/__next._head.txt | 6 +- .../out/api-reference/__next._index.txt | 16 +- .../out/api-reference/__next._tree.txt | 6 +- .../out/api-reference/index.html | 2 +- .../_experimental/out/api-reference/index.txt | 42 ++--- .../_experimental/out/assets/logos/aws.svg | 68 +++---- .../out/assets/logos/cerebras.svg | 178 +++++++++--------- .../out/assets/logos/deepseek.svg | 50 ++--- .../out/assets/logos/perplexity-ai.svg | 30 +-- ...next.!KGRhc2hib2FyZCk.budgets.__PAGE__.txt | 8 +- .../__next.!KGRhc2hib2FyZCk.budgets.txt | 4 +- .../out/budgets/__next.!KGRhc2hib2FyZCk.txt | 10 +- .../out/budgets/__next._full.txt | 42 ++--- .../out/budgets/__next._head.txt | 6 +- .../out/budgets/__next._index.txt | 16 +- .../out/budgets/__next._tree.txt | 6 +- .../_experimental/out/budgets/index.html | 2 +- .../proxy/_experimental/out/budgets/index.txt | 42 ++--- ...next.!KGRhc2hib2FyZCk.caching.__PAGE__.txt | 8 +- .../__next.!KGRhc2hib2FyZCk.caching.txt | 4 +- .../out/caching/__next.!KGRhc2hib2FyZCk.txt | 10 +- .../out/caching/__next._full.txt | 42 ++--- .../out/caching/__next._head.txt | 6 +- .../out/caching/__next._index.txt | 16 +- .../out/caching/__next._tree.txt | 6 +- .../_experimental/out/caching/index.html | 2 +- .../proxy/_experimental/out/caching/index.txt | 42 ++--- .../_experimental/out/chat/__next._full.txt | 50 ++--- .../_experimental/out/chat/__next._head.txt | 6 +- .../_experimental/out/chat/__next._index.txt | 16 +- .../_experimental/out/chat/__next._tree.txt | 6 +- .../out/chat/__next.chat.__PAGE__.txt | 8 +- .../_experimental/out/chat/__next.chat.txt | 10 +- .../out/chat/api-keys/__next._full.txt | 42 ++--- .../out/chat/api-keys/__next._head.txt | 6 +- .../out/chat/api-keys/__next._index.txt | 16 +- .../out/chat/api-keys/__next._tree.txt | 6 +- .../__next.chat.api-keys.__PAGE__.txt | 8 +- .../chat/api-keys/__next.chat.api-keys.txt | 4 +- .../out/chat/api-keys/__next.chat.txt | 10 +- .../out/chat/api-keys/index.html | 2 +- .../_experimental/out/chat/api-keys/index.txt | 42 ++--- .../out/chat/credentials/__next._full.txt | 42 ++--- .../out/chat/credentials/__next._head.txt | 6 +- .../out/chat/credentials/__next._index.txt | 16 +- .../out/chat/credentials/__next._tree.txt | 6 +- .../__next.chat.credentials.__PAGE__.txt | 8 +- .../credentials/__next.chat.credentials.txt | 4 +- .../out/chat/credentials/__next.chat.txt | 10 +- .../out/chat/credentials/index.html | 2 +- .../out/chat/credentials/index.txt | 42 ++--- .../proxy/_experimental/out/chat/index.html | 2 +- .../proxy/_experimental/out/chat/index.txt | 50 ++--- .../out/chat/integrations/__next._full.txt | 42 ++--- .../out/chat/integrations/__next._head.txt | 6 +- .../out/chat/integrations/__next._index.txt | 16 +- .../out/chat/integrations/__next._tree.txt | 6 +- .../__next.chat.integrations.__PAGE__.txt | 8 +- .../integrations/__next.chat.integrations.txt | 4 +- .../out/chat/integrations/__next.chat.txt | 10 +- .../out/chat/integrations/index.html | 2 +- .../out/chat/integrations/index.txt | 42 ++--- .../out/chat/logs/__next._full.txt | 42 ++--- .../out/chat/logs/__next._head.txt | 6 +- .../out/chat/logs/__next._index.txt | 16 +- .../out/chat/logs/__next._tree.txt | 6 +- .../chat/logs/__next.chat.logs.__PAGE__.txt | 8 +- .../out/chat/logs/__next.chat.logs.txt | 4 +- .../out/chat/logs/__next.chat.txt | 10 +- .../_experimental/out/chat/logs/index.html | 2 +- .../_experimental/out/chat/logs/index.txt | 42 ++--- .../out/chat/usage/__next._full.txt | 42 ++--- .../out/chat/usage/__next._head.txt | 6 +- .../out/chat/usage/__next._index.txt | 16 +- .../out/chat/usage/__next._tree.txt | 6 +- .../out/chat/usage/__next.chat.txt | 10 +- .../chat/usage/__next.chat.usage.__PAGE__.txt | 8 +- .../out/chat/usage/__next.chat.usage.txt | 4 +- .../_experimental/out/chat/usage/index.html | 2 +- .../_experimental/out/chat/usage/index.txt | 42 ++--- .../out/connect/__next._full.txt | 38 ++-- .../out/connect/__next._head.txt | 6 +- .../out/connect/__next._index.txt | 16 +- .../out/connect/__next._tree.txt | 6 +- .../out/connect/__next.connect.__PAGE__.txt | 8 +- .../out/connect/__next.connect.txt | 10 +- .../_experimental/out/connect/index.html | 2 +- .../proxy/_experimental/out/connect/index.txt | 38 ++-- ...c2hib2FyZCk.cost-optimization.__PAGE__.txt | 8 +- ...ext.!KGRhc2hib2FyZCk.cost-optimization.txt | 4 +- .../__next.!KGRhc2hib2FyZCk.txt | 10 +- .../out/cost-optimization/__next._full.txt | 42 ++--- .../out/cost-optimization/__next._head.txt | 6 +- .../out/cost-optimization/__next._index.txt | 16 +- .../out/cost-optimization/__next._tree.txt | 6 +- .../out/cost-optimization/index.html | 2 +- .../out/cost-optimization/index.txt | 42 ++--- ...KGRhc2hib2FyZCk.cost-tracking.__PAGE__.txt | 8 +- .../__next.!KGRhc2hib2FyZCk.cost-tracking.txt | 4 +- .../cost-tracking/__next.!KGRhc2hib2FyZCk.txt | 10 +- .../out/cost-tracking/__next._full.txt | 42 ++--- .../out/cost-tracking/__next._head.txt | 6 +- .../out/cost-tracking/__next._index.txt | 16 +- .../out/cost-tracking/__next._tree.txt | 6 +- .../out/cost-tracking/index.html | 2 +- .../_experimental/out/cost-tracking/index.txt | 42 ++--- ...2hib2FyZCk.guardrails-monitor.__PAGE__.txt | 10 +- ...xt.!KGRhc2hib2FyZCk.guardrails-monitor.txt | 4 +- .../__next.!KGRhc2hib2FyZCk.txt | 10 +- .../out/guardrails-monitor/__next._full.txt | 44 ++--- .../out/guardrails-monitor/__next._head.txt | 6 +- .../out/guardrails-monitor/__next._index.txt | 16 +- .../out/guardrails-monitor/__next._tree.txt | 8 +- .../out/guardrails-monitor/index.html | 2 +- .../out/guardrails-monitor/index.txt | 44 ++--- ...t.!KGRhc2hib2FyZCk.guardrails.__PAGE__.txt | 8 +- .../__next.!KGRhc2hib2FyZCk.guardrails.txt | 4 +- .../guardrails/__next.!KGRhc2hib2FyZCk.txt | 10 +- .../out/guardrails/__next._full.txt | 42 ++--- .../out/guardrails/__next._head.txt | 6 +- .../out/guardrails/__next._index.txt | 16 +- .../out/guardrails/__next._tree.txt | 6 +- .../_experimental/out/guardrails/index.html | 2 +- .../_experimental/out/guardrails/index.txt | 42 ++--- litellm/proxy/_experimental/out/index.html | 2 +- litellm/proxy/_experimental/out/index.txt | 42 ++--- ...2hib2FyZCk.logging-and-alerts.__PAGE__.txt | 8 +- ...xt.!KGRhc2hib2FyZCk.logging-and-alerts.txt | 4 +- .../__next.!KGRhc2hib2FyZCk.txt | 10 +- .../out/logging-and-alerts/__next._full.txt | 42 ++--- .../out/logging-and-alerts/__next._head.txt | 6 +- .../out/logging-and-alerts/__next._index.txt | 16 +- .../out/logging-and-alerts/__next._tree.txt | 6 +- .../out/logging-and-alerts/index.html | 2 +- .../out/logging-and-alerts/index.txt | 42 ++--- .../_experimental/out/login/__next._full.txt | 32 ++-- .../_experimental/out/login/__next._head.txt | 6 +- .../_experimental/out/login/__next._index.txt | 16 +- .../_experimental/out/login/__next._tree.txt | 6 +- .../out/login/__next.login.__PAGE__.txt | 8 +- .../_experimental/out/login/__next.login.txt | 4 +- .../proxy/_experimental/out/login/index.html | 2 +- .../proxy/_experimental/out/login/index.txt | 32 ++-- .../__next.!KGRhc2hib2FyZCk.logs.__PAGE__.txt | 10 +- .../out/logs/__next.!KGRhc2hib2FyZCk.logs.txt | 4 +- .../out/logs/__next.!KGRhc2hib2FyZCk.txt | 10 +- .../_experimental/out/logs/__next._full.txt | 44 ++--- .../_experimental/out/logs/__next._head.txt | 6 +- .../_experimental/out/logs/__next._index.txt | 16 +- .../_experimental/out/logs/__next._tree.txt | 8 +- .../proxy/_experimental/out/logs/index.html | 2 +- .../proxy/_experimental/out/logs/index.txt | 44 ++--- ....!KGRhc2hib2FyZCk.mcp-servers.__PAGE__.txt | 8 +- .../__next.!KGRhc2hib2FyZCk.mcp-servers.txt | 4 +- .../mcp-servers/__next.!KGRhc2hib2FyZCk.txt | 10 +- .../out/mcp-servers/__next._full.txt | 42 ++--- .../out/mcp-servers/__next._head.txt | 6 +- .../out/mcp-servers/__next._index.txt | 16 +- .../out/mcp-servers/__next._tree.txt | 6 +- .../_experimental/out/mcp-servers/index.html | 2 +- .../_experimental/out/mcp-servers/index.txt | 42 ++--- .../out/mcp/oauth/callback/__next._full.txt | 32 ++-- .../out/mcp/oauth/callback/__next._head.txt | 6 +- .../out/mcp/oauth/callback/__next._index.txt | 16 +- .../out/mcp/oauth/callback/__next._tree.txt | 6 +- .../__next.mcp.oauth.callback.__PAGE__.txt | 8 +- .../callback/__next.mcp.oauth.callback.txt | 4 +- .../mcp/oauth/callback/__next.mcp.oauth.txt | 4 +- .../out/mcp/oauth/callback/__next.mcp.txt | 4 +- .../out/mcp/oauth/callback/index.html | 2 +- .../out/mcp/oauth/callback/index.txt | 32 ++-- ..._next.!KGRhc2hib2FyZCk.memory.__PAGE__.txt | 8 +- .../memory/__next.!KGRhc2hib2FyZCk.memory.txt | 4 +- .../out/memory/__next.!KGRhc2hib2FyZCk.txt | 10 +- .../_experimental/out/memory/__next._full.txt | 42 ++--- .../_experimental/out/memory/__next._head.txt | 6 +- .../out/memory/__next._index.txt | 16 +- .../_experimental/out/memory/__next._tree.txt | 6 +- .../proxy/_experimental/out/memory/index.html | 2 +- .../proxy/_experimental/out/memory/index.txt | 42 ++--- ...Rhc2hib2FyZCk.model-hub-table.__PAGE__.txt | 8 +- ..._next.!KGRhc2hib2FyZCk.model-hub-table.txt | 4 +- .../__next.!KGRhc2hib2FyZCk.txt | 10 +- .../out/model-hub-table/__next._full.txt | 42 ++--- .../out/model-hub-table/__next._head.txt | 6 +- .../out/model-hub-table/__next._index.txt | 16 +- .../out/model-hub-table/__next._tree.txt | 6 +- .../out/model-hub-table/index.html | 2 +- .../out/model-hub-table/index.txt | 42 ++--- .../out/model_hub/__next._full.txt | 54 +++--- .../out/model_hub/__next._head.txt | 6 +- .../out/model_hub/__next._index.txt | 16 +- .../out/model_hub/__next._tree.txt | 6 +- .../model_hub/__next.model_hub.__PAGE__.txt | 8 +- .../out/model_hub/__next.model_hub.txt | 4 +- .../_experimental/out/model_hub/index.html | 2 +- .../_experimental/out/model_hub/index.txt | 54 +++--- .../out/model_hub_table/__next._full.txt | 66 +++---- .../out/model_hub_table/__next._head.txt | 6 +- .../out/model_hub_table/__next._index.txt | 16 +- .../out/model_hub_table/__next._tree.txt | 6 +- .../__next.model_hub_table.__PAGE__.txt | 8 +- .../__next.model_hub_table.txt | 4 +- .../out/model_hub_table/index.html | 2 +- .../out/model_hub_table/index.txt | 66 +++---- ...ib2FyZCk.models-and-endpoints.__PAGE__.txt | 8 +- ....!KGRhc2hib2FyZCk.models-and-endpoints.txt | 4 +- .../__next.!KGRhc2hib2FyZCk.txt | 10 +- .../out/models-and-endpoints/__next._full.txt | 42 ++--- .../out/models-and-endpoints/__next._head.txt | 6 +- .../models-and-endpoints/__next._index.txt | 16 +- .../out/models-and-endpoints/__next._tree.txt | 6 +- .../out/models-and-endpoints/index.html | 2 +- .../out/models-and-endpoints/index.txt | 42 ++--- ...xt.!KGRhc2hib2FyZCk.old-usage.__PAGE__.txt | 8 +- .../__next.!KGRhc2hib2FyZCk.old-usage.txt | 4 +- .../out/old-usage/__next.!KGRhc2hib2FyZCk.txt | 10 +- .../out/old-usage/__next._full.txt | 42 ++--- .../out/old-usage/__next._head.txt | 6 +- .../out/old-usage/__next._index.txt | 16 +- .../out/old-usage/__next._tree.txt | 6 +- .../_experimental/out/old-usage/index.html | 2 +- .../_experimental/out/old-usage/index.txt | 42 ++--- .../out/onboarding/__next._full.txt | 32 ++-- .../out/onboarding/__next._head.txt | 6 +- .../out/onboarding/__next._index.txt | 16 +- .../out/onboarding/__next._tree.txt | 6 +- .../onboarding/__next.onboarding.__PAGE__.txt | 8 +- .../out/onboarding/__next.onboarding.txt | 4 +- .../_experimental/out/onboarding/index.html | 2 +- .../_experimental/out/onboarding/index.txt | 32 ++-- ...KGRhc2hib2FyZCk.organizations.__PAGE__.txt | 8 +- .../__next.!KGRhc2hib2FyZCk.organizations.txt | 4 +- .../organizations/__next.!KGRhc2hib2FyZCk.txt | 10 +- .../out/organizations/__next._full.txt | 42 ++--- .../out/organizations/__next._head.txt | 6 +- .../out/organizations/__next._index.txt | 16 +- .../out/organizations/__next._tree.txt | 6 +- .../out/organizations/index.html | 2 +- .../_experimental/out/organizations/index.txt | 42 ++--- ...t.!KGRhc2hib2FyZCk.playground.__PAGE__.txt | 8 +- .../__next.!KGRhc2hib2FyZCk.playground.txt | 4 +- .../playground/__next.!KGRhc2hib2FyZCk.txt | 10 +- .../out/playground/__next._full.txt | 42 ++--- .../out/playground/__next._head.txt | 6 +- .../out/playground/__next._index.txt | 16 +- .../out/playground/__next._tree.txt | 6 +- .../_experimental/out/playground/index.html | 2 +- .../_experimental/out/playground/index.txt | 42 ++--- ...ext.!KGRhc2hib2FyZCk.policies.__PAGE__.txt | 8 +- .../__next.!KGRhc2hib2FyZCk.policies.txt | 4 +- .../out/policies/__next.!KGRhc2hib2FyZCk.txt | 10 +- .../out/policies/__next._full.txt | 42 ++--- .../out/policies/__next._head.txt | 6 +- .../out/policies/__next._index.txt | 16 +- .../out/policies/__next._tree.txt | 6 +- .../_experimental/out/policies/index.html | 2 +- .../_experimental/out/policies/index.txt | 42 ++--- ...ext.!KGRhc2hib2FyZCk.projects.__PAGE__.txt | 8 +- .../__next.!KGRhc2hib2FyZCk.projects.txt | 4 +- .../out/projects/__next.!KGRhc2hib2FyZCk.txt | 10 +- .../out/projects/__next._full.txt | 42 ++--- .../out/projects/__next._head.txt | 6 +- .../out/projects/__next._index.txt | 16 +- .../out/projects/__next._tree.txt | 6 +- .../_experimental/out/projects/index.html | 2 +- .../_experimental/out/projects/index.txt | 42 ++--- ...next.!KGRhc2hib2FyZCk.prompts.__PAGE__.txt | 8 +- .../__next.!KGRhc2hib2FyZCk.prompts.txt | 4 +- .../out/prompts/__next.!KGRhc2hib2FyZCk.txt | 10 +- .../out/prompts/__next._full.txt | 42 ++--- .../out/prompts/__next._head.txt | 6 +- .../out/prompts/__next._index.txt | 16 +- .../out/prompts/__next._tree.txt | 6 +- .../_experimental/out/prompts/index.html | 2 +- .../proxy/_experimental/out/prompts/index.txt | 42 ++--- ...Rhc2hib2FyZCk.router-settings.__PAGE__.txt | 8 +- ..._next.!KGRhc2hib2FyZCk.router-settings.txt | 4 +- .../__next.!KGRhc2hib2FyZCk.txt | 10 +- .../out/router-settings/__next._full.txt | 42 ++--- .../out/router-settings/__next._head.txt | 6 +- .../out/router-settings/__next._index.txt | 16 +- .../out/router-settings/__next._tree.txt | 6 +- .../out/router-settings/index.html | 2 +- .../out/router-settings/index.txt | 42 ++--- ...!KGRhc2hib2FyZCk.search-tools.__PAGE__.txt | 8 +- .../__next.!KGRhc2hib2FyZCk.search-tools.txt | 4 +- .../search-tools/__next.!KGRhc2hib2FyZCk.txt | 10 +- .../out/search-tools/__next._full.txt | 42 ++--- .../out/search-tools/__next._head.txt | 6 +- .../out/search-tools/__next._index.txt | 16 +- .../out/search-tools/__next._tree.txt | 6 +- .../_experimental/out/search-tools/index.html | 2 +- .../_experimental/out/search-tools/index.txt | 42 ++--- ..._next.!KGRhc2hib2FyZCk.skills.__PAGE__.txt | 8 +- .../skills/__next.!KGRhc2hib2FyZCk.skills.txt | 4 +- .../out/skills/__next.!KGRhc2hib2FyZCk.txt | 10 +- .../_experimental/out/skills/__next._full.txt | 42 ++--- .../_experimental/out/skills/__next._head.txt | 6 +- .../out/skills/__next._index.txt | 16 +- .../_experimental/out/skills/__next._tree.txt | 6 +- .../proxy/_experimental/out/skills/index.html | 2 +- .../proxy/_experimental/out/skills/index.txt | 42 ++--- ...GRhc2hib2FyZCk.tag-management.__PAGE__.txt | 8 +- ...__next.!KGRhc2hib2FyZCk.tag-management.txt | 4 +- .../__next.!KGRhc2hib2FyZCk.txt | 10 +- .../out/tag-management/__next._full.txt | 42 ++--- .../out/tag-management/__next._head.txt | 6 +- .../out/tag-management/__next._index.txt | 16 +- .../out/tag-management/__next._tree.txt | 6 +- .../out/tag-management/index.html | 2 +- .../out/tag-management/index.txt | 42 ++--- ...__next.!KGRhc2hib2FyZCk.teams.__PAGE__.txt | 8 +- .../teams/__next.!KGRhc2hib2FyZCk.teams.txt | 4 +- .../out/teams/__next.!KGRhc2hib2FyZCk.txt | 10 +- .../_experimental/out/teams/__next._full.txt | 42 ++--- .../_experimental/out/teams/__next._head.txt | 6 +- .../_experimental/out/teams/__next._index.txt | 16 +- .../_experimental/out/teams/__next._tree.txt | 6 +- .../proxy/_experimental/out/teams/index.html | 2 +- .../proxy/_experimental/out/teams/index.txt | 42 ++--- ...KGRhc2hib2FyZCk.tool-policies.__PAGE__.txt | 10 +- .../__next.!KGRhc2hib2FyZCk.tool-policies.txt | 4 +- .../tool-policies/__next.!KGRhc2hib2FyZCk.txt | 10 +- .../out/tool-policies/__next._full.txt | 44 ++--- .../out/tool-policies/__next._head.txt | 6 +- .../out/tool-policies/__next._index.txt | 16 +- .../out/tool-policies/__next._tree.txt | 8 +- .../out/tool-policies/index.html | 2 +- .../_experimental/out/tool-policies/index.txt | 44 ++--- ...c2hib2FyZCk.transform-request.__PAGE__.txt | 8 +- ...ext.!KGRhc2hib2FyZCk.transform-request.txt | 4 +- .../__next.!KGRhc2hib2FyZCk.txt | 10 +- .../out/transform-request/__next._full.txt | 42 ++--- .../out/transform-request/__next._head.txt | 6 +- .../out/transform-request/__next._index.txt | 16 +- .../out/transform-request/__next._tree.txt | 6 +- .../out/transform-request/index.html | 2 +- .../out/transform-request/index.txt | 42 ++--- .../out/ui-theme/__next.!KGRhc2hib2FyZCk.txt | 10 +- ...ext.!KGRhc2hib2FyZCk.ui-theme.__PAGE__.txt | 8 +- .../__next.!KGRhc2hib2FyZCk.ui-theme.txt | 4 +- .../out/ui-theme/__next._full.txt | 42 ++--- .../out/ui-theme/__next._head.txt | 6 +- .../out/ui-theme/__next._index.txt | 16 +- .../out/ui-theme/__next._tree.txt | 6 +- .../_experimental/out/ui-theme/index.html | 2 +- .../_experimental/out/ui-theme/index.txt | 42 ++--- .../out/usage/__next.!KGRhc2hib2FyZCk.txt | 10 +- ...__next.!KGRhc2hib2FyZCk.usage.__PAGE__.txt | 8 +- .../usage/__next.!KGRhc2hib2FyZCk.usage.txt | 4 +- .../_experimental/out/usage/__next._full.txt | 42 ++--- .../_experimental/out/usage/__next._head.txt | 6 +- .../_experimental/out/usage/__next._index.txt | 16 +- .../_experimental/out/usage/__next._tree.txt | 6 +- .../proxy/_experimental/out/usage/index.html | 2 +- .../proxy/_experimental/out/usage/index.txt | 42 ++--- .../out/users/__next.!KGRhc2hib2FyZCk.txt | 10 +- ...__next.!KGRhc2hib2FyZCk.users.__PAGE__.txt | 8 +- .../users/__next.!KGRhc2hib2FyZCk.users.txt | 4 +- .../_experimental/out/users/__next._full.txt | 42 ++--- .../_experimental/out/users/__next._head.txt | 6 +- .../_experimental/out/users/__next._index.txt | 16 +- .../_experimental/out/users/__next._tree.txt | 6 +- .../proxy/_experimental/out/users/index.html | 2 +- .../proxy/_experimental/out/users/index.txt | 42 ++--- .../vector-stores/__next.!KGRhc2hib2FyZCk.txt | 10 +- ...KGRhc2hib2FyZCk.vector-stores.__PAGE__.txt | 8 +- .../__next.!KGRhc2hib2FyZCk.vector-stores.txt | 4 +- .../out/vector-stores/__next._full.txt | 42 ++--- .../out/vector-stores/__next._head.txt | 6 +- .../out/vector-stores/__next._index.txt | 16 +- .../out/vector-stores/__next._tree.txt | 6 +- .../out/vector-stores/index.html | 2 +- .../_experimental/out/vector-stores/index.txt | 42 ++--- .../out/workflows/__next.!KGRhc2hib2FyZCk.txt | 10 +- ...xt.!KGRhc2hib2FyZCk.workflows.__PAGE__.txt | 8 +- .../__next.!KGRhc2hib2FyZCk.workflows.txt | 4 +- .../out/workflows/__next._full.txt | 42 ++--- .../out/workflows/__next._head.txt | 6 +- .../out/workflows/__next._index.txt | 16 +- .../out/workflows/__next._tree.txt | 6 +- .../_experimental/out/workflows/index.html | 2 +- .../_experimental/out/workflows/index.txt | 42 ++--- .../managed_id_rewriter.py | 3 +- .../proxy/test_managed_files_access_check.py | 107 ++++++++++- .../test_managed_resource_isolation.py | 44 +++++ 482 files changed, 3860 insertions(+), 3679 deletions(-) diff --git a/enterprise/litellm_enterprise/proxy/hooks/managed_files.py b/enterprise/litellm_enterprise/proxy/hooks/managed_files.py index 8821736d0ff..1f0163847c3 100644 --- a/enterprise/litellm_enterprise/proxy/hooks/managed_files.py +++ b/enterprise/litellm_enterprise/proxy/hooks/managed_files.py @@ -21,6 +21,7 @@ from litellm.llms.base_llm.managed_resources.isolation import ( build_list_page, build_owner_filter, can_access_resource, + resolve_resource_owner_id, ) from litellm.proxy._types import ( CallTypes, @@ -105,7 +106,7 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints): file_object=file_object, model_mappings=model_mappings, flat_model_file_ids=list(model_mappings.values()), - created_by=user_api_key_dict.user_id, + created_by=resolve_resource_owner_id(user_api_key_dict), team_id=user_api_key_dict.team_id, updated_by=user_api_key_dict.user_id, ) @@ -121,7 +122,7 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints): "unified_file_id": file_id, "model_mappings": json.dumps(model_mappings), "flat_model_file_ids": list(model_mappings.values()), - "created_by": user_api_key_dict.user_id, + "created_by": resolve_resource_owner_id(user_api_key_dict), "team_id": user_api_key_dict.team_id, "updated_by": user_api_key_dict.user_id, } @@ -189,7 +190,7 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints): "file_object": file_object.model_dump_json(), "model_object_id": model_object_id, "file_purpose": file_purpose, - "created_by": user_api_key_dict.user_id, + "created_by": resolve_resource_owner_id(user_api_key_dict), "team_id": user_api_key_dict.team_id, "updated_by": user_api_key_dict.user_id, "status": file_object.status, diff --git a/litellm/llms/base_llm/managed_resources/base_managed_resource.py b/litellm/llms/base_llm/managed_resources/base_managed_resource.py index 146a6aa6ae0..9c03820d91d 100644 --- a/litellm/llms/base_llm/managed_resources/base_managed_resource.py +++ b/litellm/llms/base_llm/managed_resources/base_managed_resource.py @@ -22,6 +22,7 @@ from litellm.llms.base_llm.managed_resources.isolation import ( build_list_page, build_owner_filter, can_access_resource, + resolve_resource_owner_id, ) from litellm.proxy._types import UserAPIKeyAuth from litellm.types.utils import SpecialEnums @@ -171,7 +172,7 @@ class BaseManagedResource(ABC, Generic[ResourceObjectType]): "resource_object": resource_object, "model_mappings": model_mappings, "flat_model_resource_ids": list(model_mappings.values()), - "created_by": user_api_key_dict.user_id, + "created_by": resolve_resource_owner_id(user_api_key_dict), "team_id": user_api_key_dict.team_id, "updated_by": user_api_key_dict.user_id, } @@ -193,7 +194,7 @@ class BaseManagedResource(ABC, Generic[ResourceObjectType]): "unified_resource_id": unified_resource_id, "model_mappings": json.dumps(model_mappings), "flat_model_resource_ids": list(model_mappings.values()), - "created_by": user_api_key_dict.user_id, + "created_by": resolve_resource_owner_id(user_api_key_dict), "team_id": user_api_key_dict.team_id, "updated_by": user_api_key_dict.user_id, } diff --git a/litellm/llms/base_llm/managed_resources/isolation.py b/litellm/llms/base_llm/managed_resources/isolation.py index fd1e24f3e1d..5cc2110ef95 100644 --- a/litellm/llms/base_llm/managed_resources/isolation.py +++ b/litellm/llms/base_llm/managed_resources/isolation.py @@ -3,10 +3,11 @@ Tenant-isolation helpers for managed file/batch/vector-store resources. Returns a Prisma filter and an ownership check that scope managed resources to the caller's identity: proxy admins see everything, user-keyed callers -see records they created, and service-account keys (no user_id) fall back -to the resource's owning team. Callers with no admin role and no -identifying ids are denied so an empty user_id can never select an -unscoped query. +see records they created, service-account keys (no user_id) fall back to +the resource's owning team, and keys with neither a user_id nor a team_id +fall back to their own hashed token so they can still reach the resources +they created. Callers with no admin role and no identifying ids at all +are denied so an empty user_id can never select an unscoped query. """ from typing import Any, Dict, List, Optional @@ -17,6 +18,32 @@ from litellm.proxy._types import ( ) +def resolve_resource_owner_id( + user_api_key_dict: UserAPIKeyAuth, +) -> str | None: + """Return the identity to stamp on (and match against) a managed + resource's ``created_by``. + + A key with neither a user_id nor a team_id would otherwise stamp + ``created_by=None`` and be locked out of its own resources, so it owns + them under its hashed token instead, using the ``key:`` scope prefix + already used by ``proxy/common_utils/resource_ownership.py``. ``None`` + means the caller has no usable identity of its own and must fall back + to team scoping, or be denied. + """ + if user_api_key_dict.user_id is not None: + return user_api_key_dict.user_id + + if user_api_key_dict.team_id is not None: + return None + + token = user_api_key_dict.token or user_api_key_dict.api_key + if token: + return f"key:{token}" + + return None + + def build_list_page(items: List[Any], has_more: bool = False) -> Dict[str, Any]: """Build the OpenAI-style paginated list response shape used by managed file/batch/vector-store listings. ``first_id`` and ``last_id`` are @@ -37,7 +64,8 @@ def build_owner_filter( to records the caller is allowed to see. - ``{}`` means no scoping (proxy admins). - - ``{"created_by": }`` for user-keyed callers. + - ``{"created_by": }`` for user-keyed callers, and for keys + with no user_id and no team_id (owner id is their hashed token). - ``{"team_id": }`` for service-account callers that have a team but no user_id. - ``{"OR": [...]}`` when the caller has both — listing must include @@ -60,12 +88,13 @@ def build_owner_filter( ] } - if user_id is not None: - return {"created_by": user_id} - if team_id is not None: return {"team_id": team_id} + owner_id = resolve_resource_owner_id(user_api_key_dict) + if owner_id is not None: + return {"created_by": owner_id} + return None @@ -84,8 +113,8 @@ def can_access_resource( if _user_has_admin_view(user_api_key_dict): return True - user_id = user_api_key_dict.user_id - if user_id is not None and created_by is not None and created_by == user_id: + owner_id = resolve_resource_owner_id(user_api_key_dict) + if owner_id is not None and created_by is not None and created_by == owner_id: return True team_id = user_api_key_dict.team_id diff --git a/litellm/proxy/_experimental/out/404.html b/litellm/proxy/_experimental/out/404.html index 96452afb6d3..0f9365a3e82 100644 --- a/litellm/proxy/_experimental/out/404.html +++ b/litellm/proxy/_experimental/out/404.html @@ -1 +1 @@ -404: This page could not be found.LiteLLM Dashboard

404

This page could not be found.

\ No newline at end of file +404: This page could not be found.LiteLLM Dashboard

404

This page could not be found.

\ No newline at end of file diff --git a/litellm/proxy/_experimental/out/404/index.html b/litellm/proxy/_experimental/out/404/index.html index 96452afb6d3..0f9365a3e82 100644 --- a/litellm/proxy/_experimental/out/404/index.html +++ b/litellm/proxy/_experimental/out/404/index.html @@ -1 +1 @@ -404: This page could not be found.LiteLLM Dashboard

404

This page could not be found.

\ No newline at end of file +404: This page could not be found.LiteLLM Dashboard

404

This page could not be found.

\ No newline at end of file diff --git a/litellm/proxy/_experimental/out/__next.!KGRhc2hib2FyZCk.__PAGE__.txt b/litellm/proxy/_experimental/out/__next.!KGRhc2hib2FyZCk.__PAGE__.txt index 657acb4c2e5..913093c9014 100644 --- a/litellm/proxy/_experimental/out/__next.!KGRhc2hib2FyZCk.__PAGE__.txt +++ b/litellm/proxy/_experimental/out/__next.!KGRhc2hib2FyZCk.__PAGE__.txt @@ -1,9 +1,9 @@ 1:"$Sreact.fragment" -2:I[347257,["/litellm-asset-prefix/_next/static/chunks/0vggytdohwe7o.js","/litellm-asset-prefix/_next/static/chunks/0map77ee0fk0e.js","/litellm-asset-prefix/_next/static/chunks/1jfookxfajkeo.js"],"ClientPageRoot"] -3:I[871135,["/litellm-asset-prefix/_next/static/chunks/0vggytdohwe7o.js","/litellm-asset-prefix/_next/static/chunks/0map77ee0fk0e.js","/litellm-asset-prefix/_next/static/chunks/1jfookxfajkeo.js","/litellm-asset-prefix/_next/static/chunks/1ioy8obpggx93.js","/litellm-asset-prefix/_next/static/chunks/3_3dj4vdy-3xy.js","/litellm-asset-prefix/_next/static/chunks/1zr7rrk4wkmju.js","/litellm-asset-prefix/_next/static/chunks/2cngn5bal3278.js","/litellm-asset-prefix/_next/static/chunks/2up3bks93iqds.js","/litellm-asset-prefix/_next/static/chunks/1l2mgm5v3tjci.js","/litellm-asset-prefix/_next/static/chunks/0zduf1gntl_f8.js","/litellm-asset-prefix/_next/static/chunks/0g_w4tf2inv3i.js","/litellm-asset-prefix/_next/static/chunks/1a0bgy7kzrj91.js","/litellm-asset-prefix/_next/static/chunks/0dbvgsc7ha049.js","/litellm-asset-prefix/_next/static/chunks/17-6zku8f68gf.js","/litellm-asset-prefix/_next/static/chunks/0f5fel02jwglw.js","/litellm-asset-prefix/_next/static/chunks/1vquuz09jxl5_.js","/litellm-asset-prefix/_next/static/chunks/1iakmimqrlpn0.js","/litellm-asset-prefix/_next/static/chunks/2n26sdz53rm0a.js","/litellm-asset-prefix/_next/static/chunks/11khk745tfruy.js","/litellm-asset-prefix/_next/static/chunks/00g6xfr4yow7h.js","/litellm-asset-prefix/_next/static/chunks/0-k_4_s7m108w.js","/litellm-asset-prefix/_next/static/chunks/2kcxwg1mpncp6.js","/litellm-asset-prefix/_next/static/chunks/3drq2_k-jeio2.js","/litellm-asset-prefix/_next/static/chunks/395_vbpmrlvpu.js","/litellm-asset-prefix/_next/static/chunks/2hu1vyy-5pv13.js","/litellm-asset-prefix/_next/static/chunks/2l25bmiiw9ixp.js","/litellm-asset-prefix/_next/static/chunks/1uz3jt-tj9lkf.js","/litellm-asset-prefix/_next/static/chunks/199uwr871eene.js","/litellm-asset-prefix/_next/static/chunks/2s_ce-opzrkzr.js","/litellm-asset-prefix/_next/static/chunks/2ptdxz8qnchh_.js","/litellm-asset-prefix/_next/static/chunks/17nqbxvhztf3k.js","/litellm-asset-prefix/_next/static/chunks/2c90xukbd3il6.js","/litellm-asset-prefix/_next/static/chunks/0kap_rdm2-lem.js","/litellm-asset-prefix/_next/static/chunks/09l_m9l1emin2.js","/litellm-asset-prefix/_next/static/chunks/1cea03gg5a_c7.js","/litellm-asset-prefix/_next/static/chunks/323l6h8s7ahat.js","/litellm-asset-prefix/_next/static/chunks/112n0hv3cc2rg.js","/litellm-asset-prefix/_next/static/chunks/3y674jhwchpcq.js","/litellm-asset-prefix/_next/static/chunks/105643dvf00hu.js","/litellm-asset-prefix/_next/static/chunks/12wsfsljxg4xv.js","/litellm-asset-prefix/_next/static/chunks/22iools_e0k44.js","/litellm-asset-prefix/_next/static/chunks/3f9uewf5w-e-p.js","/litellm-asset-prefix/_next/static/chunks/23-g73xaw3kap.js"],"default"] -6:I[897367,["/litellm-asset-prefix/_next/static/chunks/0vggytdohwe7o.js","/litellm-asset-prefix/_next/static/chunks/0map77ee0fk0e.js","/litellm-asset-prefix/_next/static/chunks/1jfookxfajkeo.js"],"OutletBoundary"] +2:I[347257,["/my-custom-path/_next/static/chunks/0vggytdohwe7o.js","/my-custom-path/_next/static/chunks/0map77ee0fk0e.js","/my-custom-path/_next/static/chunks/1jfookxfajkeo.js"],"ClientPageRoot"] +3:I[871135,["/my-custom-path/_next/static/chunks/0vggytdohwe7o.js","/my-custom-path/_next/static/chunks/0map77ee0fk0e.js","/my-custom-path/_next/static/chunks/1jfookxfajkeo.js","/my-custom-path/_next/static/chunks/1ioy8obpggx93.js","/my-custom-path/_next/static/chunks/3_3dj4vdy-3xy.js","/my-custom-path/_next/static/chunks/1zr7rrk4wkmju.js","/my-custom-path/_next/static/chunks/2cngn5bal3278.js","/my-custom-path/_next/static/chunks/2up3bks93iqds.js","/my-custom-path/_next/static/chunks/1l2mgm5v3tjci.js","/my-custom-path/_next/static/chunks/0zduf1gntl_f8.js","/my-custom-path/_next/static/chunks/0g_w4tf2inv3i.js","/my-custom-path/_next/static/chunks/1a0bgy7kzrj91.js","/my-custom-path/_next/static/chunks/0dbvgsc7ha049.js","/my-custom-path/_next/static/chunks/17-6zku8f68gf.js","/my-custom-path/_next/static/chunks/0f5fel02jwglw.js","/my-custom-path/_next/static/chunks/1vquuz09jxl5_.js","/my-custom-path/_next/static/chunks/1iakmimqrlpn0.js","/my-custom-path/_next/static/chunks/2n26sdz53rm0a.js","/my-custom-path/_next/static/chunks/11khk745tfruy.js","/my-custom-path/_next/static/chunks/00g6xfr4yow7h.js","/my-custom-path/_next/static/chunks/0-k_4_s7m108w.js","/my-custom-path/_next/static/chunks/2kcxwg1mpncp6.js","/my-custom-path/_next/static/chunks/3drq2_k-jeio2.js","/my-custom-path/_next/static/chunks/395_vbpmrlvpu.js","/my-custom-path/_next/static/chunks/2hu1vyy-5pv13.js","/my-custom-path/_next/static/chunks/2l25bmiiw9ixp.js","/my-custom-path/_next/static/chunks/1uz3jt-tj9lkf.js","/my-custom-path/_next/static/chunks/199uwr871eene.js","/my-custom-path/_next/static/chunks/2s_ce-opzrkzr.js","/my-custom-path/_next/static/chunks/2ptdxz8qnchh_.js","/my-custom-path/_next/static/chunks/17nqbxvhztf3k.js","/my-custom-path/_next/static/chunks/2c90xukbd3il6.js","/my-custom-path/_next/static/chunks/0kap_rdm2-lem.js","/my-custom-path/_next/static/chunks/09l_m9l1emin2.js","/my-custom-path/_next/static/chunks/1cea03gg5a_c7.js","/my-custom-path/_next/static/chunks/323l6h8s7ahat.js","/my-custom-path/_next/static/chunks/112n0hv3cc2rg.js","/my-custom-path/_next/static/chunks/3y674jhwchpcq.js","/my-custom-path/_next/static/chunks/105643dvf00hu.js","/my-custom-path/_next/static/chunks/12wsfsljxg4xv.js","/my-custom-path/_next/static/chunks/22iools_e0k44.js","/my-custom-path/_next/static/chunks/3f9uewf5w-e-p.js","/my-custom-path/_next/static/chunks/23-g73xaw3kap.js"],"default"] +6:I[897367,["/my-custom-path/_next/static/chunks/0vggytdohwe7o.js","/my-custom-path/_next/static/chunks/0map77ee0fk0e.js","/my-custom-path/_next/static/chunks/1jfookxfajkeo.js"],"OutletBoundary"] 7:"$Sreact.suspense" -0:{"rsc":["$","$1","c",{"children":[["$","$L2",null,{"Component":"$3","serverProvidedParams":{"searchParams":{},"params":{},"promises":["$@4","$@5"]}}],[["$","script","script-0",{"src":"/litellm-asset-prefix/_next/static/chunks/00g6xfr4yow7h.js","async":true}],["$","script","script-1",{"src":"/litellm-asset-prefix/_next/static/chunks/0-k_4_s7m108w.js","async":true}],["$","script","script-2",{"src":"/litellm-asset-prefix/_next/static/chunks/2kcxwg1mpncp6.js","async":true}],["$","script","script-3",{"src":"/litellm-asset-prefix/_next/static/chunks/3drq2_k-jeio2.js","async":true}],["$","script","script-4",{"src":"/litellm-asset-prefix/_next/static/chunks/395_vbpmrlvpu.js","async":true}],["$","script","script-5",{"src":"/litellm-asset-prefix/_next/static/chunks/2hu1vyy-5pv13.js","async":true}],["$","script","script-6",{"src":"/litellm-asset-prefix/_next/static/chunks/2l25bmiiw9ixp.js","async":true}],["$","script","script-7",{"src":"/litellm-asset-prefix/_next/static/chunks/1uz3jt-tj9lkf.js","async":true}],["$","script","script-8",{"src":"/litellm-asset-prefix/_next/static/chunks/199uwr871eene.js","async":true}],["$","script","script-9",{"src":"/litellm-asset-prefix/_next/static/chunks/2s_ce-opzrkzr.js","async":true}],["$","script","script-10",{"src":"/litellm-asset-prefix/_next/static/chunks/2ptdxz8qnchh_.js","async":true}],["$","script","script-11",{"src":"/litellm-asset-prefix/_next/static/chunks/17nqbxvhztf3k.js","async":true}],["$","script","script-12",{"src":"/litellm-asset-prefix/_next/static/chunks/2c90xukbd3il6.js","async":true}],["$","script","script-13",{"src":"/litellm-asset-prefix/_next/static/chunks/0kap_rdm2-lem.js","async":true}],["$","script","script-14",{"src":"/litellm-asset-prefix/_next/static/chunks/09l_m9l1emin2.js","async":true}],["$","script","script-15",{"src":"/litellm-asset-prefix/_next/static/chunks/1cea03gg5a_c7.js","async":true}],["$","script","script-16",{"src":"/litellm-asset-prefix/_next/static/chunks/323l6h8s7ahat.js","async":true}],["$","script","script-17",{"src":"/litellm-asset-prefix/_next/static/chunks/112n0hv3cc2rg.js","async":true}],["$","script","script-18",{"src":"/litellm-asset-prefix/_next/static/chunks/3y674jhwchpcq.js","async":true}],["$","script","script-19",{"src":"/litellm-asset-prefix/_next/static/chunks/105643dvf00hu.js","async":true}],["$","script","script-20",{"src":"/litellm-asset-prefix/_next/static/chunks/12wsfsljxg4xv.js","async":true}],["$","script","script-21",{"src":"/litellm-asset-prefix/_next/static/chunks/22iools_e0k44.js","async":true}],["$","script","script-22",{"src":"/litellm-asset-prefix/_next/static/chunks/3f9uewf5w-e-p.js","async":true}],["$","script","script-23",{"src":"/litellm-asset-prefix/_next/static/chunks/23-g73xaw3kap.js","async":true}]],["$","$L6",null,{"children":["$","$7",null,{"name":"Next.MetadataOutlet","children":"$@8"}]}]]}],"isPartial":false,"staleTime":300,"varyParams":null,"buildId":"0ljiPmkOdq7_yE4sZoXlJ"} +0:{"rsc":["$","$1","c",{"children":[["$","$L2",null,{"Component":"$3","serverProvidedParams":{"searchParams":{},"params":{},"promises":["$@4","$@5"]}}],[["$","script","script-0",{"src":"/my-custom-path/_next/static/chunks/00g6xfr4yow7h.js","async":true}],["$","script","script-1",{"src":"/my-custom-path/_next/static/chunks/0-k_4_s7m108w.js","async":true}],["$","script","script-2",{"src":"/my-custom-path/_next/static/chunks/2kcxwg1mpncp6.js","async":true}],["$","script","script-3",{"src":"/my-custom-path/_next/static/chunks/3drq2_k-jeio2.js","async":true}],["$","script","script-4",{"src":"/my-custom-path/_next/static/chunks/395_vbpmrlvpu.js","async":true}],["$","script","script-5",{"src":"/my-custom-path/_next/static/chunks/2hu1vyy-5pv13.js","async":true}],["$","script","script-6",{"src":"/my-custom-path/_next/static/chunks/2l25bmiiw9ixp.js","async":true}],["$","script","script-7",{"src":"/my-custom-path/_next/static/chunks/1uz3jt-tj9lkf.js","async":true}],["$","script","script-8",{"src":"/my-custom-path/_next/static/chunks/199uwr871eene.js","async":true}],["$","script","script-9",{"src":"/my-custom-path/_next/static/chunks/2s_ce-opzrkzr.js","async":true}],["$","script","script-10",{"src":"/my-custom-path/_next/static/chunks/2ptdxz8qnchh_.js","async":true}],["$","script","script-11",{"src":"/my-custom-path/_next/static/chunks/17nqbxvhztf3k.js","async":true}],["$","script","script-12",{"src":"/my-custom-path/_next/static/chunks/2c90xukbd3il6.js","async":true}],["$","script","script-13",{"src":"/my-custom-path/_next/static/chunks/0kap_rdm2-lem.js","async":true}],["$","script","script-14",{"src":"/my-custom-path/_next/static/chunks/09l_m9l1emin2.js","async":true}],["$","script","script-15",{"src":"/my-custom-path/_next/static/chunks/1cea03gg5a_c7.js","async":true}],["$","script","script-16",{"src":"/my-custom-path/_next/static/chunks/323l6h8s7ahat.js","async":true}],["$","script","script-17",{"src":"/my-custom-path/_next/static/chunks/112n0hv3cc2rg.js","async":true}],["$","script","script-18",{"src":"/my-custom-path/_next/static/chunks/3y674jhwchpcq.js","async":true}],["$","script","script-19",{"src":"/my-custom-path/_next/static/chunks/105643dvf00hu.js","async":true}],["$","script","script-20",{"src":"/my-custom-path/_next/static/chunks/12wsfsljxg4xv.js","async":true}],["$","script","script-21",{"src":"/my-custom-path/_next/static/chunks/22iools_e0k44.js","async":true}],["$","script","script-22",{"src":"/my-custom-path/_next/static/chunks/3f9uewf5w-e-p.js","async":true}],["$","script","script-23",{"src":"/my-custom-path/_next/static/chunks/23-g73xaw3kap.js","async":true}]],["$","$L6",null,{"children":["$","$7",null,{"name":"Next.MetadataOutlet","children":"$@8"}]}]]}],"isPartial":false,"staleTime":300,"varyParams":null,"buildId":"0ljiPmkOdq7_yE4sZoXlJ"} 4:{} 5:"$0:rsc:props:children:0:props:serverProvidedParams:params" 8:null diff --git a/litellm/proxy/_experimental/out/__next.!KGRhc2hib2FyZCk.txt b/litellm/proxy/_experimental/out/__next.!KGRhc2hib2FyZCk.txt index 0eba32f6bf2..f77516d31f2 100644 --- a/litellm/proxy/_experimental/out/__next.!KGRhc2hib2FyZCk.txt +++ b/litellm/proxy/_experimental/out/__next.!KGRhc2hib2FyZCk.txt @@ -1,7 +1,7 @@ 1:"$Sreact.fragment" -2:I[92825,["/litellm-asset-prefix/_next/static/chunks/0vggytdohwe7o.js","/litellm-asset-prefix/_next/static/chunks/0map77ee0fk0e.js","/litellm-asset-prefix/_next/static/chunks/1jfookxfajkeo.js"],"ClientSegmentRoot"] -3:I[216370,["/litellm-asset-prefix/_next/static/chunks/0vggytdohwe7o.js","/litellm-asset-prefix/_next/static/chunks/0map77ee0fk0e.js","/litellm-asset-prefix/_next/static/chunks/1jfookxfajkeo.js","/litellm-asset-prefix/_next/static/chunks/1ioy8obpggx93.js","/litellm-asset-prefix/_next/static/chunks/3_3dj4vdy-3xy.js","/litellm-asset-prefix/_next/static/chunks/1zr7rrk4wkmju.js","/litellm-asset-prefix/_next/static/chunks/2cngn5bal3278.js","/litellm-asset-prefix/_next/static/chunks/2up3bks93iqds.js","/litellm-asset-prefix/_next/static/chunks/1l2mgm5v3tjci.js","/litellm-asset-prefix/_next/static/chunks/0zduf1gntl_f8.js","/litellm-asset-prefix/_next/static/chunks/0g_w4tf2inv3i.js","/litellm-asset-prefix/_next/static/chunks/1a0bgy7kzrj91.js","/litellm-asset-prefix/_next/static/chunks/0dbvgsc7ha049.js","/litellm-asset-prefix/_next/static/chunks/17-6zku8f68gf.js","/litellm-asset-prefix/_next/static/chunks/0f5fel02jwglw.js","/litellm-asset-prefix/_next/static/chunks/1vquuz09jxl5_.js","/litellm-asset-prefix/_next/static/chunks/1iakmimqrlpn0.js","/litellm-asset-prefix/_next/static/chunks/2n26sdz53rm0a.js","/litellm-asset-prefix/_next/static/chunks/11khk745tfruy.js"],"default"] -4:I[339756,["/litellm-asset-prefix/_next/static/chunks/0vggytdohwe7o.js","/litellm-asset-prefix/_next/static/chunks/0map77ee0fk0e.js","/litellm-asset-prefix/_next/static/chunks/1jfookxfajkeo.js"],"default"] -5:I[837457,["/litellm-asset-prefix/_next/static/chunks/0vggytdohwe7o.js","/litellm-asset-prefix/_next/static/chunks/0map77ee0fk0e.js","/litellm-asset-prefix/_next/static/chunks/1jfookxfajkeo.js"],"default"] -0:{"rsc":["$","$1","c",{"children":[[["$","script","script-0",{"src":"/litellm-asset-prefix/_next/static/chunks/1ioy8obpggx93.js","async":true}],["$","script","script-1",{"src":"/litellm-asset-prefix/_next/static/chunks/3_3dj4vdy-3xy.js","async":true}],["$","script","script-2",{"src":"/litellm-asset-prefix/_next/static/chunks/1zr7rrk4wkmju.js","async":true}],["$","script","script-3",{"src":"/litellm-asset-prefix/_next/static/chunks/2cngn5bal3278.js","async":true}],["$","script","script-4",{"src":"/litellm-asset-prefix/_next/static/chunks/2up3bks93iqds.js","async":true}],["$","script","script-5",{"src":"/litellm-asset-prefix/_next/static/chunks/1l2mgm5v3tjci.js","async":true}],["$","script","script-6",{"src":"/litellm-asset-prefix/_next/static/chunks/0zduf1gntl_f8.js","async":true}],["$","script","script-7",{"src":"/litellm-asset-prefix/_next/static/chunks/0g_w4tf2inv3i.js","async":true}],["$","script","script-8",{"src":"/litellm-asset-prefix/_next/static/chunks/1a0bgy7kzrj91.js","async":true}],["$","script","script-9",{"src":"/litellm-asset-prefix/_next/static/chunks/0dbvgsc7ha049.js","async":true}],["$","script","script-10",{"src":"/litellm-asset-prefix/_next/static/chunks/17-6zku8f68gf.js","async":true}],["$","script","script-11",{"src":"/litellm-asset-prefix/_next/static/chunks/0f5fel02jwglw.js","async":true}],["$","script","script-12",{"src":"/litellm-asset-prefix/_next/static/chunks/1vquuz09jxl5_.js","async":true}],["$","script","script-13",{"src":"/litellm-asset-prefix/_next/static/chunks/1iakmimqrlpn0.js","async":true}],["$","script","script-14",{"src":"/litellm-asset-prefix/_next/static/chunks/2n26sdz53rm0a.js","async":true}],["$","script","script-15",{"src":"/litellm-asset-prefix/_next/static/chunks/11khk745tfruy.js","async":true}]],["$","$L2",null,{"Component":"$3","slots":{"children":["$","$L4",null,{"parallelRouterKey":"children","template":["$","$L5",null,{}],"notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]]}]},"serverProvidedParams":{"params":{},"promises":["$@6"]}}]]}],"isPartial":false,"staleTime":300,"varyParams":null,"buildId":"0ljiPmkOdq7_yE4sZoXlJ"} +2:I[92825,["/my-custom-path/_next/static/chunks/0vggytdohwe7o.js","/my-custom-path/_next/static/chunks/0map77ee0fk0e.js","/my-custom-path/_next/static/chunks/1jfookxfajkeo.js"],"ClientSegmentRoot"] +3:I[216370,["/my-custom-path/_next/static/chunks/0vggytdohwe7o.js","/my-custom-path/_next/static/chunks/0map77ee0fk0e.js","/my-custom-path/_next/static/chunks/1jfookxfajkeo.js","/my-custom-path/_next/static/chunks/1ioy8obpggx93.js","/my-custom-path/_next/static/chunks/3_3dj4vdy-3xy.js","/my-custom-path/_next/static/chunks/1zr7rrk4wkmju.js","/my-custom-path/_next/static/chunks/2cngn5bal3278.js","/my-custom-path/_next/static/chunks/2up3bks93iqds.js","/my-custom-path/_next/static/chunks/1l2mgm5v3tjci.js","/my-custom-path/_next/static/chunks/0zduf1gntl_f8.js","/my-custom-path/_next/static/chunks/0g_w4tf2inv3i.js","/my-custom-path/_next/static/chunks/1a0bgy7kzrj91.js","/my-custom-path/_next/static/chunks/0dbvgsc7ha049.js","/my-custom-path/_next/static/chunks/17-6zku8f68gf.js","/my-custom-path/_next/static/chunks/0f5fel02jwglw.js","/my-custom-path/_next/static/chunks/1vquuz09jxl5_.js","/my-custom-path/_next/static/chunks/1iakmimqrlpn0.js","/my-custom-path/_next/static/chunks/2n26sdz53rm0a.js","/my-custom-path/_next/static/chunks/11khk745tfruy.js"],"default"] +4:I[339756,["/my-custom-path/_next/static/chunks/0vggytdohwe7o.js","/my-custom-path/_next/static/chunks/0map77ee0fk0e.js","/my-custom-path/_next/static/chunks/1jfookxfajkeo.js"],"default"] +5:I[837457,["/my-custom-path/_next/static/chunks/0vggytdohwe7o.js","/my-custom-path/_next/static/chunks/0map77ee0fk0e.js","/my-custom-path/_next/static/chunks/1jfookxfajkeo.js"],"default"] +0:{"rsc":["$","$1","c",{"children":[[["$","script","script-0",{"src":"/my-custom-path/_next/static/chunks/1ioy8obpggx93.js","async":true}],["$","script","script-1",{"src":"/my-custom-path/_next/static/chunks/3_3dj4vdy-3xy.js","async":true}],["$","script","script-2",{"src":"/my-custom-path/_next/static/chunks/1zr7rrk4wkmju.js","async":true}],["$","script","script-3",{"src":"/my-custom-path/_next/static/chunks/2cngn5bal3278.js","async":true}],["$","script","script-4",{"src":"/my-custom-path/_next/static/chunks/2up3bks93iqds.js","async":true}],["$","script","script-5",{"src":"/my-custom-path/_next/static/chunks/1l2mgm5v3tjci.js","async":true}],["$","script","script-6",{"src":"/my-custom-path/_next/static/chunks/0zduf1gntl_f8.js","async":true}],["$","script","script-7",{"src":"/my-custom-path/_next/static/chunks/0g_w4tf2inv3i.js","async":true}],["$","script","script-8",{"src":"/my-custom-path/_next/static/chunks/1a0bgy7kzrj91.js","async":true}],["$","script","script-9",{"src":"/my-custom-path/_next/static/chunks/0dbvgsc7ha049.js","async":true}],["$","script","script-10",{"src":"/my-custom-path/_next/static/chunks/17-6zku8f68gf.js","async":true}],["$","script","script-11",{"src":"/my-custom-path/_next/static/chunks/0f5fel02jwglw.js","async":true}],["$","script","script-12",{"src":"/my-custom-path/_next/static/chunks/1vquuz09jxl5_.js","async":true}],["$","script","script-13",{"src":"/my-custom-path/_next/static/chunks/1iakmimqrlpn0.js","async":true}],["$","script","script-14",{"src":"/my-custom-path/_next/static/chunks/2n26sdz53rm0a.js","async":true}],["$","script","script-15",{"src":"/my-custom-path/_next/static/chunks/11khk745tfruy.js","async":true}]],["$","$L2",null,{"Component":"$3","slots":{"children":["$","$L4",null,{"parallelRouterKey":"children","template":["$","$L5",null,{}],"notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]]}]},"serverProvidedParams":{"params":{},"promises":["$@6"]}}]]}],"isPartial":false,"staleTime":300,"varyParams":null,"buildId":"0ljiPmkOdq7_yE4sZoXlJ"} 6:"$0:rsc:props:children:1:props:serverProvidedParams:params" diff --git a/litellm/proxy/_experimental/out/__next._full.txt b/litellm/proxy/_experimental/out/__next._full.txt index 7b75f27b9e6..6ea282b252e 100644 --- a/litellm/proxy/_experimental/out/__next._full.txt +++ b/litellm/proxy/_experimental/out/__next._full.txt @@ -1,32 +1,32 @@ 1:"$Sreact.fragment" -2:I[867271,["/litellm-asset-prefix/_next/static/chunks/0vggytdohwe7o.js","/litellm-asset-prefix/_next/static/chunks/0map77ee0fk0e.js","/litellm-asset-prefix/_next/static/chunks/1jfookxfajkeo.js"],"default"] -3:I[71195,["/litellm-asset-prefix/_next/static/chunks/0vggytdohwe7o.js","/litellm-asset-prefix/_next/static/chunks/0map77ee0fk0e.js","/litellm-asset-prefix/_next/static/chunks/1jfookxfajkeo.js"],"default"] -4:I[557951,["/litellm-asset-prefix/_next/static/chunks/0vggytdohwe7o.js","/litellm-asset-prefix/_next/static/chunks/0map77ee0fk0e.js","/litellm-asset-prefix/_next/static/chunks/1jfookxfajkeo.js"],"AuthProvider"] -5:I[339756,["/litellm-asset-prefix/_next/static/chunks/0vggytdohwe7o.js","/litellm-asset-prefix/_next/static/chunks/0map77ee0fk0e.js","/litellm-asset-prefix/_next/static/chunks/1jfookxfajkeo.js"],"default"] -6:I[837457,["/litellm-asset-prefix/_next/static/chunks/0vggytdohwe7o.js","/litellm-asset-prefix/_next/static/chunks/0map77ee0fk0e.js","/litellm-asset-prefix/_next/static/chunks/1jfookxfajkeo.js"],"default"] -7:I[92825,["/litellm-asset-prefix/_next/static/chunks/0vggytdohwe7o.js","/litellm-asset-prefix/_next/static/chunks/0map77ee0fk0e.js","/litellm-asset-prefix/_next/static/chunks/1jfookxfajkeo.js"],"ClientSegmentRoot"] -8:I[216370,["/litellm-asset-prefix/_next/static/chunks/0vggytdohwe7o.js","/litellm-asset-prefix/_next/static/chunks/0map77ee0fk0e.js","/litellm-asset-prefix/_next/static/chunks/1jfookxfajkeo.js","/litellm-asset-prefix/_next/static/chunks/1ioy8obpggx93.js","/litellm-asset-prefix/_next/static/chunks/3_3dj4vdy-3xy.js","/litellm-asset-prefix/_next/static/chunks/1zr7rrk4wkmju.js","/litellm-asset-prefix/_next/static/chunks/2cngn5bal3278.js","/litellm-asset-prefix/_next/static/chunks/2up3bks93iqds.js","/litellm-asset-prefix/_next/static/chunks/1l2mgm5v3tjci.js","/litellm-asset-prefix/_next/static/chunks/0zduf1gntl_f8.js","/litellm-asset-prefix/_next/static/chunks/0g_w4tf2inv3i.js","/litellm-asset-prefix/_next/static/chunks/1a0bgy7kzrj91.js","/litellm-asset-prefix/_next/static/chunks/0dbvgsc7ha049.js","/litellm-asset-prefix/_next/static/chunks/17-6zku8f68gf.js","/litellm-asset-prefix/_next/static/chunks/0f5fel02jwglw.js","/litellm-asset-prefix/_next/static/chunks/1vquuz09jxl5_.js","/litellm-asset-prefix/_next/static/chunks/1iakmimqrlpn0.js","/litellm-asset-prefix/_next/static/chunks/2n26sdz53rm0a.js","/litellm-asset-prefix/_next/static/chunks/11khk745tfruy.js"],"default"] -e:I[168027,["/litellm-asset-prefix/_next/static/chunks/0vggytdohwe7o.js","/litellm-asset-prefix/_next/static/chunks/0map77ee0fk0e.js","/litellm-asset-prefix/_next/static/chunks/1jfookxfajkeo.js"],"default",1] -:HL["/litellm-asset-prefix/_next/static/chunks/1kid9zr1--h6y.css","style"] -:HL["/litellm-asset-prefix/_next/static/chunks/23vtcpdpp2h9h.css","style"] -:HL["/litellm-asset-prefix/_next/static/media/83afe278b6a6bb3c-s.p.2bn3s6zvc0dyp.woff2","font",{"crossOrigin":"","type":"font/woff2"}] -0:{"P":null,"c":["",""],"q":"","i":false,"f":[[["",{"children":["(dashboard)",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",16],[["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/litellm-asset-prefix/_next/static/chunks/1kid9zr1--h6y.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","link","1",{"rel":"stylesheet","href":"/litellm-asset-prefix/_next/static/chunks/23vtcpdpp2h9h.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","script","script-0",{"src":"/litellm-asset-prefix/_next/static/chunks/0vggytdohwe7o.js","async":true,"nonce":"$undefined"}],["$","script","script-1",{"src":"/litellm-asset-prefix/_next/static/chunks/0map77ee0fk0e.js","async":true,"nonce":"$undefined"}],["$","script","script-2",{"src":"/litellm-asset-prefix/_next/static/chunks/1jfookxfajkeo.js","async":true,"nonce":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"className":"inter_5972bc34-module__OU16Qa__className","children":["$","$L2",null,{"children":["$","$L3",null,{"children":["$","$L4",null,{"children":["$","$L5",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L6",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]}]}]}]}]]}],{"children":[["$","$1","c",{"children":[[["$","script","script-0",{"src":"/litellm-asset-prefix/_next/static/chunks/1ioy8obpggx93.js","async":true,"nonce":"$undefined"}],["$","script","script-1",{"src":"/litellm-asset-prefix/_next/static/chunks/3_3dj4vdy-3xy.js","async":true,"nonce":"$undefined"}],["$","script","script-2",{"src":"/litellm-asset-prefix/_next/static/chunks/1zr7rrk4wkmju.js","async":true,"nonce":"$undefined"}],["$","script","script-3",{"src":"/litellm-asset-prefix/_next/static/chunks/2cngn5bal3278.js","async":true,"nonce":"$undefined"}],["$","script","script-4",{"src":"/litellm-asset-prefix/_next/static/chunks/2up3bks93iqds.js","async":true,"nonce":"$undefined"}],["$","script","script-5",{"src":"/litellm-asset-prefix/_next/static/chunks/1l2mgm5v3tjci.js","async":true,"nonce":"$undefined"}],["$","script","script-6",{"src":"/litellm-asset-prefix/_next/static/chunks/0zduf1gntl_f8.js","async":true,"nonce":"$undefined"}],["$","script","script-7",{"src":"/litellm-asset-prefix/_next/static/chunks/0g_w4tf2inv3i.js","async":true,"nonce":"$undefined"}],["$","script","script-8",{"src":"/litellm-asset-prefix/_next/static/chunks/1a0bgy7kzrj91.js","async":true,"nonce":"$undefined"}],["$","script","script-9",{"src":"/litellm-asset-prefix/_next/static/chunks/0dbvgsc7ha049.js","async":true,"nonce":"$undefined"}],["$","script","script-10",{"src":"/litellm-asset-prefix/_next/static/chunks/17-6zku8f68gf.js","async":true,"nonce":"$undefined"}],["$","script","script-11",{"src":"/litellm-asset-prefix/_next/static/chunks/0f5fel02jwglw.js","async":true,"nonce":"$undefined"}],["$","script","script-12",{"src":"/litellm-asset-prefix/_next/static/chunks/1vquuz09jxl5_.js","async":true,"nonce":"$undefined"}],["$","script","script-13",{"src":"/litellm-asset-prefix/_next/static/chunks/1iakmimqrlpn0.js","async":true,"nonce":"$undefined"}],["$","script","script-14",{"src":"/litellm-asset-prefix/_next/static/chunks/2n26sdz53rm0a.js","async":true,"nonce":"$undefined"}],["$","script","script-15",{"src":"/litellm-asset-prefix/_next/static/chunks/11khk745tfruy.js","async":true,"nonce":"$undefined"}]],["$","$L7",null,{"Component":"$8","slots":{"children":["$","$L5",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":"$L9","templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$La","forbidden":"$undefined","unauthorized":"$undefined"}]},"serverProvidedParams":{"params":{},"promises":["$@b"]}}]]}],{"children":["$Lc",{},null,false,null]},null,false,null]},null,false,null],"$Ld",false]],"m":"$undefined","G":["$e",["$Lf","$L10"]],"S":true,"h":null,"s":"$undefined","l":"$undefined","p":"$undefined","d":"$undefined","b":"0ljiPmkOdq7_yE4sZoXlJ"} -11:I[347257,["/litellm-asset-prefix/_next/static/chunks/0vggytdohwe7o.js","/litellm-asset-prefix/_next/static/chunks/0map77ee0fk0e.js","/litellm-asset-prefix/_next/static/chunks/1jfookxfajkeo.js"],"ClientPageRoot"] -12:I[871135,["/litellm-asset-prefix/_next/static/chunks/0vggytdohwe7o.js","/litellm-asset-prefix/_next/static/chunks/0map77ee0fk0e.js","/litellm-asset-prefix/_next/static/chunks/1jfookxfajkeo.js","/litellm-asset-prefix/_next/static/chunks/1ioy8obpggx93.js","/litellm-asset-prefix/_next/static/chunks/3_3dj4vdy-3xy.js","/litellm-asset-prefix/_next/static/chunks/1zr7rrk4wkmju.js","/litellm-asset-prefix/_next/static/chunks/2cngn5bal3278.js","/litellm-asset-prefix/_next/static/chunks/2up3bks93iqds.js","/litellm-asset-prefix/_next/static/chunks/1l2mgm5v3tjci.js","/litellm-asset-prefix/_next/static/chunks/0zduf1gntl_f8.js","/litellm-asset-prefix/_next/static/chunks/0g_w4tf2inv3i.js","/litellm-asset-prefix/_next/static/chunks/1a0bgy7kzrj91.js","/litellm-asset-prefix/_next/static/chunks/0dbvgsc7ha049.js","/litellm-asset-prefix/_next/static/chunks/17-6zku8f68gf.js","/litellm-asset-prefix/_next/static/chunks/0f5fel02jwglw.js","/litellm-asset-prefix/_next/static/chunks/1vquuz09jxl5_.js","/litellm-asset-prefix/_next/static/chunks/1iakmimqrlpn0.js","/litellm-asset-prefix/_next/static/chunks/2n26sdz53rm0a.js","/litellm-asset-prefix/_next/static/chunks/11khk745tfruy.js","/litellm-asset-prefix/_next/static/chunks/00g6xfr4yow7h.js","/litellm-asset-prefix/_next/static/chunks/0-k_4_s7m108w.js","/litellm-asset-prefix/_next/static/chunks/2kcxwg1mpncp6.js","/litellm-asset-prefix/_next/static/chunks/3drq2_k-jeio2.js","/litellm-asset-prefix/_next/static/chunks/395_vbpmrlvpu.js","/litellm-asset-prefix/_next/static/chunks/2hu1vyy-5pv13.js","/litellm-asset-prefix/_next/static/chunks/2l25bmiiw9ixp.js","/litellm-asset-prefix/_next/static/chunks/1uz3jt-tj9lkf.js","/litellm-asset-prefix/_next/static/chunks/199uwr871eene.js","/litellm-asset-prefix/_next/static/chunks/2s_ce-opzrkzr.js","/litellm-asset-prefix/_next/static/chunks/2ptdxz8qnchh_.js","/litellm-asset-prefix/_next/static/chunks/17nqbxvhztf3k.js","/litellm-asset-prefix/_next/static/chunks/2c90xukbd3il6.js","/litellm-asset-prefix/_next/static/chunks/0kap_rdm2-lem.js","/litellm-asset-prefix/_next/static/chunks/09l_m9l1emin2.js","/litellm-asset-prefix/_next/static/chunks/1cea03gg5a_c7.js","/litellm-asset-prefix/_next/static/chunks/323l6h8s7ahat.js","/litellm-asset-prefix/_next/static/chunks/112n0hv3cc2rg.js","/litellm-asset-prefix/_next/static/chunks/3y674jhwchpcq.js","/litellm-asset-prefix/_next/static/chunks/105643dvf00hu.js","/litellm-asset-prefix/_next/static/chunks/12wsfsljxg4xv.js","/litellm-asset-prefix/_next/static/chunks/22iools_e0k44.js","/litellm-asset-prefix/_next/static/chunks/3f9uewf5w-e-p.js","/litellm-asset-prefix/_next/static/chunks/23-g73xaw3kap.js"],"default"] -15:I[897367,["/litellm-asset-prefix/_next/static/chunks/0vggytdohwe7o.js","/litellm-asset-prefix/_next/static/chunks/0map77ee0fk0e.js","/litellm-asset-prefix/_next/static/chunks/1jfookxfajkeo.js"],"OutletBoundary"] +2:I[867271,["/my-custom-path/_next/static/chunks/0vggytdohwe7o.js","/my-custom-path/_next/static/chunks/0map77ee0fk0e.js","/my-custom-path/_next/static/chunks/1jfookxfajkeo.js"],"default"] +3:I[71195,["/my-custom-path/_next/static/chunks/0vggytdohwe7o.js","/my-custom-path/_next/static/chunks/0map77ee0fk0e.js","/my-custom-path/_next/static/chunks/1jfookxfajkeo.js"],"default"] +4:I[557951,["/my-custom-path/_next/static/chunks/0vggytdohwe7o.js","/my-custom-path/_next/static/chunks/0map77ee0fk0e.js","/my-custom-path/_next/static/chunks/1jfookxfajkeo.js"],"AuthProvider"] +5:I[339756,["/my-custom-path/_next/static/chunks/0vggytdohwe7o.js","/my-custom-path/_next/static/chunks/0map77ee0fk0e.js","/my-custom-path/_next/static/chunks/1jfookxfajkeo.js"],"default"] +6:I[837457,["/my-custom-path/_next/static/chunks/0vggytdohwe7o.js","/my-custom-path/_next/static/chunks/0map77ee0fk0e.js","/my-custom-path/_next/static/chunks/1jfookxfajkeo.js"],"default"] +7:I[92825,["/my-custom-path/_next/static/chunks/0vggytdohwe7o.js","/my-custom-path/_next/static/chunks/0map77ee0fk0e.js","/my-custom-path/_next/static/chunks/1jfookxfajkeo.js"],"ClientSegmentRoot"] +8:I[216370,["/my-custom-path/_next/static/chunks/0vggytdohwe7o.js","/my-custom-path/_next/static/chunks/0map77ee0fk0e.js","/my-custom-path/_next/static/chunks/1jfookxfajkeo.js","/my-custom-path/_next/static/chunks/1ioy8obpggx93.js","/my-custom-path/_next/static/chunks/3_3dj4vdy-3xy.js","/my-custom-path/_next/static/chunks/1zr7rrk4wkmju.js","/my-custom-path/_next/static/chunks/2cngn5bal3278.js","/my-custom-path/_next/static/chunks/2up3bks93iqds.js","/my-custom-path/_next/static/chunks/1l2mgm5v3tjci.js","/my-custom-path/_next/static/chunks/0zduf1gntl_f8.js","/my-custom-path/_next/static/chunks/0g_w4tf2inv3i.js","/my-custom-path/_next/static/chunks/1a0bgy7kzrj91.js","/my-custom-path/_next/static/chunks/0dbvgsc7ha049.js","/my-custom-path/_next/static/chunks/17-6zku8f68gf.js","/my-custom-path/_next/static/chunks/0f5fel02jwglw.js","/my-custom-path/_next/static/chunks/1vquuz09jxl5_.js","/my-custom-path/_next/static/chunks/1iakmimqrlpn0.js","/my-custom-path/_next/static/chunks/2n26sdz53rm0a.js","/my-custom-path/_next/static/chunks/11khk745tfruy.js"],"default"] +e:I[168027,["/my-custom-path/_next/static/chunks/0vggytdohwe7o.js","/my-custom-path/_next/static/chunks/0map77ee0fk0e.js","/my-custom-path/_next/static/chunks/1jfookxfajkeo.js"],"default",1] +:HL["/my-custom-path/_next/static/chunks/1kid9zr1--h6y.css","style"] +:HL["/my-custom-path/_next/static/chunks/23vtcpdpp2h9h.css","style"] +:HL["/my-custom-path/_next/static/media/83afe278b6a6bb3c-s.p.2bn3s6zvc0dyp.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +0:{"P":null,"c":["",""],"q":"","i":false,"f":[[["",{"children":["(dashboard)",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",16],[["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/my-custom-path/_next/static/chunks/1kid9zr1--h6y.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","link","1",{"rel":"stylesheet","href":"/my-custom-path/_next/static/chunks/23vtcpdpp2h9h.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","script","script-0",{"src":"/my-custom-path/_next/static/chunks/0vggytdohwe7o.js","async":true,"nonce":"$undefined"}],["$","script","script-1",{"src":"/my-custom-path/_next/static/chunks/0map77ee0fk0e.js","async":true,"nonce":"$undefined"}],["$","script","script-2",{"src":"/my-custom-path/_next/static/chunks/1jfookxfajkeo.js","async":true,"nonce":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"className":"inter_5972bc34-module__OU16Qa__className","children":["$","$L2",null,{"children":["$","$L3",null,{"children":["$","$L4",null,{"children":["$","$L5",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L6",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]}]}]}]}]]}],{"children":[["$","$1","c",{"children":[[["$","script","script-0",{"src":"/my-custom-path/_next/static/chunks/1ioy8obpggx93.js","async":true,"nonce":"$undefined"}],["$","script","script-1",{"src":"/my-custom-path/_next/static/chunks/3_3dj4vdy-3xy.js","async":true,"nonce":"$undefined"}],["$","script","script-2",{"src":"/my-custom-path/_next/static/chunks/1zr7rrk4wkmju.js","async":true,"nonce":"$undefined"}],["$","script","script-3",{"src":"/my-custom-path/_next/static/chunks/2cngn5bal3278.js","async":true,"nonce":"$undefined"}],["$","script","script-4",{"src":"/my-custom-path/_next/static/chunks/2up3bks93iqds.js","async":true,"nonce":"$undefined"}],["$","script","script-5",{"src":"/my-custom-path/_next/static/chunks/1l2mgm5v3tjci.js","async":true,"nonce":"$undefined"}],["$","script","script-6",{"src":"/my-custom-path/_next/static/chunks/0zduf1gntl_f8.js","async":true,"nonce":"$undefined"}],["$","script","script-7",{"src":"/my-custom-path/_next/static/chunks/0g_w4tf2inv3i.js","async":true,"nonce":"$undefined"}],["$","script","script-8",{"src":"/my-custom-path/_next/static/chunks/1a0bgy7kzrj91.js","async":true,"nonce":"$undefined"}],["$","script","script-9",{"src":"/my-custom-path/_next/static/chunks/0dbvgsc7ha049.js","async":true,"nonce":"$undefined"}],["$","script","script-10",{"src":"/my-custom-path/_next/static/chunks/17-6zku8f68gf.js","async":true,"nonce":"$undefined"}],["$","script","script-11",{"src":"/my-custom-path/_next/static/chunks/0f5fel02jwglw.js","async":true,"nonce":"$undefined"}],["$","script","script-12",{"src":"/my-custom-path/_next/static/chunks/1vquuz09jxl5_.js","async":true,"nonce":"$undefined"}],["$","script","script-13",{"src":"/my-custom-path/_next/static/chunks/1iakmimqrlpn0.js","async":true,"nonce":"$undefined"}],["$","script","script-14",{"src":"/my-custom-path/_next/static/chunks/2n26sdz53rm0a.js","async":true,"nonce":"$undefined"}],["$","script","script-15",{"src":"/my-custom-path/_next/static/chunks/11khk745tfruy.js","async":true,"nonce":"$undefined"}]],["$","$L7",null,{"Component":"$8","slots":{"children":["$","$L5",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":"$L9","templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$La","forbidden":"$undefined","unauthorized":"$undefined"}]},"serverProvidedParams":{"params":{},"promises":["$@b"]}}]]}],{"children":["$Lc",{},null,false,null]},null,false,null]},null,false,null],"$Ld",false]],"m":"$undefined","G":["$e",["$Lf","$L10"]],"S":true,"h":null,"s":"$undefined","l":"$undefined","p":"$undefined","d":"$undefined","b":"0ljiPmkOdq7_yE4sZoXlJ"} +11:I[347257,["/my-custom-path/_next/static/chunks/0vggytdohwe7o.js","/my-custom-path/_next/static/chunks/0map77ee0fk0e.js","/my-custom-path/_next/static/chunks/1jfookxfajkeo.js"],"ClientPageRoot"] +12:I[871135,["/my-custom-path/_next/static/chunks/0vggytdohwe7o.js","/my-custom-path/_next/static/chunks/0map77ee0fk0e.js","/my-custom-path/_next/static/chunks/1jfookxfajkeo.js","/my-custom-path/_next/static/chunks/1ioy8obpggx93.js","/my-custom-path/_next/static/chunks/3_3dj4vdy-3xy.js","/my-custom-path/_next/static/chunks/1zr7rrk4wkmju.js","/my-custom-path/_next/static/chunks/2cngn5bal3278.js","/my-custom-path/_next/static/chunks/2up3bks93iqds.js","/my-custom-path/_next/static/chunks/1l2mgm5v3tjci.js","/my-custom-path/_next/static/chunks/0zduf1gntl_f8.js","/my-custom-path/_next/static/chunks/0g_w4tf2inv3i.js","/my-custom-path/_next/static/chunks/1a0bgy7kzrj91.js","/my-custom-path/_next/static/chunks/0dbvgsc7ha049.js","/my-custom-path/_next/static/chunks/17-6zku8f68gf.js","/my-custom-path/_next/static/chunks/0f5fel02jwglw.js","/my-custom-path/_next/static/chunks/1vquuz09jxl5_.js","/my-custom-path/_next/static/chunks/1iakmimqrlpn0.js","/my-custom-path/_next/static/chunks/2n26sdz53rm0a.js","/my-custom-path/_next/static/chunks/11khk745tfruy.js","/my-custom-path/_next/static/chunks/00g6xfr4yow7h.js","/my-custom-path/_next/static/chunks/0-k_4_s7m108w.js","/my-custom-path/_next/static/chunks/2kcxwg1mpncp6.js","/my-custom-path/_next/static/chunks/3drq2_k-jeio2.js","/my-custom-path/_next/static/chunks/395_vbpmrlvpu.js","/my-custom-path/_next/static/chunks/2hu1vyy-5pv13.js","/my-custom-path/_next/static/chunks/2l25bmiiw9ixp.js","/my-custom-path/_next/static/chunks/1uz3jt-tj9lkf.js","/my-custom-path/_next/static/chunks/199uwr871eene.js","/my-custom-path/_next/static/chunks/2s_ce-opzrkzr.js","/my-custom-path/_next/static/chunks/2ptdxz8qnchh_.js","/my-custom-path/_next/static/chunks/17nqbxvhztf3k.js","/my-custom-path/_next/static/chunks/2c90xukbd3il6.js","/my-custom-path/_next/static/chunks/0kap_rdm2-lem.js","/my-custom-path/_next/static/chunks/09l_m9l1emin2.js","/my-custom-path/_next/static/chunks/1cea03gg5a_c7.js","/my-custom-path/_next/static/chunks/323l6h8s7ahat.js","/my-custom-path/_next/static/chunks/112n0hv3cc2rg.js","/my-custom-path/_next/static/chunks/3y674jhwchpcq.js","/my-custom-path/_next/static/chunks/105643dvf00hu.js","/my-custom-path/_next/static/chunks/12wsfsljxg4xv.js","/my-custom-path/_next/static/chunks/22iools_e0k44.js","/my-custom-path/_next/static/chunks/3f9uewf5w-e-p.js","/my-custom-path/_next/static/chunks/23-g73xaw3kap.js"],"default"] +15:I[897367,["/my-custom-path/_next/static/chunks/0vggytdohwe7o.js","/my-custom-path/_next/static/chunks/0map77ee0fk0e.js","/my-custom-path/_next/static/chunks/1jfookxfajkeo.js"],"OutletBoundary"] 16:"$Sreact.suspense" -18:I[897367,["/litellm-asset-prefix/_next/static/chunks/0vggytdohwe7o.js","/litellm-asset-prefix/_next/static/chunks/0map77ee0fk0e.js","/litellm-asset-prefix/_next/static/chunks/1jfookxfajkeo.js"],"ViewportBoundary"] -1a:I[897367,["/litellm-asset-prefix/_next/static/chunks/0vggytdohwe7o.js","/litellm-asset-prefix/_next/static/chunks/0map77ee0fk0e.js","/litellm-asset-prefix/_next/static/chunks/1jfookxfajkeo.js"],"MetadataBoundary"] +18:I[897367,["/my-custom-path/_next/static/chunks/0vggytdohwe7o.js","/my-custom-path/_next/static/chunks/0map77ee0fk0e.js","/my-custom-path/_next/static/chunks/1jfookxfajkeo.js"],"ViewportBoundary"] +1a:I[897367,["/my-custom-path/_next/static/chunks/0vggytdohwe7o.js","/my-custom-path/_next/static/chunks/0map77ee0fk0e.js","/my-custom-path/_next/static/chunks/1jfookxfajkeo.js"],"MetadataBoundary"] 9:["$","$L6",null,{}] a:[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":"$0:f:0:1:0:props:children:1:props:children:props:children:props:children:props:children:props:children:props:notFound:0:1:props:style","children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":"$0:f:0:1:0:props:children:1:props:children:props:children:props:children:props:children:props:children:props:notFound:0:1:props:children:props:children:1:props:style","children":404}],["$","div",null,{"style":"$0:f:0:1:0:props:children:1:props:children:props:children:props:children:props:children:props:children:props:notFound:0:1:props:children:props:children:2:props:style","children":["$","h2",null,{"style":"$0:f:0:1:0:props:children:1:props:children:props:children:props:children:props:children:props:children:props:notFound:0:1:props:children:props:children:2:props:children:props:style","children":"This page could not be found."}]}]]}]}]],[]] -c:["$","$1","c",{"children":[["$","$L11",null,{"Component":"$12","serverProvidedParams":{"searchParams":{},"params":"$0:f:0:1:1:children:0:props:children:1:props:serverProvidedParams:params","promises":["$@13","$@14"]}}],[["$","script","script-0",{"src":"/litellm-asset-prefix/_next/static/chunks/00g6xfr4yow7h.js","async":true,"nonce":"$undefined"}],["$","script","script-1",{"src":"/litellm-asset-prefix/_next/static/chunks/0-k_4_s7m108w.js","async":true,"nonce":"$undefined"}],["$","script","script-2",{"src":"/litellm-asset-prefix/_next/static/chunks/2kcxwg1mpncp6.js","async":true,"nonce":"$undefined"}],["$","script","script-3",{"src":"/litellm-asset-prefix/_next/static/chunks/3drq2_k-jeio2.js","async":true,"nonce":"$undefined"}],["$","script","script-4",{"src":"/litellm-asset-prefix/_next/static/chunks/395_vbpmrlvpu.js","async":true,"nonce":"$undefined"}],["$","script","script-5",{"src":"/litellm-asset-prefix/_next/static/chunks/2hu1vyy-5pv13.js","async":true,"nonce":"$undefined"}],["$","script","script-6",{"src":"/litellm-asset-prefix/_next/static/chunks/2l25bmiiw9ixp.js","async":true,"nonce":"$undefined"}],["$","script","script-7",{"src":"/litellm-asset-prefix/_next/static/chunks/1uz3jt-tj9lkf.js","async":true,"nonce":"$undefined"}],["$","script","script-8",{"src":"/litellm-asset-prefix/_next/static/chunks/199uwr871eene.js","async":true,"nonce":"$undefined"}],["$","script","script-9",{"src":"/litellm-asset-prefix/_next/static/chunks/2s_ce-opzrkzr.js","async":true,"nonce":"$undefined"}],["$","script","script-10",{"src":"/litellm-asset-prefix/_next/static/chunks/2ptdxz8qnchh_.js","async":true,"nonce":"$undefined"}],["$","script","script-11",{"src":"/litellm-asset-prefix/_next/static/chunks/17nqbxvhztf3k.js","async":true,"nonce":"$undefined"}],["$","script","script-12",{"src":"/litellm-asset-prefix/_next/static/chunks/2c90xukbd3il6.js","async":true,"nonce":"$undefined"}],["$","script","script-13",{"src":"/litellm-asset-prefix/_next/static/chunks/0kap_rdm2-lem.js","async":true,"nonce":"$undefined"}],["$","script","script-14",{"src":"/litellm-asset-prefix/_next/static/chunks/09l_m9l1emin2.js","async":true,"nonce":"$undefined"}],["$","script","script-15",{"src":"/litellm-asset-prefix/_next/static/chunks/1cea03gg5a_c7.js","async":true,"nonce":"$undefined"}],["$","script","script-16",{"src":"/litellm-asset-prefix/_next/static/chunks/323l6h8s7ahat.js","async":true,"nonce":"$undefined"}],["$","script","script-17",{"src":"/litellm-asset-prefix/_next/static/chunks/112n0hv3cc2rg.js","async":true,"nonce":"$undefined"}],["$","script","script-18",{"src":"/litellm-asset-prefix/_next/static/chunks/3y674jhwchpcq.js","async":true,"nonce":"$undefined"}],["$","script","script-19",{"src":"/litellm-asset-prefix/_next/static/chunks/105643dvf00hu.js","async":true,"nonce":"$undefined"}],["$","script","script-20",{"src":"/litellm-asset-prefix/_next/static/chunks/12wsfsljxg4xv.js","async":true,"nonce":"$undefined"}],["$","script","script-21",{"src":"/litellm-asset-prefix/_next/static/chunks/22iools_e0k44.js","async":true,"nonce":"$undefined"}],["$","script","script-22",{"src":"/litellm-asset-prefix/_next/static/chunks/3f9uewf5w-e-p.js","async":true,"nonce":"$undefined"}],["$","script","script-23",{"src":"/litellm-asset-prefix/_next/static/chunks/23-g73xaw3kap.js","async":true,"nonce":"$undefined"}]],["$","$L15",null,{"children":["$","$16",null,{"name":"Next.MetadataOutlet","children":"$@17"}]}]]}] +c:["$","$1","c",{"children":[["$","$L11",null,{"Component":"$12","serverProvidedParams":{"searchParams":{},"params":"$0:f:0:1:1:children:0:props:children:1:props:serverProvidedParams:params","promises":["$@13","$@14"]}}],[["$","script","script-0",{"src":"/my-custom-path/_next/static/chunks/00g6xfr4yow7h.js","async":true,"nonce":"$undefined"}],["$","script","script-1",{"src":"/my-custom-path/_next/static/chunks/0-k_4_s7m108w.js","async":true,"nonce":"$undefined"}],["$","script","script-2",{"src":"/my-custom-path/_next/static/chunks/2kcxwg1mpncp6.js","async":true,"nonce":"$undefined"}],["$","script","script-3",{"src":"/my-custom-path/_next/static/chunks/3drq2_k-jeio2.js","async":true,"nonce":"$undefined"}],["$","script","script-4",{"src":"/my-custom-path/_next/static/chunks/395_vbpmrlvpu.js","async":true,"nonce":"$undefined"}],["$","script","script-5",{"src":"/my-custom-path/_next/static/chunks/2hu1vyy-5pv13.js","async":true,"nonce":"$undefined"}],["$","script","script-6",{"src":"/my-custom-path/_next/static/chunks/2l25bmiiw9ixp.js","async":true,"nonce":"$undefined"}],["$","script","script-7",{"src":"/my-custom-path/_next/static/chunks/1uz3jt-tj9lkf.js","async":true,"nonce":"$undefined"}],["$","script","script-8",{"src":"/my-custom-path/_next/static/chunks/199uwr871eene.js","async":true,"nonce":"$undefined"}],["$","script","script-9",{"src":"/my-custom-path/_next/static/chunks/2s_ce-opzrkzr.js","async":true,"nonce":"$undefined"}],["$","script","script-10",{"src":"/my-custom-path/_next/static/chunks/2ptdxz8qnchh_.js","async":true,"nonce":"$undefined"}],["$","script","script-11",{"src":"/my-custom-path/_next/static/chunks/17nqbxvhztf3k.js","async":true,"nonce":"$undefined"}],["$","script","script-12",{"src":"/my-custom-path/_next/static/chunks/2c90xukbd3il6.js","async":true,"nonce":"$undefined"}],["$","script","script-13",{"src":"/my-custom-path/_next/static/chunks/0kap_rdm2-lem.js","async":true,"nonce":"$undefined"}],["$","script","script-14",{"src":"/my-custom-path/_next/static/chunks/09l_m9l1emin2.js","async":true,"nonce":"$undefined"}],["$","script","script-15",{"src":"/my-custom-path/_next/static/chunks/1cea03gg5a_c7.js","async":true,"nonce":"$undefined"}],["$","script","script-16",{"src":"/my-custom-path/_next/static/chunks/323l6h8s7ahat.js","async":true,"nonce":"$undefined"}],["$","script","script-17",{"src":"/my-custom-path/_next/static/chunks/112n0hv3cc2rg.js","async":true,"nonce":"$undefined"}],["$","script","script-18",{"src":"/my-custom-path/_next/static/chunks/3y674jhwchpcq.js","async":true,"nonce":"$undefined"}],["$","script","script-19",{"src":"/my-custom-path/_next/static/chunks/105643dvf00hu.js","async":true,"nonce":"$undefined"}],["$","script","script-20",{"src":"/my-custom-path/_next/static/chunks/12wsfsljxg4xv.js","async":true,"nonce":"$undefined"}],["$","script","script-21",{"src":"/my-custom-path/_next/static/chunks/22iools_e0k44.js","async":true,"nonce":"$undefined"}],["$","script","script-22",{"src":"/my-custom-path/_next/static/chunks/3f9uewf5w-e-p.js","async":true,"nonce":"$undefined"}],["$","script","script-23",{"src":"/my-custom-path/_next/static/chunks/23-g73xaw3kap.js","async":true,"nonce":"$undefined"}]],["$","$L15",null,{"children":["$","$16",null,{"name":"Next.MetadataOutlet","children":"$@17"}]}]]}] d:["$","$1","h",{"children":[null,["$","$L18",null,{"children":"$L19"}],["$","div",null,{"hidden":true,"children":["$","$L1a",null,{"children":["$","$16",null,{"name":"Next.Metadata","children":"$L1b"}]}]}],["$","meta",null,{"name":"next-size-adjust","content":""}]]}] -f:["$","link","0",{"rel":"stylesheet","href":"/litellm-asset-prefix/_next/static/chunks/1kid9zr1--h6y.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}] -10:["$","link","1",{"rel":"stylesheet","href":"/litellm-asset-prefix/_next/static/chunks/23vtcpdpp2h9h.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}] +f:["$","link","0",{"rel":"stylesheet","href":"/my-custom-path/_next/static/chunks/1kid9zr1--h6y.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}] +10:["$","link","1",{"rel":"stylesheet","href":"/my-custom-path/_next/static/chunks/23vtcpdpp2h9h.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}] b:"$0:f:0:1:1:children:0:props:children:1:props:serverProvidedParams:params" 13:{} 14:"$0:f:0:1:1:children:0:props:children:1:props:serverProvidedParams:params" 19:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]] -1c:I[27201,["/litellm-asset-prefix/_next/static/chunks/0vggytdohwe7o.js","/litellm-asset-prefix/_next/static/chunks/0map77ee0fk0e.js","/litellm-asset-prefix/_next/static/chunks/1jfookxfajkeo.js"],"IconMark"] +1c:I[27201,["/my-custom-path/_next/static/chunks/0vggytdohwe7o.js","/my-custom-path/_next/static/chunks/0map77ee0fk0e.js","/my-custom-path/_next/static/chunks/1jfookxfajkeo.js"],"IconMark"] 17:null 1b:[["$","title","0",{"children":"LiteLLM Dashboard"}],["$","meta","1",{"name":"description","content":"LiteLLM Proxy Admin UI"}],["$","link","2",{"rel":"icon","href":"/favicon.ico?favicon.3arlap5n8tyzg.ico","sizes":"48x48","type":"image/x-icon"}],["$","link","3",{"rel":"icon","href":"/get_favicon"}],["$","$L1c","4",{}]] diff --git a/litellm/proxy/_experimental/out/__next._head.txt b/litellm/proxy/_experimental/out/__next._head.txt index 8e68b3a038e..a42c68983f6 100644 --- a/litellm/proxy/_experimental/out/__next._head.txt +++ b/litellm/proxy/_experimental/out/__next._head.txt @@ -1,6 +1,6 @@ 1:"$Sreact.fragment" -2:I[897367,["/litellm-asset-prefix/_next/static/chunks/0vggytdohwe7o.js","/litellm-asset-prefix/_next/static/chunks/0map77ee0fk0e.js","/litellm-asset-prefix/_next/static/chunks/1jfookxfajkeo.js"],"ViewportBoundary"] -3:I[897367,["/litellm-asset-prefix/_next/static/chunks/0vggytdohwe7o.js","/litellm-asset-prefix/_next/static/chunks/0map77ee0fk0e.js","/litellm-asset-prefix/_next/static/chunks/1jfookxfajkeo.js"],"MetadataBoundary"] +2:I[897367,["/my-custom-path/_next/static/chunks/0vggytdohwe7o.js","/my-custom-path/_next/static/chunks/0map77ee0fk0e.js","/my-custom-path/_next/static/chunks/1jfookxfajkeo.js"],"ViewportBoundary"] +3:I[897367,["/my-custom-path/_next/static/chunks/0vggytdohwe7o.js","/my-custom-path/_next/static/chunks/0map77ee0fk0e.js","/my-custom-path/_next/static/chunks/1jfookxfajkeo.js"],"MetadataBoundary"] 4:"$Sreact.suspense" -5:I[27201,["/litellm-asset-prefix/_next/static/chunks/0vggytdohwe7o.js","/litellm-asset-prefix/_next/static/chunks/0map77ee0fk0e.js","/litellm-asset-prefix/_next/static/chunks/1jfookxfajkeo.js"],"IconMark"] +5:I[27201,["/my-custom-path/_next/static/chunks/0vggytdohwe7o.js","/my-custom-path/_next/static/chunks/0map77ee0fk0e.js","/my-custom-path/_next/static/chunks/1jfookxfajkeo.js"],"IconMark"] 0:{"rsc":["$","$1","h",{"children":[null,["$","$L2",null,{"children":[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]]}],["$","div",null,{"hidden":true,"children":["$","$L3",null,{"children":["$","$4",null,{"name":"Next.Metadata","children":[["$","title","0",{"children":"LiteLLM Dashboard"}],["$","meta","1",{"name":"description","content":"LiteLLM Proxy Admin UI"}],["$","link","2",{"rel":"icon","href":"/favicon.ico?favicon.3arlap5n8tyzg.ico","sizes":"48x48","type":"image/x-icon"}],["$","link","3",{"rel":"icon","href":"/get_favicon"}],["$","$L5","4",{}]]}]}]}],["$","meta",null,{"name":"next-size-adjust","content":""}]]}],"isPartial":false,"staleTime":300,"varyParams":null,"buildId":"0ljiPmkOdq7_yE4sZoXlJ"} diff --git a/litellm/proxy/_experimental/out/__next._index.txt b/litellm/proxy/_experimental/out/__next._index.txt index f5dd3d69ad7..31bdc353451 100644 --- a/litellm/proxy/_experimental/out/__next._index.txt +++ b/litellm/proxy/_experimental/out/__next._index.txt @@ -1,9 +1,9 @@ 1:"$Sreact.fragment" -2:I[867271,["/litellm-asset-prefix/_next/static/chunks/0vggytdohwe7o.js","/litellm-asset-prefix/_next/static/chunks/0map77ee0fk0e.js","/litellm-asset-prefix/_next/static/chunks/1jfookxfajkeo.js"],"default"] -3:I[71195,["/litellm-asset-prefix/_next/static/chunks/0vggytdohwe7o.js","/litellm-asset-prefix/_next/static/chunks/0map77ee0fk0e.js","/litellm-asset-prefix/_next/static/chunks/1jfookxfajkeo.js"],"default"] -4:I[557951,["/litellm-asset-prefix/_next/static/chunks/0vggytdohwe7o.js","/litellm-asset-prefix/_next/static/chunks/0map77ee0fk0e.js","/litellm-asset-prefix/_next/static/chunks/1jfookxfajkeo.js"],"AuthProvider"] -5:I[339756,["/litellm-asset-prefix/_next/static/chunks/0vggytdohwe7o.js","/litellm-asset-prefix/_next/static/chunks/0map77ee0fk0e.js","/litellm-asset-prefix/_next/static/chunks/1jfookxfajkeo.js"],"default"] -6:I[837457,["/litellm-asset-prefix/_next/static/chunks/0vggytdohwe7o.js","/litellm-asset-prefix/_next/static/chunks/0map77ee0fk0e.js","/litellm-asset-prefix/_next/static/chunks/1jfookxfajkeo.js"],"default"] -:HL["/litellm-asset-prefix/_next/static/chunks/1kid9zr1--h6y.css","style"] -:HL["/litellm-asset-prefix/_next/static/chunks/23vtcpdpp2h9h.css","style"] -0:{"rsc":["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/litellm-asset-prefix/_next/static/chunks/1kid9zr1--h6y.css","precedence":"next"}],["$","link","1",{"rel":"stylesheet","href":"/litellm-asset-prefix/_next/static/chunks/23vtcpdpp2h9h.css","precedence":"next"}],["$","script","script-0",{"src":"/litellm-asset-prefix/_next/static/chunks/0vggytdohwe7o.js","async":true}],["$","script","script-1",{"src":"/litellm-asset-prefix/_next/static/chunks/0map77ee0fk0e.js","async":true}],["$","script","script-2",{"src":"/litellm-asset-prefix/_next/static/chunks/1jfookxfajkeo.js","async":true}]],["$","html",null,{"lang":"en","children":["$","body",null,{"className":"inter_5972bc34-module__OU16Qa__className","children":["$","$L2",null,{"children":["$","$L3",null,{"children":["$","$L4",null,{"children":["$","$L5",null,{"parallelRouterKey":"children","template":["$","$L6",null,{}],"notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]]}]}]}]}]}]}]]}],"isPartial":false,"staleTime":300,"varyParams":null,"buildId":"0ljiPmkOdq7_yE4sZoXlJ"} +2:I[867271,["/my-custom-path/_next/static/chunks/0vggytdohwe7o.js","/my-custom-path/_next/static/chunks/0map77ee0fk0e.js","/my-custom-path/_next/static/chunks/1jfookxfajkeo.js"],"default"] +3:I[71195,["/my-custom-path/_next/static/chunks/0vggytdohwe7o.js","/my-custom-path/_next/static/chunks/0map77ee0fk0e.js","/my-custom-path/_next/static/chunks/1jfookxfajkeo.js"],"default"] +4:I[557951,["/my-custom-path/_next/static/chunks/0vggytdohwe7o.js","/my-custom-path/_next/static/chunks/0map77ee0fk0e.js","/my-custom-path/_next/static/chunks/1jfookxfajkeo.js"],"AuthProvider"] +5:I[339756,["/my-custom-path/_next/static/chunks/0vggytdohwe7o.js","/my-custom-path/_next/static/chunks/0map77ee0fk0e.js","/my-custom-path/_next/static/chunks/1jfookxfajkeo.js"],"default"] +6:I[837457,["/my-custom-path/_next/static/chunks/0vggytdohwe7o.js","/my-custom-path/_next/static/chunks/0map77ee0fk0e.js","/my-custom-path/_next/static/chunks/1jfookxfajkeo.js"],"default"] +:HL["/my-custom-path/_next/static/chunks/1kid9zr1--h6y.css","style"] +:HL["/my-custom-path/_next/static/chunks/23vtcpdpp2h9h.css","style"] +0:{"rsc":["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/my-custom-path/_next/static/chunks/1kid9zr1--h6y.css","precedence":"next"}],["$","link","1",{"rel":"stylesheet","href":"/my-custom-path/_next/static/chunks/23vtcpdpp2h9h.css","precedence":"next"}],["$","script","script-0",{"src":"/my-custom-path/_next/static/chunks/0vggytdohwe7o.js","async":true}],["$","script","script-1",{"src":"/my-custom-path/_next/static/chunks/0map77ee0fk0e.js","async":true}],["$","script","script-2",{"src":"/my-custom-path/_next/static/chunks/1jfookxfajkeo.js","async":true}]],["$","html",null,{"lang":"en","children":["$","body",null,{"className":"inter_5972bc34-module__OU16Qa__className","children":["$","$L2",null,{"children":["$","$L3",null,{"children":["$","$L4",null,{"children":["$","$L5",null,{"parallelRouterKey":"children","template":["$","$L6",null,{}],"notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]]}]}]}]}]}]}]]}],"isPartial":false,"staleTime":300,"varyParams":null,"buildId":"0ljiPmkOdq7_yE4sZoXlJ"} diff --git a/litellm/proxy/_experimental/out/__next._tree.txt b/litellm/proxy/_experimental/out/__next._tree.txt index 6bec08d009f..9bc5ad9231d 100644 --- a/litellm/proxy/_experimental/out/__next._tree.txt +++ b/litellm/proxy/_experimental/out/__next._tree.txt @@ -1,4 +1,4 @@ -:HL["/litellm-asset-prefix/_next/static/chunks/1kid9zr1--h6y.css","style"] -:HL["/litellm-asset-prefix/_next/static/chunks/23vtcpdpp2h9h.css","style"] -:HL["/litellm-asset-prefix/_next/static/media/83afe278b6a6bb3c-s.p.2bn3s6zvc0dyp.woff2","font",{"crossOrigin":"","type":"font/woff2"}] +:HL["/my-custom-path/_next/static/chunks/1kid9zr1--h6y.css","style"] +:HL["/my-custom-path/_next/static/chunks/23vtcpdpp2h9h.css","style"] +:HL["/my-custom-path/_next/static/media/83afe278b6a6bb3c-s.p.2bn3s6zvc0dyp.woff2","font",{"crossOrigin":"","type":"font/woff2"}] 0:{"tree":{"name":"","param":null,"prefetchHints":16,"slots":{"children":{"name":"(dashboard)","param":null,"prefetchHints":0,"slots":{"children":{"name":"__PAGE__","param":null,"prefetchHints":0,"slots":null}}}}},"staleTime":300,"buildId":"0ljiPmkOdq7_yE4sZoXlJ"} diff --git a/litellm/proxy/_experimental/out/_next/static/0ljiPmkOdq7_yE4sZoXlJ/_buildManifest.js b/litellm/proxy/_experimental/out/_next/static/0ljiPmkOdq7_yE4sZoXlJ/_buildManifest.js index d74e1661bbe..843dac8bbc8 100644 --- a/litellm/proxy/_experimental/out/_next/static/0ljiPmkOdq7_yE4sZoXlJ/_buildManifest.js +++ b/litellm/proxy/_experimental/out/_next/static/0ljiPmkOdq7_yE4sZoXlJ/_buildManifest.js @@ -3,7 +3,7 @@ self.__BUILD_MANIFEST = { "afterFiles": [], "beforeFiles": [ { - "source": "/litellm-asset-prefix/_next/:path+", + "source": "/my-custom-path/_next/:path+", "destination": "/_next/:path+" } ], diff --git a/litellm/proxy/_experimental/out/_next/static/chunks/0_v0ovphg1p2h.js b/litellm/proxy/_experimental/out/_next/static/chunks/0_v0ovphg1p2h.js index 332f881f2b9..55c59547726 100644 --- a/litellm/proxy/_experimental/out/_next/static/chunks/0_v0ovphg1p2h.js +++ b/litellm/proxy/_experimental/out/_next/static/chunks/0_v0ovphg1p2h.js @@ -1 +1 @@ -(globalThis.TURBOPACK||(globalThis.TURBOPACK=[])).push(["object"==typeof document?document.currentScript:void 0,913383,e=>{e.q("/litellm-asset-prefix/_next/static/media/a2a_agent.14el5-6dflh1h.png")},238564,e=>{e.q("/litellm-asset-prefix/_next/static/media/ai21.0m_u-tih8nm0v.svg")},941533,e=>{e.q("/litellm-asset-prefix/_next/static/media/aiml_api.1dq6dbpwklhlg.svg")},994636,e=>{e.q("/litellm-asset-prefix/_next/static/media/anthropic.3s95kgy8jpc64.svg")},824770,e=>{e.q("/litellm-asset-prefix/_next/static/media/assemblyai_small.0w_dslsra91uw.png")},920076,e=>{e.q("/litellm-asset-prefix/_next/static/media/baseten.41k2lnwp3c5g9.svg")},519380,e=>{e.q("/litellm-asset-prefix/_next/static/media/bedrock.2vhamxr8q6y4o.svg")},96889,e=>{e.q("/litellm-asset-prefix/_next/static/media/cerebras.1ur1xyfqk9ncz.svg")},348026,e=>{e.q("/litellm-asset-prefix/_next/static/media/cloudflare.2n1spdq7u5yki.svg")},727148,e=>{e.q("/litellm-asset-prefix/_next/static/media/cohere.22i3i2e449j5i.svg")},411703,e=>{e.q("/litellm-asset-prefix/_next/static/media/cometapi.3w0xficbg3dkk.svg")},922480,e=>{e.q("/litellm-asset-prefix/_next/static/media/cursor.1q1ev-_5l7exg.svg")},769490,e=>{e.q("/litellm-asset-prefix/_next/static/media/databricks.2hiet9qlqzn-g.svg")},829789,e=>{e.q("/litellm-asset-prefix/_next/static/media/deepgram.3-krp20p_xed3.png")},41730,e=>{e.q("/litellm-asset-prefix/_next/static/media/deepinfra.3lr_lsr7mhui6.png")},662576,e=>{e.q("/litellm-asset-prefix/_next/static/media/deepseek.3n4cu0x32i_7w.svg")},987202,e=>{e.q("/litellm-asset-prefix/_next/static/media/elevenlabs.2982m_dk2-h_y.png")},953265,e=>{e.q("/litellm-asset-prefix/_next/static/media/fal_ai.3rahrirki8sby.jpg")},346512,e=>{e.q("/litellm-asset-prefix/_next/static/media/featherless.3hmlef3fhc0h5.svg")},84416,e=>{e.q("/litellm-asset-prefix/_next/static/media/fireworks.3t1b6p8edeqyo.svg")},104602,e=>{e.q("/litellm-asset-prefix/_next/static/media/friendli.0ymiswh6l35bq.svg")},888193,e=>{e.q("/litellm-asset-prefix/_next/static/media/github_copilot.3k-8jyadoaq2u.svg")},946013,e=>{e.q("/litellm-asset-prefix/_next/static/media/google.3y8ypywwtqob_.svg")},447500,e=>{e.q("/litellm-asset-prefix/_next/static/media/groq.20csmqzsusp1k.svg")},270039,e=>{e.q("/litellm-asset-prefix/_next/static/media/huggingface.1-07ypt9ii_-p.svg")},227733,e=>{e.q("/litellm-asset-prefix/_next/static/media/hyperbolic.3le20v59sebn8.svg")},823929,e=>{e.q("/litellm-asset-prefix/_next/static/media/infinity.0s9s12bl4lccx.png")},333845,e=>{e.q("/litellm-asset-prefix/_next/static/media/jina.0ukab8o-3-5m_.png")},813878,e=>{e.q("/litellm-asset-prefix/_next/static/media/lambda.26_vz7cmyuodo.svg")},767788,e=>{e.q("/litellm-asset-prefix/_next/static/media/lmstudio.35s3-83mlhcms.svg")},808630,e=>{e.q("/litellm-asset-prefix/_next/static/media/meta_llama.1kxk24vwsem49.svg")},641266,e=>{e.q("/litellm-asset-prefix/_next/static/media/microsoft_azure.3626-7zx7wf09.svg")},468732,e=>{e.q("/litellm-asset-prefix/_next/static/media/minimax.2mfkkqc-lnsen.svg")},610503,e=>{e.q("/litellm-asset-prefix/_next/static/media/mistral.0n8jv_67hgq4h.svg")},438969,e=>{e.q("/litellm-asset-prefix/_next/static/media/moonshot.3__i9wvf37ksm.svg")},852459,e=>{e.q("/litellm-asset-prefix/_next/static/media/morph.2av06eo-t0-ve.svg")},494696,e=>{e.q("/litellm-asset-prefix/_next/static/media/nebius.2ipf7rmjccira.svg")},148973,e=>{e.q("/litellm-asset-prefix/_next/static/media/novita.0_nzmm_rl4lrf.svg")},570036,e=>{e.q("/litellm-asset-prefix/_next/static/media/nvidia_nim.1fz-5ugf_um0v.svg")},151170,e=>{e.q("/litellm-asset-prefix/_next/static/media/nvidia_triton.1aotoxig_m2w1.png")},862330,e=>{e.q("/litellm-asset-prefix/_next/static/media/ollama.144cif369atc5.svg")},956529,e=>{e.q("/litellm-asset-prefix/_next/static/media/openai_small.3rj8nedgjpevh.svg")},788158,e=>{e.q("/litellm-asset-prefix/_next/static/media/openrouter.1xk7748-_jixf.svg")},350567,e=>{e.q("/litellm-asset-prefix/_next/static/media/oracle.43kws59xxr8ig.svg")},822797,e=>{e.q("/litellm-asset-prefix/_next/static/media/perplexity-ai.2do8hoc8tw__0.svg")},840911,e=>{e.q("/litellm-asset-prefix/_next/static/media/qwen.0a49x9i08_0gz.png")},445735,e=>{e.q("/litellm-asset-prefix/_next/static/media/recraft.2f4cv-c9ad-mo.svg")},635202,e=>{e.q("/litellm-asset-prefix/_next/static/media/replicate.445bidyix2tyh.svg")},669161,e=>{e.q("/litellm-asset-prefix/_next/static/media/runway.2zzwye1fddnnn.png")},159508,e=>{e.q("/litellm-asset-prefix/_next/static/media/sambanova.1vcyu0faw1x8h.svg")},544025,e=>{e.q("/litellm-asset-prefix/_next/static/media/sap.1367hgge0s0xl.png")},100588,e=>{e.q("/litellm-asset-prefix/_next/static/media/snowflake.2_p8qqpi0r3lr.svg")},183804,e=>{e.q("/litellm-asset-prefix/_next/static/media/soniox.3nhjmssy7ybh7.svg")},865502,e=>{e.q("/litellm-asset-prefix/_next/static/media/togetherai.1wk-mouzgw5ho.svg")},96912,e=>{e.q("/litellm-asset-prefix/_next/static/media/topaz.3rjp3zvx4eags.svg")},395073,e=>{e.q("/litellm-asset-prefix/_next/static/media/v0.15trd3tb1ulop.svg")},407829,e=>{e.q("/litellm-asset-prefix/_next/static/media/vercel.1mvnwxofolt8y.svg")},846077,e=>{e.q("/litellm-asset-prefix/_next/static/media/vllm.3_gqby46r7s3x.png")},914861,e=>{e.q("/litellm-asset-prefix/_next/static/media/volcengine.23ly9ik_qc138.png")},285328,e=>{e.q("/litellm-asset-prefix/_next/static/media/voyage.0krq6ew-yr8dk.webp")},779278,e=>{e.q("/litellm-asset-prefix/_next/static/media/watsonx.19rrg39yvhpk8.svg")},325532,e=>{e.q("/litellm-asset-prefix/_next/static/media/xai.2kc3gjiopn9om.svg")},288330,e=>{e.q("/litellm-asset-prefix/_next/static/media/xinference.063cy_ievvy5u.svg")},555987,938137,301035,470524,901539,434339,857152,e=>{"use strict";var t=e.i(221688),l=e.i(950643);let i=/^(https?:|data:|blob:|\/\/)/i;e.s(["resolveLogoSrc",0,(e,a=t.serverRootPath)=>{let r;if(!e)return;if(i.test(e)||e.includes("/_next/static/"))return e;let s=(0,l.normalizeRootPath)(a);return s&&(e===s||e.startsWith(`${s}/`))?e:(r=(0,l.normalizeRootPath)(a),`${r}${e.startsWith("/")?e:`/${e}`}`)}],555987);let a={src:e.i(913383).default,width:661,height:768,blurWidth:7,blurHeight:8,blurDataURL:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAICAYAAAA1BOUGAAAAwUlEQVR42h2OvQpGcByFf5dK7oBJFoooJDaDZFA+BoVkNBikGJSUJINyGeft/Q+nznDO00O6rsM0Tfi+z6JpGizLgqqqIEVR0HUdqqqCbdus930PWZZBy7IgCAKkaYpxHJEkCaIowjAMoGmaEMcx8jxHURRo2xZ1XaMsS9B5nrjvG+u6Yp5nfN+H53nwJ9JxHMiyDO/7Mtx1Xey1bRvIcRzwPI993xGGIZqmgSAIMAwDJEkSPM9jphzHsYHruhBFET+hn6S8pf6D7AAAAABJRU5ErkJggg=="};e.s(["default",0,a],938137);let r={src:e.i(238564).default,width:103,height:24,blurWidth:0,blurHeight:0};e.s(["default",0,r],301035);let s={src:e.i(941533).default,width:115,height:24,blurWidth:0,blurHeight:0};e.s(["default",0,s],470524);let n={src:e.i(994636).default,width:46,height:46,blurWidth:0,blurHeight:0};e.s(["default",0,n],901539);let o={src:e.i(824770).default,width:28,height:28,blurWidth:8,blurHeight:8,blurDataURL:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAIAAABLbSncAAAAlElEQVR42m3OoQqDUBQGYF9mwzLbFiZsMNgwuGa4YFgYLC6PgcXuA5gFzdcgFkExGr33ogZRNIr6CAoKKgo/nAMfh/NTu3u4GWoY+0fIAsIC3C8LuIiRH9S2W5wBXsBPKaHTGGb+lZMJmCe2vPYjZe9/bMDswKMRbq9Yg9VRICcBq3p6FckINIf6o6ECwyOam/1YpwPFUFu+Lt2F2QAAAABJRU5ErkJggg=="};e.s(["default",0,o],434339);let d={src:e.i(920076).default,width:16,height:16,blurWidth:0,blurHeight:0};e.s(["default",0,d],857152)},922158,e=>{"use strict";let t={src:e.i(519380).default,width:16,height:16,blurWidth:0,blurHeight:0};e.s(["default",0,t])},896614,9774,503119,272896,144923,562171,533881,837957,227247,708889,859320,586455,921117,21296,579967,e=>{"use strict";let t={src:e.i(96889).default,width:800,height:600,blurWidth:0,blurHeight:0};e.s(["default",0,t],896614);let l={src:e.i(348026).default,width:16,height:16,blurWidth:0,blurHeight:0};e.s(["default",0,l],9774);let i={src:e.i(727148).default,width:75,height:75,blurWidth:0,blurHeight:0};e.s(["default",0,i],503119);let a={src:e.i(411703).default,width:16,height:16,blurWidth:0,blurHeight:0};e.s(["default",0,a],272896);let r={src:e.i(922480).default,width:16,height:16,blurWidth:0,blurHeight:0};e.s(["default",0,r],144923);let s={src:e.i(769490).default,width:16,height:16,blurWidth:0,blurHeight:0};e.s(["default",0,s],562171);let n={src:e.i(829789).default,width:32,height:32,blurWidth:8,blurHeight:8,blurDataURL:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAzklEQVR42k3PPWuDcBDHccUiiikYSwqhg+IkuKUpOLqK4OYrcBZcFCcH352mgy/Ch9mxEPg2+s+Q4W65zx33kyRJQpZlFOUNTdM4HN45Hi1Op0/O56+7tAHbtqmqiqZpKIqCy+Uby/rYkABhGLKuK+M4siwLt9sv1+sPpmkKEAQBwzCQpilxHDNNE3VdYxiGALqu47oujuMQRdEOyrJEVVUBFEUhSRK6rmOeZ/q+x/f97XkBHqfIsoy2bcnzHM/z9mSP2Q62dt/0c+O1/v4BYRxu3LgaR7UAAAAASUVORK5CYII="};e.s(["default",0,n],533881);let o={src:e.i(41730).default,width:225,height:225,blurWidth:8,blurHeight:8,blurDataURL:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAIAAABLbSncAAAAfklEQVR42nVOvQqDMBjM+7+RpbQ0Ji2NQhwdzGIwCMZFiCAa9STqIPhxw/3A3UeW41w/AKck87w0tqvbngkFgEDCJOPoKc+fovxKzaUGiXkOk0zei1TRzDxYEbHiIw0kzD2Aji5BqHolW9Uv0+9U01AVxivr4r8CjHX7+N27K5WIrP56XsFTAAAAAElFTkSuQmCC"};e.s(["default",0,o],837957);let d={src:e.i(662576).default,width:293,height:215,blurWidth:0,blurHeight:0};e.s(["default",0,d],227247);let A={src:e.i(987202).default,width:1563,height:1563,blurWidth:8,blurHeight:8,blurDataURL:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAA5ElEQVR42h2PPwtBURjGj3wCk93sC1iUCYXFIlGSMjC4QiiTBSE3Ge5M3e0uPoJFBhOTP8NNukXduFL3uJ1Xzz11znCe93me38uitQSL11Pe8rAanKpycbSa5Ar9UiAmJT3QXHGwHGfvj/vJcRzOfz/7fLvsW4tuJFZLehicEIUQpOs6GYZBOIfrcZPp5f0MsXDatk2yLJOqqu6A9f2YjXk7zNCJWM45KYpCmqa5A6/P+ynNGiEGIHSiwjRNsiyLBAmxPezW6U7Gx0ALIHQiFk6I5WEl6G6BB7QAQqc0a4bgxD/uH4FYoVec8m3dAAAAAElFTkSuQmCC"};e.s(["default",0,A],708889);let c={src:e.i(953265).default,width:400,height:400,blurWidth:8,blurHeight:8,blurDataURL:"data:image/jpeg;base64,/9j/4AAQSkZJRgABAgAAAQABAAD/wAARCAAIAAgDAREAAhEBAxEB/9sAQwAKBwcIBwYKCAgICwoKCw4YEA4NDQ4dFRYRGCMfJSQiHyIhJis3LyYpNCkhIjBBMTQ5Oz4+PiUuRElDPEg3PT47/9sAQwEKCwsODQ4cEBAcOygiKDs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDtP9N/tL+PO/8A7ZeV/wDFUj6L917L+r3/AMj/2Q=="};e.s(["default",0,c],859320);let u={src:e.i(346512).default,width:16,height:16,blurWidth:0,blurHeight:0};e.s(["default",0,u],586455);let h={src:e.i(84416).default,width:16,height:16,blurWidth:0,blurHeight:0};e.s(["default",0,h],921117);let g={src:e.i(104602).default,width:16,height:16,blurWidth:0,blurHeight:0};e.s(["default",0,g],21296);let m={src:e.i(888193).default,width:16,height:16,blurWidth:0,blurHeight:0};e.s(["default",0,m],579967)},336712,e=>{"use strict";let t={src:e.i(946013).default,width:16,height:16,blurWidth:0,blurHeight:0};e.s(["default",0,t])},770752,383963,862493,902860,901372,206258,176228,728685,e=>{"use strict";let t={src:e.i(447500).default,width:26,height:26,blurWidth:0,blurHeight:0};e.s(["default",0,t],770752);let l={src:e.i(270039).default,width:16,height:16,blurWidth:0,blurHeight:0};e.s(["default",0,l],383963);let i={src:e.i(227733).default,width:16,height:16,blurWidth:0,blurHeight:0};e.s(["default",0,i],862493);let a={src:e.i(823929).default,width:128,height:129,blurWidth:8,blurHeight:8,blurDataURL:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAApklEQVR42oWPsQrCMBRFY51KaUuwNktBQbcgSenQ32hCTBAHXfSfulUK4g/0/7yvdO+FB5dzXhLC2FoiJE3TfZ7nYrskyzIBVpBjTdPcu677GGPGtm1fmDd1YnVd31gIYSrLUnLOjxC9tfaLfhBCXLz307xQVZUCODvnfljoceAEpskxKeUV1w0Qo1LqobV+Lk8McI5tkDiOeZIkO9SIBr0gRm71l38QASaqNz7VMwAAAABJRU5ErkJggg=="};e.s(["default",0,a],902860);let r={src:e.i(333845).default,width:200,height:200,blurWidth:8,blurHeight:8,blurDataURL:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAIAAABLbSncAAAAXUlEQVR42p2MoRHAMAwDQwJzCcgCwQEZIDjA03gED2BzewSPWRe2qFcB3Z+kU0o/lXNea4U/0t77OYeIACDq4FLKnSKimYmIqjKzu885U6117z3GCGitxTbg/flJF7DAEmcR3WAqAAAAAElFTkSuQmCC"};e.s(["default",0,r],901372);let s={src:e.i(813878).default,width:16,height:16,blurWidth:0,blurHeight:0};e.s(["default",0,s],206258);let n={src:e.i(767788).default,width:16,height:16,blurWidth:0,blurHeight:0};e.s(["default",0,n],176228);let o={src:e.i(808630).default,width:16,height:16,blurWidth:0,blurHeight:0};e.s(["default",0,o],728685)},39182,e=>{"use strict";let t={src:e.i(641266).default,width:200,height:58,blurWidth:0,blurHeight:0};e.s(["default",0,t])},272967,551726,399495,740876,709103,277207,836473,768493,297720,e=>{"use strict";let t={src:e.i(468732).default,width:490,height:412,blurWidth:0,blurHeight:0};e.s(["default",0,t],272967);let l={src:e.i(610503).default,width:16,height:16,blurWidth:0,blurHeight:0};e.s(["default",0,l],551726);let i={src:e.i(438969).default,width:16,height:16,blurWidth:0,blurHeight:0};e.s(["default",0,i],399495);let a={src:e.i(852459).default,width:16,height:16,blurWidth:0,blurHeight:0};e.s(["default",0,a],740876);let r={src:e.i(494696).default,width:16,height:16,blurWidth:0,blurHeight:0};e.s(["default",0,r],709103);let s={src:e.i(148973).default,width:16,height:16,blurWidth:0,blurHeight:0};e.s(["default",0,s],277207);let n={src:e.i(570036).default,width:16,height:16,blurWidth:0,blurHeight:0};e.s(["default",0,n],836473);let o={src:e.i(151170).default,width:95,height:81,blurWidth:8,blurHeight:7,blurDataURL:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAHCAYAAAA1WQxeAAAA0UlEQVR42kXMvWrCUBjG8VxIL6KX0IvobXQSOrQd2kLSdMzQdMmaEErPUqoIIYtKOC5iFD9BEBwiaAY/AgpH/2oUfOGFh4cfj8blNmrNKK0zmFUZzmust2nea7u9IlkNkBMPv1Xgs3JHoXRDe1o+g8Vmyl/vnf++wU/7EdF54jW8JU6K14WTPgE3fjiCZ77kPa2kdAZZlrFcLpikXYLeN6Lxxm/zhfEsRimF5roupmli2zbGh4Gu61iWlX8URWhSSsIwxPM8hBA4jpNn3/cJgoADqC23HJzwo6wAAAAASUVORK5CYII="};e.s(["default",0,o],768493);let d={src:e.i(862330).default,width:646,height:854,blurWidth:0,blurHeight:0};e.s(["default",0,d],297720)},980385,e=>{"use strict";let t={src:e.i(956529).default,width:28,height:28,blurWidth:0,blurHeight:0};e.s(["default",0,t])},916925,247044,e=>{"use strict";var t,l=e.i(555987),i=e.i(938137),a=e.i(301035),r=e.i(470524),s=e.i(901539),n=e.i(434339),o=e.i(857152),d=e.i(922158),A=e.i(896614),c=e.i(9774),u=e.i(503119),h=e.i(272896),g=e.i(144923),m=e.i(562171),f=e.i(533881),p=e.i(837957),x=e.i(227247),b=e.i(708889),E=e.i(859320),v=e.i(586455),I=e.i(921117),C=e.i(21296),y=e.i(579967),w=e.i(336712),_=e.i(770752),O=e.i(383963),k=e.i(862493),R=e.i(902860),L=e.i(901372),N=e.i(206258),S=e.i(176228),M=e.i(728685),T=e.i(39182),B=e.i(272967),j=e.i(551726),H=e.i(399495),D=e.i(740876),U=e.i(709103),F=e.i(277207),q=e.i(836473),P=e.i(768493),W=e.i(297720),Q=e.i(980385);let G={src:e.i(788158).default,width:300,height:300,blurWidth:0,blurHeight:0},z={src:e.i(350567).default,width:231,height:30,blurWidth:0,blurHeight:0},V={src:e.i(822797).default,width:48,height:48,blurWidth:0,blurHeight:0},K={src:e.i(840911).default,width:1024,height:1024,blurWidth:8,blurHeight:8,blurDataURL:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAABE0lEQVR42gEIAff+AAAAAAAAAAAAEQwmIT8ukIAgGUpBBwYQDQQECggAAAAAAAIBBQQJBhUTFw80LUIxloZbR8+5UUS3oUA4kH0HBxEOACMUUkolFlVNCAYTERwUPzgkHFJIIhxNRCsmYVQODSAbADYffnNFKaCSDgkgHD0tjX4+MI5+CQgVEhQSLCULChgUABAKJiJTM7+vJhlYTzEkb2MxJm9iIh1NQywnZFYhH0o/AA8JIh9RMrurNiR9cQkHFhMQDSUgUkW5o1hOx60pJlxPAAcEDw4qGmJYFw80LhENKCMyKHFjUEK0nhEOJSADAwYFAAAAAAADAgcGBQMLCRUPMClCNJeEIRtLQgAAAQAAAAAAfjQxsiCQipMAAAAASUVORK5CYII="},Y={src:e.i(445735).default,width:16,height:16,blurWidth:0,blurHeight:0},J={src:e.i(635202).default,width:16,height:16,blurWidth:0,blurHeight:0},X={src:e.i(669161).default,width:320,height:320,blurWidth:8,blurHeight:8,blurDataURL:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAA7UlEQVR42lVQvc7BYBR+B/kM3w+fFm2p9m3fRhFRP7UQlTAQCVfAgE3EHZAQMTBIzBIrYbEgFhaza3ABrsF7bIYnOTl5/s5BVrvIY5MMvYTbUuy8hN8CcFIdW62CgHCKjFwie/So3PqH+b0Hc+GebkW6Lj9zUtJaHwEbFvGqWf7zOG5GJVmjcwUEAUOevQmhfKSTqJol+7f94RT+L7Yv25OV3PtsI699EByc85qpW5qcUCaMyB7orH9EgJLaTnPNgk/QfUsxKi0QtKWFzjRzA7a08IEPCivYKWkyQHAKtJUMPIVMQCAmz3FKHcELXnxEPy9E1lYCAAAAAElFTkSuQmCC"},Z={src:e.i(159508).default,width:52,height:46,blurWidth:0,blurHeight:0},$={src:e.i(544025).default,width:3300,height:2550,blurWidth:8,blurHeight:6,blurDataURL:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAGCAYAAAD+Bd/7AAAAZklEQVR42oWNTQ5AMBhE3f8mEhux4BgiIRzAwoKWplrt146fBbVhkpnNvORF+El0jvMAV4RR2qtsJZD3DyA2h6xhiMsRSTUhrRm0DYBFE4qOI285yl5CGvdW0OEYhEE/G6jgvIGv7A4KupBwIVaNAAAAAElFTkSuQmCC"},ee={src:e.i(100588).default,width:146,height:139,blurWidth:0,blurHeight:0};e.s(["default",0,ee],247044);let et={src:e.i(183804).default,width:100,height:18,blurWidth:0,blurHeight:0},el={src:e.i(865502).default,width:32,height:32,blurWidth:0,blurHeight:0},ei={src:e.i(96912).default,width:16,height:16,blurWidth:0,blurHeight:0},ea={src:e.i(395073).default,width:16,height:16,blurWidth:0,blurHeight:0},er={src:e.i(407829).default,width:16,height:16,blurWidth:0,blurHeight:0},es={src:e.i(846077).default,width:64,height:64,blurWidth:8,blurHeight:8,blurDataURL:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAIAAABLbSncAAAAo0lEQVR42lWOvQrCQBCE7718oUA6wVcQBF/AXhu1CIgcpgoi/qCNguBPwCaChYHDv9zt3e6ZeEFxq9n5GGaY/T8iq3QhWKFRkZGWzEPaaE/RDoiI5e7z0BILP7tMB2vyO3p2BJew8syvvJLGQTPEaheSVJeAQIi5F2+CWt80uHopLMEnNBpPuNc2vaXK+38A4RauTvUhbBNwDvsOlYD3DA2Se99kLK7hC5QFVQAAAABJRU5ErkJggg=="},en={src:e.i(914861).default,width:1024,height:1024,blurWidth:8,blurHeight:8,blurDataURL:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAA7ElEQVR42l2LMUsCAQBG39GFFQ02ad1oq0QIRR2hER4EGdQqdBDdQZAFEudU0NZSQ0O1VUOjiuJvEBxc3BQFEVQQdFAU9OROFwf9lg/e48Hiti8vWJekBSoIODacLK04CP79IAX8815cW2X3IYL3JIhaKuKLGdNGmMUCTo+Hq1yO72yB6HDEeTrFjm8fUVwGt3uL00eD5+6AfMvixbS5bTR5/43jcm2CooT4KNf56ltUGzafA5uoZZGpdJDlABhPr7z1TJLtMbXamMT0702T//aQu0gM5GOFvRuNkKoTDuucqRpeXePoWuPg0D8BGkpJjYhKXN4AAAAASUVORK5CYII="},eo={src:e.i(285328).default,width:400,height:399,blurWidth:8,blurHeight:8,blurDataURL:"data:image/webp;base64,UklGRgwBAABXRUJQVlA4TAABAAAvB8ABEM1VICICHggIDgIAAIDzDAACD0FoAAARAAwAAAAAAAAAAAAAAACAAABAAcBCAoAWgGyoXwUAAA8E7QQCAADg/N+nrsYIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4CClZXkg5AQAAAAA538LAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAL0HIgEAAAAAcP4ZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMsHA39Qutr2AtkROu8CASWZ/nMlNyzz5AdgWmkK65oUI6AlgIkY/M3zxIW3iABq15KT/Uz7BXidCOONHieAz1v6HqAC"},ed={src:e.i(779278).default,width:16,height:16,blurWidth:0,blurHeight:0},eA={src:e.i(325532).default,width:1e3,height:1e3,blurWidth:0,blurHeight:0},ec={src:e.i(288330).default,width:16,height:16,blurWidth:0,blurHeight:0};var eu=((t={}).A2A_Agent="A2A Agent",t.AI21="Ai21",t.AI21_CHAT="Ai21 Chat",t.AIML="AI/ML API",t.AIOHTTP_OPENAI="Aiohttp Openai",t.Anthropic="Anthropic",t.ANTHROPIC_TEXT="Anthropic Text",t.AssemblyAI="AssemblyAI",t.AUTO_ROUTER="Auto Router",t.Bedrock="Amazon Bedrock",t.BedrockMantle="Amazon Bedrock Mantle",t.SageMaker="AWS SageMaker",t.Azure="Azure",t.Azure_AI_Studio="Azure AI Foundry (Studio)",t.AZURE_TEXT="Azure Text",t.BASETEN="Baseten",t.BYTEZ="Bytez",t.Cerebras="Cerebras",t.CLARIFAI="Clarifai",t.CLOUDFLARE="Cloudflare",t.CODESTRAL="Codestral",t.Cohere="Cohere",t.COHERE_CHAT="Cohere Chat",t.COMETAPI="Cometapi",t.COMPACTIFAI="Compactifai",t.Cursor="Cursor",t.Dashscope="Dashscope",t.Databricks="Databricks (Qwen API)",t.DATAROBOT="Datarobot",t.DeepInfra="DeepInfra",t.Deepgram="Deepgram",t.Deepseek="Deepseek",t.DOCKER_MODEL_RUNNER="Docker Model Runner",t.DOTPROMPT="Dotprompt",t.ElevenLabs="ElevenLabs",t.EMPOWER="Empower",t.FalAI="Fal AI",t.FEATHERLESS_AI="Featherless Ai",t.FireworksAI="Fireworks AI",t.FRIENDLIAI="Friendliai",t.GALADRIEL="Galadriel",t.GITHUB_COPILOT="Github Copilot",t.Google_AI_Studio="Google AI Studio",t.GradientAI="GradientAI",t.Groq="Groq",t.HEROKU="Heroku",t.Hosted_Vllm="vllm",t.HUGGINGFACE="Huggingface",t.HYPERBOLIC="Hyperbolic",t.Infinity="Infinity",t.JinaAI="Jina AI",t.LAMBDA_AI="Lambda Ai",t.LEMONADE="Lemonade",t.LLAMAFILE="Llamafile",t.LM_STUDIO="Lm Studio",t.LLAMA="Meta Llama",t.MARITALK="Maritalk",t.MiniMax="MiniMax",t.MistralAI="Mistral AI",t.MOONSHOT="Moonshot",t.MORPH="Morph",t.NEBIUS="Nebius",t.NLP_CLOUD="Nlp Cloud",t.NOVITA="Novita",t.NSCALE="Nscale",t.NVIDIA_NIM="Nvidia Nim",t.Ollama="Ollama",t.OLLAMA_CHAT="Ollama Chat",t.OOBABOOGA="Oobabooga",t.OpenAI="OpenAI",t.OPENAI_LIKE="Openai Like",t.OpenAI_Compatible="OpenAI-Compatible Chat Completions (Together AI, vLLM, etc.)",t.OpenAI_Text="OpenAI Text Completion",t.OpenAI_Text_Compatible="OpenAI-Compatible Completions (legacy /v1/completions)",t.Openrouter="Openrouter",t.Oracle="Oracle Cloud Infrastructure (OCI)",t.OVHCLOUD="Ovhcloud",t.Perplexity="Perplexity",t.PETALS="Petals",t.PG_VECTOR="Pg Vector",t.PREDIBASE="Predibase",t.RECRAFT="Recraft",t.REPLICATE="Replicate",t.RunwayML="RunwayML",t.SAGEMAKER_LEGACY="Sagemaker",t.Sambanova="Sambanova",t.SAP="SAP Generative AI Hub",t.Snowflake="Snowflake",t.Soniox="Soniox",t.TEXT_COMPLETION_CODESTRAL="Text-Completion-Codestral",t.TogetherAI="TogetherAI",t.TOPAZ="Topaz",t.Triton="Triton",t.V0="V0",t.VERCEL_AI_GATEWAY="Vercel Ai Gateway",t.Vertex_AI="Vertex AI (Anthropic, Gemini, etc.)",t.VERTEX_AI_BETA="Vertex Ai Beta",t.VLLM="Vllm",t.VolcEngine="VolcEngine",t.Voyage="Voyage AI",t.WANDB="Wandb",t.WATSONX="Watsonx",t.WATSONX_TEXT="Watsonx Text",t.xAI="xAI",t.XINFERENCE="Xinference",t.ZAI="Z.AI (Zhipu AI)",t);let eh={A2A_Agent:"a2a_agent",AI21:"ai21",AI21_CHAT:"ai21_chat",AIML:"aiml",AIOHTTP_OPENAI:"aiohttp_openai",Anthropic:"anthropic",ANTHROPIC_TEXT:"anthropic_text",AssemblyAI:"assemblyai",AUTO_ROUTER:"auto_router",Azure:"azure",Azure_AI_Studio:"azure_ai",AZURE_TEXT:"azure_text",BASETEN:"baseten",Bedrock:"bedrock",BedrockMantle:"bedrock_mantle",BYTEZ:"bytez",Cerebras:"cerebras",CLARIFAI:"clarifai",CLOUDFLARE:"cloudflare",CODESTRAL:"codestral",Cohere:"cohere",COHERE_CHAT:"cohere_chat",COMETAPI:"cometapi",COMPACTIFAI:"compactifai",Cursor:"cursor",Dashscope:"dashscope",Databricks:"databricks",DATAROBOT:"datarobot",DeepInfra:"deepinfra",Deepgram:"deepgram",Deepseek:"deepseek",DOCKER_MODEL_RUNNER:"docker_model_runner",DOTPROMPT:"dotprompt",ElevenLabs:"elevenlabs",EMPOWER:"empower",FalAI:"fal_ai",FEATHERLESS_AI:"featherless_ai",FireworksAI:"fireworks_ai",FRIENDLIAI:"friendliai",GALADRIEL:"galadriel",GITHUB_COPILOT:"github_copilot",Google_AI_Studio:"gemini",GradientAI:"gradient_ai",Groq:"groq",HEROKU:"heroku",Hosted_Vllm:"hosted_vllm",HUGGINGFACE:"huggingface",HYPERBOLIC:"hyperbolic",Infinity:"infinity",JinaAI:"jina_ai",LAMBDA_AI:"lambda_ai",LEMONADE:"lemonade",LLAMAFILE:"llamafile",LLAMA:"meta_llama",LM_STUDIO:"lm_studio",MARITALK:"maritalk",MiniMax:"minimax",MistralAI:"mistral",MOONSHOT:"moonshot",MORPH:"morph",NEBIUS:"nebius",NLP_CLOUD:"nlp_cloud",NOVITA:"novita",NSCALE:"nscale",NVIDIA_NIM:"nvidia_nim",Ollama:"ollama",OLLAMA_CHAT:"ollama_chat",OOBABOOGA:"oobabooga",OpenAI:"openai",OPENAI_LIKE:"openai_like",OpenAI_Compatible:"openai",OpenAI_Text:"text-completion-openai",OpenAI_Text_Compatible:"text-completion-openai",Openrouter:"openrouter",Oracle:"oci",OVHCLOUD:"ovhcloud",Perplexity:"perplexity",PETALS:"petals",PG_VECTOR:"pg_vector",PREDIBASE:"predibase",RECRAFT:"recraft",REPLICATE:"replicate",RunwayML:"runwayml",SAGEMAKER_LEGACY:"sagemaker",SageMaker:"sagemaker_chat",Sambanova:"sambanova",SAP:"sap",Snowflake:"snowflake",Soniox:"soniox",TEXT_COMPLETION_CODESTRAL:"text-completion-codestral",TogetherAI:"together_ai",TOPAZ:"topaz",Triton:"triton",V0:"v0",VERCEL_AI_GATEWAY:"vercel_ai_gateway",Vertex_AI:"vertex_ai",VERTEX_AI_BETA:"vertex_ai_beta",VLLM:"vllm",VolcEngine:"volcengine",Voyage:"voyage",WANDB:"wandb",WATSONX:"watsonx",WATSONX_TEXT:"watsonx_text",xAI:"xai",XINFERENCE:"xinference",ZAI:"zai"},eg=new Set(["bedrock_mantle"]),em={"A2A Agent":i.default.src,Ai21:a.default.src,"Ai21 Chat":a.default.src,"AI/ML API":r.default.src,"Aiohttp Openai":Q.default.src,Anthropic:s.default.src,"Anthropic Text":s.default.src,AssemblyAI:n.default.src,Azure:T.default.src,"Azure AI Foundry (Studio)":T.default.src,"Azure Text":T.default.src,Baseten:o.default.src,"Amazon Bedrock":d.default.src,"Amazon Bedrock Mantle":d.default.src,"AWS SageMaker":d.default.src,Cerebras:A.default.src,Cloudflare:c.default.src,Codestral:j.default.src,Cohere:u.default.src,"Cohere Chat":u.default.src,Cometapi:h.default.src,Cursor:g.default.src,"Databricks (Qwen API)":m.default.src,Dashscope:K.src,Deepseek:x.default.src,Deepgram:f.default.src,DeepInfra:p.default.src,ElevenLabs:b.default.src,"Fal AI":E.default.src,"Featherless Ai":v.default.src,"Fireworks AI":I.default.src,Friendliai:C.default.src,"Github Copilot":y.default.src,"Google AI Studio":w.default.src,Groq:_.default.src,vllm:es.src,Huggingface:O.default.src,Hyperbolic:k.default.src,Infinity:R.default.src,"Jina AI":L.default.src,"Lambda Ai":N.default.src,"Lm Studio":S.default.src,"Meta Llama":M.default.src,MiniMax:B.default.src,"Mistral AI":j.default.src,Moonshot:H.default.src,Morph:D.default.src,Nebius:U.default.src,Novita:F.default.src,"Nvidia Nim":q.default.src,Ollama:W.default.src,"Ollama Chat":W.default.src,Oobabooga:Q.default.src,OpenAI:Q.default.src,"Openai Like":Q.default.src,"OpenAI Text Completion":Q.default.src,"OpenAI-Compatible Completions (legacy /v1/completions)":Q.default.src,"OpenAI-Compatible Chat Completions (Together AI, vLLM, etc.)":Q.default.src,Openrouter:G.src,"Oracle Cloud Infrastructure (OCI)":z.src,Perplexity:V.src,Recraft:Y.src,Replicate:J.src,RunwayML:X.src,Sagemaker:d.default.src,Sambanova:Z.src,"SAP Generative AI Hub":$.src,Snowflake:ee.src,Soniox:et.src,"Text-Completion-Codestral":j.default.src,TogetherAI:el.src,Topaz:ei.src,Triton:P.default.src,V0:ea.src,"Vercel Ai Gateway":er.src,"Vertex AI (Anthropic, Gemini, etc.)":w.default.src,"Vertex Ai Beta":w.default.src,Vllm:es.src,VolcEngine:en.src,"Voyage AI":eo.src,Watsonx:ed.src,"Watsonx Text":ed.src,xAI:eA.src,Xinference:ec.src};e.s(["Providers",()=>eu,"getPlaceholder",0,e=>{if("AI/ML API"===e)return"aiml/flux-pro/v1.1";if("Vertex AI (Anthropic, Gemini, etc.)"===e)return"gemini-pro";if("Anthropic"==e)return"claude-3-opus";if("Amazon Bedrock"==e)return"claude-3-opus";if("AWS SageMaker"==e)return"sagemaker/jumpstart-dft-meta-textgeneration-llama-2-7b";else if("Google AI Studio"==e)return"gemini-pro";else if("Azure AI Foundry (Studio)"==e)return"azure_ai/command-r-plus";else if("Azure"==e)return"my-deployment";else if("Oracle Cloud Infrastructure (OCI)"==e)return"oci/xai.grok-4";else if("Snowflake"==e)return"snowflake/mistral-7b";else if("Voyage AI"==e)return"voyage/";else if("Jina AI"==e)return"jina_ai/";else if("VolcEngine"==e)return"volcengine/";else if("DeepInfra"==e)return"deepinfra/";else if("Fal AI"==e)return"fal_ai/fal-ai/flux-pro/v1.1-ultra";else if("RunwayML"==e)return"runwayml/gen4_turbo";else if("Watsonx"===e)return"watsonx/ibm/granite-3-3-8b-instruct";else if("Cursor"===e)return"cursor/claude-4-sonnet";else if("Z.AI (Zhipu AI)"===e)return"zai/glm-4.5";else return"gpt-3.5-turbo"},"getProviderLogoAndName",0,e=>{if(!e)return{logo:"",displayName:"-"};if("gemini"===e.toLowerCase()){let e="Google AI Studio";return{logo:(0,l.resolveLogoSrc)(em[e])??"",displayName:e}}let t=Object.keys(eh).find(t=>eh[t].toLowerCase()===e.toLowerCase())??Object.keys(eh).find(t=>t.toLowerCase()===e.toLowerCase());if(!t)return{logo:"",displayName:e};let i=eu[t];return{logo:(0,l.resolveLogoSrc)(em[i])??"",displayName:i}},"getProviderModels",0,(e,t)=>{let l=eh[e],i=[];return e&&"object"==typeof t&&(Object.entries(t).forEach(([e,t])=>{if(null!==t&&"object"==typeof t&&"litellm_provider"in t){let a=t.litellm_provider,r="string"==typeof a&&(a.startsWith(`${l}_`)||a.startsWith(`${l}-`));(a===l||r&&!eg.has(a))&&i.push(e)}}),"Cohere"==e&&Object.entries(t).forEach(([e,t])=>{null!==t&&"object"==typeof t&&"litellm_provider"in t&&"cohere_chat"===t.litellm_provider&&i.push(e)}),"AWS SageMaker"==e&&Object.entries(t).forEach(([e,t])=>{null!==t&&"object"==typeof t&&"litellm_provider"in t&&"sagemaker_chat"===t.litellm_provider&&i.push(e)})),i},"providerLogoMap",0,em,"provider_map",0,eh],916925)},174553,e=>{"use strict";var t=e.i(843476),l=e.i(271645),i=e.i(916925),a=e.i(555987);e.s(["Logo",0,({provider:e,src:r,label:s,className:n="w-4 h-4"})=>{let[o,d]=(0,l.useState)(null),A=void 0!==e?(0,i.getProviderLogoAndName)(e).logo:(0,a.resolveLogoSrc)(r)??"",c=s??e??"";return o!==A&&A?(0,t.jsx)("img",{src:A,alt:`${c||"-"} logo`,className:n,onError:()=>{console.warn(`Logo failed to load: ${A}`),d(A)}}):(0,t.jsx)("div",{className:`${n} rounded-full bg-gray-200 flex items-center justify-center text-xs`,children:c.charAt(0)||"-"})}])},695411,e=>{"use strict";var t=e.i(602869);let l=async e=>{try{let l=await (0,t.modelHubCall)(e);if(l?.data.length>0){let e=l.data.map(e=>({model_group:e.model_group,mode:e?.mode}));return e.sort((e,t)=>e.model_group.localeCompare(t.model_group)),e}return[]}catch(e){throw console.error("Error fetching model info:",e),e}};e.s(["fetchAvailableModels",0,l])},233538,e=>{"use strict";e.s(["isDisabledReactIssue7711",0,function(e){let t=e.parentElement,l=null;for(;t&&!(t instanceof HTMLFieldSetElement);)t instanceof HTMLLegendElement&&(l=t),t=t.parentElement;let i=(null==t?void 0:t.getAttribute("disabled"))==="";return!(i&&function(e){if(!e)return!1;let t=e.previousElementSibling;for(;null!==t;){if(t instanceof HTMLLegendElement)return!1;t=t.previousElementSibling}return!0}(l))&&i}])},503269,214520,601893,694421,140721,942803,35889,722678,e=>{"use strict";var t=e.i(271645),l=e.i(914189);e.s(["useControllable",0,function(e,i,a){let[r,s]=(0,t.useState)(a),n=void 0!==e,o=(0,t.useRef)(n),d=(0,t.useRef)(!1),A=(0,t.useRef)(!1);return!n||o.current||d.current?n||!o.current||A.current||(A.current=!0,o.current=n,console.error("A component is changing from controlled to uncontrolled. This may be caused by the value changing from a defined value to undefined, which should not happen.")):(d.current=!0,o.current=n,console.error("A component is changing from uncontrolled to controlled. This may be caused by the value changing from undefined to a defined value, which should not happen.")),[n?e:r,(0,l.useEvent)(e=>(n||s(e),null==i?void 0:i(e)))]}],503269),e.s(["useDefaultValue",0,function(e){let[l]=(0,t.useState)(e);return l}],214520);let i=(0,t.createContext)(void 0);function a(){return(0,t.useContext)(i)}e.s(["useDisabled",0,a],601893);var r=e.i(174080),s=e.i(746725);function n(e={},t=null,l=[]){for(let[i,a]of Object.entries(e))!function e(t,l,i){if(Array.isArray(i))for(let[a,r]of i.entries())e(t,o(l,a.toString()),r);else i instanceof Date?t.push([l,i.toISOString()]):"boolean"==typeof i?t.push([l,i?"1":"0"]):"string"==typeof i?t.push([l,i]):"number"==typeof i?t.push([l,`${i}`]):null==i?t.push([l,""]):n(i,l,t)}(l,o(t,i),a);return l}function o(e,t){return e?e+"["+t+"]":t}e.s(["attemptSubmit",0,function(e){var t,l;let i=null!=(t=null==e?void 0:e.form)?t:e.closest("form");if(i){for(let t of i.elements)if(t!==e&&("INPUT"===t.tagName&&"submit"===t.type||"BUTTON"===t.tagName&&"submit"===t.type||"INPUT"===t.nodeName&&"image"===t.type))return void t.click();null==(l=i.requestSubmit)||l.call(i)}},"objectToFormEntries",0,n],694421);var d=e.i(700020),A=e.i(2788);let c=(0,t.createContext)(null);function u({children:e}){let l=(0,t.useContext)(c);if(!l)return t.default.createElement(t.default.Fragment,null,e);let{target:i}=l;return i?(0,r.createPortal)(t.default.createElement(t.default.Fragment,null,e),i):null}function h({setForm:e,formId:l}){return(0,t.useEffect)(()=>{if(l){let t=document.getElementById(l);t&&e(t)}},[e,l]),l?null:t.default.createElement(A.Hidden,{features:A.HiddenFeatures.Hidden,as:"input",type:"hidden",hidden:!0,readOnly:!0,ref:t=>{if(!t)return;let l=t.closest("form");l&&e(l)}})}e.s(["FormFields",0,function({data:e,form:l,disabled:i,onReset:a,overrides:r}){let[o,c]=(0,t.useState)(null),g=(0,s.useDisposables)();return(0,t.useEffect)(()=>{if(a&&o)return g.addEventListener(o,"reset",a)},[o,l,a]),t.default.createElement(u,null,t.default.createElement(h,{setForm:c,formId:l}),n(e).map(([e,a])=>t.default.createElement(A.Hidden,{features:A.HiddenFeatures.Hidden,...(0,d.compact)({key:e,as:"input",type:"hidden",hidden:!0,readOnly:!0,form:l,disabled:i,name:e,value:a,...r})})))}],140721);let g=(0,t.createContext)(void 0);function m(){return(0,t.useContext)(g)}e.s(["useProvidedId",0,m],942803);var f=e.i(835696),p=e.i(294316);let x=(0,t.createContext)(null);x.displayName="DescriptionContext";let b=Object.assign((0,d.forwardRefWithAs)(function(e,l){let i=(0,t.useId)(),r=a(),{id:s=`headlessui-description-${i}`,...n}=e,o=function e(){let l=(0,t.useContext)(x);if(null===l){let t=Error("You used a component, but it is not inside a relevant parent.");throw Error.captureStackTrace&&Error.captureStackTrace(t,e),t}return l}(),A=(0,p.useSyncRefs)(l);(0,f.useIsoMorphicEffect)(()=>o.register(s),[s,o.register]);let c=r||!1,u=(0,t.useMemo)(()=>({...o.slot,disabled:c}),[o.slot,c]),h={ref:A,...o.props,id:s};return(0,d.useRender)()({ourProps:h,theirProps:n,slot:u,defaultTag:"p",name:o.name||"Description"})}),{});e.s(["Description",0,b,"useDescribedBy",0,function(){var e,l;return null!=(l=null==(e=(0,t.useContext)(x))?void 0:e.value)?l:void 0},"useDescriptions",0,function(){let[e,i]=(0,t.useState)([]);return[e.length>0?e.join(" "):void 0,(0,t.useMemo)(()=>function(e){let a=(0,l.useEvent)(e=>(i(t=>[...t,e]),()=>i(t=>{let l=t.slice(),i=l.indexOf(e);return -1!==i&&l.splice(i,1),l}))),r=(0,t.useMemo)(()=>({register:a,slot:e.slot,name:e.name,props:e.props,value:e.value}),[a,e.slot,e.name,e.props,e.value]);return t.default.createElement(x.Provider,{value:r},e.children)},[i])]}],35889);let E=(0,t.createContext)(null);function v(e){var l,i,a;let r=null!=(i=null==(l=(0,t.useContext)(E))?void 0:l.value)?i:void 0;return(null!=(a=null==e?void 0:e.length)?a:0)>0?[r,...e].filter(Boolean).join(" "):r}E.displayName="LabelContext";let I=Object.assign((0,d.forwardRefWithAs)(function(e,i){var r;let s=(0,t.useId)(),n=function e(){let l=(0,t.useContext)(E);if(null===l){let t=Error("You used a